Browse Source

Added IMAP support for checking reported emails (#1612)

Initial support of managing reporting through IMAP.

Co-Authored-By: Jordan Wright <jmwright798@gmail.com>
pull/1749/head
Glenn Wilkinson 1 year ago
committed by Jordan Wright
parent
commit
9de32746ee
12 changed files with 1163 additions and 1 deletions
  1. +1
    -1
      config.json
  2. +62
    -0
      controllers/api/imap.go
  3. +2
    -0
      controllers/api/server.go
  4. +8
    -0
      db/db_mysql/migrations/20200116000000_0.9.0_imap.sql
  5. +8
    -0
      db/db_sqlite3/migrations/20200116000000_0.9.0_imap.sql
  6. +7
    -0
      gophish.go
  7. +352
    -0
      imap/imap.go
  8. +194
    -0
      imap/monitor.go
  9. +153
    -0
      models/imap.go
  10. +32
    -0
      static/js/src/app/gophish.js
  11. +204
    -0
      static/js/src/app/settings.js
  12. +140
    -0
      templates/settings.html

+ 1
- 1
config.json View File

@ -19,4 +19,4 @@
"filename": "",
"level": ""
}
}
}

+ 62
- 0
controllers/api/imap.go View File

@ -0,0 +1,62 @@
package api
import (
"encoding/json"
"net/http"
"time"
ctx "github.com/gophish/gophish/context"
"github.com/gophish/gophish/imap"
"github.com/gophish/gophish/models"
)
// IMAPServerValidate handles requests for the /api/imapserver/validate endpoint
func (as *Server) IMAPServerValidate(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET":
JSONResponse(w, models.Response{Success: false, Message: "Only POSTs allowed"}, http.StatusBadRequest)
case r.Method == "POST":
im := models.IMAP{}
err := json.NewDecoder(r.Body).Decode(&im)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: "Invalid request"}, http.StatusBadRequest)
return
}
err = imap.Validate(&im)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusOK)
return
}
JSONResponse(w, models.Response{Success: true, Message: "Successful login."}, http.StatusCreated)
}
}
// IMAPServer handles requests for the /api/imapserver/ endpoint
func (as *Server) IMAPServer(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET":
ss, err := models.GetIMAP(ctx.Get(r, "user_id").(int64))
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}
JSONResponse(w, ss, http.StatusOK)
// POST: Update database
case r.Method == "POST":
im := models.IMAP{}
err := json.NewDecoder(r.Body).Decode(&im)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: "Invalid data. Please check your IMAP settings."}, http.StatusBadRequest)
return
}
im.ModifiedDate = time.Now().UTC()
im.UserId = ctx.Get(r, "user_id").(int64)
err = models.PostIMAP(&im, ctx.Get(r, "user_id").(int64))
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}
JSONResponse(w, models.Response{Success: true, Message: "Successfully saved IMAP settings."}, http.StatusCreated)
}
}

+ 2
- 0
controllers/api/server.go View File

@ -48,6 +48,8 @@ func (as *Server) registerRoutes() {
router := root.PathPrefix("/api/").Subrouter()
router.Use(mid.RequireAPIKey)
router.Use(mid.EnforceViewOnly)
router.HandleFunc("/imap/", as.IMAPServer)
router.HandleFunc("/imap/validate", as.IMAPServerValidate)
router.HandleFunc("/reset", as.Reset)
router.HandleFunc("/campaigns/", as.Campaigns)
router.HandleFunc("/campaigns/summary", as.CampaignsSummary)

+ 8
- 0
db/db_mysql/migrations/20200116000000_0.9.0_imap.sql View File

@ -0,0 +1,8 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
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);
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE `imap`;

+ 8
- 0
db/db_sqlite3/migrations/20200116000000_0.9.0_imap.sql View File

@ -0,0 +1,8 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
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);
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE "imap";

+ 7
- 0
gophish.go View File

