Projects STRLCPY gophish Commits 9de32746
🤬
Revision indexing in progress... (symbol navigation in revisions will be accurate after indexed)
  • ■ ■ ■ ■ ■
    config.json
    skipped 19 lines
    20 20   "level": ""
    21 21   }
    22 22  }
     23 + 
  • ■ ■ ■ ■ ■ ■
    controllers/api/imap.go
     1 +package api
     2 + 
     3 +import (
     4 + "encoding/json"
     5 + "net/http"
     6 + "time"
     7 + 
     8 + ctx "github.com/gophish/gophish/context"
     9 + "github.com/gophish/gophish/imap"
     10 + "github.com/gophish/gophish/models"
     11 +)
     12 + 
     13 +// IMAPServerValidate handles requests for the /api/imapserver/validate endpoint
     14 +func (as *Server) IMAPServerValidate(w http.ResponseWriter, r *http.Request) {
     15 + switch {
     16 + case r.Method == "GET":
     17 + JSONResponse(w, models.Response{Success: false, Message: "Only POSTs allowed"}, http.StatusBadRequest)
     18 + case r.Method == "POST":
     19 + im := models.IMAP{}
     20 + err := json.NewDecoder(r.Body).Decode(&im)
     21 + if err != nil {
     22 + JSONResponse(w, models.Response{Success: false, Message: "Invalid request"}, http.StatusBadRequest)
     23 + return
     24 + }
     25 + err = imap.Validate(&im)
     26 + if err != nil {
     27 + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusOK)
     28 + return
     29 + }
     30 + JSONResponse(w, models.Response{Success: true, Message: "Successful login."}, http.StatusCreated)
     31 + }
     32 +}
     33 + 
     34 +// IMAPServer handles requests for the /api/imapserver/ endpoint
     35 +func (as *Server) IMAPServer(w http.ResponseWriter, r *http.Request) {
     36 + switch {
     37 + case r.Method == "GET":
     38 + ss, err := models.GetIMAP(ctx.Get(r, "user_id").(int64))
     39 + if err != nil {
     40 + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
     41 + return
     42 + }
     43 + JSONResponse(w, ss, http.StatusOK)
     44 + 
     45 + // POST: Update database
     46 + case r.Method == "POST":
     47 + im := models.IMAP{}
     48 + err := json.NewDecoder(r.Body).Decode(&im)
     49 + if err != nil {
     50 + JSONResponse(w, models.Response{Success: false, Message: "Invalid data. Please check your IMAP settings."}, http.StatusBadRequest)
     51 + return
     52 + }
     53 + im.ModifiedDate = time.Now().UTC()
     54 + im.UserId = ctx.Get(r, "user_id").(int64)
     55 + err = models.PostIMAP(&im, ctx.Get(r, "user_id").(int64))
     56 + if err != nil {
     57 + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
     58 + return
     59 + }
     60 + JSONResponse(w, models.Response{Success: true, Message: "Successfully saved IMAP settings."}, http.StatusCreated)
     61 + }
     62 +}
     63 + 
  • ■ ■ ■ ■ ■ ■
    controllers/api/server.go
    skipped 47 lines
    48 48   router := root.PathPrefix("/api/").Subrouter()
    49 49   router.Use(mid.RequireAPIKey)
    50 50   router.Use(mid.EnforceViewOnly)
     51 + router.HandleFunc("/imap/", as.IMAPServer)
     52 + router.HandleFunc("/imap/validate", as.IMAPServerValidate)
    51 53   router.HandleFunc("/reset", as.Reset)
    52 54   router.HandleFunc("/campaigns/", as.Campaigns)
    53 55   router.HandleFunc("/campaigns/summary", as.CampaignsSummary)
    skipped 30 lines
  • ■ ■ ■ ■ ■ ■
    db/db_mysql/migrations/20200116000000_0.9.0_imap.sql
     1 + 
     2 +-- +goose Up
     3 +-- SQL in section 'Up' is executed when this migration is applied
     4 +CREATE TABLE IF NOT EXISTS `imap` (user_id bigint,host varchar(255),port int,username varchar(255),password varchar(255),modified_date datetime,tls boolean,enabled boolean,folder varchar(255),restrict_domain varchar(255),delete_reported_campaign_email boolean,last_login datetime,imap_freq int);
     5 + 
     6 +-- +goose Down
     7 +-- SQL section 'Down' is executed when this migration is rolled back
     8 +DROP TABLE `imap`;
     9 + 
  • ■ ■ ■ ■ ■ ■
    db/db_sqlite3/migrations/20200116000000_0.9.0_imap.sql
     1 + 
     2 +-- +goose Up
     3 +-- SQL in section 'Up' is executed when this migration is applied
     4 +CREATE TABLE IF NOT EXISTS "imap" ("user_id" bigint, "host" varchar(255), "port" integer, "username" varchar(255), "password" varchar(255), "modified_date" datetime default CURRENT_TIMESTAMP, "tls" BOOLEAN, "enabled" BOOLEAN, "folder" varchar(255), "restrict_domain" varchar(255), "delete_reported_campaign_email" BOOLEAN, "last_login" datetime, "imap_freq" integer);
     5 + 
     6 +-- +goose Down
     7 +-- SQL section 'Down' is executed when this migration is rolled back
     8 +DROP TABLE "imap";
     9 + 
  • ■ ■ ■ ■ ■ ■
    gophish.go
    skipped 33 lines
    34 34   
    35 35   "github.com/gophish/gophish/config"
    36 36   "github.com/gophish/gophish/controllers"
     37 + "github.com/gophish/gophish/imap"
    37 38   log "github.com/gophish/gophish/logger"
    38 39   "github.com/gophish/gophish/middleware"
    39 40   "github.com/gophish/gophish/models"
    skipped 40 lines
    80 81   if err != nil {
    81 82   log.Fatal(err)
    82 83   }
     84 + 
    83 85   // Unlock any maillogs that may have been locked for processing
    84 86   // when Gophish was last shutdown.
    85 87   err = models.UnlockAllMailLogs()
    skipped 13 lines
    99 101   phishConfig := conf.PhishConf
    100 102   phishServer := controllers.NewPhishingServer(phishConfig)
    101 103   
     104 + imapMonitor := imap.NewMonitor()
     105 + 
    102 106   go adminServer.Start()
    103 107   go phishServer.Start()
     108 + go imapMonitor.Start()
    104 109   
    105 110   // Handle graceful shutdown
    106 111   c := make(chan os.Signal, 1)
    skipped 2 lines
    109 114   log.Info("CTRL+C Received... Gracefully shutting down servers")
    110 115   adminServer.Shutdown()
    111 116   phishServer.Shutdown()
     117 + imapMonitor.Shutdown()
     118 + 
    112 119  }
    113 120   
  • ■ ■ ■ ■ ■ ■
    imap/imap.go
     1 +package imap
     2 + 
     3 +// Functionality taken from https://github.com/jprobinson/eazye
     4 + 
     5 +import (
     6 + "bytes"
     7 + "crypto/tls"
     8 + "fmt"
     9 + "strconv"
     10 + "time"
     11 + 
     12 + log "github.com/gophish/gophish/logger"
     13 + "github.com/gophish/gophish/models"
     14 + "github.com/jordan-wright/email"
     15 + "github.com/mxk/go-imap/imap"
     16 +)
     17 + 
     18 +// Client interface for IMAP interactions
     19 +type Client interface {
     20 + Close(expunge bool) (cmd *imap.Command, err error)
     21 + Login(username, password string) (cmd *imap.Command, err error)
     22 + Logout(timeout time.Duration) (cmd *imap.Command, err error)
     23 + Select(mbox string, readonly bool) (cmd *imap.Command, err error)
     24 + UIDFetch(seq *imap.SeqSet, items ...string) (cmd *imap.Command, err error)
     25 + UIDSearch(spec ...imap.Field) (cmd *imap.Command, err error)
     26 + UIDStore(seq *imap.SeqSet, item string, value imap.Field) (cmd *imap.Command, err error)
     27 +}
     28 + 
     29 +// Email represents an email.Email with an included IMAP UID
     30 +type Email struct {
     31 + UID uint32 `json:"uid"`
     32 + *email.Email
     33 +}
     34 + 
     35 +// Mailbox holds onto the credentials and other information
     36 +// needed for connecting to an IMAP server.
     37 +type Mailbox struct {
     38 + Host string
     39 + TLS bool
     40 + User string
     41 + Pwd string
     42 + Folder string
     43 + // Read only mode, false (original logic) if not initialized
     44 + ReadOnly bool
     45 +}
     46 + 
     47 +// GetAll will pull all emails from the email folder and return them as a list.
     48 +func (mbox *Mailbox) GetAll(markAsRead, delete bool) ([]Email, error) {
     49 + // call chan, put 'em in a list, return
     50 + var emails []Email
     51 + responses, err := mbox.GenerateAll(markAsRead, delete)
     52 + if err != nil {
     53 + return emails, err
     54 + }
     55 + 
     56 + for resp := range responses {
     57 + if resp.Err != nil {
     58 + return emails, resp.Err
     59 + }
     60 + emails = append(emails, resp.Email)
     61 + }
     62 + 
     63 + return emails, nil
     64 +}
     65 + 
     66 +// GenerateAll will find all emails in the email folder and pass them along to the responses channel.
     67 +func (mbox *Mailbox) GenerateAll(markAsRead, delete bool) (chan Response, error) {
     68 + return mbox.generateMail("ALL", nil, markAsRead, delete)
     69 +}
     70 + 
     71 +// GetUnread will find all unread emails in the folder and return them as a list.
     72 +func (mbox *Mailbox) GetUnread(markAsRead, delete bool) ([]Email, error) {
     73 + // call chan, put 'em in a list, return
     74 + var emails []Email
     75 + 
     76 + responses, err := mbox.GenerateUnread(markAsRead, delete)
     77 + if err != nil {
     78 + return emails, err
     79 + }
     80 + 
     81 + for resp := range responses {
     82 + if resp.Err != nil {
     83 + return emails, resp.Err
     84 + }
     85 + emails = append(emails, resp.Email)
     86 + }
     87 + 
     88 + return emails, nil
     89 +}
     90 + 
     91 +// GenerateUnread will find all unread emails in the folder and pass them along to the responses channel.
     92 +func (mbox *Mailbox) GenerateUnread(markAsRead, delete bool) (chan Response, error) {
     93 + return mbox.generateMail("UNSEEN", nil, markAsRead, delete)
     94 +}
     95 + 
     96 +// MarkAsUnread will set the UNSEEN flag on a supplied slice of UIDs
     97 +func (mbox *Mailbox) MarkAsUnread(uids []uint32) error {
     98 + client, err := mbox.newClient()
     99 + if err != nil {
     100 + return err
     101 + }
     102 + defer func() {
     103 + client.Close(true)
     104 + client.Logout(30 * time.Second)
     105 + }()
     106 + for _, u := range uids {
     107 + err := alterEmail(client, u, "\\SEEN", false)
     108 + if err != nil {
     109 + return err //return on first failure
     110 + }
     111 + }
     112 + return nil
     113 + 
     114 +}
     115 + 
     116 +// DeleteEmails will delete emails from the supplied slice of UIDs
     117 +func (mbox *Mailbox) DeleteEmails(uids []uint32) error {
     118 + client, err := mbox.newClient()
     119 + if err != nil {
     120 + return err
     121 + }
     122 + defer func() {
     123 + client.Close(true)
     124 + client.Logout(30 * time.Second)
     125 + }()
     126 + for _, u := range uids {
     127 + err := deleteEmail(client, u)
     128 + if err != nil {
     129 + return err //return on first failure
     130 + }
     131 + }
     132 + return nil
     133 + 
     134 +}
     135 + 
     136 +// Validate validates supplied IMAP model by connecting to the server
     137 +func Validate(s *models.IMAP) error {
     138 + 
     139 + err := s.Validate()
     140 + if err != nil {
     141 + log.Error(err)
     142 + return err
     143 + }
     144 + 
     145 + s.Host = s.Host + ":" + strconv.Itoa(int(s.Port)) // Append port
     146 + mailServer := Mailbox{
     147 + Host: s.Host,
     148 + TLS: s.TLS,
     149 + User: s.Username,
     150 + Pwd: s.Password,
     151 + Folder: s.Folder}
     152 + 
     153 + client, err := mailServer.newClient()
     154 + if err != nil {
     155 + log.Error(err.Error())
     156 + } else {
     157 + client.Close(true)
     158 + client.Logout(30 * time.Second)
     159 + }
     160 + return err
     161 +}
     162 + 
     163 +// Response is a helper struct to wrap the email responses and possible errors.
     164 +type Response struct {
     165 + Email Email
     166 + Err error
     167 +}
     168 + 
     169 +// newClient will initiate a new IMAP connection with the given creds.
     170 +func (mbox *Mailbox) newClient() (*imap.Client, error) {
     171 + var client *imap.Client
     172 + var err error
     173 + if mbox.TLS {
     174 + client, err = imap.DialTLS(mbox.Host, new(tls.Config))
     175 + if err != nil {
     176 + return client, err
     177 + }
     178 + } else {
     179 + client, err = imap.Dial(mbox.Host)
     180 + if err != nil {
     181 + return client, err
     182 + }
     183 + }
     184 + 
     185 + _, err = client.Login(mbox.User, mbox.Pwd)
     186 + if err != nil {
     187 + return client, err
     188 + }
     189 + 
     190 + _, err = imap.Wait(client.Select(mbox.Folder, mbox.ReadOnly))
     191 + if err != nil {
     192 + return client, err
     193 + }
     194 + 
     195 + return client, nil
     196 +}
     197 + 
     198 +const dateFormat = "02-Jan-2006"
     199 + 
     200 +// findEmails will run a find the UIDs of any emails that match the search.:
     201 +func findEmails(client Client, search string, since *time.Time) (*imap.Command, error) {
     202 + var specs []imap.Field
     203 + if len(search) > 0 {
     204 + specs = append(specs, search)
     205 + }
     206 + 
     207 + if since != nil {
     208 + sinceStr := since.Format(dateFormat)
     209 + specs = append(specs, "SINCE", sinceStr)
     210 + }
     211 + 
     212 + // get headers and UID for UnSeen message in src inbox...
     213 + cmd, err := imap.Wait(client.UIDSearch(specs...))
     214 + if err != nil {
     215 + return &imap.Command{}, fmt.Errorf("uid search failed: %s", err)
     216 + }
     217 + return cmd, nil
     218 +}
     219 + 
     220 +const GenerateBufferSize = 100
     221 + 
     222 +func (mbox *Mailbox) generateMail(search string, since *time.Time, markAsRead, delete bool) (chan Response, error) {
     223 + responses := make(chan Response, GenerateBufferSize)
     224 + client, err := mbox.newClient()
     225 + if err != nil {
     226 + close(responses)
     227 + return responses, fmt.Errorf("failed to create IMAP connection: %s", err)
     228 + }
     229 + 
     230 + go func() {
     231 + defer func() {
     232 + client.Close(true)
     233 + client.Logout(30 * time.Second)
     234 + close(responses)
     235 + }()
     236 + 
     237 + var cmd *imap.Command
     238 + // find all the UIDs
     239 + cmd, err = findEmails(client, search, since)
     240 + if err != nil {
     241 + responses <- Response{Err: err}
     242 + return
     243 + }
     244 + // gotta fetch 'em all
     245 + getEmails(client, cmd, markAsRead, delete, responses)
     246 + }()
     247 + 
     248 + return responses, nil
     249 +}
     250 + 
     251 +func getEmails(client Client, cmd *imap.Command, markAsRead, delete bool, responses chan Response) {
     252 + seq := &imap.SeqSet{}
     253 + msgCount := 0
     254 + for _, rsp := range cmd.Data {
     255 + for _, uid := range rsp.SearchResults() {
     256 + msgCount++
     257 + seq.AddNum(uid)
     258 + }
     259 + }
     260 + 
     261 + if seq.Empty() {
     262 + return
     263 + }
     264 + 
     265 + fCmd, err := imap.Wait(client.UIDFetch(seq, "INTERNALDATE", "BODY[]", "UID", "RFC822.HEADER"))
     266 + if err != nil {
     267 + responses <- Response{Err: fmt.Errorf("unable to perform uid fetch: %s", err)}
     268 + return
     269 + }
     270 + 
     271 + var email Email
     272 + for _, msgData := range fCmd.Data {
     273 + msgFields := msgData.MessageInfo().Attrs
     274 + 
     275 + // make sure is a legit response before we attempt to parse it
     276 + // deal with unsolicited FETCH responses containing only flags
     277 + // I'm lookin' at YOU, Gmail!
     278 + // http://mailman13.u.washington.edu/pipermail/imap-protocol/2014-October/002355.html
     279 + // http://stackoverflow.com/questions/26262472/gmail-imap-is-sometimes-returning-bad-results-for-fetch
     280 + if _, ok := msgFields["RFC822.HEADER"]; !ok {
     281 + continue
     282 + }
     283 + 
     284 + email, err = NewEmail(msgFields)
     285 + if err != nil {
     286 + responses <- Response{Err: fmt.Errorf("unable to parse email: %s", err)}
     287 + return
     288 + }
     289 + 
     290 + responses <- Response{Email: email}
     291 + 
     292 + if !markAsRead {
     293 + err = removeSeen(client, imap.AsNumber(msgFields["UID"]))
     294 + if err != nil {
     295 + responses <- Response{Err: fmt.Errorf("unable to remove seen flag: %s", err)}
     296 + return
     297 + }
     298 + }
     299 + 
     300 + if delete {
     301 + err = deleteEmail(client, imap.AsNumber(msgFields["UID"]))
     302 + if err != nil {
     303 + responses <- Response{Err: fmt.Errorf("unable to delete email: %s", err)}
     304 + return
     305 + }
     306 + }
     307 + }
     308 + return
     309 +}
     310 + 
     311 +func deleteEmail(client Client, UID uint32) error {
     312 + return alterEmail(client, UID, "\\DELETED", true)
     313 +}
     314 + 
     315 +func removeSeen(client Client, UID uint32) error {
     316 + return alterEmail(client, UID, "\\SEEN", false)
     317 +}
     318 + 
     319 +func alterEmail(client Client, UID uint32, flag string, plus bool) error {
     320 + flg := "-FLAGS"
     321 + if plus {
     322 + flg = "+FLAGS"
     323 + }
     324 + fSeq := &imap.SeqSet{}
     325 + fSeq.AddNum(UID)
     326 + _, err := imap.Wait(client.UIDStore(fSeq, flg, flag))
     327 + if err != nil {
     328 + return err
     329 + }
     330 + 
     331 + return nil
     332 +}
     333 + 
     334 +// NewEmail will parse an imap.FieldMap into an Email. This
     335 +// will expect the message to container the internaldate and the body with
     336 +// all headers included.
     337 +func NewEmail(msgFields imap.FieldMap) (Email, error) {
     338 + 
     339 + rawBody := imap.AsBytes(msgFields["BODY[]"])
     340 + 
     341 + rawBodyStream := bytes.NewReader(rawBody)
     342 + em, err := email.NewEmailFromReader(rawBodyStream) // Parse with @jordanwright's library
     343 + if err != nil {
     344 + return Email{}, err
     345 + }
     346 + iem := Email{
     347 + Email: em,
     348 + UID: imap.AsNumber(msgFields["UID"]),
     349 + }
     350 + 
     351 + return iem, err
     352 +}
     353 + 
  • ■ ■ ■ ■ ■ ■
    imap/monitor.go
     1 +package imap
     2 + 
     3 +/* TODO:
     4 +* - Have a counter per config for number of consecutive login errors and backoff (e.g if supplied creds are incorrect)
     5 +* - Have a DB field "last_login_error" if last login failed
     6 +* - DB counter for non-campaign emails that the admin should investigate
     7 +* - Add field to User for numner of non-campaign emails reported
     8 + */
     9 +import (
     10 + "context"
     11 + "regexp"
     12 + "strconv"
     13 + "strings"
     14 + "time"
     15 + 
     16 + log "github.com/gophish/gophish/logger"
     17 + 
     18 + "github.com/gophish/gophish/models"
     19 +)
     20 + 
     21 +// Pattern for GoPhish emails e.g ?rid=AbC123
     22 +var goPhishRegex = regexp.MustCompile("(\\?rid=[A-Za-z0-9]{7})")
     23 + 
     24 +// Monitor is a worker that monitors IMAP servers for reported campaign emails
     25 +type Monitor struct {
     26 + cancel func()
     27 +}
     28 + 
     29 +// Monitor.start() checks for campaign emails
     30 +// As each account can have its own polling frequency set we need to run one Go routine for
     31 +// each, as well as keeping an eye on newly created user accounts.
     32 +func (im *Monitor) start(ctx context.Context) {
     33 + 
     34 + usermap := make(map[int64]int) // Keep track of running go routines, one per user. We assume incrementing non-repeating UIDs (for the case where users are deleted and re-added).
     35 + 
     36 + for {
     37 + select {
     38 + case <-ctx.Done():
     39 + return
     40 + default:
     41 + dbusers, err := models.GetUsers() //Slice of all user ids. Each user gets their own IMAP monitor routine.
     42 + if err != nil {
     43 + log.Error(err)
     44 + break
     45 + }
     46 + for _, dbuser := range dbusers {
     47 + if _, ok := usermap[dbuser.Id]; !ok { // If we don't currently have a running Go routine for this user, start one.
     48 + log.Info("Starting new IMAP monitor for user ", dbuser.Username)
     49 + usermap[dbuser.Id] = 1
     50 + go monitor(dbuser.Id, ctx)
     51 + }
     52 + }
     53 + time.Sleep(10 * time.Second) // Every ten seconds we check if a new user has been created
     54 + }
     55 + }
     56 +}
     57 + 
     58 +// monitor will continuously login to the IMAP settings associated to the supplied user id (if the user account has IMAP settings, and they're enabled.)
     59 +// It also verifies the user account exists, and returns if not (for the case of a user being deleted).
     60 +func monitor(uid int64, ctx context.Context) {
     61 + 
     62 + for {
     63 + select {
     64 + case <-ctx.Done():
     65 + return
     66 + default:
     67 + // 1. Check if user exists, if not, return.
     68 + _, err := models.GetUser(uid)
     69 + if err != nil { // Not sure if there's a better way to determine user existence via id.
     70 + log.Info("User ", uid, " seems to have been deleted. Stopping IMAP monitor for this user.")
     71 + return
     72 + }
     73 + // 2. Check if user has IMAP settings.
     74 + imapSettings, err := models.GetIMAP(uid)
     75 + if err != nil {
     76 + log.Error(err)
     77 + break
     78 + }
     79 + if len(imapSettings) > 0 {
     80 + im := imapSettings[0]
     81 + // 3. Check if IMAP is enabled
     82 + if im.Enabled {
     83 + log.Debug("Checking IMAP for user ", uid, ": ", im.Username, "@", im.Host)
     84 + checkForNewEmails(im)
     85 + time.Sleep((time.Duration(im.IMAPFreq) - 10) * time.Second) // Subtract 10 to compensate for the default sleep of 10 at the bottom
     86 + }
     87 + }
     88 + }
     89 + time.Sleep(10 * time.Second)
     90 + }
     91 +}
     92 + 
     93 +// NewMonitor returns a new instance of imap.Monitor
     94 +func NewMonitor() *Monitor {
     95 + 
     96 + im := &Monitor{}
     97 + return im
     98 +}
     99 + 
     100 +// Start launches the IMAP campaign monitor
     101 +func (im *Monitor) Start() error {
     102 + log.Info("Starting IMAP monitor manager")
     103 + ctx, cancel := context.WithCancel(context.Background()) // ctx is the derivedContext
     104 + im.cancel = cancel
     105 + go im.start(ctx)
     106 + return nil
     107 +}
     108 + 
     109 +// Shutdown attempts to gracefully shutdown the IMAP monitor.
     110 +func (im *Monitor) Shutdown() error {
     111 + log.Info("Shutting down IMAP monitor manager")
     112 + im.cancel()
     113 + return nil
     114 +}
     115 + 
     116 +// checkForNewEmails logs into an IMAP account and checks unread emails
     117 +// for the rid campaign identifier.
     118 +func checkForNewEmails(im models.IMAP) {
     119 + 
     120 + im.Host = im.Host + ":" + strconv.Itoa(int(im.Port)) // Append port
     121 + mailServer := Mailbox{
     122 + Host: im.Host,
     123 + TLS: im.TLS,
     124 + User: im.Username,
     125 + Pwd: im.Password,
     126 + Folder: im.Folder}
     127 + 
     128 + msgs, err := mailServer.GetUnread(true, false)
     129 + if err != nil {
     130 + log.Error(err)
     131 + return
     132 + }
     133 + // Update last_succesful_login here via im.Host
     134 + err = models.SuccessfulLogin(&im)
     135 + 
     136 + if len(msgs) > 0 {
     137 + var reportingFailed []uint32 // UIDs of emails that were unable to be reported to phishing server, mark as unread
     138 + var campaignEmails []uint32 // UIDs of campaign emails. If DeleteReportedCampaignEmail is true, we will delete these
     139 + for _, m := range msgs {
     140 + // Check if sender is from company's domain, if enabled. TODO: Make this an IMAP filter
     141 + if im.RestrictDomain != "" { // e.g domainResitct = widgets.com
     142 + splitEmail := strings.Split(m.Email.From, "@")
     143 + senderDomain := splitEmail[len(splitEmail)-1]
     144 + if senderDomain != im.RestrictDomain {
     145 + log.Debug("Ignoring email as not from company domain: ", senderDomain)
     146 + continue
     147 + }
     148 + }
     149 + 
     150 + body := string(append(m.Email.Text, m.Email.HTML...)) // Not sure if we need to check the Text as well as the HTML. Perhaps sometimes Text only emails won't have an HTML component?
     151 + rid := goPhishRegex.FindString(body)
     152 + 
     153 + if rid != "" {
     154 + rid = rid[5:]
     155 + log.Infof("User '%s' reported email with rid %s", m.Email.From, rid)
     156 + result, err := models.GetResult(rid)
     157 + if err != nil {
     158 + log.Error("Error reporting GoPhish email with rid ", rid, ": ", err.Error())
     159 + reportingFailed = append(reportingFailed, m.UID)
     160 + } else {
     161 + err = result.HandleEmailReport(models.EventDetails{})
     162 + if err != nil {
     163 + log.Error("Error updating GoPhish email with rid ", rid, ": ", err.Error())
     164 + } else {
     165 + if im.DeleteReportedCampaignEmail == true {
     166 + campaignEmails = append(campaignEmails, m.UID)
     167 + }
     168 + }
     169 + }
     170 + } else {
     171 + // In the future this should be an alert in Gophish
     172 + log.Debugf("User '%s' reported email with subject '%s'. This is not a GoPhish campaign; you should investigate it.\n", m.Email.From, m.Email.Subject)
     173 + }
     174 + // Check if any emails were unable to be reported, so we can mark them as unread
     175 + if len(reportingFailed) > 0 {
     176 + log.Debugf("Marking %d emails as unread as failed to report\n", len(reportingFailed))
     177 + err := mailServer.MarkAsUnread(reportingFailed) // Set emails as unread that we failed to report to GoPhish
     178 + if err != nil {
     179 + log.Error("Unable to mark emails as unread: ", err.Error())
     180 + }
     181 + }
     182 + // If the DeleteReportedCampaignEmail flag is set, delete reported Gophish campaign emails
     183 + if im.DeleteReportedCampaignEmail == true && len(campaignEmails) > 0 {
     184 + log.Debugf("Deleting %d campaign emails\n", len(campaignEmails))
     185 + err := mailServer.DeleteEmails(campaignEmails) // Delete GoPhish campaign emails.
     186 + if err != nil {
     187 + log.Error("Failed to delete emails: ", err.Error())
     188 + }
     189 + }
     190 + }
     191 + } else {
     192 + log.Debug("No new emails for ", im.Username)
     193 + }
     194 +}
     195 + 
  • ■ ■ ■ ■ ■ ■
    models/imap.go
     1 +package models
     2 + 
     3 +import (
     4 + "errors"
     5 + "net"
     6 + "time"
     7 + 
     8 + log "github.com/gophish/gophish/logger"
     9 +)
     10 + 
     11 +const DefaultIMAPFolder = "INBOX"
     12 +const DefaultIMAPFreq = 60 // Every 60 seconds
     13 + 
     14 +// IMAP contains the attributes needed to handle logging into an IMAP server to check
     15 +// for reported emails
     16 +type IMAP struct {
     17 + UserId int64 `json:"-" gorm:"column:user_id"`
     18 + Enabled bool `json:"enabled"`
     19 + Host string `json:"host"`
     20 + Port uint16 `json:"port,string,omitempty"`
     21 + Username string `json:"username"`
     22 + Password string `json:"password"`
     23 + TLS bool `json:"tls"`
     24 + Folder string `json:"folder"`
     25 + RestrictDomain string `json:"restrict_domain"`
     26 + DeleteReportedCampaignEmail bool `json:"delete_reported_campaign_email"`
     27 + LastLogin time.Time `json:"last_login,omitempty"`
     28 + ModifiedDate time.Time `json:"modified_date"`
     29 + IMAPFreq uint32 `json:"imap_freq,string,omitempty"`
     30 +}
     31 + 
     32 +// ErrIMAPHostNotSpecified is thrown when there is no Host specified
     33 +// in the IMAP configuration
     34 +var ErrIMAPHostNotSpecified = errors.New("No IMAP Host specified")
     35 + 
     36 +// ErrIMAPPortNotSpecified is thrown when there is no Port specified
     37 +// in the IMAP configuration
     38 +var ErrIMAPPortNotSpecified = errors.New("No IMAP Port specified")
     39 + 
     40 +// ErrInvalidIMAPHost indicates that the IMAP server string is invalid
     41 +var ErrInvalidIMAPHost = errors.New("Invalid IMAP server address")
     42 + 
     43 +// ErrInvalidIMAPPort indicates that the IMAP Port is invalid
     44 +var ErrInvalidIMAPPort = errors.New("Invalid IMAP Port")
     45 + 
     46 +// ErrIMAPUsernameNotSpecified is thrown when there is no Username specified
     47 +// in the IMAP configuration
     48 +var ErrIMAPUsernameNotSpecified = errors.New("No Username specified")
     49 + 
     50 +// ErrIMAPPasswordNotSpecified is thrown when there is no Password specified
     51 +// in the IMAP configuration
     52 +var ErrIMAPPasswordNotSpecified = errors.New("No Password specified")
     53 + 
     54 +// ErrInvalidIMAPFreq is thrown when the frequency for polling the
     55 +// IMAP server is invalid
     56 +var ErrInvalidIMAPFreq = errors.New("Invalid polling frequency.")
     57 + 
     58 +// TableName specifies the database tablename for Gorm to use
     59 +func (im IMAP) TableName() string {
     60 + return "imap"
     61 +}
     62 + 
     63 +// Validate ensures that IMAP configs/connections are valid
     64 +func (im *IMAP) Validate() error {
     65 + switch {
     66 + case im.Host == "":
     67 + return ErrIMAPHostNotSpecified
     68 + case im.Port == 0:
     69 + return ErrIMAPPortNotSpecified
     70 + case im.Username == "":
     71 + return ErrIMAPUsernameNotSpecified
     72 + case im.Password == "":
     73 + return ErrIMAPPasswordNotSpecified
     74 + }
     75 + 
     76 + // Set the default value for Folder
     77 + if im.Folder == "" {
     78 + im.Folder = DefaultIMAPFolder
     79 + }
     80 + 
     81 + // Make sure im.Host is an IP or hostname. NB will fail if unable to resolve the hostname.
     82 + ip := net.ParseIP(im.Host)
     83 + _, err := net.LookupHost(im.Host)
     84 + if ip == nil && err != nil {
     85 + return ErrInvalidIMAPHost
     86 + }
     87 + 
     88 + // Make sure 1 >= port <= 65535
     89 + if im.Port < 1 || im.Port > 65535 {
     90 + return ErrInvalidIMAPPort
     91 + }
     92 + 
     93 + // Make sure the polling frequency is between every 30 seconds and every year
     94 + // If not set it to the default
     95 + if im.IMAPFreq < 30 || im.IMAPFreq > 31540000 {
     96 + im.IMAPFreq = DefaultIMAPFreq
     97 + }
     98 + 
     99 + return nil
     100 +}
     101 + 
     102 +// GetIMAP returns the IMAP server owned by the given user.
     103 +func GetIMAP(uid int64) ([]IMAP, error) {
     104 + im := []IMAP{}
     105 + count := 0
     106 + err := db.Where("user_id=?", uid).Find(&im).Count(&count).Error
     107 + 
     108 + if err != nil {
     109 + log.Error(err)
     110 + return im, err
     111 + }
     112 + return im, nil
     113 +}
     114 + 
     115 +// PostIMAP updates IMAP settings for a user in the database.
     116 +func PostIMAP(im *IMAP, uid int64) error {
     117 + err := im.Validate()
     118 + if err != nil {
     119 + log.Error(err)
     120 + return err
     121 + }
     122 + 
     123 + // Delete old entry. TODO: Save settings and if fails to Save below replace with original
     124 + err = DeleteIMAP(uid)
     125 + if err != nil {
     126 + log.Error(err)
     127 + return err
     128 + }
     129 + 
     130 + // Insert new settings into the DB
     131 + err = db.Save(im).Error
     132 + if err != nil {
     133 + log.Error("Unable to save to database: ", err.Error())
     134 + }
     135 + return err
     136 +}
     137 + 
     138 +// DeleteIMAP deletes the existing IMAP in the database.
     139 +func DeleteIMAP(uid int64) error {
     140 + err := db.Where("user_id=?", uid).Delete(&IMAP{}).Error
     141 + if err != nil {
     142 + log.Error(err)
     143 + }
     144 + return err
     145 +}
     146 + 
     147 +func SuccessfulLogin(im *IMAP) error {
     148 + err := db.Model(&im).Where("user_id = ?", im.UserId).Update("last_login", time.Now().UTC()).Error
     149 + if err != nil {
     150 + log.Error("Unable to update database: ", err.Error())
     151 + }
     152 + return err
     153 +}
     154 + 
  • ■ ■ ■ ■ ■ ■
    static/js/src/app/gophish.js
    skipped 9 lines
    10 10   <i class=\"fa fa-check-circle\"></i> " + message + "</div>")
    11 11  }
    12 12   
     13 +// Fade message after n seconds
     14 +function errorFlashFade(message, fade) {
     15 + $("#flashes").empty()
     16 + $("#flashes").append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
     17 + <i class=\"fa fa-exclamation-circle\"></i> " + message + "</div>")
     18 + setTimeout(function(){
     19 + $("#flashes").empty()
     20 + }, fade * 1000);
     21 +}
     22 +// Fade message after n seconds
     23 +function successFlashFade(message, fade) {
     24 + $("#flashes").empty()
     25 + $("#flashes").append("<div style=\"text-align:center\" class=\"alert alert-success\">\
     26 + <i class=\"fa fa-check-circle\"></i> " + message + "</div>")
     27 + setTimeout(function(){
     28 + $("#flashes").empty()
     29 + }, fade * 1000);
     30 + 
     31 +}
     32 + 
    13 33  function modalError(message) {
    14 34   $("#modal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
    15 35   <i class=\"fa fa-exclamation-circle\"></i> " + message + "</div>")
    skipped 179 lines
    195 215   // delete() - Deletes a SMTP at DELETE /smtp/:id
    196 216   delete: function (id) {
    197 217   return query("/smtp/" + id, "DELETE", {}, false)
     218 + }
     219 + },
     220 + // IMAP containts the endpoints for /imap/
     221 + IMAP: {
     222 + get: function() {
     223 + return query("/imap/", "GET", {}, !1)
     224 + },
     225 + post: function(e) {
     226 + return query("/imap/", "POST", e, !1)
     227 + },
     228 + validate: function(e) {
     229 + return query("/imap/validate", "POST", e, true)
    198 230   }
    199 231   },
    200 232   // users contains the endpoints for /users
    skipped 80 lines
  • ■ ■ ■ ■ ■ ■
    static/js/src/app/settings.js
    1 1  $(document).ready(function () {
     2 + $('[data-toggle="tooltip"]').tooltip();
    2 3   $("#apiResetForm").submit(function (e) {
    3 4   api.reset()
    4 5   .success(function (response) {
    skipped 16 lines
    21 22   })
    22 23   return false
    23 24   })
     25 + //$("#imapForm").submit(function (e) {
     26 + $("#savesettings").click(function() {
     27 + var imapSettings = {}
     28 + imapSettings.host = $("#imaphost").val()
     29 + imapSettings.port = $("#imapport").val()
     30 + imapSettings.username = $("#imapusername").val()
     31 + imapSettings.password = $("#imappassword").val()
     32 + imapSettings.enabled = $('#use_imap').prop('checked')
     33 + imapSettings.tls = $('#use_tls').prop('checked')
     34 + 
     35 + //Advanced settings
     36 + imapSettings.folder = $("#folder").val()
     37 + imapSettings.imap_freq = $("#imapfreq").val()
     38 + imapSettings.restrict_domain = $("#restrictdomain").val()
     39 + imapSettings.delete_reported_campaign_email = $('#deletecampaign').prop('checked')
     40 +
     41 + //To avoid unmarshalling error in controllers/api/imap.go. It would fail gracefully, but with a generic error.
     42 + if (imapSettings.host == ""){
     43 + errorFlash("No IMAP Host specified")
     44 + document.body.scrollTop = 0;
     45 + document.documentElement.scrollTop = 0;
     46 + return false
     47 + }
     48 + if (imapSettings.port == ""){
     49 + errorFlash("No IMAP Port specified")
     50 + document.body.scrollTop = 0;
     51 + document.documentElement.scrollTop = 0;
     52 + return false
     53 + }
     54 + if (isNaN(imapSettings.port) || imapSettings.port <1 || imapSettings.port > 65535 ){
     55 + errorFlash("Invalid IMAP Port")
     56 + document.body.scrollTop = 0;
     57 + document.documentElement.scrollTop = 0;
     58 + return false
     59 + }
     60 + if (imapSettings.imap_freq == ""){
     61 + imapSettings.imap_freq = "60"
     62 + }
     63 + 
     64 + api.IMAP.post(imapSettings).done(function (data) {
     65 + if (data.success == true) {
     66 + successFlashFade("Successfully updated IMAP settings.", 2)
     67 + } else {
     68 + errorFlash("Unable to update IMAP settings.")
     69 + }
     70 + })
     71 + .success(function (data){
     72 + loadIMAPSettings()
     73 + })
     74 + .fail(function (data) {
     75 + errorFlash(data.responseJSON.message)
     76 + })
     77 + .always(function (data){
     78 + document.body.scrollTop = 0;
     79 + document.documentElement.scrollTop = 0;
     80 + })
     81 +
     82 + return false
     83 + })
     84 + 
     85 + $("#validateimap").click(function() {
     86 + 
     87 + // Query validate imap server endpoint
     88 + var server = {}
     89 + server.host = $("#imaphost").val()
     90 + server.port = $("#imapport").val()
     91 + server.username = $("#imapusername").val()
     92 + server.password = $("#imappassword").val()
     93 + server.tls = $('#use_tls').prop('checked')
     94 + 
     95 + //To avoid unmarshalling error in controllers/api/imap.go. It would fail gracefully, but with a generic error.
     96 + if (server.host == ""){
     97 + errorFlash("No IMAP Host specified")
     98 + document.body.scrollTop = 0;
     99 + document.documentElement.scrollTop = 0;
     100 + return false
     101 + }
     102 + if (server.port == ""){
     103 + errorFlash("No IMAP Port specified")
     104 + document.body.scrollTop = 0;
     105 + document.documentElement.scrollTop = 0;
     106 + return false
     107 + }
     108 + if (isNaN(server.port) || server.port <1 || server.port > 65535 ){
     109 + errorFlash("Invalid IMAP Port")
     110 + document.body.scrollTop = 0;
     111 + document.documentElement.scrollTop = 0;
     112 + return false
     113 + }
     114 + 
     115 + var oldHTML = $("#validateimap").html();
     116 + // Disable inputs and change button text
     117 + $("#imaphost").attr("disabled", true);
     118 + $("#imapport").attr("disabled", true);
     119 + $("#imapusername").attr("disabled", true);
     120 + $("#imappassword").attr("disabled", true);
     121 + $("#use_imap").attr("disabled", true);
     122 + $("#use_tls").attr("disabled", true);
     123 + $("#folder").attr("disabled", true);
     124 + $("#restrictdomain").attr("disabled", true);
     125 + $('#deletecampaign').attr("disabled", true);
     126 + $('#lastlogin').attr("disabled", true);
     127 + $('#imapfreq').attr("disabled", true);
     128 + $("#validateimap").attr("disabled", true);
     129 + $("#validateimap").html("<i class='fa fa-circle-o-notch fa-spin'></i> Testing...");
     130 +
     131 + api.IMAP.validate(server).done(function(data) {
     132 + if (data.success == true) {
     133 + Swal.fire({
     134 + title: "Success",
     135 + html: "Logged into <b>" + $("#imaphost").val() + "</b>",
     136 + type: "success",
     137 + })
     138 + } else {
     139 + Swal.fire({
     140 + title: "Failed!",
     141 + html: "Unable to login to <b>" + $("#imaphost").val() + "</b>.",
     142 + type: "error",
     143 + showCancelButton: true,
     144 + cancelButtonText: "Close",
     145 + confirmButtonText: "More Info",
     146 + confirmButtonColor: "#428bca",
     147 + allowOutsideClick: false,
     148 + }).then(function(result) {
     149 + if (result.value) {
     150 + Swal.fire({
     151 + title: "Error:",
     152 + text: data.message,
     153 + })
     154 + }
     155 + })
     156 + }
     157 +
     158 + })
     159 + .fail(function() {
     160 + Swal.fire({
     161 + title: "Failed!",
     162 + text: "An unecpected error occured.",
     163 + type: "error",
     164 + })
     165 + })
     166 + .always(function() {
     167 + //Re-enable inputs and change button text
     168 + $("#imaphost").attr("disabled", false);
     169 + $("#imapport").attr("disabled", false);
     170 + $("#imapusername").attr("disabled", false);
     171 + $("#imappassword").attr("disabled", false);
     172 + $("#use_imap").attr("disabled", false);
     173 + $("#use_tls").attr("disabled", false);
     174 + $("#folder").attr("disabled", false);
     175 + $("#restrictdomain").attr("disabled", false);
     176 + $('#deletecampaign').attr("disabled", false);
     177 + $('#lastlogin').attr("disabled", false);
     178 + $('#imapfreq').attr("disabled", false);
     179 + $("#validateimap").attr("disabled", false);
     180 + $("#validateimap").html(oldHTML);
     181 + 
     182 + });
     183 + 
     184 + }); //end testclick
     185 + 
     186 + $("#reporttab").click(function() {
     187 + loadIMAPSettings()
     188 + })
     189 + 
     190 + $("#advanced").click(function() {
     191 + $("#advancedarea").toggle();
     192 + })
     193 + 
     194 + function loadIMAPSettings(){
     195 + api.IMAP.get()
     196 + .success(function (imap) {
     197 + if (imap.length == 0){
     198 + $('#lastlogindiv').hide()
     199 + } else {
     200 + imap = imap[0]
     201 + if (imap.enabled == false){
     202 + $('#lastlogindiv').hide()
     203 + } else {
     204 + $('#lastlogindiv').show()
     205 + }
     206 + $("#imapusername").val(imap.username)
     207 + $("#imaphost").val(imap.host)
     208 + $("#imapport").val(imap.port)
     209 + $("#imappassword").val(imap.password)
     210 + $('#use_tls').prop('checked', imap.tls)
     211 + $('#use_imap').prop('checked', imap.enabled)
     212 + $("#folder").val(imap.folder)
     213 + $("#restrictdomain").val(imap.restrict_domain)
     214 + $('#deletecampaign').prop('checked', imap.delete_reported_campaign_email)
     215 + $('#lastloginraw').val(imap.last_login)
     216 + $('#lastlogin').val(moment.utc(imap.last_login).fromNow())
     217 + $('#imapfreq').val(imap.imap_freq)
     218 + }
     219 + 
     220 + })
     221 + .error(function () {
     222 + errorFlash("Error fetching IMAP settings")
     223 + })
     224 + }
     225 + 
    24 226   var use_map = localStorage.getItem('gophish.use_map')
    25 227   $("#use_map").prop('checked', JSON.parse(use_map))
    26 228   $("#use_map").on('change', function () {
    27 229   localStorage.setItem('gophish.use_map', JSON.stringify(this.checked))
    28 230   })
     231 + 
     232 + loadIMAPSettings()
    29 233  })
  • ■ ■ ■ ■ ■ ■
    templates/settings.html
    skipped 9 lines
    10 10   data-toggle="tab">Account Settings</a></li>
    11 11   <li role="uiSettings"><a href="#uiSettings" aria-controls="uiSettings" role="tab" data-toggle="tab">UI
    12 12   Settings</a></li>
     13 + <li role="reportingSettings"><a href="#reportingSettings" aria-controls="reportingSettings" role="tab" id="reporttab"
     14 + data-toggle="tab">Reporting Settings</a></li>
    13 15   </ul>
    14 16   <!-- Tab Panes -->
    15 17   <div class="tab-content">
    skipped 66 lines
    82 84   <label for="use_map">Show campaign results map</label>
    83 85   </div>
    84 86   </div>
     87 + <!-- Reporting Settings Begin -->
     88 + <div role="tabpanel" class="tab-pane" id="reportingSettings">
     89 + <form id="imapForm" >
     90 + <br />
     91 + <div class="row">
     92 + <div class="col-md-6">
     93 + Monitor an IMAP account for emails reported by users.
     94 + </div>
     95 + </div>
     96 + <br />
     97 + 
     98 + <div class="row">
     99 + <div class="col-md-6">
     100 + <div class="checkbox checkbox-primary">
     101 + <input id="use_imap" type="checkbox">
     102 + <label for="use_imap">Enable Email Account Monitoring</label>
     103 + </div>
     104 + </div>
     105 + </div>
     106 + <br />
     107 + 
     108 +
     109 + <div class="row">
     110 + <label for="imaphost" class="col-sm-2 control-label form-label">IMAP Host:</label>
     111 + <div class="col-md-6">
     112 + <input type="text" id="imaphost" name="imaphost" placeholder="imap.example.com"
     113 + class="form-control" />
     114 + </div>
     115 + </div>
     116 + <br />
     117 + 
     118 + <div class="row">
     119 + <label for="imapport" class="col-sm-2 control-label form-label">IMAP Port:</label>
     120 + <div class="col-md-6">
     121 + <input type="text" id="imapport" name="imapport" placeholder="993"
     122 + class="form-control" />
     123 + </div>
     124 + </div>
     125 + <br />
     126 + 
     127 + <div class="row">
     128 + <label for="imapusername" class="col-sm-2 control-label form-label">IMAP Username:</label>
     129 + <div class="col-md-6">
     130 + <input type="text" id="imapusername" name="imapusername" placeholder="Username"
     131 + class="form-control" />
     132 + </div>
     133 + </div>
     134 + <br />
     135 + 
     136 + <div class="row">
     137 + <label for="imappassword" class="col-sm-2 control-label form-label">IMAP Password:</label>
     138 + <div class="col-md-6">
     139 + <input type="password" id="imappassword" name="imappassword" placeholder="Password" autocomplete="off"
     140 + class="form-control" />
     141 + </div>
     142 + </div>
     143 + <br />
     144 + 
     145 + 
     146 + <div class="row">
     147 + <label for="use_tls" class="col-sm-2 control-label form-label">Use TLS:</label>
     148 + <div class="col-md-6">
     149 + <div class="checkbox checkbox-primary">
     150 + <input id="use_tls" type="checkbox">
     151 + <label for="use_tls"></label>
     152 + </div>
     153 +
     154 + </div>
     155 + </div>
     156 + 
     157 + <!-- Advanced Settings-->
     158 + <div id="advancedarea" style="display: none;">
     159 + <hr>
     160 + <div class="row">
     161 + <label for="folder" class="col-sm-2 control-label form-label">Folder:</label>
     162 + <div class="col-md-6">
     163 + <input type="text" id="folder" name="folder" placeholder="Leave blank for default of INBOX."
     164 + class="form-control" />
     165 + </div>
     166 + </div>
     167 + <br />
     168 + 
     169 + <div class="row">
     170 + <label for="folder" class="col-sm-2 control-label form-label" data-toggle="tooltip" title="How often to check for new emails. 30 seconds minimum.">Polling frequency:</label>
     171 + <div class="col-md-6">
     172 + <input type="number" id="imapfreq" name="imapfreq" placeholder="Leave blank for default of every 60 seconds."
     173 + class="form-control" />
     174 + </div>
     175 + </div>
     176 + <br />
     177 +
     178 + <div class="row">
     179 + <label for="restrictdomain" class="col-sm-2 control-label form-label" data-toggle="tooltip" title="Only check emails reported from the supplied domain.">Restrict to domain:</label>
     180 + <div class="col-md-6">
     181 + <input type="text" id="restrictdomain" name="restrictdomain" placeholder="e.g. widgets.com. Leave blank for all domains."
     182 + class="form-control" />
     183 + </div>
     184 + </div>
     185 + <br />
     186 +
     187 + <div class="row">
     188 + <label for="deletecampaign" class="col-sm-2 control-label form-label" data-toggle="tooltip" title="Delete campaign emails after they've been reported.">Delete campaigns emails:</label>
     189 + <div class="col-md-6">
     190 + <div class="checkbox checkbox-primary">
     191 + <input id="deletecampaign" type="checkbox">
     192 + <label for="deletecampaign"></label>
     193 + </div>
     194 +
     195 + </div>
     196 + </div>
     197 + <br />
     198 +
     199 + <div class="row" id="lastlogindiv">
     200 + <label for="lastlogin" class="col-sm-2 control-label form-label">Last succesful login:</label>
     201 + <div class="col-md-6">
     202 + <input type="text" id="lastlogin" name="lastlogin" placeholder="Checking..." disabled
     203 + class="form-control border-0" />
     204 + </div>
     205 + </div>
     206 + <br />
     207 + <input type="hidden" id="lastloginraw" name="lastloginraw" value="">
     208 + 
     209 + </div>
     210 + 
     211 + <div class="row">
     212 + <label for="advancedsettings" class="col-sm-2 control-label form-label"></label>
     213 + <div class="col-md-6 text-right">
     214 + <button class="btn-xs btn-link" id="advanced" type="button">Advanced Settings</button>
     215 + </div>
     216 + </div>
     217 + 
     218 + <button class="btn btn-primary" id ="savesettings" type="button"><i class="fa fa-save"></i> Save</button>
     219 + <button class="btn btn-primary" id="validateimap" type="button"><i class="fa fa-wrench"></i> Test Settings</button>
     220 + 
     221 + 
     222 + </form>
     223 + </div>
     224 + <!-- Reporting Settings End -->
    85 225   </div>
    86 226  </div>
    87 227  {{end}} {{define "scripts"}}
    skipped 2 lines
Please wait...
Page is in error, reload to recover