2017-12-09 21:42:07 +00:00
package models
import (
2019-04-23 22:31:30 +00:00
"crypto/rand"
2017-12-09 21:42:07 +00:00
"errors"
"fmt"
"io"
"math"
2019-04-23 22:31:30 +00:00
"math/big"
2017-12-09 21:42:07 +00:00
"net/mail"
2019-04-23 22:31:30 +00:00
"os"
2017-12-09 21:42:07 +00:00
"time"
"github.com/gophish/gomail"
2018-06-19 02:37:59 +00:00
"github.com/gophish/gophish/config"
2018-05-04 00:07:41 +00:00
log "github.com/gophish/gophish/logger"
2017-12-09 21:42:07 +00:00
"github.com/gophish/gophish/mailer"
)
// MaxSendAttempts set to 8 since we exponentially backoff after each failed send
// attempt. This will give us a maximum send delay of 256 minutes, or about 4.2 hours.
var MaxSendAttempts = 8
2018-12-16 03:38:51 +00:00
// ErrMaxSendAttempts is thrown when the maximum number of sending attempts for a given
2017-12-09 21:42:07 +00:00
// MailLog is exceeded.
var ErrMaxSendAttempts = errors . New ( "max send attempts exceeded" )
2020-08-15 16:03:52 +00:00
// processAttachment is used to to keep track of which email attachments have templated values.
// This allows us to skip re-templating attach
var processAttachment = map [ [ 20 ] byte ] bool { } // Considered using attachmentLookup[campaignid][filehash] but given the low number of files current approach should be fine
2017-12-09 21:42:07 +00:00
// MailLog is a struct that holds information about an email that is to be
// sent out.
type MailLog struct {
Id int64 ` json:"-" `
UserId int64 ` json:"-" `
CampaignId int64 ` json:"campaign_id" `
RId string ` json:"id" `
SendDate time . Time ` json:"send_date" `
SendAttempt int ` json:"send_attempt" `
Processing bool ` json:"-" `
2020-03-01 02:19:54 +00:00
cachedCampaign * Campaign
2017-12-09 21:42:07 +00:00
}
// GenerateMailLog creates a new maillog for the given campaign and
// result. It sets the initial send date to match the campaign's launch date.
2018-09-02 16:17:52 +00:00
func GenerateMailLog ( c * Campaign , r * Result , sendDate time . Time ) error {
2017-12-09 21:42:07 +00:00
m := & MailLog {
UserId : c . UserId ,
CampaignId : c . Id ,
RId : r . RId ,
2018-09-02 16:17:52 +00:00
SendDate : sendDate ,
2017-12-09 21:42:07 +00:00
}
2018-12-15 21:42:32 +00:00
return db . Save ( m ) . Error
2017-12-09 21:42:07 +00:00
}
// Backoff sets the MailLog SendDate to be the next entry in an exponential
// backoff. ErrMaxRetriesExceeded is thrown if this maillog has been retried
// too many times. Backoff also unlocks the maillog so that it can be processed
// again in the future.
func ( m * MailLog ) Backoff ( reason error ) error {
r , err := GetResult ( m . RId )
if err != nil {
return err
}
2018-05-27 02:26:34 +00:00
if m . SendAttempt == MaxSendAttempts {
r . HandleEmailError ( ErrMaxSendAttempts )
return ErrMaxSendAttempts
}
2017-12-09 21:42:07 +00:00
// Add an error, since we had to backoff because of a
// temporary error of some sort during the SMTP transaction
m . SendAttempt ++
backoffDuration := math . Pow ( 2 , float64 ( m . SendAttempt ) )
m . SendDate = m . SendDate . Add ( time . Minute * time . Duration ( backoffDuration ) )
err = db . Save ( m ) . Error
if err != nil {
return err
}
2018-05-27 02:26:34 +00:00
err = r . HandleEmailBackoff ( reason , m . SendDate )
2017-12-09 21:42:07 +00:00
if err != nil {
return err
}
err = m . Unlock ( )
return err
}
// Unlock removes the processing flag so the maillog can be processed again
func ( m * MailLog ) Unlock ( ) error {
m . Processing = false
return db . Save ( & m ) . Error
}
// Lock sets the processing flag so that other processes cannot modify the maillog
func ( m * MailLog ) Lock ( ) error {
m . Processing = true
return db . Save ( & m ) . Error
}
// Error sets the error status on the models.Result that the
// maillog refers to. Since MailLog errors are permanent,
// this action also deletes the maillog.
func ( m * MailLog ) Error ( e error ) error {
r , err := GetResult ( m . RId )
if err != nil {
2018-05-04 00:07:41 +00:00
log . Warn ( err )
2017-12-09 21:42:07 +00:00
return err
}
2018-05-27 02:26:34 +00:00
err = r . HandleEmailError ( e )
2017-12-09 21:42:07 +00:00
if err != nil {
2018-05-04 00:07:41 +00:00
log . Warn ( err )
2017-12-09 21:42:07 +00:00
return err
}
err = db . Delete ( m ) . Error
return err
}
// Success deletes the maillog from the database and updates the underlying
// campaign result.
func ( m * MailLog ) Success ( ) error {
r , err := GetResult ( m . RId )
if err != nil {
return err
}
2018-05-27 02:26:34 +00:00
err = r . HandleEmailSent ( )
2017-12-09 21:42:07 +00:00
if err != nil {
return err
}
err = db . Delete ( m ) . Error
2020-05-26 02:46:36 +00:00
return err
2017-12-09 21:42:07 +00:00
}
// GetDialer returns a dialer based on the maillog campaign's SMTP configuration
func ( m * MailLog ) GetDialer ( ) ( mailer . Dialer , error ) {
2020-03-01 02:19:54 +00:00
c := m . cachedCampaign
if c == nil {
campaign , err := GetCampaignMailContext ( m . CampaignId , m . UserId )
if err != nil {
return nil , err
}
c = & campaign
2017-12-09 21:42:07 +00:00
}
return c . SMTP . GetDialer ( )
}
2020-03-01 02:19:54 +00:00
// CacheCampaign allows bulk-mail workers to cache the otherwise expensive
// campaign lookup operation by providing a pointer to the campaign here.
func ( m * MailLog ) CacheCampaign ( campaign * Campaign ) error {
if campaign . Id != m . CampaignId {
return fmt . Errorf ( "incorrect campaign provided for caching. expected %d got %d" , m . CampaignId , campaign . Id )
}
m . cachedCampaign = campaign
return nil
}
2017-12-09 21:42:07 +00:00
// Generate fills in the details of a gomail.Message instance with
// the correct headers and body from the campaign and recipient listed in
// the maillog. We accept the gomail.Message as an argument so that the caller
// can choose to re-use the message across recipients.
func ( m * MailLog ) Generate ( msg * gomail . Message ) error {
r , err := GetResult ( m . RId )
if err != nil {
return err
}
2020-03-01 02:19:54 +00:00
c := m . cachedCampaign
if c == nil {
campaign , err := GetCampaignMailContext ( m . CampaignId , m . UserId )
if err != nil {
return err
}
c = & campaign
2017-12-09 21:42:07 +00:00
}
2018-06-09 02:20:52 +00:00
2017-12-09 21:42:07 +00:00
f , err := mail . ParseAddress ( c . SMTP . FromAddress )
if err != nil {
return err
}
msg . SetAddressHeader ( "From" , f . Address , f . Name )
2018-06-09 02:20:52 +00:00
2020-03-01 02:19:54 +00:00
ptx , err := NewPhishingTemplateContext ( c , r . BaseRecipient , r . RId )
2018-01-13 23:49:42 +00:00
if err != nil {
return err
}
2018-02-23 04:10:50 +00:00
2018-06-19 02:37:59 +00:00
// Add the transparency headers
msg . SetHeader ( "X-Mailer" , config . ServerName )
2018-12-15 21:42:32 +00:00
if conf . ContactAddress != "" {
msg . SetHeader ( "X-Gophish-Contact" , conf . ContactAddress )
2018-06-19 02:37:59 +00:00
}
2019-04-23 22:31:30 +00:00
// Add Message-Id header as described in RFC 2822.
messageID , err := m . generateMessageID ( )
if err != nil {
return err
}
msg . SetHeader ( "Message-Id" , messageID )
2017-12-09 21:42:07 +00:00
// Parse the customHeader templates
for _ , header := range c . SMTP . Headers {
2018-06-09 02:20:52 +00:00
key , err := ExecuteTemplate ( header . Key , ptx )
2017-12-09 21:42:07 +00:00
if err != nil {
2018-05-04 00:07:41 +00:00
log . Warn ( err )
2017-12-09 21:42:07 +00:00
}
2018-06-09 02:20:52 +00:00
value , err := ExecuteTemplate ( header . Value , ptx )
2017-12-09 21:42:07 +00:00
if err != nil {
2018-05-04 00:07:41 +00:00
log . Warn ( err )
2017-12-09 21:42:07 +00:00
}
// Add our header immediately
msg . SetHeader ( key , value )
}
// Parse remaining templates
2018-06-09 02:20:52 +00:00
subject , err := ExecuteTemplate ( c . Template . Subject , ptx )
2020-08-15 16:03:52 +00:00
2017-12-09 21:42:07 +00:00
if err != nil {
2018-05-04 00:07:41 +00:00
log . Warn ( err )
2017-12-09 21:42:07 +00:00
}
2018-02-10 19:46:08 +00:00
// don't set Subject header if the subject is empty
if len ( subject ) != 0 {
msg . SetHeader ( "Subject" , subject )
}
2017-12-09 21:42:07 +00:00
msg . SetHeader ( "To" , r . FormatAddress ( ) )
if c . Template . Text != "" {
2018-06-09 02:20:52 +00:00
text , err := ExecuteTemplate ( c . Template . Text , ptx )
2017-12-09 21:42:07 +00:00
if err != nil {
2018-05-04 00:07:41 +00:00
log . Warn ( err )
2017-12-09 21:42:07 +00:00
}
msg . SetBody ( "text/plain" , text )
}
if c . Template . HTML != "" {
2018-06-09 02:20:52 +00:00
html , err := ExecuteTemplate ( c . Template . HTML , ptx )
2017-12-09 21:42:07 +00:00
if err != nil {
2018-05-04 00:07:41 +00:00
log . Warn ( err )
2017-12-09 21:42:07 +00:00
}
if c . Template . Text == "" {
msg . SetBody ( "text/html" , html )
} else {
msg . AddAlternative ( "text/html" , html )
}
}
// Attach the files
for _ , a := range c . Template . Attachments {
msg . Attach ( func ( a Attachment ) ( string , gomail . FileSetting , gomail . FileSetting ) {
h := map [ string ] [ ] string { "Content-ID" : { fmt . Sprintf ( "<%s>" , a . Name ) } }
return a . Name , gomail . SetCopyFunc ( func ( w io . Writer ) error {
2020-08-16 13:38:37 +00:00
content , err := a . ApplyTemplate ( ptx )
2020-08-15 16:03:52 +00:00
if err != nil {
return err
}
2020-08-16 13:38:37 +00:00
_ , err = io . Copy ( w , content )
2017-12-09 21:42:07 +00:00
return err
} ) , gomail . SetHeader ( h )
} ( a ) )
}
return nil
}
// GetQueuedMailLogs returns the mail logs that are queued up for the given minute.
func GetQueuedMailLogs ( t time . Time ) ( [ ] * MailLog , error ) {
ms := [ ] * MailLog { }
err := db . Where ( "send_date <= ? AND processing = ?" , t , false ) .
Find ( & ms ) . Error
if err != nil {
2018-05-04 00:07:41 +00:00
log . Warn ( err )
2017-12-09 21:42:07 +00:00
}
return ms , err
}
// GetMailLogsByCampaign returns all of the mail logs for a given campaign.
func GetMailLogsByCampaign ( cid int64 ) ( [ ] * MailLog , error ) {
ms := [ ] * MailLog { }
err := db . Where ( "campaign_id = ?" , cid ) . Find ( & ms ) . Error
return ms , err
}
// LockMailLogs locks or unlocks a slice of maillogs for processing.
func LockMailLogs ( ms [ ] * MailLog , lock bool ) error {
tx := db . Begin ( )
for i := range ms {
ms [ i ] . Processing = lock
2018-01-13 23:49:42 +00:00
err := tx . Save ( ms [ i ] ) . Error
2017-12-09 21:42:07 +00:00
if err != nil {
tx . Rollback ( )
return err
}
}
tx . Commit ( )
return nil
}
// UnlockAllMailLogs removes the processing lock for all maillogs
// in the database. This is intended to be called when Gophish is started
// so that any previously locked maillogs can resume processing.
func UnlockAllMailLogs ( ) error {
2018-12-15 21:42:32 +00:00
return db . Model ( & MailLog { } ) . Update ( "processing" , false ) . Error
2017-12-09 21:42:07 +00:00
}
2019-04-23 22:31:30 +00:00
var maxBigInt = big . NewInt ( math . MaxInt64 )
// generateMessageID generates and returns a string suitable for an RFC 2822
// compliant Message-ID, e.g.:
// <1444789264909237300.3464.1819418242800517193@DESKTOP01>
//
// The following parameters are used to generate a Message-ID:
// - The nanoseconds since Epoch
// - The calling PID
// - A cryptographically random int64
// - The sending hostname
func ( m * MailLog ) generateMessageID ( ) ( string , error ) {
t := time . Now ( ) . UnixNano ( )
pid := os . Getpid ( )
rint , err := rand . Int ( rand . Reader , maxBigInt )
if err != nil {
return "" , err
}
h , err := os . Hostname ( )
// If we can't get the hostname, we'll use localhost
if err != nil {
h = "localhost.localdomain"
}
msgid := fmt . Sprintf ( "<%d.%d.%d@%s>" , t , pid , rint , h )
return msgid , nil
}