@ -34,6 +34,7 @@ import (
"github.com/gophish/gophish/config"
"github.com/gophish/gophish/controllers"
"github.com/gophish/gophish/imap"
log "github.com/gophish/gophish/logger"
"github.com/gophish/gophish/middleware"
"github.com/gophish/gophish/models"
@ -80,6 +81,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
// Unlock any maillogs that may have been locked for processing
// when Gophish was last shutdown.
err = models.UnlockAllMailLogs()
@ -99,8 +101,11 @@ func main() {
phishConfig := conf.PhishConf
phishServer := controllers.NewPhishingServer(phishConfig)
imapMonitor := imap.NewMonitor()
go adminServer.Start()
go phishServer.Start()
go imapMonitor.Start()
// Handle graceful shutdown
c := make(chan os.Signal, 1)
@ -109,4 +114,6 @@ func main() {
log.Info("CTRL+C Received... Gracefully shutting down servers")
adminServer.Shutdown()
phishServer.Shutdown()
imapMonitor.Shutdown()
}

+ 352
- 0
imap/imap.go View File

@ -0,0 +1,352 @@
package imap
// Functionality taken from https://github.com/jprobinson/eazye
import (
"bytes"
"crypto/tls"
"fmt"
"strconv"
"time"
log "github.com/gophish/gophish/logger"
"github.com/gophish/gophish/models"
"github.com/jordan-wright/email"
"github.com/mxk/go-imap/imap"
)
// Client interface for IMAP interactions
type Client interface {
Close(expunge bool) (cmd *imap.Command, err error)
Login(username, password string) (cmd *imap.Command, err error)
Logout(timeout time.Duration) (cmd *imap.Command, err error)
Select(mbox string, readonly bool) (cmd *imap.Command, err error)
UIDFetch(seq *imap.SeqSet, items ...string) (cmd *imap.Command, err error)
UIDSearch(spec ...imap.Field) (cmd *imap.Command, err error)
UIDStore(seq *imap.SeqSet, item string, value imap.Field) (cmd *imap.Command, err error)
}
// Email represents an email.Email with an included IMAP UID
type Email struct {
UID uint32 `json:"uid"`
*email.Email
}
// Mailbox holds onto the credentials and other information
// needed for connecting to an IMAP server.
type Mailbox struct {
Host string
TLS bool
User string
Pwd string
Folder string
// Read only mode, false (original logic) if not initialized
ReadOnly bool
}
// GetAll will pull all emails from the email folder and return them as a list.
func (mbox *Mailbox) GetAll(markAsRead, delete bool) ([]Email, error) {
// call chan, put 'em in a list, return
var emails []Email
responses, err := mbox.GenerateAll(markAsRead, delete)
if err != nil {
return emails, err
}
for resp := range responses {
if resp.Err != nil {
return emails, resp.Err
}
emails = append(emails, resp.Email)
}
return emails, nil
}
// GenerateAll will find all emails in the email folder and pass them along to the responses channel.
func (mbox *Mailbox) GenerateAll(markAsRead, delete bool) (chan Response, error) {
return mbox.generateMail("ALL", nil, markAsRead, delete)
}
// GetUnread will find all unread emails in the folder and return them as a list.
func (mbox *Mailbox) GetUnread(markAsRead, delete bool) ([]Email, error) {
// call chan, put 'em in a list, return
var emails []Email
responses, err := mbox.GenerateUnread(markAsRead, delete)
if err != nil {
return emails, err
}
for resp := range responses {
if resp.Err != nil {
return emails, resp.Err
}
emails = append(emails, resp.Email)
}
return emails, nil
}
// GenerateUnread will find all unread emails in the folder and pass them along to the responses channel.
func (mbox *Mailbox) GenerateUnread(markAsRead, delete bool) (chan Response, error) {
return mbox.generateMail("UNSEEN", nil, markAsRead, delete)
}
// MarkAsUnread will set the UNSEEN flag on a supplied slice of UIDs
func (mbox *Mailbox) MarkAsUnread(uids []uint32) error {
client, err := mbox.newClient()
if err != nil {
return err
}
defer func() {
client.Close(true)
client.Logout(30 * time.Second)
}()
for _, u := range uids {
err := alterEmail(client, u, "\\SEEN", false)
if err != nil {
return err //return on first failure
}
}
return nil
}
// DeleteEmails will delete emails from the supplied slice of UIDs
func (mbox *Mailbox) DeleteEmails(uids []uint32) error {
client, err := mbox.newClient()
if err != nil {
return err
}
defer func() {
client.Close(true)
client.Logout(30 * time.Second)
}()
for _, u := range uids {
err := deleteEmail(client, u)
if err != nil {
return err //return on first failure
}
}
return nil
}
// Validate validates supplied IMAP model by connecting to the server
func Validate(s *models.IMAP) error {
err := s.Validate()
if err != nil {
log.Error(err)
return err
}
s.Host = s.Host + ":" + strconv.Itoa(int(s.Port)) // Append port
mailServer := Mailbox{
Host: s.Host,
TLS: s.TLS,
User: s.Username,
Pwd: s.Password,
Folder: s.Folder}
client, err := mailServer.newClient()
if err != nil {
log.Error(err.Error())
} else {
client.Close(true)
client.Logout(30 * time.Second)
}
return err
}
// Response is a helper struct to wrap the email responses and possible errors.
type Response struct {
Email Email
Err error
}
// newClient will initiate a new IMAP connection with the given creds.
func (mbox *Mailbox) newClient() (*imap.Client, error) {
var client *imap.Client
var err error
if mbox.TLS {
client, err = imap.DialTLS(mbox.Host, new(tls.Config))
if err != nil {
return client, err
}
} else {
client, err = imap.Dial(mbox.Host)
if err != nil {
return client, err
}
}
_, err = client.Login(mbox.User, mbox.Pwd)
if err != nil {
return client, err
}
_, err = imap.Wait(client.Select(mbox.Folder, mbox.ReadOnly))
if err != nil {
return client, err
}
return client, nil
}
const dateFormat = "02-Jan-2006"
// findEmails will run a find the UIDs of any emails that match the search.:
func findEmails(client Client, search string, since *time.Time) (*imap.Command, error) {
var specs []imap.Field
if len(search) > 0 {
specs = append(specs, search)
}
if since != nil {
sinceStr := since.Format(dateFormat)
specs = append(specs, "SINCE", sinceStr)
}
// get headers and UID for UnSeen message in src inbox...
cmd, err := imap.Wait(client.UIDSearch(specs...))
if err != nil {
return &imap.Command{}, fmt.Errorf("uid search failed: %s", err)
}
return cmd, nil
}
const GenerateBufferSize = 100
func (mbox *Mailbox) generateMail(search string, since *time.Time, markAsRead, delete bool) (chan Response, error) {
responses := make(chan Response, GenerateBufferSize)
client, err := mbox.newClient()
if err != nil {
close(responses)
return responses, fmt.Errorf("failed to create IMAP connection: %s", err)
}
go func() {
defer func() {
client.Close(true)
client.Logout(30 * time.Second)
close(responses)
}()
var cmd *imap.Command
// find all the UIDs
cmd, err = findEmails(client, search, since)
if err != nil {
responses <- Response{Err: err}
return
}
// gotta fetch 'em all
getEmails(client, cmd, markAsRead, delete, responses)
}()
return responses, nil
}
func getEmails(client Client, cmd *imap.Command, markAsRead, delete bool, responses chan Response) {
seq := &imap.SeqSet{}
msgCount := 0
for _, rsp := range cmd.Data {
for _, uid := range rsp.SearchResults() {
msgCount++
seq.AddNum(uid)
}
}
if seq.Empty() {
return
}
fCmd, err := imap.Wait(client.UIDFetch(seq, "INTERNALDATE", "BODY[]", "UID", "RFC822.HEADER"))
if err != nil {
responses <- Response{Err: fmt.Errorf("unable to perform uid fetch: %s", err)}
return
}
var email Email
for _, msgData := range fCmd.Data {
msgFields := msgData.MessageInfo().Attrs
// make sure is a legit response before we attempt to parse it
// deal with unsolicited FETCH responses containing only flags
// I'm lookin' at YOU, Gmail!
// http://mailman13.u.washington.edu/pipermail/imap-protocol/2014-October/002355.html
// http://stackoverflow.com/questions/26262472/gmail-imap-is-sometimes-returning-bad-results-for-fetch
if _, ok := msgFields["RFC822.HEADER"]; !ok {
continue
}
email, err = NewEmail(msgFields)
if err != nil {
responses <- Response{Err: fmt.Errorf("unable to parse email: %s", err)}
return
}
responses <- Response{Email: email}
if !markAsRead {
err = removeSeen(client, imap.AsNumber(msgFields["UID"]))
if err != nil {
responses <- Response{Err: fmt.Errorf("unable to remove seen flag: %s", err)}
return
}
}
if delete {
err = deleteEmail(client, imap.AsNumber(msgFields["UID"]))
if err != nil {
responses <- Response{Err: fmt.Errorf("unable to delete email: %s", err)}
return
}
}
}
return
}
func deleteEmail(client Client, UID uint32) error {
return alterEmail(client, UID, "\\DELETED", true)
}
func removeSeen(client Client, UID uint32) error {
return alterEmail(client, UID, "\\SEEN", false)
}
func alterEmail(client Client, UID uint32, flag string, plus bool) error {
flg := "-FLAGS"
if plus {
flg = "+FLAGS"
}
fSeq := &imap.SeqSet{}
fSeq.AddNum(UID)
_, err := imap.Wait(client.UIDStore(fSeq, flg, flag))
if err != nil {
return err
}
return nil
}
// NewEmail will parse an imap.FieldMap into an Email. This
// will expect the message to container the internaldate and the body with
// all headers included.
func NewEmail(msgFields imap.FieldMap) (Email, error) {
rawBody := imap.AsBytes(msgFields["BODY[]"])
rawBodyStream := bytes.NewReader(rawBody)
em, err := email.NewEmailFromReader(rawBodyStream) // Parse with @jordanwright's library
if err != nil {
return Email{}, err
}
iem := Email{
Email: em,
UID: imap.AsNumber(msgFields["UID"]),
}
return iem, err
}

+ 194
- 0
imap/monitor.go View File

@ -0,0 +1,194 @@
package imap
/* TODO:
* - Have a counter per config for number of consecutive login errors and backoff (e.g if supplied creds are incorrect)
* - Have a DB field "last_login_error" if last login failed
* - DB counter for non-campaign emails that the admin should investigate
* - Add field to User for numner of non-campaign emails reported
*/
import (
"context"
"regexp"
"strconv"
"strings"
"time"
log "github.com/gophish/gophish/logger"
"github.com/gophish/gophish/models"
)
// Pattern for GoPhish emails e.g ?rid=AbC123
var goPhishRegex = regexp.MustCompile("(\\?rid=[A-Za-z0-9]{7})")
// Monitor is a worker that monitors IMAP servers for reported campaign emails
type Monitor struct {
cancel func()
}
// Monitor.start() checks for campaign emails
// As each account can have its own polling frequency set we need to run one Go routine for
// each, as well as keeping an eye on newly created user accounts.
func (im *Monitor) start(ctx context.Context) {
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).
for {
select {
case <-ctx.Done():
return
default:
dbusers, err := models.GetUsers() //Slice of all user ids. Each user gets their own IMAP monitor routine.
if err != nil {
log.Error(err)
break
}
for _, dbuser := range dbusers {
if _, ok := usermap[dbuser.Id]; !ok { // If we don't currently have a running Go routine for this user, start one.
log.Info("Starting new IMAP monitor for user ", dbuser.Username)
usermap[dbuser.Id] = 1
go monitor(dbuser.Id, ctx)
}
}
time.Sleep(10 * time.Second) // Every ten seconds we check if a new user has been created
}
}
}
// 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.)
// It also verifies the user account exists, and returns if not (for the case of a user being deleted).
func monitor(uid int64, ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
// 1. Check if user exists, if not, return.
_, err := models.GetUser(uid)
if err != nil { // Not sure if there's a better way to determine user existence via id.
log.Info("User ", uid, " seems to have been deleted. Stopping IMAP monitor for this user.")
return
}
// 2. Check if user has IMAP settings.
imapSettings, err := models.GetIMAP(uid)
if err != nil {
log.Error(err)
break
}
if len(imapSettings) > 0 {
im := imapSettings[0]
// 3. Check if IMAP is enabled
if im.Enabled {
log.Debug("Checking IMAP for user ", uid, ": ", im.Username, "@", im.Host)
checkForNewEmails(im)
time.Sleep((time.Duration(im.IMAPFreq) - 10) * time.Second) // Subtract 10 to compensate for the default sleep of 10 at the bottom
}
}
}
time.Sleep(10 * time.Second)
}
}
// NewMonitor returns a new instance of imap.Monitor
func NewMonitor() *Monitor {
im := &Monitor{}
return im
}
// Start launches the IMAP campaign monitor
func (im *Monitor) Start() error {
log.Info("Starting IMAP monitor manager")
ctx, cancel := context.WithCancel(context.Background()) // ctx is the derivedContext
im.cancel = cancel
go im.start(ctx)
return nil
}
// Shutdown attempts to gracefully shutdown the IMAP monitor.
func (im *Monitor) Shutdown() error {
log.Info("Shutting down IMAP monitor manager")
im.cancel()
return nil
}
// checkForNewEmails logs into an IMAP account and checks unread emails
// for the rid campaign identifier.
func checkForNewEmails(im models.IMAP) {
im.Host = im.Host + ":" + strconv.Itoa(int(im.Port)) // Append port
mailServer := Mailbox{
Host: im.Host,
TLS: im.TLS,
User: im.Username,
Pwd: im.Password,
Folder: im.Folder}
msgs, err := mailServer.GetUnread(true, false)
if err != nil {
log.Error(err)
return
}
// Update last_succesful_login here via im.Host
err = models.SuccessfulLogin(&im)
if len(msgs) > 0 {
var reportingFailed []uint32 // UIDs of emails that were unable to be reported to phishing server, mark as unread
var campaignEmails []uint32 // UIDs of campaign emails. If DeleteReportedCampaignEmail is true, we will delete these
for _, m := range msgs {
// Check if sender is from company's domain, if enabled. TODO: Make this an IMAP filter
if im.RestrictDomain != "" { // e.g domainResitct = widgets.com
splitEmail := strings.Split(m.Email.From, "@")
senderDomain := splitEmail[len(splitEmail)-1]
if senderDomain != im.RestrictDomain {
log.Debug("Ignoring email as not from company domain: ", senderDomain)
continue
}
}
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?
rid := goPhishRegex.FindString(body)
if rid != "" {
rid = rid[5:]
log.Infof("User '%s' reported email with rid %s", m.Email.From, rid)
result, err := models.GetResult(rid)
if err != nil {
log.Error("Error reporting GoPhish email with rid ", rid, ": ", err.Error())
reportingFailed = append(reportingFailed, m.UID)
} else {
err = result.HandleEmailReport(models.EventDetails{})
if err != nil {
log.Error("Error updating GoPhish email with rid ", rid, ": ", err.Error())
} else {
if im.DeleteReportedCampaignEmail == true {
campaignEmails = append(campaignEmails, m.UID)
}
}
}
} else {
// In the future this should be an alert in Gophish
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)
}
// Check if any emails were unable to be reported, so we can mark them as unread
if len(reportingFailed) > 0 {
log.Debugf("Marking %d emails as unread as failed to report\n", len(reportingFailed))
err := mailServer.MarkAsUnread(reportingFailed) // Set emails as unread that we failed to report to GoPhish
if err != nil {
log.Error("Unable to mark emails as unread: ", err.Error())
}
}
// If the DeleteReportedCampaignEmail flag is set, delete reported Gophish campaign emails
if im.DeleteReportedCampaignEmail == true && len(campaignEmails) > 0 {
log.Debugf("Deleting %d campaign emails\n", len(campaignEmails))
err := mailServer.DeleteEmails(campaignEmails) // Delete GoPhish campaign emails.
if err != nil {
log.Error("Failed to delete emails: ", err.Error())
}
}
}
} else {
log.Debug("No new emails for ", im.Username)
}
}

