Initial commit.

master
Icedream 2018-03-23 00:35:29 +01:00
commit fad5d15987
Signed by: icedream
GPG Key ID: 1573F6D8EFE4D0CF
6 changed files with 679 additions and 0 deletions

52
.gitignore vendored Normal file
View File

@ -0,0 +1,52 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
# Built binaries
smtplogparser
*.exe
*.test
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
*.un~
Session.vim
.netrwhist
*~
# Log files
*.log

312
internal/log.go Normal file
View File

@ -0,0 +1,312 @@
package internal
import (
"bufio"
"encoding/csv"
"io"
"log"
"regexp"
"strings"
"time"
)
const (
AnonymizedText = "**ANONYMIZED**"
)
// Syntax of a message:
// DD-MM-YYYY hh:mm:ss LogLevel Service [\[ID\]] LogMessage[ (MessageID)][.]
// Example failing mail:
// 19-03-2018 03:54:07 3 Fuzzy-Filter (40935FC192D) phase 1 (ex) 325ms result: Major=spam Minor=normal Fallback=spam Virus=
var (
// 05-03-2018 15:36:14 2 SMTPClient [E8F00BA5] (4DBAB2C02A3) Message delivered to: <person@kunde.de>
rxSuccessfulMessage = regexp.MustCompile(`^Message\s+delivered\s+to:\s+<(.+)>\s*$`)
rxMapMessageID = regexp.MustCompile(`^MessageID: .+\s*$`)
rxSubject = regexp.MustCompile(`^Testing: (.+)\s*$`)
rxSenderAddress = regexp.MustCompile(`(?i)^Recieve: MAIL\s+FROM:\s*<(.+)>\s*`)
rxRecipientAddress = regexp.MustCompile(`(?i)^Recieve: RCPT\s+TO:\s*<(.+)>\s*`)
/*
05-03-2018 15:36:13 3 SMTPClient [E8F00BA5] (4DBAB2C02A3) Receive on MAIL FROM: <MaxMustermann@daimler.com> = 250 2.1.0 Sender OK
05-03-2018 15:36:13 3 SMTPClient [E8F00BA5] (4DBAB2C02A3) Receive on RCPT TO: <person@kunde.de> = 250 2.1.5 Recipient OK
*/
rxSendMailFrom = regexp.MustCompile(`^Receive on MAIL\s+FROM:\s*<(.+)>\s+`)
rxSendRcptTo = regexp.MustCompile(`^Receive on RCPT\s+TO:\s*<(.+)>\s+`)
maxMessageAge = 30 * time.Second
)
type Log struct {
// Maps sessions by SMTP server session ID
smtpServerSessions map[string]*SMTPServerSession
CSVWriter *csv.Writer
AnonymizeSubject bool
}
func (this *Log) flushSingle(sessionId string) (err error) {
session := this.smtpServerSessions[sessionId]
delete(this.smtpServerSessions, sessionId)
if this.CSVWriter != nil {
if err := this.CSVWriter.Write(session.csv()); err != nil {
return err
}
}
return
}
func (this *Log) flush(timeout time.Duration, timestamp time.Time) (err error) {
outerLoop:
for {
for id, session := range this.smtpServerSessions {
if timestamp.Sub(session.Timestamp) >= timeout {
err = this.flushSingle(id)
if err != nil {
return
}
continue outerLoop
}
}
break
}
return
}
func (this *Log) Parse(r io.Reader) (err error) {
this.smtpServerSessions = map[string]*SMTPServerSession{}
textreader := bufio.NewScanner(r)
textreader.Scan()
thisLine := textreader.Text()
nextLine := ""
var session *SMTPServerSession
if this.CSVWriter != nil {
if err = this.CSVWriter.Write(sessionCSVHeader); err != nil {
return
}
}
for {
hasNextLineAvailable := textreader.Scan()
if hasNextLineAvailable {
nextLine = textreader.Text()
// Try parsing first two fields of the upcoming line. If it doesn't
// work, assume leakage of previous line content.
fields := strings.Fields(nextLine)
if len(fields) < 2 {
// this is a continuation (too few fields)
thisLine = strings.TrimSpace(thisLine) + nextLine
continue
}
_, err = time.Parse("02-01-2006 15:04:05", strings.Join(fields[0:2], " "))
if err != nil {
// this is a continuation (first two fields don't follow usual line syntax)
err = nil
thisLine = strings.TrimSpace(thisLine) + nextLine
continue
}
}
if len(strings.TrimSpace(thisLine)) > 0 {
fields := strings.Fields(thisLine)
if len(fields) < 2 {
log.Print("WARNING: Found a line with less than 2 fields to work with. Skipping:", thisLine)
} else {
timestamp, err := time.Parse("02-01-2006 15:04:05", strings.Join(fields[0:2], " "))
if err != nil {
// first two fields don't follow usual line syntax
return err
}
serviceName := fields[3]
sessionId := ""
messageId := ""
logMessage := ""
logMessageStartIndex := 4
logMessageEndIndex := len(fields)
// Is the session ID stored in the message?
// For this, fields[4] needs to start with [ and needs to contain an 8-char long ID.
if strings.HasPrefix(fields[4], "[") &&
strings.HasSuffix(fields[4], "]") &&
len(fields[4]) == 1+8+1 {
// Yup, there's an ID here!
sessionId = fields[4][1:9]
logMessageStartIndex = 5
}
// Check if there is a message ID at the end of the log message
messageIdPart := strings.TrimRight(fields[len(fields)-1], ".")
if strings.HasPrefix(messageIdPart, "(") &&
strings.HasSuffix(messageIdPart, ")") &&
len(messageIdPart) == 1+11+1 {
// Yup, there's a message ID!
messageId = messageIdPart[1:12]
logMessageEndIndex--
} else if strings.HasPrefix(fields[logMessageStartIndex], "(") &&
strings.HasSuffix(fields[logMessageStartIndex], ")") &&
len(fields[logMessageStartIndex]) == 1+11+1 {
// Message ID but in fields[logMessageStartIndex], so shift start of log message
messageId = fields[logMessageStartIndex][1:12]
logMessageStartIndex++
}
logMessage = strings.Join(fields[logMessageStartIndex:logMessageEndIndex], " ")
//log.Println(serviceName, "-", sessionId, "-", messageId, "-", logMessage)
switch serviceName {
case "SMTPClient":
// Could contain success message for delivery
//log.Println(">> parsing for delivery success")
if rxSuccessfulMessage.MatchString(logMessage) {
//log.Println(">> got delivery success!")
session = this.FindSMTPServerSessionByMessageId(messageId, timestamp)
if session == nil {
log.Println("WARNING: Found a delivery success message for a message we don't know:", logMessage)
break
}
session.IsSent = true
// immediately unregister
if err = this.flushSingle(session.SMTPServerSessionID); err != nil {
return err
}
}
// Could contain sender
subm := rxSendMailFrom.FindStringSubmatch(logMessage)
if subm != nil {
session = this.FindSMTPServerSessionByMessageId(messageId, timestamp)
if session == nil {
log.Println("WARNING: Found a log line about the sender for a passed message we don't know:", logMessage)
break
}
session.SenderAddress = subm[1]
break
}
// Could contain recipient
subm = rxSendRcptTo.FindStringSubmatch(logMessage)
if subm != nil {
session = this.FindSMTPServerSessionByMessageId(messageId, timestamp)
if session == nil {
log.Println("WARNING: Found a log line about the recipient for a passed message we don't know:", logMessage)
break
}
session.RecipientAddress = subm[1]
break
}
case "SMTPServer":
if len(sessionId) > 0 {
session = this.createOrFetchSMTPServerSession(sessionId, timestamp)
}
// Could contain recipient
subm := rxRecipientAddress.FindStringSubmatch(logMessage)
//log.Println(">> parsing for recipient")
if subm != nil {
//log.Println(">> got recipient")
// We got submatches, this has a recipient!
session.RecipientAddress = subm[1]
break
}
// Could contain sender
subm = rxSenderAddress.FindStringSubmatch(logMessage)
//log.Println(">> parsing for sender")
if subm != nil {
//log.Println(">> got sender")
// We got submatches, this has a sender!
session.SenderAddress = subm[1]
break
}
// Could have metadata about message ID
//log.Println(">> parsing for message ID map")
if len(messageId) > 0 {
//log.Println(">> got message ID map 1")
subm = rxMapMessageID.FindStringSubmatch(logMessage)
if subm != nil {
//log.Println(">> got message ID map 2")
// We got submatches, this lets us map session ID to message ID!
session.MessageID = messageId
//log.Println(messageId, "->", sessionId)
break
}
}
case "SBL-Filter", "SWL-Filter":
// Could contain the subject
//log.Println(">> parsing for subject")
subm := rxSubject.FindStringSubmatch(logMessage)
if subm != nil {
//log.Println(">> got subject")
// We got submatches, this has a subject!
subject := subm[1]
session = this.FindSMTPServerSessionByMessageId(messageId, timestamp)
if session == nil {
log.Println("WARNING: Found a subject line for a message we don't know:", logMessage)
break
}
if this.AnonymizeSubject {
session.Subject = AnonymizedText
} else {
session.Subject = subject
}
}
}
this.flush(maxMessageAge, timestamp)
}
}
if !hasNextLineAvailable {
break
}
thisLine = nextLine
}
this.flush(0, time.Now())
if this.CSVWriter != nil {
this.CSVWriter.Flush()
}
/*if err = textreader.Err(); err != nil {
return
}*/
err = textreader.Err()
return
}
func (this *Log) createOrFetchSMTPServerSession(sessionId string, timestamp time.Time) (session *SMTPServerSession) {
if len(sessionId) <= 0 {
panic("Empty session ID")
}
session, ok := this.smtpServerSessions[sessionId]
if !ok {
session = &SMTPServerSession{
SMTPServerSessionID: sessionId,
Timestamp: timestamp,
}
this.smtpServerSessions[sessionId] = session
}
return
}
func (this *Log) FindSMTPServerSessionByMessageId(messageId string, timestamp time.Time) (session *SMTPServerSession) {
for _, currentSession := range this.smtpServerSessions {
if currentSession.IsMatchByMessageId(messageId, timestamp) {
return currentSession
}
}
return nil
}

