mirror of https://github.com/gophish/gophish
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
parent
caede2e40b
commit
9de32746ee
|
@ -19,4 +19,4 @@
|
|||
"filename": "",
|
||||
"level": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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`;
|
|
@ -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";
|
|
@ -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()
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
})
|
|
@ -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…
Reference in New Issue