You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
252 lines
8.8 KiB
252 lines
8.8 KiB
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/DusanKasan/parsemail"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// When a new message is received, this function is called to store it.
|
|
func MailSaveMessage(remoteAddr string, from string, to string, r io.Reader) error {
|
|
// We need the message body in bytes to save.
|
|
b, err := ioutil.ReadAll(r)
|
|
if err != nil { // If we can't read, we have an issue.
|
|
return err
|
|
}
|
|
// The email parser expects an io.Reader, but as we already read the reader passed. We must make a new one.
|
|
reader := bytes.NewReader(b)
|
|
// Parse the email
|
|
email, err := parsemail.Parse(reader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Generate a UUID for this message.
|
|
UUID := uuid.New().String()
|
|
|
|
// Save message body using database or file if configured.
|
|
if app.config.MailPath == "db" {
|
|
// Save to database.
|
|
message := Messages{}
|
|
message.UUID = UUID
|
|
message.Message = b
|
|
app.db.Create(&message)
|
|
} else {
|
|
// If the directory configured in the config does not exist... We must fail.
|
|
if _, err := os.Stat(app.config.MailPath); err != nil {
|
|
return fmt.Errorf("Mail directory does not exist: %s", app.config.MailPath)
|
|
}
|
|
// Create the file.
|
|
messagePath := path.Join(app.config.MailPath, UUID)
|
|
fp, err := os.Create(messagePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Write to the file
|
|
fp.Write(b)
|
|
fp.Close()
|
|
}
|
|
|
|
// Create a message log entry with parsed email.
|
|
messageEntry := MessageLog{}
|
|
messageEntry.UUID = UUID
|
|
messageEntry.MessageID = email.MessageID
|
|
if len(email.From) <= 0 {
|
|
messageEntry.From = from
|
|
} else {
|
|
messageEntry.From = email.From[0].Address
|
|
}
|
|
if len(email.To) <= 0 {
|
|
messageEntry.To = to
|
|
} else {
|
|
messageEntry.To = email.To[0].Address
|
|
}
|
|
messageEntry.Subject = email.Subject
|
|
|
|
messageEntry.PlainText = email.TextBody != ""
|
|
messageEntry.HTML = email.HTMLBody != ""
|
|
messageEntry.Attachments = len(email.Attachments) > 0
|
|
|
|
// If a spam level header exists, parse the score.
|
|
spamScore := email.Header.Get("X-SPAM-LEVEL")
|
|
rxScore := regexp.MustCompile("Spam detection results:\\s+([0-9]+)")
|
|
matches := rxScore.FindStringSubmatch(spamScore)
|
|
if len(matches) == 2 {
|
|
spamScoreI, err := strconv.Atoi(matches[1])
|
|
if err != nil {
|
|
spamScoreI = 0
|
|
}
|
|
messageEntry.SpamScore = spamScoreI
|
|
}
|
|
|
|
// Get the source IP from the earliest received header. Default to remote address which is sending the message.
|
|
// As messages are likely forwarded to this server, the earliest received header is what we want here.
|
|
messageEntry.SourceIP = remoteAddr
|
|
// Regex to parse the received header with hostname/ip address.
|
|
rxAddr := regexp.MustCompile("from ([A-Za-z0-9-.]+) \\(.* \\[([0-9a-fA-F.:]+)\\]\\)")
|
|
// Loop through all entires of received headers.
|
|
for _, header := range email.Header["Received"] {
|
|
// Parse the header.
|
|
matches := rxAddr.FindStringSubmatch(header)
|
|
if len(matches) == 3 {
|
|
// If we got an source IP from the header, save it.
|
|
messageEntry.SourceIP = matches[1] + " (" + matches[2] + ")"
|
|
}
|
|
}
|
|
|
|
messageEntry.Size = len(b)
|
|
messageEntry.Received = time.Now()
|
|
messageEntry.Status = "unknown" // We start as unknown and the status is updated by syslog.
|
|
|
|
// Save the message entry.
|
|
app.db.Create(&messageEntry)
|
|
log.Printf("SMTP: Received message from %s (%d bytes)", messageEntry.From, messageEntry.Size)
|
|
|
|
// Notify websocket subscribers of new message.
|
|
app.httpServer.wsInterface.sendMessage("receivedNewMessage", messageEntry)
|
|
|
|
// Update message count.
|
|
app.messageCount++
|
|
app.httpServer.wsInterface.sendMessage("updateMessageCount", app.messageCount)
|
|
return nil
|
|
}
|
|
|
|
// Finds and outputs a reader for the message body based on UUID.
|
|
func MailGetMessageData(UUID string) (r io.Reader, err error) {
|
|
// If we are configured to use the database for storage, then we should check if the UUID is in the database.
|
|
// Otherwise, we check the path set to see if a file exists with the UUID.
|
|
if app.config.MailPath == "db" {
|
|
// Search database for message body by UUID.
|
|
var message Messages
|
|
app.db.Where("uuid = ?", UUID).First(&message)
|
|
// If not found, we provide an error.
|
|
if message.UUID == "" {
|
|
err = fmt.Errorf(APINoMessage)
|
|
return
|
|
}
|
|
// Create a reader for the message data.
|
|
r = bytes.NewReader(message.Message)
|
|
} else {
|
|
// Verify that the UUID exists in the file system.
|
|
if _, err = os.Stat(path.Join(app.config.MailPath, UUID)); err != nil {
|
|
return
|
|
}
|
|
// If the file exists, we open it to return.
|
|
r, err = os.Open(path.Join(app.config.MailPath, UUID))
|
|
}
|
|
// Return reader.
|
|
return
|
|
}
|
|
|
|
// To try and make the syslog code light weight, this function was created
|
|
// to update the status of messages to what was parsed in the syslog.
|
|
// This function will read an update queue map of syslog ids with updated statuses.
|
|
func RunSysLogMailUpdateQueue() {
|
|
ticker := time.NewTicker(5 * time.Second)
|
|
for _ = range ticker.C { // Every 5 seconds.
|
|
// We want to keep track as to rather statuses were updated to notify subscribers.
|
|
updated := false
|
|
|
|
// Copy the update queue so that we can empty the main update queue.
|
|
updateQueue := make(map[string]bool)
|
|
for key, val := range app.sysLogMailUpdateQueue {
|
|
updateQueue[key] = val
|
|
}
|
|
// We empty the main update queue as the syslog may have status changes during this run.
|
|
// We want to ensure that those changes do not get lost.
|
|
app.sysLogMailUpdateQueue = make(map[string]bool)
|
|
|
|
// Loop the update queue.
|
|
for sidHostname, _ := range updateQueue {
|
|
// Update queue should contain syslog id and hostname separated by a colon.
|
|
// We pair the syslog id with the hostname just incase different hosts use the same syslog id.
|
|
s := strings.Split(sidHostname, ":")
|
|
// If there is more or less than 2 parts, this is invalid.
|
|
if len(s) != 2 {
|
|
continue
|
|
}
|
|
sid := s[0]
|
|
hostname := s[1]
|
|
|
|
// Pull the syslog id information from the database.
|
|
var match SysLogIDInfo
|
|
app.db.Where("s_id = ? AND hostname = ?", sid, hostname).First(&match)
|
|
// If nothing was returned, or this one is set to be ignored... We will stop here.
|
|
// When a syslog id is ignored, it is likely due to it being either the main message received before sending out,
|
|
// or it is the message forwarded to Mail Archive which is not the main mail delivery status.
|
|
if match.SID == "" || match.Ignore {
|
|
continue
|
|
}
|
|
|
|
// Pull the message log entry matching the message id associated with the syslog id.
|
|
var messageEntry MessageLog
|
|
app.db.Where("message_id = ?", match.MessageID).First(&messageEntry)
|
|
// If we found the message log entry, we can update the status to match our syslog id delivery status.
|
|
if messageEntry.UUID != "" {
|
|
messageEntry.Status = match.Status
|
|
app.db.Save(&messageEntry)
|
|
// As we updated a message log entry, we want to inform the subscribers that an update occurred.
|
|
updated = true
|
|
}
|
|
}
|
|
// If we updated the status of a message log entry, we need to inform subscribers connected to websocket.
|
|
if updated {
|
|
app.httpServer.wsInterface.sendMessage("messageStatusesUpdated", true)
|
|
}
|
|
}
|
|
}
|
|
|
|
// This function will run a database cleanup of old messages every 30 minutes.
|
|
func RunDatabaseCleanup() {
|
|
ticker := time.NewTicker(30 * time.Minute)
|
|
for _ = range ticker.C {
|
|
// Get the oldest date we will allow at this point in time based on the configured maximum age.
|
|
maxAge := time.Now().Add(app.config.MaxAge * time.Second * -1)
|
|
|
|
// We want to just pull UUID and message id of the old messages to be cleaned up.
|
|
type MessageIDs struct {
|
|
UUID string
|
|
MessageID string
|
|
}
|
|
var messageIDs []MessageIDs
|
|
app.db.Table("message_logs").Select("uuid,message_id").Where("received <= ?", maxAge).Scan(&messageIDs)
|
|
|
|
// Loop through all found old messages to clean up the database.
|
|
for _, message := range messageIDs {
|
|
// Find syslog id information entries matching this message.
|
|
var matches []SysLogIDInfo
|
|
app.db.Where("message_id = ?", message.MessageID).Find(&matches)
|
|
// With each found syslog id, we need to delete the syslog messages and the syslog id information.
|
|
for _, match := range matches {
|
|
app.db.Where("s_id = ? AND hostname = ?", match.SID, match.Hostname).Delete(SysLogMessage{})
|
|
app.db.Delete(&match)
|
|
}
|
|
|
|
// Delete the message log entry for this message.
|
|
app.db.Where("uuid = ?", message.UUID).Delete(MessageLog{})
|
|
// Delete message data matching the UUID for the message.
|
|
app.db.Where("uuid = ?", message.UUID).Delete(Messages{})
|
|
// If the configured mail storage path is not the database, remove it from the file system.
|
|
if app.config.MailPath != "db" {
|
|
if _, err := os.Stat(path.Join(app.config.MailPath, message.UUID)); err == nil {
|
|
os.Remove(path.Join(app.config.MailPath, message.UUID))
|
|
}
|
|
}
|
|
// Update message count.
|
|
app.messageCount--
|
|
}
|
|
|
|
// Send updated message count.
|
|
app.httpServer.wsInterface.sendMessage("updateMessageCount", app.messageCount)
|
|
}
|
|
}
|