14
internal/parse.go Normal file
View File

@ -0,0 +1,14 @@
package internal
import (
//"errors"
"encoding/csv"
"io"
)
func TransformLog(r io.Reader, w io.Writer) (err error) {
l := new(Log)
l.CSVWriter = csv.NewWriter(w)
err = l.Parse(r)
return
}

176
internal/parse_test.go Normal file
View File

@ -0,0 +1,176 @@
package internal
import (
"bytes"
"encoding/csv"
"io/ioutil"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
const (
testMessageFailed = `19-03-2018 03:54:05 2 SMTPServer [F1AF6651] New connection from 80.249.239.48
19-03-2018 03:54:05 3 SMTPServer Testing 80.249.239.48 on bl.spamcop.net
19-03-2018 03:54:05 3 SMTPServer [F1AF6651] Send: 220 mail.kunde.de SMTP server ready
19-03-2018 03:54:05 3 SMTPServer [F1AF6651] Recieve: EHLO site.azauto.com.ua
19-03-2018 03:54:05 3 SMTPServer [F1AF6651] Send: 250-OK
19-03-2018 03:54:05 3 SMTPServer [F1AF6651] Send: 250-STARTTLS
19-03-2018 03:54:05 3 SMTPServer [F1AF6651] Send: 250 SIZE 104857600
19-03-2018 03:54:05 3 SMTPServer [F1AF6651] Ehlo Greeting from: [80.249.239.48] - site.azauto.com.ua
19-03-2018 03:54:06 3 SMTPServer [F1AF6651] Recieve: MAIL FROM:<info@byphil.comt>
19-03-2018 03:54:06 2 SMTPServer [F1AF6651] Mail from: info@byphil.comt
19-03-2018 03:54:06 3 SMTPServer [F1AF6651] Send: 250 OK smtp ready for info@byphil.comt
19-03-2018 03:54:06 3 SMTPServer [F1AF6651] Recieve: RCPT TO:<abteilung@kunde.de>
19-03-2018 03:54:06 3 SMTPServer RVC-Filter testing: abteilung@kunde.de
19-03-2018 03:54:06 3 SMTPServer [F1AF6651] Send: 250 OK smtp ready for abteilung@kunde.de
19-03-2018 03:54:06 2 SMTPServer [F1AF6651] Mail to: <abteilung@kunde.de> accepted
19-03-2018 03:54:06 3 SMTPServer [F1AF6651] Recieve: DATA
19-03-2018 03:54:06 3 SMTPServer [F1AF6651] Send: 354 Send message. End with CRLF.CRLF
19-03-2018 03:54:06 3 SMTPServer [F1AF6651] MessageID: <00D6549C_3B615A6F_reddoxx@mail.kunde.de> (40935FC192D).
19-03-2018 03:54:06 3 SMTPServer [F1AF6651] TransactionID: F84F9E0625A025C54F7DBD19AC13DEBF4C2736FB (40935FC192D).
19-03-2018 03:54:06 3 SMTPServer [F1AF6651] Saving message ... (40935FC192D)
19-03-2018 03:54:06 3 SMTPServer [F1AF6651] Send: 250 message queued (40935FC192D)
19-03-2018 03:54:06 2 SMTPServer [F1AF6651] Message queued (40935FC192D)
19-03-2018 03:54:06 3 SMTPServer [F1AF6651] Recieve: QUIT
19-03-2018 03:54:06 3 SMTPServer [F1AF6651] Send: 221 closing connection
19-03-2018 03:54:06 2 SMTPServer [F1AF6651] Client disconnected.
19-03-2018 03:54:06 2 SMTPServer [F1AF6651] Disconnected from 80.249.239.48
19-03-2018 03:54:07 3 Validator [EB5FAD39] Thread started. Validating message (40935FC192D)
19-03-2018 03:54:07 3 Validator [EB5FAD39] Load message ... (40935FC192D)
19-03-2018 03:54:07 3 Validator [EB5FAD39] Load message finished. (40935FC192D)
19-03-2018 03:54:07 2 MailSealer [EB5FAD39] Message has no secure components. (40935FC192D)
19-03-2018 03:54:07 3 Validator [EB5FAD39] Starting validation of Message (40935FC192D)
19-03-2018 03:54:07 3 Validator [EB5FAD39] Using Profile: (1) Default-Filterprofile for <abteilung@kunde.de>
19-03-2018 03:54:07 3 DWL-Filter Testing (envelope): info@byphil.comt (40935FC192D)
19-03-2018 03:54:07 3 AWL-Filter Testing (envelope): info@byphil.comt (40935FC192D)
19-03-2018 03:54:07 3 AWL-Filter info@byphil.comt is *NOT* on sender address whitelist. 0ms (40935FC192D)
19-03-2018 03:54:07 3 SWL-Filter Testing:
Finanzdienstleiter (40935FC192D)
19-03-2018 03:54:07 3 RBL-Filter Testing 80.249.239.48 on bl.spamcop.net (40935FC192D)
19-03-2018 03:54:07 3 Advanced-RBL-Filter Testing (40935FC192D)
19-03-2018 03:54:07 3 Fuzzy-Filter Testing phase 1 (40935FC192D)
19-03-2018 03:54:07 3 Fuzzy-Filter (40935FC192D) phase 1 (ex) 325ms result: Major=spam Minor=normal Fallback=spam Virus=
19-03-2018 03:54:07 2 Fuzzy-Filter (40935FC192D) blocked by phase 1
19-03-2018 03:54:07 2 Validator [EB5FAD39] Filter: 'Fuzzy-Filter' will prevent archiving.
19-03-2018 03:54:07 3 DBL-Filter Testing (envelope): info@byphil.comt (40935FC192D)
19-03-2018 03:54:07 3 ABL-Filter Testing (envelope): info@byphil.comt (40935FC192D)
19-03-2018 03:54:07 3 SBL-Filter Testing: Finanzdienstleiter (40935FC192D)
19-03-2018 03:54:07 3 VirusScanner No Virus found in message (40935FC192D)
19-03-2018 03:54:07 3 Validator [EB5FAD39] - abteilung@kunde.de validated. Result: 30
19-03-2018 03:54:07 3 Validator [EB5FAD39] Thread terminated.
19-03-2018 03:54:07 3 Validator [EB5FAD39] Validation of Message (40935FC192D) finished.`
testMessageSuccess = `05-03-2018 15:36:13 3 SMTPServer [EEA1EB23] Recieve: RCPT TO:<person@kunde.de>
05-03-2018 15:36:13 3 SMTPServer RVC-Filter testing: person@kunde.de
05-03-2018 15:36:13 3 SMTPServer [EEA1EB23] Send: 250 OK smtp ready for person@kunde.de
05-03-2018 15:36:13 2 SMTPServer [EEA1EB23] Mail to: <person@kunde.de> accepted
05-03-2018 15:36:13 3 SMTPServer [EEA1EB23] Recieve: DATA
05-03-2018 15:36:13 3 SMTPServer [EEA1EB23] Send: 354 Send message. End with CRLF.CRLF
05-03-2018 15:36:13 3 SMTPServer [EEA1EB23] MessageID: <754a924065cc4c15ace114d9196e16ef@DE35S004EXC61.wp.corpintra.net> (4DBAB2C02A3).
05-03-2018 15:36:13 3 SMTPServer [EEA1EB23] TransactionID: 82CDA2B813955EB42DCDB70BDAED57F2A6C53C29 (4DBAB2C02A3).
05-03-2018 15:36:13 3 SMTPServer [EEA1EB23] Saving message ... (4DBAB2C02A3)
05-03-2018 15:36:13 3 SMTPServer [EEA1EB23] Send: 250 message queued (4DBAB2C02A3)
05-03-2018 15:36:13 2 SMTPServer [EEA1EB23] Message queued (4DBAB2C02A3)
05-03-2018 15:36:13 3 SMTPServer [EEA1EB23] Recieve: QUIT
05-03-2018 15:36:13 3 SMTPServer [EEA1EB23] Send: 221 closing connection
05-03-2018 15:36:13 2 SMTPServer [EEA1EB23] Client disconnected.
05-03-2018 15:36:13 2 SMTPServer [EEA1EB23] Disconnected from 141.113.102.113
05-03-2018 15:36:13 3 Validator [E8F016EA] Thread started. Validating message (4DBAB2C02A3)
05-03-2018 15:36:13 3 Validator [E8F016EA] Load message ... (4DBAB2C02A3)
05-03-2018 15:36:13 3 Validator [E8F016EA] Load message finished. (4DBAB2C02A3)
05-03-2018 15:36:13 2 MailSealer [E8F016EA] Message has no secure components. (4DBAB2C02A3)
05-03-2018 15:36:13 3 Validator [E8F016EA] Starting validation of Message (4DBAB2C02A3)
05-03-2018 15:36:13 3 Validator [E8F016EA] Using Profile: (1) Default-Filterprofile for <person@kunde.de>
05-03-2018 15:36:13 3 DWL-Filter Testing (envelope): MaxMustermann@daimler.com (4DBAB2C02A3)
05-03-2018 15:36:13 3 AWL-Filter Testing (envelope): MaxMustermann@daimler.com (4DBAB2C02A3)
05-03-2018 15:36:13 3 AWL-Filter MaxMustermann@daimler.com is *NOT* on sender address whitelist. 0ms (4DBAB2C02A3)
05-03-2018 15:36:13 3 SWL-Filter Testing: AW: Urlaubsplanung (4DBAB2C02A3)
05-03-2018 15:36:13 3 RBL-Filter Testing 141.113.102.113 on bl.spamcop.net (4DBAB2C02A3)
05-03-2018 15:36:13 3 Advanced-RBL-Filter Testing (4DBAB2C02A3)
05-03-2018 15:36:13 3 Fuzzy-Filter Testing phase 1 (4DBAB2C02A3)
05-03-2018 15:36:13 3 Fuzzy-Filter (4DBAB2C02A3) phase 1 (ex) 161ms result: Major=clean Minor=normal Fallback=clean Virus=
05-03-2018 15:36:13 3 Fuzzy-Filter Testing phase 2 (4DBAB2C02A3)
05-03-2018 15:36:13 3 Fuzzy-Filter Testing 1d28528ffe972a11b3789fac292200b2 (4DBAB2C02A3)
05-03-2018 15:36:13 3 DBL-Filter Testing (envelope): MaxMustermann@daimler.com (4DBAB2C02A3)
05-03-2018 15:36:13 3 ABL-Filter Testing (envelope): MaxMustermann@daimler.com (4DBAB2C02A3)
05-03-2018 15:36:13 3 SBL-Filter Testing: AW: Urlaubsplanung (4DBAB2C02A3)
05-03-2018 15:36:13 3 VirusScanner No Virus found in message (4DBAB2C02A3)
05-03-2018 15:36:13 3 Validator [E8F016EA] - person@kunde.de validated. Result: 0
05-03-2018 15:36:13 3 Validator Wait for MailDepot lock ...
05-03-2018 15:36:13 3 Validator MailDepot locked.
05-03-2018 15:36:13 3 Archive ArchiveMessage 2.0 start ... - (4DBAB2C02A3)
05-03-2018 15:36:13 2 Archive No policy matched. Message will be archived.
05-03-2018 15:36:13 3 Archive ArchiveMessage 2.0 before critical section ... - (4DBAB2C02A3)
05-03-2018 15:36:13 2 Archive Archiving for MailDepot 2.0 - (4DBAB2C02A3)
05-03-2018 15:36:13 3 Archive Add MailDepot 2.0 spooler entry: 82CDA2B813955EB42DCDB70BDAED57F2A6C53C29.rdxmt0
05-03-2018 15:36:13 3 Archive ArchiveMessage 2.0 finished. - (4DBAB2C02A3)
05-03-2018 15:36:13 3 Validator MailDepot unlocked.
05-03-2018 15:36:13 3 Validator [E8F016EA] Thread terminated.
05-03-2018 15:36:13 3 Validator [E8F016EA] Validation of Message (4DBAB2C02A3) finished.
05-03-2018 15:36:13 3 SMTPClient [4041136960] Query queue
05-03-2018 15:36:13 3 SMTPClient [E8F00BA5] Thread started. Sending message (4DBAB2C02A3)
05-03-2018 15:36:13 2 SMTPClient [E8F00BA5] (4DBAB2C02A3) Sending message for: <MaxMustermann@daimler.com>
05-03-2018 15:36:13 3 SMTPClient [E8F00BA5] (4DBAB2C02A3) connecting to: 10.1.0.18:25
05-03-2018 15:36:13 3 SMTPClient [E8F00BA5] (4DBAB2C02A3) Receive on HELO: 250 XSHADOW
05-03-2018 15:36:13 3 SMTPClient [E8F00BA5] (4DBAB2C02A3) Receive on MAIL FROM: <MaxMustermann@daimler.com> = 250 2.1.0 Sender OK
05-03-2018 15:36:13 3 SMTPClient [E8F00BA5] (4DBAB2C02A3) Receive on RCPT TO: <person@kunde.de> = 250 2.1.5 Recipient OK
05-03-2018 15:36:14 3 SMTPClient [E8F00BA5] (4DBAB2C02A3) Data result: 250 2.6.0 <754a924065cc4c15ace114d9196e16ef@DE35S004EXC61.wp.corpintra.net> [InternalId=1161195] Queued mail for delivery
05-03-2018 15:36:14 2 SMTPClient [E8F00BA5] (4DBAB2C02A3) Message delivered to: <person@kunde.de>
05-03-2018 15:36:14 3 SMTPClient [E8F00BA5] Thread terminated.`
)
func Test_Message_Successful(t *testing.T) {
buf := new(bytes.Buffer)
transformer := new(Log)
transformer.CSVWriter = csv.NewWriter(buf)
err := transformer.Parse(strings.NewReader(testMessageSuccess))
require.NotEqual(t, buf.Len(), 0)
require.Nil(t, err)
require.Equal(t, strings.Join(sessionCSVHeader, ",")+
"\n2018-03-05,15:36:13,MaxMustermann@daimler.com,person@kunde.de,AW: Urlaubsplanung,true,EEA1EB23,4DBAB2C02A3",
strings.TrimSpace(buf.String()))
}
func Test_Message_DupeSessionId(t *testing.T) {
buf := new(bytes.Buffer)
transformer := new(Log)
transformer.CSVWriter = csv.NewWriter(buf)
err := transformer.Parse(strings.NewReader(testMessageSuccess + "\n" + testMessageSuccess))
require.NotEqual(t, buf.Len(), 0)
require.Nil(t, err)
require.Equal(t, strings.Join(sessionCSVHeader, ",")+
"\n2018-03-05,15:36:13,MaxMustermann@daimler.com,person@kunde.de,AW: Urlaubsplanung,true,EEA1EB23,4DBAB2C02A3"+
"\n2018-03-05,15:36:13,MaxMustermann@daimler.com,person@kunde.de,AW: Urlaubsplanung,true,EEA1EB23,4DBAB2C02A3",
strings.TrimSpace(buf.String()))
}
func Test_Message_Failed(t *testing.T) {
buf := new(bytes.Buffer)
transformer := new(Log)
transformer.CSVWriter = csv.NewWriter(buf)
err := transformer.Parse(strings.NewReader(testMessageFailed))
require.NotEqual(t, buf.Len(), 0)
require.Nil(t, err)
require.Equal(t, strings.Join(sessionCSVHeader, ",")+
"\n2018-03-19,03:54:05,info@byphil.comt,abteilung@kunde.de,Finanzdienstleiter,false,F1AF6651,40935FC192D",
strings.TrimSpace(buf.String()))
}
func Benchmark_Messages(b *testing.B) {
output := ioutil.Discard
transformer := new(Log)
transformer.CSVWriter = csv.NewWriter(output)
for n := 0; n < b.N; n++ {
transformer.Parse(strings.NewReader(testMessageSuccess))
}
}

50
internal/serversession.go Normal file
View File

@ -0,0 +1,50 @@
package internal
import (
"fmt"
"strings"
"time"
)
var (
sessionCSVHeader = []string{
"Date",
"Time",
"From",
"To",
"Subject",
"Passed",
"SMTPServer session ID",
"Message short ID",
}
)
type SMTPServerSession struct {
SMTPServerSessionID string
MessageID string
RecipientAddress string
SenderAddress string
Subject string
IsSent bool
Timestamp time.Time
}
func (sss *SMTPServerSession) csv() []string {
return []string{
sss.Timestamp.Format("2006-01-02"),
sss.Timestamp.Format("15:04:05"),
sss.SenderAddress,
sss.RecipientAddress,
sss.Subject,
fmt.Sprintf("%v", sss.IsSent),
sss.SMTPServerSessionID,
sss.MessageID,
}
}
func (sss *SMTPServerSession) IsMatchByMessageId(messageId string, timestamp time.Time) (result bool) {
result = strings.EqualFold(messageId, sss.MessageID) &&
timestamp.Sub(sss.Timestamp) < maxMessageAge
return
}

75
main.go Normal file
View File

@ -0,0 +1,75 @@
package main
import (
"encoding/csv"
"io"
"log"
"os"
"path/filepath"
"strings"
"github.com/alecthomas/kingpin"
"local/smtpparse/internal"
)
var (
flagInputFiles = kingpin.Flag("input-file", "Input files. Can be glob patterns like `*.log` and `-` for standard input pipe.").Short('i').Required().Strings()
flagOutputFile = kingpin.Flag("output-file", "Output file. Can be `-` for standard output pipe.").Default("-").Short('o').String()
flagRecipients = kingpin.Flag("recipients", "List of recipients to find entries for. Defaults to everyone.").Short('r').Strings()
flagAnonymize = kingpin.Flag("anonymize", "Anonymizes mail subjects.").Bool()
)
func main() {
kingpin.Parse()
// Open input file
readers := []io.Reader{}
for _, globPattern := range *flagInputFiles {
// allow stdin
if globPattern == "-" {
readers = append(readers, os.Stdin, strings.NewReader("\n"))
continue
}
// check for files of pattern
matches, err := filepath.Glob(globPattern)
if err != nil {
log.Fatal(err)
}
for _, match := range matches {
f, err := os.Open(match)
if err != nil {
log.Fatal(err)
}
defer f.Close()
readers = append(readers, f, strings.NewReader("\n"))
}
}
r := io.MultiReader(readers...)
// Create output
var outputWriter io.Writer
if *flagOutputFile == "-" {
outputWriter = os.Stdout
} else {
f, err := os.Create(*flagOutputFile)
if err != nil {
log.Fatal(err)
}
defer f.Close()
outputWriter = f
}
// Parse
transformer := new(internal.Log)
transformer.AnonymizeSubject = *flagAnonymize
transformer.CSVWriter = csv.NewWriter(outputWriter)
err := transformer.Parse(r)
if err != nil {
log.Fatal(err)
}
}