🎣 Open-Source Phishing Toolkit
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.

321 lines
8.4 KiB

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