github.com/gophish/gophish@v0.12.2-0.20230915144530-8e7929441393/models/maillog.go (about)

     1  package models
     2  
     3  import (
     4  	"crypto/rand"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"math"
     9  	"math/big"
    10  	"net/mail"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/gophish/gomail"
    17  	"github.com/gophish/gophish/config"
    18  	log "github.com/gophish/gophish/logger"
    19  	"github.com/gophish/gophish/mailer"
    20  )
    21  
    22  // MaxSendAttempts set to 8 since we exponentially backoff after each failed send
    23  // attempt. This will give us a maximum send delay of 256 minutes, or about 4.2 hours.
    24  var MaxSendAttempts = 8
    25  
    26  // ErrMaxSendAttempts is thrown when the maximum number of sending attempts for a given
    27  // MailLog is exceeded.
    28  var ErrMaxSendAttempts = errors.New("max send attempts exceeded")
    29  
    30  // Attachments with these file extensions have inline disposition
    31  var embeddedFileExtensions = []string{".jpg", ".jpeg", ".png", ".gif"}
    32  
    33  // MailLog is a struct that holds information about an email that is to be
    34  // sent out.
    35  type MailLog struct {
    36  	Id          int64     `json:"-"`
    37  	UserId      int64     `json:"-"`
    38  	CampaignId  int64     `json:"campaign_id"`
    39  	RId         string    `json:"id"`
    40  	SendDate    time.Time `json:"send_date"`
    41  	SendAttempt int       `json:"send_attempt"`
    42  	Processing  bool      `json:"-"`
    43  
    44  	cachedCampaign *Campaign
    45  }
    46  
    47  // GenerateMailLog creates a new maillog for the given campaign and
    48  // result. It sets the initial send date to match the campaign's launch date.
    49  func GenerateMailLog(c *Campaign, r *Result, sendDate time.Time) error {
    50  	m := &MailLog{
    51  		UserId:     c.UserId,
    52  		CampaignId: c.Id,
    53  		RId:        r.RId,
    54  		SendDate:   sendDate,
    55  	}
    56  	return db.Save(m).Error
    57  }
    58  
    59  // Backoff sets the MailLog SendDate to be the next entry in an exponential
    60  // backoff. ErrMaxRetriesExceeded is thrown if this maillog has been retried
    61  // too many times. Backoff also unlocks the maillog so that it can be processed
    62  // again in the future.
    63  func (m *MailLog) Backoff(reason error) error {
    64  	r, err := GetResult(m.RId)
    65  	if err != nil {
    66  		return err
    67  	}
    68  	if m.SendAttempt == MaxSendAttempts {
    69  		r.HandleEmailError(ErrMaxSendAttempts)
    70  		return ErrMaxSendAttempts
    71  	}
    72  	// Add an error, since we had to backoff because of a
    73  	// temporary error of some sort during the SMTP transaction
    74  	m.SendAttempt++
    75  	backoffDuration := math.Pow(2, float64(m.SendAttempt))
    76  	m.SendDate = m.SendDate.Add(time.Minute * time.Duration(backoffDuration))
    77  	err = db.Save(m).Error
    78  	if err != nil {
    79  		return err
    80  	}
    81  	err = r.HandleEmailBackoff(reason, m.SendDate)
    82  	if err != nil {
    83  		return err
    84  	}
    85  	err = m.Unlock()
    86  	return err
    87  }
    88  
    89  // Unlock removes the processing flag so the maillog can be processed again
    90  func (m *MailLog) Unlock() error {
    91  	m.Processing = false
    92  	return db.Save(&m).Error
    93  }
    94  
    95  // Lock sets the processing flag so that other processes cannot modify the maillog
    96  func (m *MailLog) Lock() error {
    97  	m.Processing = true
    98  	return db.Save(&m).Error
    99  }
   100  
   101  // Error sets the error status on the models.Result that the
   102  // maillog refers to. Since MailLog errors are permanent,
   103  // this action also deletes the maillog.
   104  func (m *MailLog) Error(e error) error {
   105  	r, err := GetResult(m.RId)
   106  	if err != nil {
   107  		log.Warn(err)
   108  		return err
   109  	}
   110  	err = r.HandleEmailError(e)
   111  	if err != nil {
   112  		log.Warn(err)
   113  		return err
   114  	}
   115  	err = db.Delete(m).Error
   116  	return err
   117  }
   118  
   119  // Success deletes the maillog from the database and updates the underlying
   120  // campaign result.
   121  func (m *MailLog) Success() error {
   122  	r, err := GetResult(m.RId)
   123  	if err != nil {
   124  		return err
   125  	}
   126  	err = r.HandleEmailSent()
   127  	if err != nil {
   128  		return err
   129  	}
   130  	err = db.Delete(m).Error
   131  	return err
   132  }
   133  
   134  // GetDialer returns a dialer based on the maillog campaign's SMTP configuration
   135  func (m *MailLog) GetDialer() (mailer.Dialer, error) {
   136  	c := m.cachedCampaign
   137  	if c == nil {
   138  		campaign, err := GetCampaignMailContext(m.CampaignId, m.UserId)
   139  		if err != nil {
   140  			return nil, err
   141  		}
   142  		c = &campaign
   143  	}
   144  	return c.SMTP.GetDialer()
   145  }
   146  
   147  // CacheCampaign allows bulk-mail workers to cache the otherwise expensive
   148  // campaign lookup operation by providing a pointer to the campaign here.
   149  func (m *MailLog) CacheCampaign(campaign *Campaign) error {
   150  	if campaign.Id != m.CampaignId {
   151  		return fmt.Errorf("incorrect campaign provided for caching. expected %d got %d", m.CampaignId, campaign.Id)
   152  	}
   153  	m.cachedCampaign = campaign
   154  	return nil
   155  }
   156  
   157  func (m *MailLog) GetSmtpFrom() (string, error) {
   158  	c, err := GetCampaign(m.CampaignId, m.UserId)
   159  	if err != nil {
   160  		return "", err
   161  	}
   162  
   163  	f, err := mail.ParseAddress(c.SMTP.FromAddress)
   164  	return f.Address, err
   165  }
   166  
   167  // Generate fills in the details of a gomail.Message instance with
   168  // the correct headers and body from the campaign and recipient listed in
   169  // the maillog. We accept the gomail.Message as an argument so that the caller
   170  // can choose to re-use the message across recipients.
   171  func (m *MailLog) Generate(msg *gomail.Message) error {
   172  	r, err := GetResult(m.RId)
   173  	if err != nil {
   174  		return err
   175  	}
   176  	c := m.cachedCampaign
   177  	if c == nil {
   178  		campaign, err := GetCampaignMailContext(m.CampaignId, m.UserId)
   179  		if err != nil {
   180  			return err
   181  		}
   182  		c = &campaign
   183  	}
   184  
   185  	f, err := mail.ParseAddress(c.Template.EnvelopeSender)
   186  	if err != nil {
   187  		f, err = mail.ParseAddress(c.SMTP.FromAddress)
   188  		if err != nil {
   189  			return err
   190  		}
   191  	}
   192  	msg.SetAddressHeader("From", f.Address, f.Name)
   193  
   194  	ptx, err := NewPhishingTemplateContext(c, r.BaseRecipient, r.RId)
   195  	if err != nil {
   196  		return err
   197  	}
   198  
   199  	// Add the transparency headers
   200  	msg.SetHeader("X-Mailer", config.ServerName)
   201  	if conf.ContactAddress != "" {
   202  		msg.SetHeader("X-Gophish-Contact", conf.ContactAddress)
   203  	}
   204  
   205  	// Add Message-Id header as described in RFC 2822.
   206  	messageID, err := m.generateMessageID()
   207  	if err != nil {
   208  		return err
   209  	}
   210  	msg.SetHeader("Message-Id", messageID)
   211  
   212  	// Parse the customHeader templates
   213  	for _, header := range c.SMTP.Headers {
   214  		key, err := ExecuteTemplate(header.Key, ptx)
   215  		if err != nil {
   216  			log.Warn(err)
   217  		}
   218  
   219  		value, err := ExecuteTemplate(header.Value, ptx)
   220  		if err != nil {
   221  			log.Warn(err)
   222  		}
   223  
   224  		// Add our header immediately
   225  		msg.SetHeader(key, value)
   226  	}
   227  
   228  	// Parse remaining templates
   229  	subject, err := ExecuteTemplate(c.Template.Subject, ptx)
   230  
   231  	if err != nil {
   232  		log.Warn(err)
   233  	}
   234  	// don't set Subject header if the subject is empty
   235  	if subject != "" {
   236  		msg.SetHeader("Subject", subject)
   237  	}
   238  
   239  	msg.SetHeader("To", r.FormatAddress())
   240  	if c.Template.Text != "" {
   241  		text, err := ExecuteTemplate(c.Template.Text, ptx)
   242  		if err != nil {
   243  			log.Warn(err)
   244  		}
   245  		msg.SetBody("text/plain", text)
   246  	}
   247  	if c.Template.HTML != "" {
   248  		html, err := ExecuteTemplate(c.Template.HTML, ptx)
   249  		if err != nil {
   250  			log.Warn(err)
   251  		}
   252  		if c.Template.Text == "" {
   253  			msg.SetBody("text/html", html)
   254  		} else {
   255  			msg.AddAlternative("text/html", html)
   256  		}
   257  	}
   258  	// Attach the files
   259  	for _, a := range c.Template.Attachments {
   260  		addAttachment(msg, a, ptx)
   261  	}
   262  
   263  	return nil
   264  }
   265  
   266  // GetQueuedMailLogs returns the mail logs that are queued up for the given minute.
   267  func GetQueuedMailLogs(t time.Time) ([]*MailLog, error) {
   268  	ms := []*MailLog{}
   269  	err := db.Where("send_date <= ? AND processing = ?", t, false).
   270  		Find(&ms).Error
   271  	if err != nil {
   272  		log.Warn(err)
   273  	}
   274  	return ms, err
   275  }
   276  
   277  // GetMailLogsByCampaign returns all of the mail logs for a given campaign.
   278  func GetMailLogsByCampaign(cid int64) ([]*MailLog, error) {
   279  	ms := []*MailLog{}
   280  	err := db.Where("campaign_id = ?", cid).Find(&ms).Error
   281  	return ms, err
   282  }
   283  
   284  // LockMailLogs locks or unlocks a slice of maillogs for processing.
   285  func LockMailLogs(ms []*MailLog, lock bool) error {
   286  	tx := db.Begin()
   287  	for i := range ms {
   288  		ms[i].Processing = lock
   289  		err := tx.Save(ms[i]).Error
   290  		if err != nil {
   291  			tx.Rollback()
   292  			return err
   293  		}
   294  	}
   295  	tx.Commit()
   296  	return nil
   297  }
   298  
   299  // UnlockAllMailLogs removes the processing lock for all maillogs
   300  // in the database. This is intended to be called when Gophish is started
   301  // so that any previously locked maillogs can resume processing.
   302  func UnlockAllMailLogs() error {
   303  	return db.Model(&MailLog{}).Update("processing", false).Error
   304  }
   305  
   306  var maxBigInt = big.NewInt(math.MaxInt64)
   307  
   308  // generateMessageID generates and returns a string suitable for an RFC 2822
   309  // compliant Message-ID, e.g.:
   310  // <1444789264909237300.3464.1819418242800517193@DESKTOP01>
   311  //
   312  // The following parameters are used to generate a Message-ID:
   313  // - The nanoseconds since Epoch
   314  // - The calling PID
   315  // - A cryptographically random int64
   316  // - The sending hostname
   317  func (m *MailLog) generateMessageID() (string, error) {
   318  	t := time.Now().UnixNano()
   319  	pid := os.Getpid()
   320  	rint, err := rand.Int(rand.Reader, maxBigInt)
   321  	if err != nil {
   322  		return "", err
   323  	}
   324  	h, err := os.Hostname()
   325  	// If we can't get the hostname, we'll use localhost
   326  	if err != nil {
   327  		h = "localhost.localdomain"
   328  	}
   329  	msgid := fmt.Sprintf("<%d.%d.%d@%s>", t, pid, rint, h)
   330  	return msgid, nil
   331  }
   332  
   333  // Check if an attachment should have inline disposition based on
   334  // its file extension.
   335  func shouldEmbedAttachment(name string) bool {
   336  	ext := filepath.Ext(name)
   337  	for _, v := range embeddedFileExtensions {
   338  		if strings.EqualFold(ext, v) {
   339  			return true
   340  		}
   341  	}
   342  	return false
   343  }
   344  
   345  // Add an attachment to a gomail message, with the Content-Disposition
   346  // header set to inline or attachment depending on its file extension.
   347  func addAttachment(msg *gomail.Message, a Attachment, ptx PhishingTemplateContext) {
   348  	copyFunc := gomail.SetCopyFunc(func(c Attachment) func(w io.Writer) error {
   349  		return func(w io.Writer) error {
   350  			reader, err := a.ApplyTemplate(ptx)
   351  			if err != nil {
   352  				return err
   353  			}
   354  			_, err = io.Copy(w, reader)
   355  			return err
   356  		}
   357  	}(a))
   358  	if shouldEmbedAttachment(a.Name) {
   359  		msg.Embed(a.Name, copyFunc)
   360  	} else {
   361  		msg.Attach(a.Name, copyFunc)
   362  	}
   363  }