package parser import ( "bufio" "encoding/json" "errors" "fmt" "io" "regexp" "sort" "strconv" "strings" "text/tabwriter" "time" ) type QEntry struct { Id string Status string Date time.Time Size int Sender string Recipients []string Reason string } type MailQ struct { NumTotal int NumActive int NumHold int NumDeferred int Entries []QEntry } const SortableDateFormat = "2006-01-02 15:04:05" func (m QEntry) String() string { var recipientSuffix string if len(m.Recipients) > 1 { recipientSuffix = ",..." } return fmt.Sprintf("[%s] %s <%s> -> {%d}<%s>%s (%s, %d bytes)", m.Date.Format(SortableDateFormat), m.Id, m.Sender, len(m.Recipients), m.Recipients[0], recipientSuffix, m.Status, m.Size) } func (m QEntry) MachineReadableString() string { return fmt.Sprintf("%s_%s_%s_%d_%s_%s_%s", m.Id, m.Status, m.Date.Format(SortableDateFormat), m.Size, m.Sender, m.Reason, strings.Join(m.Recipients, ",")) } func (m QEntry) DetailedString() string { var reasonStr string if m.Reason == "" { reasonStr = "-/-" } return fmt.Sprintf("Id: %s\nDate: %s\nStatus: %s\nReason: %s\nSize: %d\nSender: %s\nRecipients: %s", m.Id, m.Date.Format(SortableDateFormat), m.Status, reasonStr, m.Size, m.Sender, strings.Join(m.Recipients, ", ")) } type qEntrySortConfig struct { attributes []qEntryAttributeSortConfig } type qEntryAttributeSortConfig struct { attribute string order string } func NewSortConfig() qEntrySortConfig { return qEntrySortConfig{} } func isQEntryAttribute(attributeName string) bool { var isValidAttribute bool switch attributeName { case "Id", "Status", "Date", "Size", "Sender", "Recipients", "Reason": isValidAttribute = true default: isValidAttribute = false } return isValidAttribute } func (c qEntrySortConfig) By(attributeName string, order string) qEntrySortConfig { if !isQEntryAttribute(attributeName) { panic(fmt.Sprintf("Invalid sort attribute: '%s' given!", attributeName)) } if order != "ASC" && order != "DESC" { panic(fmt.Sprintf("Invalid sort order '%s' given!", order)) } newAttributeConfig := qEntryAttributeSortConfig{attribute: attributeName, order: order} c.attributes = append(c.attributes, newAttributeConfig) return c } func (queue MailQ) Sort(config qEntrySortConfig) { if len(config.attributes) == 0 { return } fmt.Printf("SortConfig: %q\n", config) sort.Slice(queue.Entries, func(a int, b int) bool { for _, sortBy := range config.attributes { var cmp bool var skip bool // Not sure if these are necessary yet cmp = false skip = false switch sortBy.attribute { case "Id": if queue.Entries[a].Id == queue.Entries[b].Id { skip = true } cmp = queue.Entries[a].Id < queue.Entries[b].Id case "Status": if queue.Entries[a].Status == queue.Entries[b].Status { skip = true } cmp = queue.Entries[a].Status < queue.Entries[b].Status case "Date": if queue.Entries[a].Date.Equal(queue.Entries[b].Date) { skip = true } cmp = queue.Entries[a].Date.Before(queue.Entries[b].Date) case "Size": if queue.Entries[a].Size == queue.Entries[b].Size { skip = true } cmp = queue.Entries[a].Size < queue.Entries[b].Size case "Sender": if queue.Entries[a].Sender == queue.Entries[b].Sender { skip = true } cmp = queue.Entries[a].Sender < queue.Entries[b].Sender case "Recipients": if len(queue.Entries[a].Recipients) == len(queue.Entries[b].Recipients) { skip = true } cmp = len(queue.Entries[a].Recipients) < len(queue.Entries[b].Recipients) case "Reason": if queue.Entries[a].Reason == queue.Entries[b].Reason { skip = true } cmp = queue.Entries[a].Reason < queue.Entries[b].Reason default: // TODO: Handle this error case? } if skip == true { continue } else { if sortBy.order == "ASC" { return cmp } else if sortBy.order == "DESC" { return !cmp } } } return false }) } func (queue MailQ) PrintMachineReadable(writer io.Writer) { for _, entry := range queue.Entries { fmt.Fprintf(writer, "%s\n", entry.MachineReadableString()) } } func (queue MailQ) PrintHumanReadable(writer io.Writer) { if len(queue.Entries) == 0 { fmt.Fprintf(writer, "Mail queue is empty\n") } else { fmt.Fprintf(writer, "%d entries total (%d active, %d deferred, %d on hold)\n\n", len(queue.Entries), queue.NumActive, queue.NumDeferred, queue.NumHold) tabWriter := tabwriter.NewWriter(writer, 2, 2, 1, ' ', tabwriter.TabIndent) fmt.Fprintf(tabWriter, "| %s\t| %s\t| %s\t| %s\t| %s\t| %s\t| %s\t| %s\t| \n", "Date", "Id", "Status", "Size", "Sender", "#", "First Recipient", "Reason") for _, entry := range queue.Entries { _, writeError := fmt.Fprintf(tabWriter, "| %s\t| %s\t| %s\t| %d\t| %s\t| {%d}\t| %s\t| %s\t| \n", entry.Date.Format(SortableDateFormat), entry.Id, entry.Status, entry.Size, entry.Sender, len(entry.Recipients), entry.Recipients[0], entry.Reason) if writeError != nil { // A writeError is expected once the reader was closed break } } tabWriter.Flush() } } func (queue MailQ) PrintJSON(writer io.Writer) { bytes, err := json.Marshal(queue) if err != nil { fmt.Fprintf(writer, "Error encoding queue to JSON: %s\n", err.Error()) } else { fmt.Fprintf(writer, "%s", bytes) } } func ParseMailQ(dataSource io.Reader) (MailQ, error) { const dateFormat = "2006 Mon Jan _2 15:04:05" var messageIdStart = regexp.MustCompile("^[0-9A-Za-z]+[*!]? ") scanner := bufio.NewScanner(dataSource) var line string scanner.Scan() line = scanner.Text() if strings.HasPrefix(line, "Mail queue is empty") { // If mail queue is empty, there is nothing to do return MailQ{}, nil } else if strings.HasPrefix(line, "-Queue ID-") == false { // Abort if input does not look like output from mailq(1) return MailQ{}, errors.New("Sorry, this does not look like output from mailq(1).") } var queue MailQ var currentMail QEntry for scanner.Scan() { // Read input line by line line := scanner.Text() fields := strings.Fields(line) if strings.HasPrefix(line, "--") { // Handle the summary line at the end of mailq output break } else if messageIdStart.MatchString(line) { // Handle line starting next mail in queue messageId := fields[0] queue.NumTotal += 1 if strings.HasSuffix(messageId, "*") { queue.NumActive += 1 currentMail.Status = "active" } else if strings.HasSuffix(messageId, "!") { currentMail.Status = "hold" queue.NumHold += 1 } else { currentMail.Status = "deferred" queue.NumDeferred += 1 } dateString := strconv.Itoa(time.Now().Year()) + " " + strings.Join(fields[2:6], " ") mailDate, _ := time.Parse(dateFormat, dateString) currentMail.Id = strings.TrimRight(messageId, "*!") currentMail.Size, _ = strconv.Atoi(fields[1]) currentMail.Date = mailDate currentMail.Sender = fields[6] continue } else if strings.HasPrefix(line, "(") && strings.HasSuffix(line, ")") { // Handle reason for deferred status (if deferred at all, may be missing) currentMail.Reason = strings.Trim(strings.TrimSpace(line), "()") continue } else if len(fields) == 1 { // Handle line with one of the mail recipients currentMail.Recipients = append(currentMail.Recipients, fields[0]) continue } else if len(fields) == 0 { // If the next line is empty, make sure to push current mail to list // and create a new struct for the next mail to process queue.Entries = append(queue.Entries, currentMail) currentMail = QEntry{} } } if scanner.Err() != nil { // If the scanner failed, let our caller know. return MailQ{}, errors.New("Something went wrong with reading from stdin. Sorry!") } return queue, nil }