+ 153
- 0
models/imap.go View File

@ -0,0 +1,153 @@
package models
import (
"errors"
"net"
"time"
log "github.com/gophish/gophish/logger"
)
const DefaultIMAPFolder = "INBOX"
const DefaultIMAPFreq = 60 // Every 60 seconds
// IMAP contains the attributes needed to handle logging into an IMAP server to check
// for reported emails
type IMAP struct {
UserId int64 `json:"-" gorm:"column:user_id"`
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port uint16 `json:"port,string,omitempty"`
Username string `json:"username"`
Password string `json:"password"`
TLS bool `json:"tls"`
Folder string `json:"folder"`
RestrictDomain string `json:"restrict_domain"`
DeleteReportedCampaignEmail bool `json:"delete_reported_campaign_email"`
LastLogin time.Time `json:"last_login,omitempty"`
ModifiedDate time.Time `json:"modified_date"`
IMAPFreq uint32 `json:"imap_freq,string,omitempty"`
}
// ErrIMAPHostNotSpecified is thrown when there is no Host specified
// in the IMAP configuration
var ErrIMAPHostNotSpecified = errors.New("No IMAP Host specified")
// ErrIMAPPortNotSpecified is thrown when there is no Port specified
// in the IMAP configuration
var ErrIMAPPortNotSpecified = errors.New("No IMAP Port specified")
// ErrInvalidIMAPHost indicates that the IMAP server string is invalid
var ErrInvalidIMAPHost = errors.New("Invalid IMAP server address")
// ErrInvalidIMAPPort indicates that the IMAP Port is invalid
var ErrInvalidIMAPPort = errors.New("Invalid IMAP Port")
// ErrIMAPUsernameNotSpecified is thrown when there is no Username specified
// in the IMAP configuration
var ErrIMAPUsernameNotSpecified = errors.New("No Username specified")
// ErrIMAPPasswordNotSpecified is thrown when there is no Password specified
// in the IMAP configuration
var ErrIMAPPasswordNotSpecified = errors.New("No Password specified")
// ErrInvalidIMAPFreq is thrown when the frequency for polling the
// IMAP server is invalid
var ErrInvalidIMAPFreq = errors.New("Invalid polling frequency.")
// TableName specifies the database tablename for Gorm to use
func (im IMAP) TableName() string {
return "imap"
}
// Validate ensures that IMAP configs/connections are valid
func (im *IMAP) Validate() error {
switch {
case im.Host == "":
return ErrIMAPHostNotSpecified
case im.Port == 0:
return ErrIMAPPortNotSpecified
case im.Username == "":
return ErrIMAPUsernameNotSpecified
case im.Password == "":
return ErrIMAPPasswordNotSpecified
}
// Set the default value for Folder
if im.Folder == "" {
im.Folder = DefaultIMAPFolder
}
// Make sure im.Host is an IP or hostname. NB will fail if unable to resolve the hostname.
ip := net.ParseIP(im.Host)
_, err := net.LookupHost(im.Host)
if ip == nil && err != nil {
return ErrInvalidIMAPHost
}
// Make sure 1 >= port <= 65535
if im.Port < 1 || im.Port > 65535 {
return ErrInvalidIMAPPort
}
// Make sure the polling frequency is between every 30 seconds and every year
// If not set it to the default
if im.IMAPFreq < 30 || im.IMAPFreq > 31540000 {
im.IMAPFreq = DefaultIMAPFreq
}
return nil
}
// GetIMAP returns the IMAP server owned by the given user.
func GetIMAP(uid int64) ([]IMAP, error) {
im := []IMAP{}
count := 0
err := db.Where("user_id=?", uid).Find(&im).Count(&count).Error
if err != nil {
log.Error(err)
return im, err
}
return im, nil
}
// PostIMAP updates IMAP settings for a user in the database.
func PostIMAP(im *IMAP, uid int64) error {
err := im.Validate()
if err != nil {
log.Error(err)
return err
}
// Delete old entry. TODO: Save settings and if fails to Save below replace with original
err = DeleteIMAP(uid)
if err != nil {
log.Error(err)
return err
}
// Insert new settings into the DB
err = db.Save(im).Error
if err != nil {
log.Error("Unable to save to database: ", err.Error())
}
return err
}
// DeleteIMAP deletes the existing IMAP in the database.
func DeleteIMAP(uid int64) error {
err := db.Where("user_id=?", uid).Delete(&IMAP{}).Error
if err != nil {
log.Error(err)
}
return err
}
func SuccessfulLogin(im *IMAP) error {
err := db.Model(&im).Where("user_id = ?", im.UserId).Update("last_login", time.Now().UTC()).Error
if err != nil {
log.Error("Unable to update database: ", err.Error())
}
return err
}

