mirror of https://github.com/gophish/gophish
parent
18d92a8f74
commit
76ece15b71
|
@ -29,6 +29,7 @@ type Config struct {
|
||||||
DBName string `json:"db_name"`
|
DBName string `json:"db_name"`
|
||||||
DBPath string `json:"db_path"`
|
DBPath string `json:"db_path"`
|
||||||
MigrationsPath string `json:"migrations_prefix"`
|
MigrationsPath string `json:"migrations_prefix"`
|
||||||
|
TestFlag bool `json:"test_flag"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conf contains the initialized configuration struct
|
// Conf contains the initialized configuration struct
|
||||||
|
@ -48,4 +49,6 @@ func LoadConfig(filepath string) {
|
||||||
|
|
||||||
// Choosing the migrations directory based on the database used.
|
// Choosing the migrations directory based on the database used.
|
||||||
Conf.MigrationsPath = Conf.MigrationsPath + Conf.DBName
|
Conf.MigrationsPath = Conf.MigrationsPath + Conf.DBName
|
||||||
|
// Explicitly set the TestFlag to false to prevent config.json overrides
|
||||||
|
Conf.TestFlag = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,6 +83,11 @@ func API_Campaigns(w http.ResponseWriter, r *http.Request) {
|
||||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// If the campaign is scheduled to launch immediately, send it to the worker.
|
||||||
|
// Otherwise, the worker will pick it up at the scheduled time
|
||||||
|
if c.Status == models.CAMPAIGN_IN_PROGRESS {
|
||||||
|
go Worker.LaunchCampaign(c)
|
||||||
|
}
|
||||||
JSONResponse(w, c, http.StatusCreated)
|
JSONResponse(w, c, http.StatusCreated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -645,7 +650,9 @@ func API_Import_Site(w http.ResponseWriter, r *http.Request) {
|
||||||
// API_Send_Test_Email sends a test email using the template name
|
// API_Send_Test_Email sends a test email using the template name
|
||||||
// and Target given.
|
// and Target given.
|
||||||
func API_Send_Test_Email(w http.ResponseWriter, r *http.Request) {
|
func API_Send_Test_Email(w http.ResponseWriter, r *http.Request) {
|
||||||
s := &models.SendTestEmailRequest{}
|
s := &models.SendTestEmailRequest{
|
||||||
|
ErrorChan: make(chan error),
|
||||||
|
}
|
||||||
if r.Method != "POST" {
|
if r.Method != "POST" {
|
||||||
JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest)
|
JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
@ -706,7 +713,7 @@ func API_Send_Test_Email(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the test email
|
// Send the test email
|
||||||
err = worker.SendTestEmail(s)
|
err = Worker.SendTestEmail(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
-- +goose Up
|
||||||
|
-- SQL in section 'Up' is executed when this migration is applied
|
||||||
|
CREATE TABLE IF NOT EXISTS "mail_logs" (
|
||||||
|
"id" integer primary key autoincrement,
|
||||||
|
"campaign_id" integer,
|
||||||
|
"user_id" integer,
|
||||||
|
"send_date" datetime,
|
||||||
|
"send_attempt" integer,
|
||||||
|
"r_id" varchar(255),
|
||||||
|
"processing" boolean);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- SQL section 'Down' is executed when this migration is rolled back
|
||||||
|
DROP TABLE "mail_logs"
|
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
-- +goose Up
|
||||||
|
-- SQL in section 'Up' is executed when this migration is applied
|
||||||
|
ALTER TABLE results ADD COLUMN send_date DATETIME;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- SQL section 'Down' is executed when this migration is rolled back
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
-- +goose Up
|
||||||
|
-- SQL in section 'Up' is executed when this migration is applied
|
||||||
|
CREATE TABLE IF NOT EXISTS "mail_logs" (
|
||||||
|
"id" integer primary key autoincrement,
|
||||||
|
"campaign_id" integer,
|
||||||
|
"user_id" integer,
|
||||||
|
"send_date" datetime,
|
||||||
|
"send_attempt" integer,
|
||||||
|
"r_id" varchar(255),
|
||||||
|
"processing" boolean);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- SQL section 'Down' is executed when this migration is rolled back
|
||||||
|
DROP TABLE "mail_logs"
|
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
-- +goose Up
|
||||||
|
-- SQL in section 'Up' is executed when this migration is applied
|
||||||
|
ALTER TABLE results ADD COLUMN send_date DATETIME;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- SQL section 'Down' is executed when this migration is rolled back
|
||||||
|
|
17
gophish.go
17
gophish.go
|
@ -26,9 +26,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"fmt"
|
"context"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -40,6 +40,7 @@ import (
|
||||||
"github.com/gophish/gophish/auth"
|
"github.com/gophish/gophish/auth"
|
||||||
"github.com/gophish/gophish/config"
|
"github.com/gophish/gophish/config"
|
||||||
"github.com/gophish/gophish/controllers"
|
"github.com/gophish/gophish/controllers"
|
||||||
|
"github.com/gophish/gophish/mailer"
|
||||||
"github.com/gophish/gophish/models"
|
"github.com/gophish/gophish/models"
|
||||||
"github.com/gophish/gophish/util"
|
"github.com/gophish/gophish/util"
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
|
@ -66,10 +67,20 @@ func main() {
|
||||||
// Load the config
|
// Load the config
|
||||||
config.LoadConfig(*configPath)
|
config.LoadConfig(*configPath)
|
||||||
config.Version = string(version)
|
config.Version = string(version)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go mailer.Mailer.Start(ctx)
|
||||||
// Setup the global variables and settings
|
// Setup the global variables and settings
|
||||||
err = models.Setup()
|
err = models.Setup()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
Logger.Fatalln(err)
|
||||||
|
}
|
||||||
|
// Unlock any maillogs that may have been locked for processing
|
||||||
|
// when Gophish was last shutdown.
|
||||||
|
err = models.UnlockAllMailLogs()
|
||||||
|
if err != nil {
|
||||||
|
Logger.Fatalln(err)
|
||||||
}
|
}
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
|
|
@ -0,0 +1,184 @@
|
||||||
|
package mailer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/textproto"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/gophish/gomail"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaxReconnectAttempts is the maximum number of times we should reconnect to a server
|
||||||
|
var MaxReconnectAttempts = 10
|
||||||
|
|
||||||
|
// ErrMaxConnectAttempts is thrown when the maximum number of reconnect attempts
|
||||||
|
// is reached.
|
||||||
|
var ErrMaxConnectAttempts = errors.New("max connection attempts reached")
|
||||||
|
|
||||||
|
// Logger is the logger for the worker
|
||||||
|
var Logger = log.New(os.Stdout, " ", log.Ldate|log.Ltime|log.Lshortfile)
|
||||||
|
|
||||||
|
// Sender exposes the common operations required for sending email.
|
||||||
|
type Sender interface {
|
||||||
|
Send(from string, to []string, msg io.WriterTo) error
|
||||||
|
Close() error
|
||||||
|
Reset() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dialer dials to an SMTP server and returns the SendCloser
|
||||||
|
type Dialer interface {
|
||||||
|
Dial() (Sender, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mail is an interface that handles the common operations for email messages
|
||||||
|
type Mail interface {
|
||||||
|
Backoff(reason error) error
|
||||||
|
Error(err error) error
|
||||||
|
Success() error
|
||||||
|
Generate(msg *gomail.Message) error
|
||||||
|
GetDialer() (Dialer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mailer is a global instance of the mailer that can
|
||||||
|
// be used in applications. It is the responsibility of the application
|
||||||
|
// to call Mailer.Start()
|
||||||
|
var Mailer *MailWorker
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Mailer = NewMailWorker()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MailWorker is the worker that receives slices of emails
|
||||||
|
// on a channel to send. It's assumed that every slice of emails received is meant
|
||||||
|
// to be sent to the same server.
|
||||||
|
type MailWorker struct {
|
||||||
|
Queue chan []Mail
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMailWorker returns an instance of MailWorker with the mail queue
|
||||||
|
// initialized.
|
||||||
|
func NewMailWorker() *MailWorker {
|
||||||
|
return &MailWorker{
|
||||||
|
Queue: make(chan []Mail),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start launches the mail worker to begin listening on the Queue channel
|
||||||
|
// for new slices of Mail instances to process.
|
||||||
|
func (mw *MailWorker) Start(ctx context.Context) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case ms := <-mw.Queue:
|
||||||
|
go func(ctx context.Context, ms []Mail) {
|
||||||
|
Logger.Printf("Mailer got %d mail to send", len(ms))
|
||||||
|
dialer, err := ms[0].GetDialer()
|
||||||
|
if err != nil {
|
||||||
|
errorMail(err, ms)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendMail(ctx, dialer, ms)
|
||||||
|
}(ctx, ms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorMail is a helper to handle erroring out a slice of Mail instances
|
||||||
|
// in the case that an unrecoverable error occurs.
|
||||||
|
func errorMail(err error, ms []Mail) {
|
||||||
|
for _, m := range ms {
|
||||||
|
m.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dialHost attempts to make a connection to the host specified by the Dialer.
|
||||||
|
// It returns MaxReconnectAttempts if the number of connection attempts has been
|
||||||
|
// exceeded.
|
||||||
|
func dialHost(ctx context.Context, dialer Dialer) (Sender, error) {
|
||||||
|
sendAttempt := 0
|
||||||
|
var sender Sender
|
||||||
|
var err error
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, nil
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sender, err = dialer.Dial()
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sendAttempt++
|
||||||
|
if sendAttempt == MaxReconnectAttempts {
|
||||||
|
err = ErrMaxConnectAttempts
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sender, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendMail attempts to send the provided Mail instances.
|
||||||
|
// If the context is cancelled before all of the mail are sent,
|
||||||
|
// sendMail just returns and does not modify those emails.
|
||||||
|
func sendMail(ctx context.Context, dialer Dialer, ms []Mail) {
|
||||||
|
sender, err := dialHost(ctx, dialer)
|
||||||
|
if err != nil {
|
||||||
|
errorMail(err, ms)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer sender.Close()
|
||||||
|
message := gomail.NewMessage()
|
||||||
|
for _, m := range ms {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
message.Reset()
|
||||||
|
|
||||||
|
err = m.Generate(message)
|
||||||
|
if err != nil {
|
||||||
|
m.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = gomail.Send(sender, message)
|
||||||
|
if err != nil {
|
||||||
|
if te, ok := err.(*textproto.Error); ok {
|
||||||
|
switch {
|
||||||
|
// If it's a temporary error, we should backoff and try again later.
|
||||||
|
// We'll reset the connection so future messages don't incur a
|
||||||
|
// different error (see https://github.com/gophish/gophish/issues/787).
|
||||||
|
case te.Code >= 400 && te.Code <= 499:
|
||||||
|
m.Backoff(err)
|
||||||
|
sender.Reset()
|
||||||
|
continue
|
||||||
|
// Otherwise, if it's a permanent error, we shouldn't backoff this message,
|
||||||
|
// since the RFC specifies that running the same commands won't work next time.
|
||||||
|
// We should reset our sender and error this message out.
|
||||||
|
case te.Code >= 500 && te.Code <= 599:
|
||||||
|
m.Error(err)
|
||||||
|
sender.Reset()
|
||||||
|
continue
|
||||||
|
// If something else happened, let's just error out and reset the
|
||||||
|
// sender
|
||||||
|
default:
|
||||||
|
m.Error(err)
|
||||||
|
sender.Reset()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m.Error(err)
|
||||||
|
sender.Reset()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.Success()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,282 @@
|
||||||
|
package mailer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/textproto"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MailerSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateMessages(dialer Dialer) []Mail {
|
||||||
|
to := []string{"to@example.com"}
|
||||||
|
|
||||||
|
messageContents := []io.WriterTo{
|
||||||
|
bytes.NewBuffer([]byte("First email")),
|
||||||
|
bytes.NewBuffer([]byte("Second email")),
|
||||||
|
}
|
||||||
|
|
||||||
|
m1 := newMockMessage("first@example.com", to, messageContents[0])
|
||||||
|
m2 := newMockMessage("second@example.com", to, messageContents[1])
|
||||||
|
|
||||||
|
m1.setDialer(func() (Dialer, error) { return dialer, nil })
|
||||||
|
|
||||||
|
messages := []Mail{m1, m2}
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockErrorSender(err error) *mockSender {
|
||||||
|
sender := newMockSender()
|
||||||
|
// The sending function will send a temporary error to emulate
|
||||||
|
// a backoff.
|
||||||
|
sender.setSend(func(mm *mockMessage) error {
|
||||||
|
if len(sender.messages) == 1 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sender.messageChan <- mm
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return sender
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *MailerSuite) TestDialHost() {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
md := newMockDialer()
|
||||||
|
md.setDial(md.unreachableDial)
|
||||||
|
_, err := dialHost(ctx, md)
|
||||||
|
if err != ErrMaxConnectAttempts {
|
||||||
|
ms.T().Fatalf("Didn't receive expected ErrMaxConnectAttempts. Got: %s", err)
|
||||||
|
}
|
||||||
|
if md.dialCount != MaxReconnectAttempts {
|
||||||
|
ms.T().Fatalf("Unexpected number of reconnect attempts. Expected %d, Got %d", MaxReconnectAttempts, md.dialCount)
|
||||||
|
}
|
||||||
|
md.setDial(md.defaultDial)
|
||||||
|
_, err = dialHost(ctx, md)
|
||||||
|
if err != nil {
|
||||||
|
ms.T().Fatalf("Unexpected error when dialing the mock host: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *MailerSuite) TestMailWorkerStart() {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
mw := NewMailWorker()
|
||||||
|
go func(ctx context.Context) {
|
||||||
|
mw.Start(ctx)
|
||||||
|
}(ctx)
|
||||||
|
|
||||||
|
sender := newMockSender()
|
||||||
|
dialer := newMockDialer()
|
||||||
|
dialer.setDial(func() (Sender, error) {
|
||||||
|
return sender, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
messages := generateMessages(dialer)
|
||||||
|
|
||||||
|
// Send the campaign
|
||||||
|
mw.Queue <- messages
|
||||||
|
|
||||||
|
got := []*mockMessage{}
|
||||||
|
|
||||||
|
idx := 0
|
||||||
|
for message := range sender.messageChan {
|
||||||
|
got = append(got, message)
|
||||||
|
original := messages[idx].(*mockMessage)
|
||||||
|
if original.from != message.from {
|
||||||
|
ms.T().Fatalf("Invalid message received. Expected %s, Got %s", original.from, message.from)
|
||||||
|
}
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
if len(got) != len(messages) {
|
||||||
|
ms.T().Fatalf("Unexpected number of messages received. Expected %d Got %d", len(got), len(messages))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *MailerSuite) TestBackoff() {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
mw := NewMailWorker()
|
||||||
|
go func(ctx context.Context) {
|
||||||
|
mw.Start(ctx)
|
||||||
|
}(ctx)
|
||||||
|
|
||||||
|
expectedError := &textproto.Error{
|
||||||
|
Code: 400,
|
||||||
|
Msg: "Temporary error",
|
||||||
|
}
|
||||||
|
|
||||||
|
sender := newMockErrorSender(expectedError)
|
||||||
|
dialer := newMockDialer()
|
||||||
|
dialer.setDial(func() (Sender, error) {
|
||||||
|
return sender, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
messages := generateMessages(dialer)
|
||||||
|
|
||||||
|
// Send the campaign
|
||||||
|
mw.Queue <- messages
|
||||||
|
|
||||||
|
got := []*mockMessage{}
|
||||||
|
|
||||||
|
for message := range sender.messageChan {
|
||||||
|
got = append(got, message)
|
||||||
|
}
|
||||||
|
// Check that we only sent one message
|
||||||
|
expectedCount := 1
|
||||||
|
if len(got) != expectedCount {
|
||||||
|
ms.T().Fatalf("Unexpected number of messages received. Expected %d Got %d", len(got), expectedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that it's the correct message
|
||||||
|
originalFrom := messages[1].(*mockMessage).from
|
||||||
|
if got[0].from != originalFrom {
|
||||||
|
ms.T().Fatalf("Invalid message received. Expected %s, Got %s", originalFrom, got[0].from)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the first message performed a backoff
|
||||||
|
backoffCount := messages[0].(*mockMessage).backoffCount
|
||||||
|
if backoffCount != expectedCount {
|
||||||
|
ms.T().Fatalf("Did not receive expected backoff. Got backoffCount %d, Expected %d", backoffCount, expectedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that there was a reset performed on the sender
|
||||||
|
if sender.resetCount != expectedCount {
|
||||||
|
ms.T().Fatalf("Did not receive expected reset. Got resetCount %d, expected %d", sender.resetCount, expectedCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *MailerSuite) TestPermError() {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
mw := NewMailWorker()
|
||||||
|
go func(ctx context.Context) {
|
||||||
|
mw.Start(ctx)
|
||||||
|
}(ctx)
|
||||||
|
|
||||||
|
expectedError := &textproto.Error{
|
||||||
|
Code: 500,
|
||||||
|
Msg: "Permanent error",
|
||||||
|
}
|
||||||
|
|
||||||
|
sender := newMockErrorSender(expectedError)
|
||||||
|
dialer := newMockDialer()
|
||||||
|
dialer.setDial(func() (Sender, error) {
|
||||||
|
return sender, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
messages := generateMessages(dialer)
|
||||||
|
|
||||||
|
// Send the campaign
|
||||||
|
mw.Queue <- messages
|
||||||
|
|
||||||
|
got := []*mockMessage{}
|
||||||
|
|
||||||
|
for message := range sender.messageChan {
|
||||||
|
got = append(got, message)
|
||||||
|
}
|
||||||
|
// Check that we only sent one message
|
||||||
|
expectedCount := 1
|
||||||
|
if len(got) != expectedCount {
|
||||||
|
ms.T().Fatalf("Unexpected number of messages received. Expected %d Got %d", len(got), expectedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that it's the correct message
|
||||||
|
originalFrom := messages[1].(*mockMessage).from
|
||||||
|
if got[0].from != originalFrom {
|
||||||
|
ms.T().Fatalf("Invalid message received. Expected %s, Got %s", originalFrom, got[0].from)
|
||||||
|
}
|
||||||
|
|
||||||
|
message := messages[0].(*mockMessage)
|
||||||
|
|
||||||
|
// Check that the first message did not perform a backoff
|
||||||
|
expectedBackoffCount := 0
|
||||||
|
backoffCount := message.backoffCount
|
||||||
|
if backoffCount != expectedBackoffCount {
|
||||||
|
ms.T().Fatalf("Did not receive expected backoff. Got backoffCount %d, Expected %d", backoffCount, expectedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that there was a reset performed on the sender
|
||||||
|
if sender.resetCount != expectedCount {
|
||||||
|
ms.T().Fatalf("Did not receive expected reset. Got resetCount %d, expected %d", sender.resetCount, expectedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the email errored out appropriately
|
||||||
|
if !reflect.DeepEqual(message.err, expectedError) {
|
||||||
|
ms.T().Fatalf("Did not received expected error. Got %#v\nExpected %#v", message.err, expectedError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *MailerSuite) TestUnknownError() {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
mw := NewMailWorker()
|
||||||
|
go func(ctx context.Context) {
|
||||||
|
mw.Start(ctx)
|
||||||
|
}(ctx)
|
||||||
|
|
||||||
|
expectedError := errors.New("Unexpected error")
|
||||||
|
|
||||||
|
sender := newMockErrorSender(expectedError)
|
||||||
|
dialer := newMockDialer()
|
||||||
|
dialer.setDial(func() (Sender, error) {
|
||||||
|
return sender, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
messages := generateMessages(dialer)
|
||||||
|
|
||||||
|
// Send the campaign
|
||||||
|
mw.Queue <- messages
|
||||||
|
|
||||||
|
got := []*mockMessage{}
|
||||||
|
|
||||||
|
for message := range sender.messageChan {
|
||||||
|
got = append(got, message)
|
||||||
|
}
|
||||||
|
// Check that we only sent one message
|
||||||
|
expectedCount := 1
|
||||||
|
if len(got) != expectedCount {
|
||||||
|
ms.T().Fatalf("Unexpected number of messages received. Expected %d Got %d", len(got), expectedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that it's the correct message
|
||||||
|
originalFrom := messages[1].(*mockMessage).from
|
||||||
|
if got[0].from != originalFrom {
|
||||||
|
ms.T().Fatalf("Invalid message received. Expected %s, Got %s", originalFrom, got[0].from)
|
||||||
|
}
|
||||||
|
|
||||||
|
message := messages[0].(*mockMessage)
|
||||||
|
|
||||||
|
// Check that the first message did not perform a backoff
|
||||||
|
expectedBackoffCount := 0
|
||||||
|
backoffCount := message.backoffCount
|
||||||
|
if backoffCount != expectedBackoffCount {
|
||||||
|
ms.T().Fatalf("Did not receive expected backoff. Got backoffCount %d, Expected %d", backoffCount, expectedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that there was a reset performed on the sender
|
||||||
|
if sender.resetCount != expectedCount {
|
||||||
|
ms.T().Fatalf("Did not receive expected reset. Got resetCount %d, expected %d", sender.resetCount, expectedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the email errored out appropriately
|
||||||
|
if !reflect.DeepEqual(message.err, expectedError) {
|
||||||
|
ms.T().Fatalf("Did not received expected error. Got %#v\nExpected %#v", message.err, expectedError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMailerSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(MailerSuite))
|
||||||
|
}
|
|
@ -0,0 +1,176 @@
|
||||||
|
package mailer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gophish/gomail"
|
||||||
|
)
|
||||||
|
|
||||||
|
// errHostUnreachable is a mock error to represent a host
|
||||||
|
// being unreachable
|
||||||
|
var errHostUnreachable = errors.New("host unreachable")
|
||||||
|
|
||||||
|
// errDialerUnavailable is a mock error to represent a dialer
|
||||||
|
// being unavailable (perhaps an error getting the dialer config
|
||||||
|
// or a database error)
|
||||||
|
var errDialerUnavailable = errors.New("dialer unavailable")
|
||||||
|
|
||||||
|
// mockDialer keeps track of calls to Dial
|
||||||
|
type mockDialer struct {
|
||||||
|
dialCount int
|
||||||
|
dial func() (Sender, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newMockDialer returns a new instance of the mockDialer with the default
|
||||||
|
// dialer set.
|
||||||
|
func newMockDialer() *mockDialer {
|
||||||
|
md := &mockDialer{}
|
||||||
|
md.dial = md.defaultDial
|
||||||
|
return md
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultDial simply returns a mockSender
|
||||||
|
func (md *mockDialer) defaultDial() (Sender, error) {
|
||||||
|
return newMockSender(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unreachableDial is to simulate network error conditions in which
|
||||||
|
// a host is unavailable.
|
||||||
|
func (md *mockDialer) unreachableDial() (Sender, error) {
|
||||||
|
return nil, errHostUnreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial increments the internal dial count. Otherwise, it's a no-op for the mock client.
|
||||||
|
func (md *mockDialer) Dial() (Sender, error) {
|
||||||
|
md.dialCount++
|
||||||
|
return md.dial()
|
||||||
|
}
|
||||||
|
|
||||||
|
// setDial sets the Dial function for the mockDialer
|
||||||
|
func (md *mockDialer) setDial(dial func() (Sender, error)) {
|
||||||
|
md.dial = dial
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockSender is a mock gomail.Sender used for testing.
|
||||||
|
type mockSender struct {
|
||||||
|
messages []*mockMessage
|
||||||
|
status string
|
||||||
|
send func(*mockMessage) error
|
||||||
|
messageChan chan *mockMessage
|
||||||
|
resetCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockSender() *mockSender {
|
||||||
|
ms := &mockSender{
|
||||||
|
status: "ehlo",
|
||||||
|
messageChan: make(chan *mockMessage),
|
||||||
|
}
|
||||||
|
ms.send = ms.defaultSend
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *mockSender) setSend(send func(*mockMessage) error) {
|
||||||
|
ms.send = send
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *mockSender) defaultSend(mm *mockMessage) error {
|
||||||
|
ms.messageChan <- mm
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send just appends the provided message record to the internal slice
|
||||||
|
func (ms *mockSender) Send(from string, to []string, msg io.WriterTo) error {
|
||||||
|
mm := newMockMessage(from, to, msg)
|
||||||
|
ms.messages = append(ms.messages, mm)
|
||||||
|
ms.status = "sent"
|
||||||
|
return ms.send(mm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close is a noop for the mock client
|
||||||
|
func (ms *mockSender) Close() error {
|
||||||
|
ms.status = "closed"
|
||||||
|
close(ms.messageChan)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset sets the status to "Reset". In practice, this would reset the connection
|
||||||
|
// to the same state as if the client had just sent an EHLO command.
|
||||||
|
func (ms *mockSender) Reset() error {
|
||||||
|
ms.status = "reset"
|
||||||
|
ms.resetCount++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockMessage holds the information sent via a call to MockClient.Send()
|
||||||
|
type mockMessage struct {
|
||||||
|
from string
|
||||||
|
to []string
|
||||||
|
message []byte
|
||||||
|
sendAt time.Time
|
||||||
|
backoffCount int
|
||||||
|
getdialer func() (Dialer, error)
|
||||||
|
err error
|
||||||
|
finished bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockMessage(from string, to []string, msg io.WriterTo) *mockMessage {
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
msg.WriteTo(buff)
|
||||||
|
mm := &mockMessage{
|
||||||
|
from: from,
|
||||||
|
to: to,
|
||||||
|
message: buff.Bytes(),
|
||||||
|
sendAt: time.Now(),
|
||||||
|
}
|
||||||
|
mm.getdialer = mm.defaultDialer
|
||||||
|
return mm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mm *mockMessage) setDialer(dialer func() (Dialer, error)) {
|
||||||
|
mm.getdialer = dialer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mm *mockMessage) defaultDialer() (Dialer, error) {
|
||||||
|
return newMockDialer(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mm *mockMessage) errorDialer() (Dialer, error) {
|
||||||
|
return nil, errDialerUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mm *mockMessage) GetDialer() (Dialer, error) {
|
||||||
|
return mm.getdialer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mm *mockMessage) Backoff(reason error) error {
|
||||||
|
mm.backoffCount++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mm *mockMessage) Error(err error) error {
|
||||||
|
mm.err = err
|
||||||
|
mm.finished = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mm *mockMessage) Finish() error {
|
||||||
|
mm.finished = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mm *mockMessage) Generate(message *gomail.Message) error {
|
||||||
|
message.SetHeaders(map[string][]string{
|
||||||
|
"From": {mm.from},
|
||||||
|
"To": mm.to,
|
||||||
|
})
|
||||||
|
message.SetBody("text/html", string(mm.message))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mm *mockMessage) Success() error {
|
||||||
|
mm.finished = true
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -108,29 +108,6 @@ func (c *Campaign) Validate() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTestEmailRequest is the structure of a request
|
|
||||||
// to send a test email to test an SMTP connection
|
|
||||||
type SendTestEmailRequest struct {
|
|
||||||
Template Template `json:"template"`
|
|
||||||
Page Page `json:"page"`
|
|
||||||
SMTP SMTP `json:"smtp"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
Tracker string `json:"tracker"`
|
|
||||||
TrackingURL string `json:"tracking_url"`
|
|
||||||
From string `json:"from"`
|
|
||||||
Target
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate ensures the SendTestEmailRequest structure
|
|
||||||
// is valid.
|
|
||||||
func (s *SendTestEmailRequest) Validate() error {
|
|
||||||
switch {
|
|
||||||
case s.Email == "":
|
|
||||||
return ErrEmailNotSpecified
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateStatus changes the campaign status appropriately
|
// UpdateStatus changes the campaign status appropriately
|
||||||
func (c *Campaign) UpdateStatus(s string) error {
|
func (c *Campaign) UpdateStatus(s string) error {
|
||||||
// This could be made simpler, but I think there's a bug in gorm
|
// This could be made simpler, but I think there's a bug in gorm
|
||||||
|
@ -141,7 +118,7 @@ func (c *Campaign) UpdateStatus(s string) error {
|
||||||
func (c *Campaign) AddEvent(e Event) error {
|
func (c *Campaign) AddEvent(e Event) error {
|
||||||
e.CampaignId = c.Id
|
e.CampaignId = c.Id
|
||||||
e.Time = time.Now().UTC()
|
e.Time = time.Now().UTC()
|
||||||
return db.Debug().Save(&e).Error
|
return db.Save(&e).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDetails retrieves the related attributes of the campaign
|
// getDetails retrieves the related attributes of the campaign
|
||||||
|
@ -363,12 +340,15 @@ func PostCampaign(c *Campaign, uid int64) error {
|
||||||
c.UserId = uid
|
c.UserId = uid
|
||||||
c.CreatedDate = time.Now().UTC()
|
c.CreatedDate = time.Now().UTC()
|
||||||
c.CompletedDate = time.Time{}
|
c.CompletedDate = time.Time{}
|
||||||
c.Status = CAMPAIGN_CREATED
|
c.Status = CAMPAIGN_QUEUED
|
||||||
if c.LaunchDate.IsZero() {
|
if c.LaunchDate.IsZero() {
|
||||||
c.LaunchDate = time.Now().UTC()
|
c.LaunchDate = c.CreatedDate
|
||||||
} else {
|
} else {
|
||||||
c.LaunchDate = c.LaunchDate.UTC()
|
c.LaunchDate = c.LaunchDate.UTC()
|
||||||
}
|
}
|
||||||
|
if c.LaunchDate.Before(c.CreatedDate) || c.LaunchDate.Equal(c.CreatedDate) {
|
||||||
|
c.Status = CAMPAIGN_IN_PROGRESS
|
||||||
|
}
|
||||||
// Check to make sure all the groups already exist
|
// Check to make sure all the groups already exist
|
||||||
for i, g := range c.Groups {
|
for i, g := range c.Groups {
|
||||||
c.Groups[i], err = GetGroupByName(g.Name, uid)
|
c.Groups[i], err = GetGroupByName(g.Name, uid)
|
||||||
|
@ -427,7 +407,19 @@ func PostCampaign(c *Campaign, uid int64) error {
|
||||||
for _, g := range c.Groups {
|
for _, g := range c.Groups {
|
||||||
// Insert a result for each target in the group
|
// Insert a result for each target in the group
|
||||||
for _, t := range g.Targets {
|
for _, t := range g.Targets {
|
||||||
r := &Result{Email: t.Email, Position: t.Position, Status: STATUS_SENDING, CampaignId: c.Id, UserId: c.UserId, FirstName: t.FirstName, LastName: t.LastName}
|
r := &Result{
|
||||||
|
Email: t.Email,
|
||||||
|
Position: t.Position,
|
||||||
|
Status: STATUS_SCHEDULED,
|
||||||
|
CampaignId: c.Id,
|
||||||
|
UserId: c.UserId,
|
||||||
|
FirstName: t.FirstName,
|
||||||
|
LastName: t.LastName,
|
||||||
|
SendDate: c.LaunchDate,
|
||||||
|
}
|
||||||
|
if c.Status == CAMPAIGN_IN_PROGRESS {
|
||||||
|
r.Status = STATUS_SENDING
|
||||||
|
}
|
||||||
err = r.GenerateId()
|
err = r.GenerateId()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Logger.Println(err)
|
Logger.Println(err)
|
||||||
|
@ -439,9 +431,13 @@ func PostCampaign(c *Campaign, uid int64) error {
|
||||||
Logger.Println(err)
|
Logger.Println(err)
|
||||||
}
|
}
|
||||||
c.Results = append(c.Results, *r)
|
c.Results = append(c.Results, *r)
|
||||||
|
err = GenerateMailLog(c, r)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.Status = CAMPAIGN_QUEUED
|
|
||||||
err = db.Save(c).Error
|
err = db.Save(c).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/mail"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gophish/gomail"
|
||||||
|
"github.com/gophish/gophish/mailer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SendTestEmailRequest is the structure of a request
|
||||||
|
// to send a test email to test an SMTP connection.
|
||||||
|
// This type implements the mailer.Mail interface.
|
||||||
|
type SendTestEmailRequest struct {
|
||||||
|
Template Template `json:"template"`
|
||||||
|
Page Page `json:"page"`
|
||||||
|
SMTP SMTP `json:"smtp"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Tracker string `json:"tracker"`
|
||||||
|
TrackingURL string `json:"tracking_url"`
|
||||||
|
From string `json:"from"`
|
||||||
|
Target
|
||||||
|
ErrorChan chan (error) `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate ensures the SendTestEmailRequest structure
|
||||||
|
// is valid.
|
||||||
|
func (s *SendTestEmailRequest) Validate() error {
|
||||||
|
switch {
|
||||||
|
case s.Email == "":
|
||||||
|
return ErrEmailNotSpecified
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backoff treats temporary errors as permanent since this is expected to be a
|
||||||
|
// synchronous operation. It returns any errors given back to the ErrorChan
|
||||||
|
func (s *SendTestEmailRequest) Backoff(reason error) error {
|
||||||
|
s.ErrorChan <- reason
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns an error on the ErrorChan.
|
||||||
|
func (s *SendTestEmailRequest) Error(err error) error {
|
||||||
|
s.ErrorChan <- err
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success returns nil on the ErrorChan to indicate that the email was sent
|
||||||
|
// successfully.
|
||||||
|
func (s *SendTestEmailRequest) Success() error {
|
||||||
|
s.ErrorChan <- nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate fills in the details of a gomail.Message with the contents
|
||||||
|
// from the SendTestEmailRequest.
|
||||||
|
func (s *SendTestEmailRequest) Generate(msg *gomail.Message) error {
|
||||||
|
f, err := mail.ParseAddress(s.SMTP.FromAddress)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fn := f.Name
|
||||||
|
if fn == "" {
|
||||||
|
fn = f.Address
|
||||||
|
}
|
||||||
|
msg.SetAddressHeader("From", f.Address, f.Name)
|
||||||
|
|
||||||
|
// Parse the customHeader templates
|
||||||
|
for _, header := range s.SMTP.Headers {
|
||||||
|
key, err := buildTemplate(header.Key, s)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := buildTemplate(header.Value, s)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add our header immediately
|
||||||
|
msg.SetHeader(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse remaining templates
|
||||||
|
subject, err := buildTemplate(s.Template.Subject, s)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
}
|
||||||
|
msg.SetHeader("Subject", subject)
|
||||||
|
|
||||||
|
msg.SetHeader("To", s.FormatAddress())
|
||||||
|
if s.Template.Text != "" {
|
||||||
|
text, err := buildTemplate(s.Template.Text, s)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
}
|
||||||
|
msg.SetBody("text/plain", text)
|
||||||
|
}
|
||||||
|
if s.Template.HTML != "" {
|
||||||
|
html, err := buildTemplate(s.Template.HTML, s)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
}
|
||||||
|
if s.Template.Text == "" {
|
||||||
|
msg.SetBody("text/html", html)
|
||||||
|
} else {
|
||||||
|
msg.AddAlternative("text/html", html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Attach the files
|
||||||
|
for _, a := range s.Template.Attachments {
|
||||||
|
msg.Attach(func(a Attachment) (string, gomail.FileSetting, gomail.FileSetting) {
|
||||||
|
h := map[string][]string{"Content-ID": {fmt.Sprintf("<%s>", a.Name)}}
|
||||||
|
return a.Name, gomail.SetCopyFunc(func(w io.Writer) error {
|
||||||
|
decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(a.Content))
|
||||||
|
_, err = io.Copy(w, decoder)
|
||||||
|
return err
|
||||||
|
}), gomail.SetHeader(h)
|
||||||
|
}(a))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDialer returns the mailer.Dialer for the underlying SMTP object
|
||||||
|
func (s *SendTestEmailRequest) GetDialer() (mailer.Dialer, error) {
|
||||||
|
return s.SMTP.GetDialer()
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gophish/gomail"
|
||||||
|
"github.com/jordan-wright/email"
|
||||||
|
check "gopkg.in/check.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *ModelsSuite) TestEmailNotPresent(ch *check.C) {
|
||||||
|
req := &SendTestEmailRequest{}
|
||||||
|
ch.Assert(req.Validate(), check.Equals, ErrEmailNotSpecified)
|
||||||
|
req.Email = "test@example.com"
|
||||||
|
ch.Assert(req.Validate(), check.Equals, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ModelsSuite) TestEmailRequestBackoff(ch *check.C) {
|
||||||
|
req := &SendTestEmailRequest{
|
||||||
|
ErrorChan: make(chan error),
|
||||||
|
}
|
||||||
|
expected := errors.New("Temporary Error")
|
||||||
|
go func() {
|
||||||
|
err = req.Backoff(expected)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
}()
|
||||||
|
ch.Assert(<-req.ErrorChan, check.Equals, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ModelsSuite) TestEmailRequestError(ch *check.C) {
|
||||||
|
req := &SendTestEmailRequest{
|
||||||
|
ErrorChan: make(chan error),
|
||||||
|
}
|
||||||
|
expected := errors.New("Temporary Error")
|
||||||
|
go func() {
|
||||||
|
err = req.Error(expected)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
}()
|
||||||
|
ch.Assert(<-req.ErrorChan, check.Equals, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ModelsSuite) TestEmailRequestSuccess(ch *check.C) {
|
||||||
|
req := &SendTestEmailRequest{
|
||||||
|
ErrorChan: make(chan error),
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
err = req.Success()
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
}()
|
||||||
|
ch.Assert(<-req.ErrorChan, check.Equals, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ModelsSuite) TestEmailRequestGenerate(ch *check.C) {
|
||||||
|
smtp := SMTP{
|
||||||
|
FromAddress: "from@example.com",
|
||||||
|
}
|
||||||
|
template := Template{
|
||||||
|
Name: "Test Template",
|
||||||
|
Subject: "{{.FirstName}} - Subject",
|
||||||
|
Text: "{{.Email}} - Text",
|
||||||
|
HTML: "{{.Email}} - HTML",
|
||||||
|
}
|
||||||
|
target := Target{
|
||||||
|
FirstName: "First",
|
||||||
|
LastName: "Last",
|
||||||
|
Email: "firstlast@example.com",
|
||||||
|
}
|
||||||
|
req := &SendTestEmailRequest{
|
||||||
|
SMTP: smtp,
|
||||||
|
Template: template,
|
||||||
|
Target: target,
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := gomail.NewMessage()
|
||||||
|
err = req.Generate(msg)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
|
||||||
|
expected := &email.Email{
|
||||||
|
Subject: fmt.Sprintf("%s - Subject", req.FirstName),
|
||||||
|
Text: []byte(fmt.Sprintf("%s - Text", req.Email)),
|
||||||
|
HTML: []byte(fmt.Sprintf("%s - HTML", req.Email)),
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBuff := &bytes.Buffer{}
|
||||||
|
_, err = msg.WriteTo(msgBuff)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
|
||||||
|
got, err := email.NewEmailFromReader(msgBuff)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
ch.Assert(got.Subject, check.Equals, expected.Subject)
|
||||||
|
ch.Assert(string(got.Text), check.Equals, string(expected.Text))
|
||||||
|
ch.Assert(string(got.HTML), check.Equals, string(expected.HTML))
|
||||||
|
}
|
|
@ -0,0 +1,326 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"net/mail"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gophish/gomail"
|
||||||
|
"github.com/gophish/gophish/mailer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaxSendAttempts set to 8 since we exponentially backoff after each failed send
|
||||||
|
// attempt. This will give us a maximum send delay of 256 minutes, or about 4.2 hours.
|
||||||
|
var MaxSendAttempts = 8
|
||||||
|
|
||||||
|
// ErrMaxSendAttempts is thrown when the maximum number of sending attemps for a given
|
||||||
|
// MailLog is exceeded.
|
||||||
|
var ErrMaxSendAttempts = errors.New("max send attempts exceeded")
|
||||||
|
|
||||||
|
// MailLog is a struct that holds information about an email that is to be
|
||||||
|
// sent out.
|
||||||
|
type MailLog struct {
|
||||||
|
Id int64 `json:"-"`
|
||||||
|
UserId int64 `json:"-"`
|
||||||
|
CampaignId int64 `json:"campaign_id"`
|
||||||
|
RId string `json:"id"`
|
||||||
|
SendDate time.Time `json:"send_date"`
|
||||||
|
SendAttempt int `json:"send_attempt"`
|
||||||
|
Processing bool `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateMailLog creates a new maillog for the given campaign and
|
||||||
|
// result. It sets the initial send date to match the campaign's launch date.
|
||||||
|
func GenerateMailLog(c *Campaign, r *Result) error {
|
||||||
|
m := &MailLog{
|
||||||
|
UserId: c.UserId,
|
||||||
|
CampaignId: c.Id,
|
||||||
|
RId: r.RId,
|
||||||
|
SendDate: c.LaunchDate,
|
||||||
|
}
|
||||||
|
err = db.Save(m).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backoff sets the MailLog SendDate to be the next entry in an exponential
|
||||||
|
// backoff. ErrMaxRetriesExceeded is thrown if this maillog has been retried
|
||||||
|
// too many times. Backoff also unlocks the maillog so that it can be processed
|
||||||
|
// again in the future.
|
||||||
|
func (m *MailLog) Backoff(reason error) error {
|
||||||
|
if m.SendAttempt == MaxSendAttempts {
|
||||||
|
err = m.addError(ErrMaxSendAttempts)
|
||||||
|
return ErrMaxSendAttempts
|
||||||
|
}
|
||||||
|
r, err := GetResult(m.RId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Add an error, since we had to backoff because of a
|
||||||
|
// temporary error of some sort during the SMTP transaction
|
||||||
|
err = m.addError(reason)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.SendAttempt++
|
||||||
|
backoffDuration := math.Pow(2, float64(m.SendAttempt))
|
||||||
|
m.SendDate = m.SendDate.Add(time.Minute * time.Duration(backoffDuration))
|
||||||
|
err = db.Save(m).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.Status = STATUS_RETRY
|
||||||
|
r.SendDate = m.SendDate
|
||||||
|
err = db.Save(r).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = m.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock removes the processing flag so the maillog can be processed again
|
||||||
|
func (m *MailLog) Unlock() error {
|
||||||
|
m.Processing = false
|
||||||
|
return db.Save(&m).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock sets the processing flag so that other processes cannot modify the maillog
|
||||||
|
func (m *MailLog) Lock() error {
|
||||||
|
m.Processing = true
|
||||||
|
return db.Save(&m).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// addError adds an error to the associated campaign
|
||||||
|
func (m *MailLog) addError(e error) error {
|
||||||
|
c, err := GetCampaign(m.CampaignId, m.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// This is redundant in the case of permanent
|
||||||
|
// errors, but the extra query makes for
|
||||||
|
// a cleaner API.
|
||||||
|
r, err := GetResult(m.RId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
es := struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}{
|
||||||
|
Error: e.Error(),
|
||||||
|
}
|
||||||
|
ej, err := json.Marshal(es)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
}
|
||||||
|
err = c.AddEvent(Event{Email: r.Email, Message: EVENT_SENDING_ERROR, Details: string(ej)})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error sets the error status on the models.Result that the
|
||||||
|
// maillog refers to. Since MailLog errors are permanent,
|
||||||
|
// this action also deletes the maillog.
|
||||||
|
func (m *MailLog) Error(e error) error {
|
||||||
|
Logger.Printf("Erroring out result %s\n", m.RId)
|
||||||
|
r, err := GetResult(m.RId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Update the result
|
||||||
|
err = r.UpdateStatus(ERROR)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Update the campaign events
|
||||||
|
err = m.addError(e)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.Delete(m).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success deletes the maillog from the database and updates the underlying
|
||||||
|
// campaign result.
|
||||||
|
func (m *MailLog) Success() error {
|
||||||
|
r, err := GetResult(m.RId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = r.UpdateStatus(EVENT_SENT)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c, err := GetCampaign(m.CampaignId, m.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = c.AddEvent(Event{Email: r.Email, Message: EVENT_SENT})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.Delete(m).Error
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDialer returns a dialer based on the maillog campaign's SMTP configuration
|
||||||
|
func (m *MailLog) GetDialer() (mailer.Dialer, error) {
|
||||||
|
c, err := GetCampaign(m.CampaignId, m.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.SMTP.GetDialer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildTemplate creates a templated string based on the provided
|
||||||
|
// template body and data.
|
||||||
|
func buildTemplate(text string, data interface{}) (string, error) {
|
||||||
|
buff := bytes.Buffer{}
|
||||||
|
tmpl, err := template.New("template").Parse(text)
|
||||||
|
if err != nil {
|
||||||
|
return buff.String(), err
|
||||||
|
}
|
||||||
|
err = tmpl.Execute(&buff, data)
|
||||||
|
return buff.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate fills in the details of a gomail.Message instance with
|
||||||
|
// the correct headers and body from the campaign and recipient listed in
|
||||||
|
// the maillog. We accept the gomail.Message as an argument so that the caller
|
||||||
|
// can choose to re-use the message across recipients.
|
||||||
|
func (m *MailLog) Generate(msg *gomail.Message) error {
|
||||||
|
r, err := GetResult(m.RId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c, err := GetCampaign(m.CampaignId, m.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f, err := mail.ParseAddress(c.SMTP.FromAddress)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fn := f.Name
|
||||||
|
if fn == "" {
|
||||||
|
fn = f.Address
|
||||||
|
}
|
||||||
|
msg.SetAddressHeader("From", f.Address, f.Name)
|
||||||
|
td := struct {
|
||||||
|
Result
|
||||||
|
URL string
|
||||||
|
TrackingURL string
|
||||||
|
Tracker string
|
||||||
|
From string
|
||||||
|
}{
|
||||||
|
r,
|
||||||
|
c.URL + "?rid=" + r.RId,
|
||||||
|
c.URL + "/track?rid=" + r.RId,
|
||||||
|
"<img alt='' style='display: none' src='" + c.URL + "/track?rid=" + r.RId + "'/>",
|
||||||
|
fn,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the customHeader templates
|
||||||
|
for _, header := range c.SMTP.Headers {
|
||||||
|
key, err := buildTemplate(header.Key, td)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := buildTemplate(header.Value, td)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add our header immediately
|
||||||
|
msg.SetHeader(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse remaining templates
|
||||||
|
subject, err := buildTemplate(c.Template.Subject, td)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
}
|
||||||
|
msg.SetHeader("Subject", subject)
|
||||||
|
|
||||||
|
msg.SetHeader("To", r.FormatAddress())
|
||||||
|
if c.Template.Text != "" {
|
||||||
|
text, err := buildTemplate(c.Template.Text, td)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
}
|
||||||
|
msg.SetBody("text/plain", text)
|
||||||
|
}
|
||||||
|
if c.Template.HTML != "" {
|
||||||
|
html, err := buildTemplate(c.Template.HTML, td)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
}
|
||||||
|
if c.Template.Text == "" {
|
||||||
|
msg.SetBody("text/html", html)
|
||||||
|
} else {
|
||||||
|
msg.AddAlternative("text/html", html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Attach the files
|
||||||
|
for _, a := range c.Template.Attachments {
|
||||||
|
msg.Attach(func(a Attachment) (string, gomail.FileSetting, gomail.FileSetting) {
|
||||||
|
h := map[string][]string{"Content-ID": {fmt.Sprintf("<%s>", a.Name)}}
|
||||||
|
return a.Name, gomail.SetCopyFunc(func(w io.Writer) error {
|
||||||
|
decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(a.Content))
|
||||||
|
_, err = io.Copy(w, decoder)
|
||||||
|
return err
|
||||||
|
}), gomail.SetHeader(h)
|
||||||
|
}(a))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQueuedMailLogs returns the mail logs that are queued up for the given minute.
|
||||||
|
func GetQueuedMailLogs(t time.Time) ([]*MailLog, error) {
|
||||||
|
ms := []*MailLog{}
|
||||||
|
err := db.Where("send_date <= ? AND processing = ?", t, false).
|
||||||
|
Find(&ms).Error
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
}
|
||||||
|
return ms, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMailLogsByCampaign returns all of the mail logs for a given campaign.
|
||||||
|
func GetMailLogsByCampaign(cid int64) ([]*MailLog, error) {
|
||||||
|
ms := []*MailLog{}
|
||||||
|
err := db.Where("campaign_id = ?", cid).Find(&ms).Error
|
||||||
|
return ms, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// LockMailLogs locks or unlocks a slice of maillogs for processing.
|
||||||
|
func LockMailLogs(ms []*MailLog, lock bool) error {
|
||||||
|
tx := db.Begin()
|
||||||
|
for i := range ms {
|
||||||
|
ms[i].Processing = lock
|
||||||
|
err := tx.Debug().Save(ms[i]).Error
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tx.Commit()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnlockAllMailLogs removes the processing lock for all maillogs
|
||||||
|
// in the database. This is intended to be called when Gophish is started
|
||||||
|
// so that any previously locked maillogs can resume processing.
|
||||||
|
func UnlockAllMailLogs() error {
|
||||||
|
err = db.Model(&MailLog{}).Update("processing", false).Error
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,240 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net/textproto"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gophish/gomail"
|
||||||
|
"github.com/jordan-wright/email"
|
||||||
|
"gopkg.in/check.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *ModelsSuite) TestGetQueuedMailLogs(ch *check.C) {
|
||||||
|
campaign := s.createCampaign(ch)
|
||||||
|
ms, err := GetQueuedMailLogs(campaign.LaunchDate)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
got := make(map[string]*MailLog)
|
||||||
|
for _, m := range ms {
|
||||||
|
got[m.RId] = m
|
||||||
|
}
|
||||||
|
for _, r := range campaign.Results {
|
||||||
|
if m, ok := got[r.RId]; ok {
|
||||||
|
ch.Assert(m.RId, check.Equals, r.RId)
|
||||||
|
ch.Assert(m.CampaignId, check.Equals, campaign.Id)
|
||||||
|
ch.Assert(m.SendDate, check.Equals, campaign.LaunchDate)
|
||||||
|
ch.Assert(m.UserId, check.Equals, campaign.UserId)
|
||||||
|
ch.Assert(m.SendAttempt, check.Equals, 0)
|
||||||
|
} else {
|
||||||
|
ch.Fatalf("Result not found in maillogs: %s", r.RId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ModelsSuite) TestMailLogBackoff(ch *check.C) {
|
||||||
|
campaign := s.createCampaign(ch)
|
||||||
|
result := campaign.Results[0]
|
||||||
|
m := &MailLog{}
|
||||||
|
err := db.Where("r_id=? AND campaign_id=?", result.RId, campaign.Id).
|
||||||
|
Find(m).Error
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
ch.Assert(m.SendAttempt, check.Equals, 0)
|
||||||
|
ch.Assert(m.SendDate, check.Equals, campaign.LaunchDate)
|
||||||
|
|
||||||
|
expectedError := &textproto.Error{
|
||||||
|
Code: 500,
|
||||||
|
Msg: "Recipient not found",
|
||||||
|
}
|
||||||
|
for i := m.SendAttempt; i < MaxSendAttempts; i++ {
|
||||||
|
err = m.Lock()
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
ch.Assert(m.Processing, check.Equals, true)
|
||||||
|
|
||||||
|
expectedDuration := math.Pow(2, float64(m.SendAttempt+1))
|
||||||
|
expectedSendDate := m.SendDate.Add(time.Minute * time.Duration(expectedDuration))
|
||||||
|
err = m.Backoff(expectedError)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
ch.Assert(m.SendDate, check.Equals, expectedSendDate)
|
||||||
|
ch.Assert(m.Processing, check.Equals, false)
|
||||||
|
result, err := GetResult(m.RId)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
ch.Assert(result.SendDate, check.Equals, expectedSendDate)
|
||||||
|
ch.Assert(result.Status, check.Equals, STATUS_RETRY)
|
||||||
|
}
|
||||||
|
// Get our updated campaign and check for the added event
|
||||||
|
campaign, err = GetCampaign(campaign.Id, int64(1))
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
|
||||||
|
// We expect MaxSendAttempts + the initial campaign created event
|
||||||
|
ch.Assert(len(campaign.Events), check.Equals, MaxSendAttempts+1)
|
||||||
|
|
||||||
|
// Check that we receive our error after meeting the maximum send attempts
|
||||||
|
err = m.Backoff(expectedError)
|
||||||
|
ch.Assert(err, check.Equals, ErrMaxSendAttempts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ModelsSuite) TestMailLogError(ch *check.C) {
|
||||||
|
campaign := s.createCampaign(ch)
|
||||||
|
result := campaign.Results[0]
|
||||||
|
m := &MailLog{}
|
||||||
|
err := db.Where("r_id=? AND campaign_id=?", result.RId, campaign.Id).
|
||||||
|
Find(m).Error
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
ch.Assert(m.RId, check.Equals, result.RId)
|
||||||
|
|
||||||
|
expectedError := &textproto.Error{
|
||||||
|
Code: 500,
|
||||||
|
Msg: "Recipient not found",
|
||||||
|
}
|
||||||
|
err = m.Error(expectedError)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
|
||||||
|
// Get our result and make sure the status is set correctly
|
||||||
|
result, err = GetResult(result.RId)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
ch.Assert(result.Status, check.Equals, ERROR)
|
||||||
|
|
||||||
|
// Get our updated campaign and check for the added event
|
||||||
|
campaign, err = GetCampaign(campaign.Id, int64(1))
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
|
||||||
|
expectedEventLength := 2
|
||||||
|
ch.Assert(len(campaign.Events), check.Equals, expectedEventLength)
|
||||||
|
|
||||||
|
gotEvent := campaign.Events[1]
|
||||||
|
es := struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}{
|
||||||
|
Error: expectedError.Error(),
|
||||||
|
}
|
||||||
|
ej, _ := json.Marshal(es)
|
||||||
|
expectedEvent := Event{
|
||||||
|
Id: gotEvent.Id,
|
||||||
|
Email: result.Email,
|
||||||
|
Message: EVENT_SENDING_ERROR,
|
||||||
|
CampaignId: campaign.Id,
|
||||||
|
Details: string(ej),
|
||||||
|
Time: gotEvent.Time,
|
||||||
|
}
|
||||||
|
ch.Assert(gotEvent, check.DeepEquals, expectedEvent)
|
||||||
|
|
||||||
|
ms, err := GetMailLogsByCampaign(campaign.Id)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
ch.Assert(len(ms), check.Equals, len(campaign.Results)-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ModelsSuite) TestMailLogSuccess(ch *check.C) {
|
||||||
|
campaign := s.createCampaign(ch)
|
||||||
|
result := campaign.Results[0]
|
||||||
|
m := &MailLog{}
|
||||||
|
err := db.Where("r_id=? AND campaign_id=?", result.RId, campaign.Id).
|
||||||
|
Find(m).Error
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
ch.Assert(m.RId, check.Equals, result.RId)
|
||||||
|
|
||||||
|
err = m.Success()
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
|
||||||
|
// Get our result and make sure the status is set correctly
|
||||||
|
result, err = GetResult(result.RId)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
ch.Assert(result.Status, check.Equals, EVENT_SENT)
|
||||||
|
|
||||||
|
// Get our updated campaign and check for the added event
|
||||||
|
campaign, err = GetCampaign(campaign.Id, int64(1))
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
|
||||||
|
expectedEventLength := 2
|
||||||
|
ch.Assert(len(campaign.Events), check.Equals, expectedEventLength)
|
||||||
|
|
||||||
|
gotEvent := campaign.Events[1]
|
||||||
|
expectedEvent := Event{
|
||||||
|
Id: gotEvent.Id,
|
||||||
|
Email: result.Email,
|
||||||
|
Message: EVENT_SENT,
|
||||||
|
CampaignId: campaign.Id,
|
||||||
|
Time: gotEvent.Time,
|
||||||
|
}
|
||||||
|
ch.Assert(gotEvent, check.DeepEquals, expectedEvent)
|
||||||
|
|
||||||
|
ms, err := GetMailLogsByCampaign(campaign.Id)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
ch.Assert(len(ms), check.Equals, len(campaign.Results)-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ModelsSuite) TestGenerateMailLog(ch *check.C) {
|
||||||
|
campaign := Campaign{
|
||||||
|
Id: 1,
|
||||||
|
UserId: 1,
|
||||||
|
LaunchDate: time.Now().UTC(),
|
||||||
|
}
|
||||||
|
result := Result{
|
||||||
|
RId: "abc1234",
|
||||||
|
}
|
||||||
|
err := GenerateMailLog(&campaign, &result)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
|
||||||
|
m := MailLog{}
|
||||||
|
err = db.Where("r_id=?", result.RId).Find(&m).Error
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
ch.Assert(m.RId, check.Equals, result.RId)
|
||||||
|
ch.Assert(m.CampaignId, check.Equals, campaign.Id)
|
||||||
|
ch.Assert(m.SendDate, check.Equals, campaign.LaunchDate)
|
||||||
|
ch.Assert(m.UserId, check.Equals, campaign.UserId)
|
||||||
|
ch.Assert(m.SendAttempt, check.Equals, 0)
|
||||||
|
ch.Assert(m.Processing, check.Equals, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ModelsSuite) TestMailLogGenerate(ch *check.C) {
|
||||||
|
campaign := s.createCampaign(ch)
|
||||||
|
result := campaign.Results[0]
|
||||||
|
m := &MailLog{}
|
||||||
|
err := db.Where("r_id=? AND campaign_id=?", result.RId, campaign.Id).
|
||||||
|
Find(m).Error
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
|
||||||
|
msg := gomail.NewMessage()
|
||||||
|
err = m.Generate(msg)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
|
||||||
|
expected := &email.Email{
|
||||||
|
Subject: fmt.Sprintf("%s - Subject", result.RId),
|
||||||
|
Text: []byte(fmt.Sprintf("%s - Text", result.RId)),
|
||||||
|
HTML: []byte(fmt.Sprintf("%s - HTML", result.RId)),
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBuff := &bytes.Buffer{}
|
||||||
|
_, err = msg.WriteTo(msgBuff)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
|
||||||
|
got, err := email.NewEmailFromReader(msgBuff)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
ch.Assert(got.Subject, check.Equals, expected.Subject)
|
||||||
|
ch.Assert(string(got.Text), check.Equals, string(expected.Text))
|
||||||
|
ch.Assert(string(got.HTML), check.Equals, string(expected.HTML))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ModelsSuite) TestUnlockAllMailLogs(ch *check.C) {
|
||||||
|
campaign := s.createCampaign(ch)
|
||||||
|
ms, err := GetMailLogsByCampaign(campaign.Id)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
for _, m := range ms {
|
||||||
|
ch.Assert(m.Processing, check.Equals, false)
|
||||||
|
}
|
||||||
|
err = LockMailLogs(ms, true)
|
||||||
|
ms, err = GetMailLogsByCampaign(campaign.Id)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
for _, m := range ms {
|
||||||
|
ch.Assert(m.Processing, check.Equals, true)
|
||||||
|
}
|
||||||
|
err = UnlockAllMailLogs()
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
ms, err = GetMailLogsByCampaign(campaign.Id)
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
for _, m := range ms {
|
||||||
|
ch.Assert(m.Processing, check.Equals, false)
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,9 +36,13 @@ const (
|
||||||
EVENT_OPENED string = "Email Opened"
|
EVENT_OPENED string = "Email Opened"
|
||||||
EVENT_CLICKED string = "Clicked Link"
|
EVENT_CLICKED string = "Clicked Link"
|
||||||
EVENT_DATA_SUBMIT string = "Submitted Data"
|
EVENT_DATA_SUBMIT string = "Submitted Data"
|
||||||
|
EVENT_PROXY_REQUEST string = "Proxied request"
|
||||||
STATUS_SUCCESS string = "Success"
|
STATUS_SUCCESS string = "Success"
|
||||||
|
STATUS_QUEUED string = "Queued"
|
||||||
STATUS_SENDING string = "Sending"
|
STATUS_SENDING string = "Sending"
|
||||||
STATUS_UNKNOWN string = "Unknown"
|
STATUS_UNKNOWN string = "Unknown"
|
||||||
|
STATUS_SCHEDULED string = "Scheduled"
|
||||||
|
STATUS_RETRY string = "Retrying"
|
||||||
ERROR string = "Error"
|
ERROR string = "Error"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
"github.com/gophish/gophish/config"
|
"github.com/gophish/gophish/config"
|
||||||
|
@ -37,12 +39,61 @@ func (s *ModelsSuite) TearDownTest(c *check.C) {
|
||||||
db.Delete(GroupTarget{})
|
db.Delete(GroupTarget{})
|
||||||
db.Delete(SMTP{})
|
db.Delete(SMTP{})
|
||||||
db.Delete(Page{})
|
db.Delete(Page{})
|
||||||
|
db.Delete(Result{})
|
||||||
|
db.Delete(MailLog{})
|
||||||
|
db.Delete(Campaign{})
|
||||||
|
|
||||||
// Reset users table to default state.
|
// Reset users table to default state.
|
||||||
db.Not("id", 1).Delete(User{})
|
db.Not("id", 1).Delete(User{})
|
||||||
db.Model(User{}).Update("username", "admin")
|
db.Model(User{}).Update("username", "admin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ModelsSuite) createCampaignDependencies(ch *check.C) Campaign {
|
||||||
|
group := Group{Name: "Test Group"}
|
||||||
|
group.Targets = []Target{
|
||||||
|
Target{Email: "test1@example.com", FirstName: "First", LastName: "Example"},
|
||||||
|
Target{Email: "test2@example.com", FirstName: "Second", LastName: "Example"},
|
||||||
|
}
|
||||||
|
group.UserId = 1
|
||||||
|
ch.Assert(PostGroup(&group), check.Equals, nil)
|
||||||
|
|
||||||
|
// Add a template
|
||||||
|
t := Template{Name: "Test Template"}
|
||||||
|
t.Subject = "{{.RId}} - Subject"
|
||||||
|
t.Text = "{{.RId}} - Text"
|
||||||
|
t.HTML = "{{.RId}} - HTML"
|
||||||
|
t.UserId = 1
|
||||||
|
ch.Assert(PostTemplate(&t), check.Equals, nil)
|
||||||
|
|
||||||
|
// Add a landing page
|
||||||
|
p := Page{Name: "Test Page"}
|
||||||
|
p.HTML = "<html>Test</html>"
|
||||||
|
p.UserId = 1
|
||||||
|
ch.Assert(PostPage(&p), check.Equals, nil)
|
||||||
|
|
||||||
|
// Add a sending profile
|
||||||
|
smtp := SMTP{Name: "Test Page"}
|
||||||
|
smtp.UserId = 1
|
||||||
|
smtp.Host = "example.com"
|
||||||
|
smtp.FromAddress = "test@test.com"
|
||||||
|
ch.Assert(PostSMTP(&smtp), check.Equals, nil)
|
||||||
|
|
||||||
|
c := Campaign{Name: "Test campaign"}
|
||||||
|
c.UserId = 1
|
||||||
|
c.Template = t
|
||||||
|
c.Page = p
|
||||||
|
c.SMTP = smtp
|
||||||
|
c.Groups = []Group{group}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ModelsSuite) createCampaign(ch *check.C) Campaign {
|
||||||
|
c := s.createCampaignDependencies(ch)
|
||||||
|
// Setup and "launch" our campaign
|
||||||
|
ch.Assert(PostCampaign(&c, c.UserId), check.Equals, nil)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ModelsSuite) TestGetUser(c *check.C) {
|
func (s *ModelsSuite) TestGetUser(c *check.C) {
|
||||||
u, err := GetUser(1)
|
u, err := GetUser(1)
|
||||||
c.Assert(err, check.Equals, nil)
|
c.Assert(err, check.Equals, nil)
|
||||||
|
@ -123,14 +174,15 @@ func (s *ModelsSuite) TestGetGroupsNoGroups(c *check.C) {
|
||||||
|
|
||||||
func (s *ModelsSuite) TestGetGroup(c *check.C) {
|
func (s *ModelsSuite) TestGetGroup(c *check.C) {
|
||||||
// Add group.
|
// Add group.
|
||||||
PostGroup(&Group{
|
originalGroup := &Group{
|
||||||
Name: "Test Group",
|
Name: "Test Group",
|
||||||
Targets: []Target{Target{Email: "test@example.com"}},
|
Targets: []Target{Target{Email: "test@example.com"}},
|
||||||
UserId: 1,
|
UserId: 1,
|
||||||
})
|
}
|
||||||
|
c.Assert(PostGroup(originalGroup), check.Equals, nil)
|
||||||
|
|
||||||
// Get group and test result.
|
// Get group and test result.
|
||||||
group, err := GetGroup(1, 1)
|
group, err := GetGroup(originalGroup.Id, 1)
|
||||||
c.Assert(err, check.Equals, nil)
|
c.Assert(err, check.Equals, nil)
|
||||||
c.Assert(len(group.Targets), check.Equals, 1)
|
c.Assert(len(group.Targets), check.Equals, 1)
|
||||||
c.Assert(group.Name, check.Equals, "Test Group")
|
c.Assert(group.Name, check.Equals, "Test Group")
|
||||||
|
@ -367,3 +419,25 @@ func (s *ModelsSuite) TestFormatAddress(c *check.C) {
|
||||||
}
|
}
|
||||||
c.Assert(r.FormatAddress(), check.Equals, r.Email)
|
c.Assert(r.FormatAddress(), check.Equals, r.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ModelsSuite) TestResultSendingStatus(ch *check.C) {
|
||||||
|
c := s.createCampaignDependencies(ch)
|
||||||
|
ch.Assert(PostCampaign(&c, c.UserId), check.Equals, nil)
|
||||||
|
// This campaign wasn't scheduled, so we expect the status to
|
||||||
|
// be sending
|
||||||
|
fmt.Println("Campaign STATUS")
|
||||||
|
fmt.Println(c.Status)
|
||||||
|
for _, r := range c.Results {
|
||||||
|
ch.Assert(r.Status, check.Equals, STATUS_SENDING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (s *ModelsSuite) TestResultScheduledStatus(ch *check.C) {
|
||||||
|
c := s.createCampaignDependencies(ch)
|
||||||
|
c.LaunchDate = time.Now().UTC().Add(time.Hour * time.Duration(1))
|
||||||
|
ch.Assert(PostCampaign(&c, c.UserId), check.Equals, nil)
|
||||||
|
// This campaign wasn't scheduled, so we expect the status to
|
||||||
|
// be sending
|
||||||
|
for _, r := range c.Results {
|
||||||
|
ch.Assert(r.Status, check.Equals, STATUS_SCHEDULED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
"github.com/oschwald/maxminddb-golang"
|
"github.com/oschwald/maxminddb-golang"
|
||||||
|
@ -24,18 +25,19 @@ type mmGeoPoint struct {
|
||||||
// Result contains the fields for a result object,
|
// Result contains the fields for a result object,
|
||||||
// which is a representation of a target in a campaign.
|
// which is a representation of a target in a campaign.
|
||||||
type Result struct {
|
type Result struct {
|
||||||
Id int64 `json:"-"`
|
Id int64 `json:"-"`
|
||||||
CampaignId int64 `json:"-"`
|
CampaignId int64 `json:"-"`
|
||||||
UserId int64 `json:"-"`
|
UserId int64 `json:"-"`
|
||||||
RId string `json:"id"`
|
RId string `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
FirstName string `json:"first_name"`
|
FirstName string `json:"first_name"`
|
||||||
LastName string `json:"last_name"`
|
LastName string `json:"last_name"`
|
||||||
Position string `json:"position"`
|
Position string `json:"position"`
|
||||||
Status string `json:"status" sql:"not null"`
|
Status string `json:"status" sql:"not null"`
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
Latitude float64 `json:"latitude"`
|
Latitude float64 `json:"latitude"`
|
||||||
Longitude float64 `json:"longitude"`
|
Longitude float64 `json:"longitude"`
|
||||||
|
SendDate time.Time `json:"send_date"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateStatus updates the status of the result in the database
|
// UpdateStatus updates the status of the result in the database
|
||||||
|
|
|
@ -1,15 +1,32 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gophish/gomail"
|
||||||
|
"github.com/gophish/gophish/mailer"
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Dialer is a wrapper around a standard gomail.Dialer in order
|
||||||
|
// to implement the mailer.Dialer interface. This allows us to better
|
||||||
|
// separate the mailer package as opposed to forcing a connection
|
||||||
|
// between mailer and gomail.
|
||||||
|
type Dialer struct {
|
||||||
|
*gomail.Dialer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial wraps the gomail dialer's Dial command
|
||||||
|
func (d *Dialer) Dial() (mailer.Sender, error) {
|
||||||
|
return d.Dialer.Dial()
|
||||||
|
}
|
||||||
|
|
||||||
// SMTP contains the attributes needed to handle the sending of campaign emails
|
// SMTP contains the attributes needed to handle the sending of campaign emails
|
||||||
type SMTP struct {
|
type SMTP struct {
|
||||||
Id int64 `json:"id" gorm:"column:id; primary_key:yes"`
|
Id int64 `json:"id" gorm:"column:id; primary_key:yes"`
|
||||||
|
@ -76,6 +93,34 @@ func (s *SMTP) Validate() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDialer returns a dialer for the given SMTP profile
|
||||||
|
func (s *SMTP) GetDialer() (mailer.Dialer, error) {
|
||||||
|
// Setup the message and dial
|
||||||
|
hp := strings.Split(s.Host, ":")
|
||||||
|
if len(hp) < 2 {
|
||||||
|
hp = append(hp, "25")
|
||||||
|
}
|
||||||
|
// Any issues should have been caught in validation, but we'll
|
||||||
|
// double check here.
|
||||||
|
port, err := strconv.Atoi(hp[1])
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
d := gomail.NewDialer(hp[0], port, s.Username, s.Password)
|
||||||
|
d.TLSConfig = &tls.Config{
|
||||||
|
ServerName: s.Host,
|
||||||
|
InsecureSkipVerify: s.IgnoreCertErrors,
|
||||||
|
}
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
hostname = "localhost"
|
||||||
|
}
|
||||||
|
d.LocalName = hostname
|
||||||
|
return &Dialer{d}, err
|
||||||
|
}
|
||||||
|
|
||||||
// GetSMTPs returns the SMTPs owned by the given user.
|
// GetSMTPs returns the SMTPs owned by the given user.
|
||||||
func GetSMTPs(uid int64) ([]SMTP, error) {
|
func GetSMTPs(uid int64) ([]SMTP, error) {
|
||||||
ss := []SMTP{}
|
ss := []SMTP{}
|
||||||
|
@ -84,7 +129,7 @@ func GetSMTPs(uid int64) ([]SMTP, error) {
|
||||||
Logger.Println(err)
|
Logger.Println(err)
|
||||||
return ss, err
|
return ss, err
|
||||||
}
|
}
|
||||||
for i, _ := range ss {
|
for i := range ss {
|
||||||
err = db.Where("smtp_id=?", ss[i].Id).Find(&ss[i].Headers).Error
|
err = db.Where("smtp_id=?", ss[i].Id).Find(&ss[i].Headers).Error
|
||||||
if err != nil && err != gorm.ErrRecordNotFound {
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
Logger.Println(err)
|
Logger.Println(err)
|
||||||
|
@ -137,7 +182,7 @@ func PostSMTP(s *SMTP) error {
|
||||||
Logger.Println(err)
|
Logger.Println(err)
|
||||||
}
|
}
|
||||||
// Save custom headers
|
// Save custom headers
|
||||||
for i, _ := range s.Headers {
|
for i := range s.Headers {
|
||||||
s.Headers[i].SMTPId = s.Id
|
s.Headers[i].SMTPId = s.Id
|
||||||
err := db.Save(&s.Headers[i]).Error
|
err := db.Save(&s.Headers[i]).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -166,7 +211,7 @@ func PutSMTP(s *SMTP) error {
|
||||||
Logger.Println(err)
|
Logger.Println(err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for i, _ := range s.Headers {
|
for i := range s.Headers {
|
||||||
s.Headers[i].SMTPId = s.Id
|
s.Headers[i].SMTPId = s.Id
|
||||||
err := db.Save(&s.Headers[i]).Error
|
err := db.Save(&s.Headers[i]).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
check "gopkg.in/check.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *ModelsSuite) TestSMTPGetDialer(ch *check.C) {
|
||||||
|
host := "localhost"
|
||||||
|
port := 25
|
||||||
|
smtp := SMTP{
|
||||||
|
Host: fmt.Sprintf("%s:%d", host, port),
|
||||||
|
IgnoreCertErrors: false,
|
||||||
|
}
|
||||||
|
d, err := smtp.GetDialer()
|
||||||
|
ch.Assert(err, check.Equals, nil)
|
||||||
|
|
||||||
|
dialer := d.(*Dialer).Dialer
|
||||||
|
ch.Assert(dialer.Host, check.Equals, host)
|
||||||
|
ch.Assert(dialer.Port, check.Equals, port)
|
||||||
|
ch.Assert(dialer.TLSConfig.ServerName, check.Equals, smtp.Host)
|
||||||
|
ch.Assert(dialer.TLSConfig.InsecureSkipVerify, check.Equals, smtp.IgnoreCertErrors)
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -72,6 +72,18 @@ var statuses = {
|
||||||
icon: "fa-spinner",
|
icon: "fa-spinner",
|
||||||
point: "ct-point-sending"
|
point: "ct-point-sending"
|
||||||
},
|
},
|
||||||
|
"Retrying": {
|
||||||
|
color: "#6c7a89",
|
||||||
|
label: "label-default",
|
||||||
|
icon: "fa-clock-o",
|
||||||
|
point: "ct-point-error"
|
||||||
|
},
|
||||||
|
"Scheduled": {
|
||||||
|
color: "#428bca",
|
||||||
|
label: "label-primary",
|
||||||
|
icon: "fa-clock-o",
|
||||||
|
point: "ct-point-sending"
|
||||||
|
},
|
||||||
"Campaign Created": {
|
"Campaign Created": {
|
||||||
label: "label-success",
|
label: "label-success",
|
||||||
icon: "fa-rocket"
|
icon: "fa-rocket"
|
||||||
|
@ -268,7 +280,9 @@ function renderTimeline(data) {
|
||||||
"first_name": data[2],
|
"first_name": data[2],
|
||||||
"last_name": data[3],
|
"last_name": data[3],
|
||||||
"email": data[4],
|
"email": data[4],
|
||||||
"position": data[5]
|
"position": data[5],
|
||||||
|
"status": data[6],
|
||||||
|
"send_date": data[7]
|
||||||
}
|
}
|
||||||
results = '<div class="timeline col-sm-12 well well-lg">' +
|
results = '<div class="timeline col-sm-12 well well-lg">' +
|
||||||
'<h6>Timeline for ' + escapeHtml(record.first_name) + ' ' + escapeHtml(record.last_name) +
|
'<h6>Timeline for ' + escapeHtml(record.first_name) + ' ' + escapeHtml(record.last_name) +
|
||||||
|
@ -317,6 +331,15 @@ function renderTimeline(data) {
|
||||||
results += '</div></div>'
|
results += '</div></div>'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// Add the scheduled send event at the bottom
|
||||||
|
if (record.status == "Scheduled" || record.status == "Retrying") {
|
||||||
|
results += '<div class="timeline-entry">' +
|
||||||
|
' <div class="timeline-bar"></div>'
|
||||||
|
results +=
|
||||||
|
' <div class="timeline-icon ' + statuses[record.status].label + '">' +
|
||||||
|
' <i class="fa ' + statuses[record.status].icon + '"></i></div>' +
|
||||||
|
' <div class="timeline-message">' + "Scheduled to send at " + record.send_date + '</span>'
|
||||||
|
}
|
||||||
results += '</div></div>'
|
results += '</div></div>'
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
@ -485,6 +508,22 @@ var updateMap = function (results) {
|
||||||
map.bubbles(bubbles)
|
map.bubbles(bubbles)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a status label for use in the results datatable
|
||||||
|
* @param {string} status
|
||||||
|
* @param {moment(datetime)} send_date
|
||||||
|
*/
|
||||||
|
function createStatusLabel(status, send_date) {
|
||||||
|
var label = statuses[status].label || "label-default";
|
||||||
|
var statusColumn = "<span class=\"label " + label + "\">" + status + "</span>"
|
||||||
|
// Add the tooltip if the email is scheduled to be sent
|
||||||
|
if (status == "Scheduled" || status == "Retrying") {
|
||||||
|
var sendDateMessage = "Scheduled to send at " + send_date
|
||||||
|
statusColumn = "<span class=\"label " + label + "\" data-toggle=\"tooltip\" data-placement=\"top\" data-html=\"true\" title=\"" + sendDateMessage + "\">" + status + "</span>"
|
||||||
|
}
|
||||||
|
return statusColumn
|
||||||
|
}
|
||||||
|
|
||||||
/* poll - Queries the API and updates the UI with the results
|
/* poll - Queries the API and updates the UI with the results
|
||||||
*
|
*
|
||||||
* Updates:
|
* Updates:
|
||||||
|
@ -564,10 +603,12 @@ function poll() {
|
||||||
var rid = rowData[0]
|
var rid = rowData[0]
|
||||||
$.each(campaign.results, function (j, result) {
|
$.each(campaign.results, function (j, result) {
|
||||||
if (result.id == rid) {
|
if (result.id == rid) {
|
||||||
var label = statuses[result.status].label || "label-default";
|
rowData[7] = moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a')
|
||||||
rowData[6] = "<span class=\"label " + label + "\">" + result.status + "</span>"
|
rowData[6] = result.status
|
||||||
resultsTable.row(i).data(rowData)
|
resultsTable.row(i).data(rowData)
|
||||||
if (row.child.isShown()) {
|
if (row.child.isShown()) {
|
||||||
|
$(row.node()).find("i").removeClass("fa-caret-right")
|
||||||
|
$(row.node()).find("i").addClass("fa-caret-down")
|
||||||
row.child(renderTimeline(row.data()))
|
row.child(renderTimeline(row.data()))
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -577,6 +618,7 @@ function poll() {
|
||||||
resultsTable.draw(false)
|
resultsTable.draw(false)
|
||||||
/* Update the map information */
|
/* Update the map information */
|
||||||
updateMap(campaign.results)
|
updateMap(campaign.results)
|
||||||
|
$('[data-toggle="tooltip"]').tooltip()
|
||||||
$("#refresh_message").hide()
|
$("#refresh_message").hide()
|
||||||
$("#refresh_btn").show()
|
$("#refresh_btn").show()
|
||||||
})
|
})
|
||||||
|
@ -599,8 +641,6 @@ function load() {
|
||||||
$('#complete_button').text('Completed!');
|
$('#complete_button').text('Completed!');
|
||||||
doPoll = false;
|
doPoll = false;
|
||||||
}
|
}
|
||||||
// Setup tooltips
|
|
||||||
$('[data-toggle="tooltip"]').tooltip()
|
|
||||||
// Setup viewing the details of a result
|
// Setup viewing the details of a result
|
||||||
$("#resultsTable").on("click", ".timeline-event-details", function () {
|
$("#resultsTable").on("click", ".timeline-event-details", function () {
|
||||||
// Show the parameters
|
// Show the parameters
|
||||||
|
@ -622,15 +662,22 @@ function load() {
|
||||||
[2, "asc"]
|
[2, "asc"]
|
||||||
],
|
],
|
||||||
columnDefs: [{
|
columnDefs: [{
|
||||||
orderable: false,
|
orderable: false,
|
||||||
targets: "no-sort"
|
targets: "no-sort"
|
||||||
}, {
|
}, {
|
||||||
className: "details-control",
|
className: "details-control",
|
||||||
"targets": [1]
|
"targets": [1]
|
||||||
}, {
|
}, {
|
||||||
"visible": false,
|
"visible": false,
|
||||||
"targets": [0]
|
"targets": [0, 7]
|
||||||
}]
|
},
|
||||||
|
{
|
||||||
|
"render": function (data, type, row) {
|
||||||
|
return createStatusLabel(data, row[7])
|
||||||
|
},
|
||||||
|
"targets": [6]
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
resultsTable.clear();
|
resultsTable.clear();
|
||||||
var email_series_data = {}
|
var email_series_data = {}
|
||||||
|
@ -639,7 +686,6 @@ function load() {
|
||||||
email_series_data[k] = 0
|
email_series_data[k] = 0
|
||||||
});
|
});
|
||||||
$.each(campaign.results, function (i, result) {
|
$.each(campaign.results, function (i, result) {
|
||||||
label = statuses[result.status].label || "label-default";
|
|
||||||
resultsTable.row.add([
|
resultsTable.row.add([
|
||||||
result.id,
|
result.id,
|
||||||
"<i class=\"fa fa-caret-right\"></i>",
|
"<i class=\"fa fa-caret-right\"></i>",
|
||||||
|
@ -647,7 +693,8 @@ function load() {
|
||||||
escapeHtml(result.last_name) || "",
|
escapeHtml(result.last_name) || "",
|
||||||
escapeHtml(result.email) || "",
|
escapeHtml(result.email) || "",
|
||||||
escapeHtml(result.position) || "",
|
escapeHtml(result.position) || "",
|
||||||
"<span class=\"label " + label + "\">" + result.status + "</span>"
|
result.status,
|
||||||
|
moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a')
|
||||||
])
|
])
|
||||||
email_series_data[result.status]++;
|
email_series_data[result.status]++;
|
||||||
// Backfill status values
|
// Backfill status values
|
||||||
|
@ -657,6 +704,8 @@ function load() {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
resultsTable.draw();
|
resultsTable.draw();
|
||||||
|
// Setup tooltips
|
||||||
|
$('[data-toggle="tooltip"]').tooltip()
|
||||||
// Setup the individual timelines
|
// Setup the individual timelines
|
||||||
$('#resultsTable tbody').on('click', 'td.details-control', function () {
|
$('#resultsTable tbody').on('click', 'td.details-control', function () {
|
||||||
var tr = $(this).closest('tr');
|
var tr = $(this).closest('tr');
|
||||||
|
@ -667,14 +716,12 @@ function load() {
|
||||||
tr.removeClass('shown');
|
tr.removeClass('shown');
|
||||||
$(this).find("i").removeClass("fa-caret-down")
|
$(this).find("i").removeClass("fa-caret-down")
|
||||||
$(this).find("i").addClass("fa-caret-right")
|
$(this).find("i").addClass("fa-caret-right")
|
||||||
row.invalidate('dom').draw(false)
|
|
||||||
} else {
|
} else {
|
||||||
// Open this row
|
// Open this row
|
||||||
$(this).find("i").removeClass("fa-caret-right")
|
$(this).find("i").removeClass("fa-caret-right")
|
||||||
$(this).find("i").addClass("fa-caret-down")
|
$(this).find("i").addClass("fa-caret-down")
|
||||||
row.child(renderTimeline(row.data())).show();
|
row.child(renderTimeline(row.data())).show();
|
||||||
tr.addClass('shown');
|
tr.addClass('shown');
|
||||||
row.invalidate('dom').draw(false)
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Setup the graphs
|
// Setup the graphs
|
||||||
|
|
|
@ -25,4 +25,8 @@ THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Package worker contains the functionality for the background worker process.
|
// Package worker contains the functionality for the background worker process.
|
||||||
|
// It starts a background service that polls every minute for scheduled campaigns
|
||||||
|
// to be launched.
|
||||||
|
// If a campaign is found, it gathers the maillogs associated with the campaign and
|
||||||
|
// sends them to the mailer package to be processed.
|
||||||
package worker
|
package worker
|
||||||
|
|
396
worker/worker.go
396
worker/worker.go
|
@ -1,23 +1,12 @@
|
||||||
package worker
|
package worker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net/mail"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gophish/gophish/mailer"
|
||||||
"github.com/gophish/gophish/models"
|
"github.com/gophish/gophish/models"
|
||||||
"gopkg.in/gomail.v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Logger is the logger for the worker
|
// Logger is the logger for the worker
|
||||||
|
@ -31,343 +20,84 @@ func New() *Worker {
|
||||||
return &Worker{}
|
return &Worker{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start launches the worker to poll the database every minute for any jobs.
|
// Start launches the worker to poll the database every minute for any pending maillogs
|
||||||
// If a job is found, it launches the job
|
// that need to be processed.
|
||||||
func (w *Worker) Start() {
|
func (w *Worker) Start() {
|
||||||
Logger.Println("Background Worker Started Successfully - Waiting for Campaigns")
|
Logger.Println("Background Worker Started Successfully - Waiting for Campaigns")
|
||||||
for t := range time.Tick(1 * time.Minute) {
|
for t := range time.Tick(1 * time.Minute) {
|
||||||
cs, err := models.GetQueuedCampaigns(t.UTC())
|
ms, err := models.GetQueuedMailLogs(t.UTC())
|
||||||
// Not really sure of a clean way to catch errors per campaign...
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Logger.Println(err)
|
Logger.Println(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, c := range cs {
|
// Lock the MailLogs (they will be unlocked after processing)
|
||||||
go func(c models.Campaign) {
|
err = models.LockMailLogs(ms, true)
|
||||||
processCampaign(&c)
|
if err != nil {
|
||||||
}(c)
|
Logger.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// We'll group the maillogs by campaign ID to (sort of) group
|
||||||
|
// them by sending profile. This lets the mailer re-use the Sender
|
||||||
|
// instead of having to re-connect to the SMTP server for every
|
||||||
|
// email.
|
||||||
|
msg := make(map[int64][]mailer.Mail)
|
||||||
|
for _, m := range ms {
|
||||||
|
msg[m.CampaignId] = append(msg[m.CampaignId], m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, we process each group of maillogs in parallel
|
||||||
|
for cid, msc := range msg {
|
||||||
|
go func(cid int64, msc []mailer.Mail) {
|
||||||
|
uid := msc[0].(*models.MailLog).UserId
|
||||||
|
c, err := models.GetCampaign(cid, uid)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
errorMail(err, msc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.Status == models.CAMPAIGN_QUEUED {
|
||||||
|
err := c.UpdateStatus(models.CAMPAIGN_IN_PROGRESS)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Logger.Printf("Sending %d maillogs to Mailer", len(msc))
|
||||||
|
mailer.Mailer.Queue <- msc
|
||||||
|
}(cid, msc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func processCampaign(c *models.Campaign) {
|
// LaunchCampaign starts a campaign
|
||||||
Logger.Printf("Worker received: %s", c.Name)
|
func (w *Worker) LaunchCampaign(c models.Campaign) {
|
||||||
err := c.UpdateStatus(models.CAMPAIGN_IN_PROGRESS)
|
ms, err := models.GetMailLogsByCampaign(c.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Logger.Println(err)
|
Logger.Println(err)
|
||||||
}
|
|
||||||
f, err := mail.ParseAddress(c.SMTP.FromAddress)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
fn := f.Name
|
|
||||||
if fn == "" {
|
|
||||||
fn = f.Address
|
|
||||||
}
|
|
||||||
// Setup the message and dial
|
|
||||||
hp := strings.Split(c.SMTP.Host, ":")
|
|
||||||
if len(hp) < 2 {
|
|
||||||
hp = append(hp, "25")
|
|
||||||
}
|
|
||||||
// Any issues should have been caught in validation, so we just log
|
|
||||||
port, err := strconv.Atoi(hp[1])
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
d := gomail.NewDialer(hp[0], port, c.SMTP.Username, c.SMTP.Password)
|
|
||||||
d.TLSConfig = &tls.Config{
|
|
||||||
ServerName: c.SMTP.Host,
|
|
||||||
InsecureSkipVerify: c.SMTP.IgnoreCertErrors,
|
|
||||||
}
|
|
||||||
hostname, err := os.Hostname()
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
hostname = "localhost"
|
|
||||||
}
|
|
||||||
d.LocalName = hostname
|
|
||||||
s, err := d.Dial()
|
|
||||||
// Short circuit if we have an err
|
|
||||||
// However, we still need to update each target
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
for _, t := range c.Results {
|
|
||||||
es := struct {
|
|
||||||
Error string `json:"error"`
|
|
||||||
}{
|
|
||||||
Error: err.Error(),
|
|
||||||
}
|
|
||||||
ej, err := json.Marshal(es)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
err = t.UpdateStatus(models.ERROR)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
err = c.AddEvent(models.Event{Email: t.Email, Message: models.EVENT_SENDING_ERROR, Details: string(ej)})
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Send each email
|
models.LockMailLogs(ms, true)
|
||||||
e := gomail.NewMessage()
|
// This is required since you cannot pass a slice of values
|
||||||
for _, t := range c.Results {
|
// that implements an interface as a slice of that interface.
|
||||||
e.SetAddressHeader("From", f.Address, f.Name)
|
mailEntries := []mailer.Mail{}
|
||||||
td := struct {
|
for _, m := range ms {
|
||||||
models.Result
|
mailEntries = append(mailEntries, m)
|
||||||
URL string
|
|
||||||
TrackingURL string
|
|
||||||
Tracker string
|
|
||||||
From string
|
|
||||||
}{
|
|
||||||
t,
|
|
||||||
c.URL + "?rid=" + t.RId,
|
|
||||||
c.URL + "/track?rid=" + t.RId,
|
|
||||||
"<img alt='' style='display: none' src='" + c.URL + "/track?rid=" + t.RId + "'/>",
|
|
||||||
fn,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the customHeader templates
|
|
||||||
for _, header := range c.SMTP.Headers {
|
|
||||||
parsedHeader := struct {
|
|
||||||
Key bytes.Buffer
|
|
||||||
Value bytes.Buffer
|
|
||||||
}{}
|
|
||||||
keytmpl, err := template.New("text_template").Parse(header.Key)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
err = keytmpl.Execute(&parsedHeader.Key, td)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
valtmpl, err := template.New("text_template").Parse(header.Value)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
err = valtmpl.Execute(&parsedHeader.Value, td)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add our header immediately
|
|
||||||
e.SetHeader(parsedHeader.Key.String(), parsedHeader.Value.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse remaining templates
|
|
||||||
var subjBuff bytes.Buffer
|
|
||||||
tmpl, err := template.New("text_template").Parse(c.Template.Subject)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
err = tmpl.Execute(&subjBuff, td)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
e.SetHeader("Subject", subjBuff.String())
|
|
||||||
Logger.Println("Creating email using template")
|
|
||||||
e.SetHeader("To", t.FormatAddress())
|
|
||||||
if c.Template.Text != "" {
|
|
||||||
var textBuff bytes.Buffer
|
|
||||||
tmpl, err = template.New("text_template").Parse(c.Template.Text)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
err = tmpl.Execute(&textBuff, td)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
e.SetBody("text/plain", textBuff.String())
|
|
||||||
}
|
|
||||||
if c.Template.HTML != "" {
|
|
||||||
var htmlBuff bytes.Buffer
|
|
||||||
tmpl, err = template.New("html_template").Parse(c.Template.HTML)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
err = tmpl.Execute(&htmlBuff, td)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
if c.Template.Text == "" {
|
|
||||||
e.SetBody("text/html", htmlBuff.String())
|
|
||||||
} else {
|
|
||||||
e.AddAlternative("text/html", htmlBuff.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Attach the files
|
|
||||||
for _, a := range c.Template.Attachments {
|
|
||||||
e.Attach(func(a models.Attachment) (string, gomail.FileSetting, gomail.FileSetting) {
|
|
||||||
h := map[string][]string{"Content-ID": {fmt.Sprintf("<%s>", a.Name)}}
|
|
||||||
return a.Name, gomail.SetCopyFunc(func(w io.Writer) error {
|
|
||||||
decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(a.Content))
|
|
||||||
_, err = io.Copy(w, decoder)
|
|
||||||
return err
|
|
||||||
}), gomail.SetHeader(h)
|
|
||||||
}(a))
|
|
||||||
}
|
|
||||||
Logger.Printf("Sending Email to %s\n", t.Email)
|
|
||||||
err = gomail.Send(s, e)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
es := struct {
|
|
||||||
Error string `json:"error"`
|
|
||||||
}{
|
|
||||||
Error: err.Error(),
|
|
||||||
}
|
|
||||||
ej, err := json.Marshal(es)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
err = t.UpdateStatus(models.ERROR)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
err = c.AddEvent(models.Event{Email: t.Email, Message: models.EVENT_SENDING_ERROR, Details: string(ej)})
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err = t.UpdateStatus(models.EVENT_SENT)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
err = c.AddEvent(models.Event{Email: t.Email, Message: models.EVENT_SENT})
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e.Reset()
|
|
||||||
}
|
|
||||||
err = c.UpdateStatus(models.CAMPAIGN_EMAILS_SENT)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
}
|
||||||
|
mailer.Mailer.Queue <- mailEntries
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendTestEmail(s *models.SendTestEmailRequest) error {
|
// SendTestEmail sends a test email
|
||||||
f, err := mail.ParseAddress(s.SMTP.FromAddress)
|
func (w *Worker) SendTestEmail(s *models.SendTestEmailRequest) error {
|
||||||
if err != nil {
|
go func() {
|
||||||
Logger.Println(err)
|
mailer.Mailer.Queue <- []mailer.Mail{s}
|
||||||
return err
|
}()
|
||||||
}
|
return <-s.ErrorChan
|
||||||
hp := strings.Split(s.SMTP.Host, ":")
|
}
|
||||||
if len(hp) < 2 {
|
|
||||||
hp = append(hp, "25")
|
// errorMail is a helper to handle erroring out a slice of Mail instances
|
||||||
}
|
// in the case that an unrecoverable error occurs.
|
||||||
port, err := strconv.Atoi(hp[1])
|
func errorMail(err error, ms []mailer.Mail) {
|
||||||
if err != nil {
|
for _, m := range ms {
|
||||||
Logger.Println(err)
|
m.Error(err)
|
||||||
return err
|
}
|
||||||
}
|
|
||||||
d := gomail.NewDialer(hp[0], port, s.SMTP.Username, s.SMTP.Password)
|
|
||||||
d.TLSConfig = &tls.Config{
|
|
||||||
ServerName: s.SMTP.Host,
|
|
||||||
InsecureSkipVerify: s.SMTP.IgnoreCertErrors,
|
|
||||||
}
|
|
||||||
hostname, err := os.Hostname()
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
hostname = "localhost"
|
|
||||||
}
|
|
||||||
d.LocalName = hostname
|
|
||||||
dc, err := d.Dial()
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
Logger.Println("Creating email using template")
|
|
||||||
e := gomail.NewMessage()
|
|
||||||
// Parse the customHeader templates
|
|
||||||
for _, header := range s.SMTP.Headers {
|
|
||||||
parsedHeader := struct {
|
|
||||||
Key bytes.Buffer
|
|
||||||
Value bytes.Buffer
|
|
||||||
}{}
|
|
||||||
keytmpl, err := template.New("text_template").Parse(header.Key)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
err = keytmpl.Execute(&parsedHeader.Key, s)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
valtmpl, err := template.New("text_template").Parse(header.Value)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
err = valtmpl.Execute(&parsedHeader.Value, s)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add our header immediately
|
|
||||||
e.SetHeader(parsedHeader.Key.String(), parsedHeader.Value.String())
|
|
||||||
}
|
|
||||||
e.SetAddressHeader("From", f.Address, f.Name)
|
|
||||||
e.SetHeader("To", s.FormatAddress())
|
|
||||||
// Parse the templates
|
|
||||||
var subjBuff bytes.Buffer
|
|
||||||
tmpl, err := template.New("text_template").Parse(s.Template.Subject)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
err = tmpl.Execute(&subjBuff, s)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
e.SetHeader("Subject", subjBuff.String())
|
|
||||||
if s.Template.Text != "" {
|
|
||||||
var textBuff bytes.Buffer
|
|
||||||
tmpl, err = template.New("text_template").Parse(s.Template.Text)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
err = tmpl.Execute(&textBuff, s)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
e.SetBody("text/plain", textBuff.String())
|
|
||||||
}
|
|
||||||
if s.Template.HTML != "" {
|
|
||||||
var htmlBuff bytes.Buffer
|
|
||||||
tmpl, err = template.New("html_template").Parse(s.Template.HTML)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
err = tmpl.Execute(&htmlBuff, s)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
}
|
|
||||||
// If we don't have a text part, make the html the root part
|
|
||||||
if s.Template.Text == "" {
|
|
||||||
e.SetBody("text/html", htmlBuff.String())
|
|
||||||
} else {
|
|
||||||
e.AddAlternative("text/html", htmlBuff.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Attach the files
|
|
||||||
for _, a := range s.Template.Attachments {
|
|
||||||
e.Attach(func(a models.Attachment) (string, gomail.FileSetting) {
|
|
||||||
return a.Name, gomail.SetCopyFunc(func(w io.Writer) error {
|
|
||||||
decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(a.Content))
|
|
||||||
_, err = io.Copy(w, decoder)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}(a))
|
|
||||||
}
|
|
||||||
Logger.Printf("Sending Email to %s\n", s.Email)
|
|
||||||
err = gomail.Send(dc, e)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println(err)
|
|
||||||
// For now, let's split the error and return
|
|
||||||
// the last element (the most descriptive error message)
|
|
||||||
serr := strings.Split(err.Error(), ":")
|
|
||||||
return errors.New(serr[len(serr)-1])
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
package worker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gophish/gophish/config"
|
||||||
|
"github.com/gophish/gophish/models"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WorkerSuite is a suite of tests to cover API related functions
|
||||||
|
type WorkerSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
ApiKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkerSuite) SetupSuite() {
|
||||||
|
config.Conf.DBName = "sqlite3"
|
||||||
|
config.Conf.DBPath = ":memory:"
|
||||||
|
config.Conf.MigrationsPath = "../db/db_sqlite3/migrations/"
|
||||||
|
err := models.Setup()
|
||||||
|
if err != nil {
|
||||||
|
s.T().Fatalf("Failed creating database: %v", err)
|
||||||
|
}
|
||||||
|
s.Nil(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkerSuite) TearDownTest() {
|
||||||
|
campaigns, _ := models.GetCampaigns(1)
|
||||||
|
for _, campaign := range campaigns {
|
||||||
|
models.DeleteCampaign(campaign.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkerSuite) SetupTest() {
|
||||||
|
config.Conf.TestFlag = true
|
||||||
|
// Add a group
|
||||||
|
group := models.Group{Name: "Test Group"}
|
||||||
|
group.Targets = []models.Target{
|
||||||
|
models.Target{Email: "test1@example.com", FirstName: "First", LastName: "Example"},
|
||||||
|
models.Target{Email: "test2@example.com", FirstName: "Second", LastName: "Example"},
|
||||||
|
}
|
||||||
|
group.UserId = 1
|
||||||
|
models.PostGroup(&group)
|
||||||
|
|
||||||
|
// Add a template
|
||||||
|
t := models.Template{Name: "Test Template"}
|
||||||
|
t.Subject = "Test subject"
|
||||||
|
t.Text = "Text text"
|
||||||
|
t.HTML = "<html>Test</html>"
|
||||||
|
t.UserId = 1
|
||||||
|
models.PostTemplate(&t)
|
||||||
|
|
||||||
|
// Add a landing page
|
||||||
|
p := models.Page{Name: "Test Page"}
|
||||||
|
p.HTML = "<html>Test</html>"
|
||||||
|
p.UserId = 1
|
||||||
|
models.PostPage(&p)
|
||||||
|
|
||||||
|
// Add a sending profile
|
||||||
|
smtp := models.SMTP{Name: "Test Page"}
|
||||||
|
smtp.UserId = 1
|
||||||
|
smtp.Host = "example.com"
|
||||||
|
smtp.FromAddress = "test@test.com"
|
||||||
|
models.PostSMTP(&smtp)
|
||||||
|
|
||||||
|
// Setup and "launch" our campaign
|
||||||
|
// Set the status such that no emails are attempted
|
||||||
|
c := models.Campaign{Name: "Test campaign"}
|
||||||
|
c.UserId = 1
|
||||||
|
c.Template = t
|
||||||
|
c.Page = p
|
||||||
|
c.SMTP = smtp
|
||||||
|
c.Groups = []models.Group{group}
|
||||||
|
models.PostCampaign(&c, c.UserId)
|
||||||
|
c.UpdateStatus(models.CAMPAIGN_EMAILS_SENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkerSuite) TestMailSendSuccess() {
|
||||||
|
// TODO
|
||||||
|
}
|
Loading…
Reference in New Issue