mailq-inspector/parser/parser.go

256 lines
7.6 KiB
Go
Raw Normal View History

2018-12-29 01:42:50 +01:00
package parser
import (
2018-12-29 18:09:44 +01:00
"bufio"
2018-12-31 21:22:23 +01:00
"encoding/json"
2018-12-29 18:09:44 +01:00
"errors"
"fmt"
2018-12-31 17:16:32 +01:00
"io"
2018-12-29 18:09:44 +01:00
"regexp"
2019-01-01 12:41:15 +01:00
"sort"
2018-12-29 18:09:44 +01:00
"strconv"
"strings"
2018-12-31 20:36:14 +01:00
"text/tabwriter"
2018-12-29 18:09:44 +01:00
"time"
2018-12-29 01:42:50 +01:00
)
2018-12-29 18:16:54 +01:00
type QEntry struct {
Id string
Status string
Date time.Time
Size int
Sender string
Recipients []string
Reason string
2018-12-29 01:42:50 +01:00
}
2018-12-31 17:16:32 +01:00
type MailQ struct {
NumTotal int
NumActive int
NumHold int
NumDeferred int
Entries []QEntry
}
2018-12-31 16:54:01 +01:00
const SortableDateFormat = "2006-01-02 15:04:05"
2018-12-29 18:16:54 +01:00
func (m QEntry) String() string {
2018-12-31 17:16:32 +01:00
var recipientSuffix string
if len(m.Recipients) > 1 {
recipientSuffix = ",..."
}
2018-12-31 16:54:01 +01:00
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)
}
2018-12-31 20:36:14 +01:00
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, ","))
2018-12-31 20:36:14 +01:00
}
func (m QEntry) DetailedString() string {
2018-12-31 17:16:32 +01:00
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, ", "))
2018-12-29 01:42:50 +01:00
}
2019-01-01 14:36:50 +01:00
type qEntrySortConfig struct {
attributes []qEntryAttributeSortConfig
}
type qEntryAttributeSortConfig struct {
attribute string
order string
}
func NewSortConfig() qEntrySortConfig {
return qEntrySortConfig{}
}
2019-01-01 15:09:47 +01:00
func isQEntryAttribute(attributeName string) bool {
var isValidAttribute bool
switch attributeName {
2019-01-01 15:13:18 +01:00
case "Id", "Status", "Date", "Size", "Sender", "Recipients", "Reason":
2019-01-01 15:09:47 +01:00
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))
}
2019-01-01 15:13:18 +01:00
if order != "ASC" && order != "DESC" {
panic(fmt.Sprintf("Invalid sort order '%s' given!", order))
}
2019-01-01 15:09:47 +01:00
newAttributeConfig := qEntryAttributeSortConfig{attribute: attributeName, order: order}
2019-01-01 14:36:50 +01:00
c.attributes = append(c.attributes, newAttributeConfig)
return c
}
func (queue MailQ) Sort(config qEntrySortConfig) {
if len(config.attributes) == 0 {
2019-01-01 12:41:15 +01:00
return
}
2019-01-01 15:09:47 +01:00
fmt.Printf("SortConfig: %q\n", config)
2019-01-01 12:41:15 +01:00
sort.Slice(queue.Entries, func(a int, b int) bool {
2019-01-01 14:36:50 +01:00
for _, sortBy := range config.attributes {
2019-01-01 14:49:30 +01:00
var cmp bool
var skip bool
// Not sure if these are necessary yet
cmp = false
skip = false
2019-01-01 14:36:50 +01:00
switch sortBy.attribute {
2019-01-01 12:41:15 +01:00
case "Id":
2019-01-01 15:17:21 +01:00
if queue.Entries[a].Id == queue.Entries[b].Id {
2019-01-01 14:49:30 +01:00
skip = true
2019-01-01 12:41:15 +01:00
}
2019-01-01 14:49:30 +01:00
cmp = queue.Entries[a].Id < queue.Entries[b].Id
2019-01-01 12:41:15 +01:00
case "Status":
2019-01-01 15:17:21 +01:00
if queue.Entries[a].Status == queue.Entries[b].Status {
2019-01-01 14:49:30 +01:00
skip = true
2019-01-01 12:41:15 +01:00
}
2019-01-01 14:49:30 +01:00
cmp = queue.Entries[a].Status < queue.Entries[b].Status
2019-01-01 12:41:15 +01:00
case "Date":
2019-01-01 15:17:21 +01:00
if queue.Entries[a].Date.Equal(queue.Entries[b].Date) {
2019-01-01 14:49:30 +01:00
skip = true
2019-01-01 12:41:15 +01:00
}
2019-01-01 14:49:30 +01:00
cmp = queue.Entries[a].Date.Before(queue.Entries[b].Date)
2019-01-01 12:41:15 +01:00
case "Size":
2019-01-01 15:17:21 +01:00
if queue.Entries[a].Size == queue.Entries[b].Size {
2019-01-01 14:49:30 +01:00
skip = true
2019-01-01 12:41:15 +01:00
}
2019-01-01 14:49:30 +01:00
cmp = queue.Entries[a].Size < queue.Entries[b].Size
2019-01-01 12:41:15 +01:00
case "Sender":
2019-01-01 15:17:21 +01:00
if queue.Entries[a].Sender == queue.Entries[b].Sender {
2019-01-01 14:49:30 +01:00
skip = true
2019-01-01 12:41:15 +01:00
}
2019-01-01 14:49:30 +01:00
cmp = queue.Entries[a].Sender < queue.Entries[b].Sender
2019-01-01 12:41:15 +01:00
case "Recipients":
2019-01-01 15:17:21 +01:00
if len(queue.Entries[a].Recipients) == len(queue.Entries[b].Recipients) {
2019-01-01 14:49:30 +01:00
skip = true
2019-01-01 12:41:15 +01:00
}
2019-01-01 14:49:30 +01:00
cmp = len(queue.Entries[a].Recipients) < len(queue.Entries[b].Recipients)
2019-01-01 12:41:15 +01:00
case "Reason":
2019-01-01 15:17:21 +01:00
if queue.Entries[a].Reason == queue.Entries[b].Reason {
2019-01-01 14:49:30 +01:00
skip = true
2019-01-01 12:41:15 +01:00
}
2019-01-01 14:49:30 +01:00
cmp = queue.Entries[a].Reason < queue.Entries[b].Reason
2019-01-01 12:41:15 +01:00
default:
// TODO: Handle this error case?
}
2019-01-01 14:49:30 +01:00
if skip == true {
continue
} else {
if sortBy.order == "ASC" {
return cmp
} else if sortBy.order == "DESC" {
return !cmp
}
}
2019-01-01 12:41:15 +01:00
}
return false
})
}
2018-12-31 20:36:14 +01:00
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()
}
}
2018-12-31 21:22:23 +01:00
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)
}
}
2018-12-31 17:16:32 +01:00
func ParseMailQ(dataSource io.Reader) (MailQ, error) {
2018-12-29 18:09:44 +01:00
const dateFormat = "2006 Mon Jan _2 15:04:05"
var messageIdStart = regexp.MustCompile("^[0-9A-Za-z]+[*!]? ")
2018-12-29 22:02:23 +01:00
scanner := bufio.NewScanner(dataSource)
2018-12-31 17:16:32 +01:00
var line string
scanner.Scan()
line = scanner.Text()
if strings.HasPrefix(line, "Mail queue is empty") {
2018-12-29 18:09:44 +01:00
// If mail queue is empty, there is nothing to do
2018-12-31 17:16:32 +01:00
return MailQ{}, nil
2018-12-29 18:09:44 +01:00
} else if strings.HasPrefix(line, "-Queue ID-") == false {
// Abort if input does not look like output from mailq(1)
2018-12-31 17:16:32 +01:00
return MailQ{}, errors.New("Sorry, this does not look like output from mailq(1).")
2018-12-29 18:09:44 +01:00
}
2018-12-31 17:16:32 +01:00
var queue MailQ
2018-12-29 18:16:54 +01:00
var currentMail QEntry
2018-12-29 18:09:44 +01:00
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]
2018-12-31 17:16:32 +01:00
queue.NumTotal += 1
2018-12-29 18:09:44 +01:00
if strings.HasSuffix(messageId, "*") {
2018-12-31 17:16:32 +01:00
queue.NumActive += 1
currentMail.Status = "active"
2018-12-29 18:09:44 +01:00
} else if strings.HasSuffix(messageId, "!") {
currentMail.Status = "hold"
2018-12-31 17:16:32 +01:00
queue.NumHold += 1
2018-12-29 18:09:44 +01:00
} else {
currentMail.Status = "deferred"
2018-12-31 17:16:32 +01:00
queue.NumDeferred += 1
2018-12-29 18:09:44 +01:00
}
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]
2018-12-29 18:09:44 +01:00
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), "()")
2018-12-29 18:09:44 +01:00
continue
} else if len(fields) == 1 {
// Handle line with one of the mail recipients
currentMail.Recipients = append(currentMail.Recipients, fields[0])
2018-12-29 18:09:44 +01:00
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
2018-12-31 17:16:32 +01:00
queue.Entries = append(queue.Entries, currentMail)
2018-12-29 18:16:54 +01:00
currentMail = QEntry{}
2018-12-29 18:09:44 +01:00
}
}
if scanner.Err() != nil {
// If the scanner failed, let our caller know.
2018-12-31 17:16:32 +01:00
return MailQ{}, errors.New("Something went wrong with reading from stdin. Sorry!")
2018-12-29 18:09:44 +01:00
}
2018-12-31 17:16:32 +01:00
return queue, nil
2018-12-29 01:42:50 +01:00
}