package smtp import ( "dove/config" mailModel "dove/models/mail" mailRepo "dove/repositories/mail" "dove/utils/email" "dove/utils/errors" "dove/utils/logger" "dove/utils/storage" "fmt" "io" "net" "strings" "time" gosmtp "github.com/emersion/go-smtp" ) type Session struct { fromAddress string toAddresses []string } func (self *Session) AuthPlain(username string, password string) error { if !config.SMTP.AuthRequired { return nil } if username != config.SMTP.Username || password != config.SMTP.Password { logger.Warnf(LogPrefix, AuthFailed, username) return errors.Error(InvalidCredentials) } return nil } func (self *Session) Mail(senderAddress string, _ *gosmtp.MailOptions) error { logger.Debugf(LogPrefix, MailFrom, senderAddress) self.fromAddress = senderAddress return nil } func (self *Session) Rcpt(recipientAddress string, _ *gosmtp.RcptOptions) error { logger.Debugf(LogPrefix, Recipient, recipientAddress) self.toAddresses = append(self.toAddresses, recipientAddress) return nil } func (self *Session) Data(messageReader io.Reader) error { rawMessage, readError := io.ReadAll(messageReader) if readError != nil { return readError } logger.Infof(LogPrefix, MessageReceived, len(rawMessage), self.fromAddress, self.toAddresses) parsedEmail, parseError := email.Parse(rawMessage) if parseError != nil { logger.Errorf(LogPrefix, MessageParseFailed, parseError) return parseError } deliveredToLocal := false for _, recipientAddress := range self.toAddresses { recipientDomain := extractDomain(recipientAddress) if recipientDomain == "" || !domainHasMXRecords(recipientDomain) { logger.Warnf(LogPrefix, UnknownRecipientDomain, recipientDomain) continue } recipientMailbox := mailRepo.FindMailboxByAddress(recipientAddress) if recipientMailbox == nil { aliasMailbox := mailRepo.FindMailboxByAlias(recipientAddress) if aliasMailbox != nil { recipientMailbox = aliasMailbox } } if recipientMailbox != nil { deliverError := deliverToLocalMailbox(recipientMailbox, parsedEmail, rawMessage) if deliverError != nil { logger.Errorf(LogPrefix, DeliveryFailed, recipientAddress, deliverError) continue } logger.Infof(LogPrefix, DeliverySuccess, recipientAddress) deliveredToLocal = true } else { logger.Debugf(LogPrefix, RecipientNotLocal, recipientAddress) } } if !deliveredToLocal { logger.Warnf(LogPrefix, NoLocalRecipients, self.fromAddress) } return nil } func (self *Session) Reset() { self.fromAddress = "" self.toAddresses = nil } func (self *Session) Logout() error { return nil } func deliverToLocalMailbox(mailbox *mailModel.Mailbox, parsedEmail *email.ParsedEmail, rawMessage []byte) error { inboxFolder := mailRepo.FindFolderBySlug(mailbox.ID, "inbox") if inboxFolder == nil { return errors.Error("Inbox folder not found for mailbox %s.", mailbox.Address) } filename := fmt.Sprintf("%d", time.Now().UnixNano()) toAddressesJoined := strings.Join(parsedEmail.ToAddresses, ", ") ccAddressesJoined := strings.Join(parsedEmail.CcAddresses, ", ") bccAddressesJoined := strings.Join(parsedEmail.BccAddresses, ", ") attachmentCount := 0 inlineCount := 0 for _, attachment := range parsedEmail.Attachments { if attachment.IsInline { inlineCount++ } else { attachmentCount++ } } emailRecord := &mailModel.Email{ MailboxID: mailbox.ID, FolderID: inboxFolder.ID, MessageID: parsedEmail.MessageID, Filename: filename, FromAddress: parsedEmail.FromAddress, FromName: parsedEmail.FromName, ToAddresses: toAddressesJoined, CcAddresses: ccAddressesJoined, BccAddresses: bccAddressesJoined, ReplyToAddress: parsedEmail.ReplyToAddress, ReturnPath: parsedEmail.ReturnPath, Subject: parsedEmail.Subject, Snippet: parsedEmail.Snippet, Size: parsedEmail.Size, IsRead: false, AttachmentCount: attachmentCount, InlineCount: inlineCount, } if createError := mailRepo.CreateEmail(emailRecord); createError != nil { return createError } for _, parsedAttachment := range parsedEmail.Attachments { attachmentRecord := &mailModel.Attachment{ EmailID: emailRecord.ID, Filename: parsedAttachment.Filename, ContentType: parsedAttachment.ContentType, ContentID: parsedAttachment.ContentID, Size: parsedAttachment.Size, IsInline: parsedAttachment.IsInline, } mailRepo.CreateAttachment(attachmentRecord) } if writeError := storage.WriteMailFile(mailbox.Address, "inbox", filename, rawMessage); writeError != nil { logger.Warnf(LogPrefix, MessageStoreFailed, writeError) } return nil } func extractDomain(emailAddress string) string { atIndex := strings.LastIndex(emailAddress, "@") if atIndex < 0 || atIndex >= len(emailAddress)-1 { return "" } return emailAddress[atIndex+1:] } func domainHasMXRecords(domainName string) bool { mxRecords, lookupError := net.LookupMX(domainName) if lookupError != nil { logger.Debugf(LogPrefix, MXLookupFailed, domainName, lookupError) return false } return len(mxRecords) > 0 }