github.com/merlinepedra/gopphish-attack@v0.9.0/models/maillog.go (about)

     1  package models
     2  
     3  import (
     4  	"crypto/rand"
     5  	"encoding/base64"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"math"
    10  	"math/big"
    11  	"net/mail"
    12  	"os"
    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  // MailLog is a struct that holds information about an email that is to be
    31  // sent out.
    32  type MailLog struct {
    33  	Id          int64     `json:"-"`
    34  	UserId      int64     `json:"-"`
    35  	CampaignId  int64     `json:"campaign_id"`
    36  	RId         string    `json:"id"`
    37  	SendDate    time.Time `json:"send_date"`
    38  	SendAttempt int       `json:"send_attempt"`
    39  	Processing  bool      `json:"-"`
    40  }
    41  
    42  // GenerateMailLog creates a new maillog for the given campaign and
    43  // result. It sets the initial send date to match the campaign's launch date.
    44  func GenerateMailLog(c *Campaign, r *Result, sendDate time.Time) error {
    45  	m := &MailLog{
    46  		UserId:     c.UserId,
    47  		CampaignId: c.Id,
    48  		RId:        r.RId,
    49  		SendDate:   sendDate,
    50  	}
    51  	return db.Save(m).Error
    52  }
    53  
    54  // Backoff sets the MailLog SendDate to be the next entry in an exponential
    55  // backoff. ErrMaxRetriesExceeded is thrown if this maillog has been retried
    56  // too many times. Backoff also unlocks the maillog so that it can be processed
    57  // again in the future.
    58  func (m *MailLog) Backoff(reason error) error {
    59  	r, err := GetResult(m.RId)
    60  	if err != nil {
    61  		return err
    62  	}
    63  	if m.SendAttempt == MaxSendAttempts {
    64  		r.HandleEmailError(ErrMaxSendAttempts)
    65  		return ErrMaxSendAttempts
    66  	}
    67  	// Add an error, since we had to backoff because of a
    68  	// temporary error of some sort during the SMTP transaction
    69  	m.SendAttempt++
    70  	backoffDuration := math.Pow(2, float64(m.SendAttempt))
    71  	m.SendDate = m.SendDate.Add(time.Minute * time.Duration(backoffDuration))
    72  	err = db.Save(m).Error
    73  	if err != nil {
    74  		return err
    75  	}
    76  	err = r.HandleEmailBackoff(reason, m.SendDate)
    77  	if err != nil {
    78  		return err
    79  	}
    80  	err = m.Unlock()
    81  	return err
    82  }
    83  
    84  // Unlock removes the processing flag so the maillog can be processed again
    85  func (m *MailLog) Unlock() error {
    86  	m.Processing = false
    87  	return db.Save(&m).Error
    88  }
    89  
    90  // Lock sets the processing flag so that other processes cannot modify the maillog
    91  func (m *MailLog) Lock() error {
    92  	m.Processing = true
    93  	return db.Save(&m).Error
    94  }
    95  
    96  // Error sets the error status on the models.Result that the
    97  // maillog refers to. Since MailLog errors are permanent,
    98  // this action also deletes the maillog.
    99  func (m *MailLog) Error(e error) error {
   100  	r, err := GetResult(m.RId)
   101  	if err != nil {
   102  		log.Warn(err)
   103  		return err
   104  	}
   105  	err = r.HandleEmailError(e)
   106  	if err != nil {
   107  		log.Warn(err)
   108  		return err
   109  	}
   110  	err = db.Delete(m).Error
   111  	return err
   112  }
   113  
   114  // Success deletes the maillog from the database and updates the underlying
   115  // campaign result.
   116  func (m *MailLog) Success() error {
   117  	r, err := GetResult(m.RId)
   118  	if err != nil {
   119  		return err
   120  	}
   121  	err = r.HandleEmailSent()
   122  	if err != nil {
   123  		return err
   124  	}
   125  	err = db.Delete(m).Error
   126  	return nil
   127  }
   128  
   129  // GetDialer returns a dialer based on the maillog campaign's SMTP configuration
   130  func (m *MailLog) GetDialer() (mailer.Dialer, error) {
   131  	c, err := GetCampaign(m.CampaignId, m.UserId)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	return c.SMTP.GetDialer()
   136  }
   137  
   138  // Generate fills in the details of a gomail.Message instance with
   139  // the correct headers and body from the campaign and recipient listed in
   140  // the maillog. We accept the gomail.Message as an argument so that the caller
   141  // can choose to re-use the message across recipients.
   142  func (m *MailLog) Generate(msg *gomail.Message) error {
   143  	r, err := GetResult(m.RId)
   144  	if err != nil {
   145  		return err
   146  	}
   147  	c, err := GetCampaign(m.CampaignId, m.UserId)
   148  	if err != nil {
   149  		return err
   150  	}
   151  
   152  	f, err := mail.ParseAddress(c.SMTP.FromAddress)
   153  	if err != nil {
   154  		return err
   155  	}
   156  	msg.SetAddressHeader("From", f.Address, f.Name)
   157  
   158  	ptx, err := NewPhishingTemplateContext(&c, r.BaseRecipient, r.RId)
   159  	if err != nil {
   160  		return err
   161  	}
   162  
   163  	// Add the transparency headers
   164  	msg.SetHeader("X-Mailer", config.ServerName)
   165  	if conf.ContactAddress != "" {
   166  		msg.SetHeader("X-Gophish-Contact", conf.ContactAddress)
   167  	}
   168  
   169  	// Add Message-Id header as described in RFC 2822.
   170  	messageID, err := m.generateMessageID()
   171  	if err != nil {
   172  		return err
   173  	}
   174  	msg.SetHeader("Message-Id", messageID)
   175  
   176  	// Parse the customHeader templates
   177  	for _, header := range c.SMTP.Headers {
   178  		key, err := ExecuteTemplate(header.Key, ptx)
   179  		if err != nil {
   180  			log.Warn(err)
   181  		}
   182  
   183  		value, err := ExecuteTemplate(header.Value, ptx)
   184  		if err != nil {
   185  			log.Warn(err)
   186  		}
   187  
   188  		// Add our header immediately
   189  		msg.SetHeader(key, value)
   190  	}
   191  
   192  	// Parse remaining templates
   193  	subject, err := ExecuteTemplate(c.Template.Subject, ptx)
   194  	if err != nil {
   195  		log.Warn(err)
   196  	}
   197  	// don't set Subject header if the subject is empty
   198  	if len(subject) != 0 {
   199  		msg.SetHeader("Subject", subject)
   200  	}
   201  
   202  	msg.SetHeader("To", r.FormatAddress())
   203  	if c.Template.Text != "" {
   204  		text, err := ExecuteTemplate(c.Template.Text, ptx)
   205  		if err != nil {
   206  			log.Warn(err)
   207  		}
   208  		msg.SetBody("text/plain", text)
   209  	}
   210  	if c.Template.HTML != "" {
   211  		html, err := ExecuteTemplate(c.Template.HTML, ptx)
   212  		if err != nil {
   213  			log.Warn(err)
   214  		}
   215  		if c.Template.Text == "" {
   216  			msg.SetBody("text/html", html)
   217  		} else {
   218  			msg.AddAlternative("text/html", html)
   219  		}
   220  	}
   221  	// Attach the files
   222  	for _, a := range c.Template.Attachments {
   223  		msg.Attach(func(a Attachment) (string, gomail.FileSetting, gomail.FileSetting) {
   224  			h := map[string][]string{"Content-ID": {fmt.Sprintf("<%s>", a.Name)}}
   225  			return a.Name, gomail.SetCopyFunc(func(w io.Writer) error {
   226  				decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(a.Content))
   227  				_, err = io.Copy(w, decoder)
   228  				return err
   229  			}), gomail.SetHeader(h)
   230  		}(a))
   231  	}
   232  
   233  	return nil
   234  }
   235  
   236  // GetQueuedMailLogs returns the mail logs that are queued up for the given minute.
   237  func GetQueuedMailLogs(t time.Time) ([]*MailLog, error) {
   238  	ms := []*MailLog{}
   239  	err := db.Where("send_date <= ? AND processing = ?", t, false).
   240  		Find(&ms).Error
   241  	if err != nil {
   242  		log.Warn(err)
   243  	}
   244  	return ms, err
   245  }
   246  
   247  // GetMailLogsByCampaign returns all of the mail logs for a given campaign.
   248  func GetMailLogsByCampaign(cid int64) ([]*MailLog, error) {
   249  	ms := []*MailLog{}
   250  	err := db.Where("campaign_id = ?", cid).Find(&ms).Error
   251  	return ms, err
   252  }
   253  
   254  // LockMailLogs locks or unlocks a slice of maillogs for processing.
   255  func LockMailLogs(ms []*MailLog, lock bool) error {
   256  	tx := db.Begin()
   257  	for i := range ms {
   258  		ms[i].Processing = lock
   259  		err := tx.Save(ms[i]).Error
   260  		if err != nil {
   261  			tx.Rollback()
   262  			return err
   263  		}
   264  	}
   265  	tx.Commit()
   266  	return nil
   267  }
   268  
   269  // UnlockAllMailLogs removes the processing lock for all maillogs
   270  // in the database. This is intended to be called when Gophish is started
   271  // so that any previously locked maillogs can resume processing.
   272  func UnlockAllMailLogs() error {
   273  	return db.Model(&MailLog{}).Update("processing", false).Error
   274  }
   275  
   276  var maxBigInt = big.NewInt(math.MaxInt64)
   277  
   278  // generateMessageID generates and returns a string suitable for an RFC 2822
   279  // compliant Message-ID, e.g.:
   280  // <1444789264909237300.3464.1819418242800517193@DESKTOP01>
   281  //
   282  // The following parameters are used to generate a Message-ID:
   283  // - The nanoseconds since Epoch
   284  // - The calling PID
   285  // - A cryptographically random int64
   286  // - The sending hostname
   287  func (m *MailLog) generateMessageID() (string, error) {
   288  	t := time.Now().UnixNano()
   289  	pid := os.Getpid()
   290  	rint, err := rand.Int(rand.Reader, maxBigInt)
   291  	if err != nil {
   292  		return "", err
   293  	}
   294  	h, err := os.Hostname()
   295  	// If we can't get the hostname, we'll use localhost
   296  	if err != nil {
   297  		h = "localhost.localdomain"
   298  	}
   299  	msgid := fmt.Sprintf("<%d.%d.%d@%s>", t, pid, rint, h)
   300  	return msgid, nil
   301  }