+ 32
- 0
static/js/src/app/gophish.js View File

@ -10,6 +10,26 @@ function successFlash(message) {
<i class=\"fa fa-check-circle\"></i> " + message + "</div>")
}
// Fade message after n seconds
function errorFlashFade(message, fade) {
$("#flashes").empty()
$("#flashes").append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
<i class=\"fa fa-exclamation-circle\"></i> " + message + "</div>")
setTimeout(function(){
$("#flashes").empty()
}, fade * 1000);
}
// Fade message after n seconds
function successFlashFade(message, fade) {
$("#flashes").empty()
$("#flashes").append("<div style=\"text-align:center\" class=\"alert alert-success\">\
<i class=\"fa fa-check-circle\"></i> " + message + "</div>")
setTimeout(function(){
$("#flashes").empty()
}, fade * 1000);
}
function modalError(message) {
$("#modal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
<i class=\"fa fa-exclamation-circle\"></i> " + message + "</div>")
@ -197,6 +217,18 @@ var api = {
return query("/smtp/" + id, "DELETE", {}, false)
}
},
// IMAP containts the endpoints for /imap/
IMAP: {
get: function() {
return query("/imap/", "GET", {}, !1)
},
post: function(e) {
return query("/imap/", "POST", e, !1)
},
validate: function(e) {
return query("/imap/validate", "POST", e, true)
}
},
// users contains the endpoints for /users
users: {
// get() - Queries the API for GET /users

+ 204
- 0
static/js/src/app/settings.js View File

@ -1,4 +1,5 @@
$(document).ready(function () {
$('[data-toggle="tooltip"]').tooltip();
$("#apiResetForm").submit(function (e) {
api.reset()
.success(function (response) {
@ -21,9 +22,212 @@ $(document).ready(function () {
})
return false
})
//$("#imapForm").submit(function (e) {
$("#savesettings").click(function() {
var imapSettings = {}
imapSettings.host = $("#imaphost").val()
imapSettings.port = $("#imapport").val()
imapSettings.username = $("#imapusername").val()
imapSettings.password = $("#imappassword").val()
imapSettings.enabled = $('#use_imap').prop('checked')
imapSettings.tls = $('#use_tls').prop('checked')
//Advanced settings
imapSettings.folder = $("#folder").val()
imapSettings.imap_freq = $("#imapfreq").val()
imapSettings.restrict_domain = $("#restrictdomain").val()
imapSettings.delete_reported_campaign_email = $('#deletecampaign').prop('checked')
//To avoid unmarshalling error in controllers/api/imap.go. It would fail gracefully, but with a generic error.
if (imapSettings.host == ""){
errorFlash("No IMAP Host specified")
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
return false
}
if (imapSettings.port == ""){
errorFlash("No IMAP Port specified")
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
return false
}
if (isNaN(imapSettings.port) || imapSettings.port <1 || imapSettings.port > 65535 ){
errorFlash("Invalid IMAP Port")
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
return false
}
if (imapSettings.imap_freq == ""){
imapSettings.imap_freq = "60"
}
api.IMAP.post(imapSettings).done(function (data) {
if (data.success == true) {
successFlashFade("Successfully updated IMAP settings.", 2)
} else {
errorFlash("Unable to update IMAP settings.")
}
})
.success(function (data){
loadIMAPSettings()
})
.fail(function (data) {
errorFlash(data.responseJSON.message)
})
.always(function (data){
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
})
return false
})
$("#validateimap").click(function() {
// Query validate imap server endpoint
var server = {}
server.host = $("#imaphost").val()
server.port = $("#imapport").val()
server.username = $("#imapusername").val()
server.password = $("#imappassword").val()
server.tls = $('#use_tls').prop('checked')
//To avoid unmarshalling error in controllers/api/imap.go. It would fail gracefully, but with a generic error.
if (server.host == ""){
errorFlash("No IMAP Host specified")
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
return false
}
if (server.port == ""){
errorFlash("No IMAP Port specified")
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
return false
}
if (isNaN(server.port) || server.port <1 || server.port > 65535 ){
errorFlash("Invalid IMAP Port")
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
return false
}
var oldHTML = $("#validateimap").html();
// Disable inputs and change button text
$("#imaphost").attr("disabled", true);
$("#imapport").attr("disabled", true);
$("#imapusername").attr("disabled", true);
$("#imappassword").attr("disabled", true);
$("#use_imap").attr("disabled", true);
$("#use_tls").attr("disabled", true);
$("#folder").attr("disabled", true);
$("#restrictdomain").attr("disabled", true);
$('#deletecampaign').attr("disabled", true);
$('#lastlogin').attr("disabled", true);
$('#imapfreq').attr("disabled", true);
$("#validateimap").attr("disabled", true);
$("#validateimap").html("<i class='fa fa-circle-o-notch fa-spin'></i> Testing...");
api.IMAP.validate(server).done(function(data) {
if (data.success == true) {
Swal.fire({
title: "Success",
html: "Logged into <b>" + $("#imaphost").val() + "</b>",
type: "success",
})
} else {
Swal.fire({
title: "Failed!",
html: "Unable to login to <b>" + $("#imaphost").val() + "</b>.",
type: "error",
showCancelButton: true,
cancelButtonText: "Close",
confirmButtonText: "More Info",
confirmButtonColor: "#428bca",
allowOutsideClick: false,
}).then(function(result) {
if (result.value) {
Swal.fire({
title: "Error:",
text: data.message,
})
}
})
}
})
.fail(function() {
Swal.fire({
title: "Failed!",
text: "An unecpected error occured.",
type: "error",
})
})
.always(function() {
//Re-enable inputs and change button text
$("#imaphost").attr("disabled", false);
$("#imapport").attr("disabled", false);
$("#imapusername").attr("disabled", false);
$("#imappassword").attr("disabled", false);
$("#use_imap").attr("disabled", false);
$("#use_tls").attr("disabled", false);
$("#folder").attr("disabled", false);
$("#restrictdomain").attr("disabled", false);
$('#deletecampaign').attr("disabled", false);
$('#lastlogin').attr("disabled", false);
$('#imapfreq').attr("disabled", false);
$("#validateimap").attr("disabled", false);
$("#validateimap").html(oldHTML);
});
}); //end testclick
$("#reporttab").click(function() {
loadIMAPSettings()
})
$("#advanced").click(function() {
$("#advancedarea").toggle();
})
function loadIMAPSettings(){
api.IMAP.get()
.success(function (imap) {
if (imap.length == 0){
$('#lastlogindiv').hide()
} else {
imap = imap[0]
if (imap.enabled == false){
$('#lastlogindiv').hide()
} else {
$('#lastlogindiv').show()
}
$("#imapusername").val(imap.username)
$("#imaphost").val(imap.host)
$("#imapport").val(imap.port)
$("#imappassword").val(imap.password)
$('#use_tls').prop('checked', imap.tls)
$('#use_imap').prop('checked', imap.enabled)
$("#folder").val(imap.folder)
$("#restrictdomain").val(imap.restrict_domain)
$('#deletecampaign').prop('checked', imap.delete_reported_campaign_email)
$('#lastloginraw').val(imap.last_login)
$('#lastlogin').val(moment.utc(imap.last_login).fromNow())
$('#imapfreq').val(imap.imap_freq)
}
})
.error(function () {
errorFlash("Error fetching IMAP settings")
})
}
var use_map = localStorage.getItem('gophish.use_map')
$("#use_map").prop('checked', JSON.parse(use_map))
$("#use_map").on('change', function () {
localStorage.setItem('gophish.use_map', JSON.stringify(this.checked))
})
loadIMAPSettings()
})

+ 140
- 0
templates/settings.html View File

@ -10,6 +10,8 @@
data-toggle="tab">Account Settings</a></li>
<li role="uiSettings"><a href="#uiSettings" aria-controls="uiSettings" role="tab" data-toggle="tab">UI
Settings</a></li>
<li role="reportingSettings"><a href="#reportingSettings" aria-controls="reportingSettings" role="tab" id="reporttab"
data-toggle="tab">Reporting Settings</a></li>
</ul>
<!-- Tab Panes -->
<div class="tab-content">
@ -82,6 +84,144 @@
<label for="use_map">Show campaign results map</label>
</div>
</div>
<!-- Reporting Settings Begin -->
<div role="tabpanel" class="tab-pane" id="reportingSettings">
<form id="imapForm" >
<br />
<div class="row">
<div class="col-md-6">
Monitor an IMAP account for emails reported by users.
</div>
</div>
<br />
<div class="row">
<div class="col-md-6">
<div class="checkbox checkbox-primary">
<input id="use_imap" type="checkbox">
<label for="use_imap">Enable Email Account Monitoring</label>
</div>
</div>
</div>
<br />
<div class="row">
<label for="imaphost" class="col-sm-2 control-label form-label">IMAP Host:</label>
<div class="col-md-6">
<input type="text" id="imaphost" name="imaphost" placeholder="imap.example.com"
class="form-control" />
</div>
</div>
<br />
<div class="row">
<label for="imapport" class="col-sm-2 control-label form-label">IMAP Port:</label>
<div class="col-md-6">
<input type="text" id="imapport" name="imapport" placeholder="993"
class="form-control" />
</div>
</div>
<br />
<div class="row">
<label for="imapusername" class="col-sm-2 control-label form-label">IMAP Username:</label>
<div class="col-md-6">
<input type="text" id="imapusername" name="imapusername" placeholder="Username"
class="form-control" />
</div>
</div>
<br />
<div class="row">
<label for="imappassword" class="col-sm-2 control-label form-label">IMAP Password:</label>
<div class="col-md-6">
<input type="password" id="imappassword" name="imappassword" placeholder="Password" autocomplete="off"
class="form-control" />
</div>
</div>
<br />
<div class="row">
<label for="use_tls" class="col-sm-2 control-label form-label">Use TLS:</label>
<div class="col-md-6">
<div class="checkbox checkbox-primary">
<input id="use_tls" type="checkbox">
<label for="use_tls"></label>
</div>
</div>
</div>
<!-- Advanced Settings-->
<div id="advancedarea" style="display: none;">
<hr>
<div class="row">
<label for="folder" class="col-sm-2 control-label form-label">Folder:</label>
<div class="col-md-6">
<input type="text" id="folder" name="folder" placeholder="Leave blank for default of INBOX."
class="form-control" />
</div>
</div>
<br />
<div class="row">
<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>
<div class="col-md-6">
<input type="number" id="imapfreq" name="imapfreq" placeholder="Leave blank for default of every 60 seconds."
class="form-control" />
</div>
</div>
<br />
<div class="row">
<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>
<div class="col-md-6">
<input type="text" id="restrictdomain" name="restrictdomain" placeholder="e.g. widgets.com. Leave blank for all domains."
class="form-control" />
</div>
</div>
<br />
<div class="row">
<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>
<div class="col-md-6">
<div class="checkbox checkbox-primary">
<input id="deletecampaign" type="checkbox">
<label for="deletecampaign"></label>
</div>
</div>
</div>
<br />
<div class="row" id="lastlogindiv">
<label for="lastlogin" class="col-sm-2 control-label form-label">Last succesful login:</label>
<div class="col-md-6">
<input type="text" id="lastlogin" name="lastlogin" placeholder="Checking..." disabled
class="form-control border-0" />
</div>
</div>
<br />
<input type="hidden" id="lastloginraw" name="lastloginraw" value="">
</div>
<div class="row">
<label for="advancedsettings" class="col-sm-2 control-label form-label"></label>
<div class="col-md-6 text-right">
<button class="btn-xs btn-link" id="advanced" type="button">Advanced Settings</button>
</div>
</div>
<button class="btn btn-primary" id ="savesettings" type="button"><i class="fa fa-save"></i> Save</button>
<button class="btn btn-primary" id="validateimap" type="button"><i class="fa fa-wrench"></i> Test Settings</button>
</form>
</div>
<!-- Reporting Settings End -->
</div>
</div>
{{end}} {{define "scripts"}}

Loading…
Cancel
Save