986 custom envelope sender remerge (#2334)

* Adds the ability to specify an envelope sender in templates (#986)

Authored-by: ChessSpider <ChessSpider@users.noreply.github.com>
Authored-by: Olivier MEDOC <o_medoc@yahoo.fr>
Authored-by: ptitdoc <ptitdoc@free.fr>
pull/2318/head^2
ptitdoc 2022-03-25 16:24:49 +01:00 committed by GitHub
parent e0acb99734
commit bb516ef7ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 151 additions and 9 deletions

View File

@ -3,6 +3,7 @@ package api
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/mail"
ctx "github.com/gophish/gophish/context" ctx "github.com/gophish/gophish/context"
log "github.com/gophish/gophish/logger" log "github.com/gophish/gophish/logger"
@ -93,7 +94,19 @@ func (as *Server) SendTestEmail(w http.ResponseWriter, r *http.Request) {
} }
s.SMTP = smtp s.SMTP = smtp
} }
s.FromAddress = s.SMTP.FromAddress
_, err = mail.ParseAddress(s.Template.EnvelopeSender)
if err != nil {
_, err = mail.ParseAddress(s.SMTP.FromAddress)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
} else {
s.FromAddress = s.SMTP.FromAddress
}
} else {
s.FromAddress = s.Template.EnvelopeSender
}
// Validate the given request // Validate the given request
if err = s.Validate(); err != nil { if err = s.Validate(); err != nil {

View File

@ -0,0 +1,8 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
ALTER TABLE templates ADD COLUMN envelope_sender varchar(255);
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back

View File

@ -0,0 +1,8 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
ALTER TABLE templates ADD COLUMN envelope_sender varchar(255);
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back

View File

@ -55,6 +55,7 @@ type Mail interface {
Success() error Success() error
Generate(msg *gomail.Message) error Generate(msg *gomail.Message) error
GetDialer() (Dialer, error) GetDialer() (Dialer, error)
GetSmtpFrom() (string, error)
} }
// MailWorker is the worker that receives slices of emails // MailWorker is the worker that receives slices of emails
@ -160,7 +161,14 @@ func sendMail(ctx context.Context, dialer Dialer, ms []Mail) {
m.Error(err) m.Error(err)
continue continue
} }
err = gomail.Send(sender, message)
smtp_from, err := m.GetSmtpFrom()
if err != nil {
m.Error(err)
continue
}
err = gomail.SendCustomFrom(sender, smtp_from, message)
if err != nil { if err != nil {
if te, ok := err.(*textproto.Error); ok { if te, ok := err.(*textproto.Error); ok {
switch { switch {
@ -215,6 +223,8 @@ func sendMail(ctx context.Context, dialer Dialer, ms []Mail) {
} }
} }
log.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"smtp_from": smtp_from,
"envelope_from": message.GetHeader("From")[0],
"email": message.GetHeader("To")[0], "email": message.GetHeader("To")[0],
}).Info("Email sent") }).Info("Email sent")
m.Success() m.Success()

View File

@ -162,6 +162,10 @@ func (mm *mockMessage) Generate(message *gomail.Message) error {
return nil return nil
} }
func (mm *mockMessage) GetSmtpFrom() (string, error) {
return mm.from, nil
}
func (mm *mockMessage) Success() error { func (mm *mockMessage) Success() error {
mm.finished = true mm.finished = true
return nil return nil

View File

@ -77,6 +77,10 @@ func (s *EmailRequest) Success() error {
return nil return nil
} }
func (s *EmailRequest) GetSmtpFrom() (string, error) {
return s.SMTP.FromAddress, nil
}
// PostEmailRequest stores a SendTestEmailRequest in the database. // PostEmailRequest stores a SendTestEmailRequest in the database.
func PostEmailRequest(s *EmailRequest) error { func PostEmailRequest(s *EmailRequest) error {
// Generate an ID to be used in the underlying Result object // Generate an ID to be used in the underlying Result object
@ -99,7 +103,7 @@ func GetEmailRequestByResultId(id string) (EmailRequest, error) {
// Generate fills in the details of a gomail.Message with the contents // Generate fills in the details of a gomail.Message with the contents
// from the SendTestEmailRequest. // from the SendTestEmailRequest.
func (s *EmailRequest) Generate(msg *gomail.Message) error { func (s *EmailRequest) Generate(msg *gomail.Message) error {
f, err := mail.ParseAddress(s.FromAddress) f, err := mail.ParseAddress(s.getFromAddress())
if err != nil { if err != nil {
return err return err
} }

View File

@ -106,6 +106,37 @@ func (s *ModelsSuite) TestEmailRequestGenerate(ch *check.C) {
} }
} }
func (s *ModelsSuite) TestGetSmtpFrom(ch *check.C) {
smtp := SMTP{
FromAddress: "from@example.com",
}
template := Template{
Name: "Test Template",
Subject: "{{.FirstName}} - Subject",
Text: "{{.Email}} - Text",
HTML: "{{.Email}} - HTML",
}
req := &EmailRequest{
SMTP: smtp,
Template: template,
URL: "http://127.0.0.1/{{.Email}}",
BaseRecipient: BaseRecipient{
FirstName: "First",
LastName: "Last",
Email: "firstlast@example.com",
},
FromAddress: smtp.FromAddress,
RId: fmt.Sprintf("%s-foobar", PreviewPrefix),
}
msg := gomail.NewMessage()
err := req.Generate(msg)
smtp_from, err := req.GetSmtpFrom()
ch.Assert(err, check.Equals, nil)
ch.Assert(smtp_from, check.Equals, "from@example.com")
}
func (s *ModelsSuite) TestEmailRequestURLTemplating(ch *check.C) { func (s *ModelsSuite) TestEmailRequestURLTemplating(ch *check.C) {
smtp := SMTP{ smtp := SMTP{
FromAddress: "from@example.com", FromAddress: "from@example.com",

View File

@ -149,6 +149,16 @@ func (m *MailLog) CacheCampaign(campaign *Campaign) error {
return nil return nil
} }
func (m *MailLog) GetSmtpFrom() (string, error) {
c, err := GetCampaign(m.CampaignId, m.UserId)
if err != nil {
return "", err
}
f, err := mail.ParseAddress(c.SMTP.FromAddress)
return f.Address, err
}
// Generate fills in the details of a gomail.Message instance with // Generate fills in the details of a gomail.Message instance with
// the correct headers and body from the campaign and recipient listed in // 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 // the maillog. We accept the gomail.Message as an argument so that the caller
@ -167,9 +177,12 @@ func (m *MailLog) Generate(msg *gomail.Message) error {
c = &campaign c = &campaign
} }
f, err := mail.ParseAddress(c.SMTP.FromAddress) f, err := mail.ParseAddress(c.Template.EnvelopeSender)
if err != nil { if err != nil {
return err f, err = mail.ParseAddress(c.SMTP.FromAddress)
if err != nil {
return err
}
} }
msg.SetAddressHeader("From", f.Address, f.Name) msg.SetAddressHeader("From", f.Address, f.Name)

View File

@ -213,15 +213,51 @@ func (s *ModelsSuite) TestGenerateMailLog(ch *check.C) {
ch.Assert(m.Processing, check.Equals, false) ch.Assert(m.Processing, check.Equals, false)
} }
func (s *ModelsSuite) TestMailLogGetSmtpFrom(ch *check.C) {
template := Template{
Name: "OverrideSmtpFrom",
UserId: 1,
Text: "dummytext",
HTML: "Dummyhtml",
Subject: "Dummysubject",
EnvelopeSender: "spoofing@example.com",
}
ch.Assert(PostTemplate(&template), check.Equals, nil)
campaign := s.createCampaignDependencies(ch)
campaign.Template = template
ch.Assert(PostCampaign(&campaign, campaign.UserId), check.Equals, nil)
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)
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.From, check.Equals, "spoofing@example.com")
}
func (s *ModelsSuite) TestMailLogGenerate(ch *check.C) { func (s *ModelsSuite) TestMailLogGenerate(ch *check.C) {
campaign := s.createCampaign(ch) campaign := s.createCampaign(ch)
result := campaign.Results[0] result := campaign.Results[0]
expected := &email.Email{ expected := &email.Email{
From: "test@test.com", // Default smtp.FromAddress
Subject: fmt.Sprintf("%s - Subject", result.RId), Subject: fmt.Sprintf("%s - Subject", result.RId),
Text: []byte(fmt.Sprintf("%s - Text", result.RId)), Text: []byte(fmt.Sprintf("%s - Text", result.RId)),
HTML: []byte(fmt.Sprintf("%s - HTML", result.RId)), HTML: []byte(fmt.Sprintf("%s - HTML", result.RId)),
} }
got := s.emailFromFirstMailLog(campaign, ch) got := s.emailFromFirstMailLog(campaign, ch)
ch.Assert(got.From, check.Equals, expected.From)
ch.Assert(got.Subject, check.Equals, expected.Subject) ch.Assert(got.Subject, check.Equals, expected.Subject)
ch.Assert(string(got.Text), check.Equals, string(expected.Text)) ch.Assert(string(got.Text), check.Equals, string(expected.Text))
ch.Assert(string(got.HTML), check.Equals, string(expected.HTML)) ch.Assert(string(got.HTML), check.Equals, string(expected.HTML))

View File

@ -2,6 +2,7 @@ package models
import ( import (
"errors" "errors"
"net/mail"
"time" "time"
log "github.com/gophish/gophish/logger" log "github.com/gophish/gophish/logger"
@ -13,6 +14,7 @@ type Template struct {
Id int64 `json:"id" gorm:"column:id; primary_key:yes"` Id int64 `json:"id" gorm:"column:id; primary_key:yes"`
UserId int64 `json:"-" gorm:"column:user_id"` UserId int64 `json:"-" gorm:"column:user_id"`
Name string `json:"name"` Name string `json:"name"`
EnvelopeSender string `json:"envelope_sender"`
Subject string `json:"subject"` Subject string `json:"subject"`
Text string `json:"text"` Text string `json:"text"`
HTML string `json:"html" gorm:"column:html"` HTML string `json:"html" gorm:"column:html"`
@ -33,6 +35,11 @@ func (t *Template) Validate() error {
return ErrTemplateNameNotSpecified return ErrTemplateNameNotSpecified
case t.Text == "" && t.HTML == "": case t.Text == "" && t.HTML == "":
return ErrTemplateMissingParameter return ErrTemplateMissingParameter
case t.EnvelopeSender != "":
_, err := mail.ParseAddress(t.EnvelopeSender)
if err != nil {
return err
}
} }
if err := ValidateTemplate(t.HTML); err != nil { if err := ValidateTemplate(t.HTML); err != nil {
return err return err

File diff suppressed because one or more lines are too long

View File

@ -20,6 +20,7 @@ function save(idx) {
} }
template.name = $("#name").val() template.name = $("#name").val()
template.subject = $("#subject").val() template.subject = $("#subject").val()
template.envelope_sender = $("#envelope-sender").val()
template.html = CKEDITOR.instances["html_editor"].getData(); template.html = CKEDITOR.instances["html_editor"].getData();
// Fix the URL Scheme added by CKEditor (until we can remove it from the plugin) // Fix the URL Scheme added by CKEditor (until we can remove it from the plugin)
template.html = template.html.replace(/https?:\/\/{{\.URL}}/gi, "{{.URL}}") template.html = template.html.replace(/https?:\/\/{{\.URL}}/gi, "{{.URL}}")
@ -189,6 +190,7 @@ function edit(idx) {
template = templates[idx] template = templates[idx]
$("#name").val(template.name) $("#name").val(template.name)
$("#subject").val(template.subject) $("#subject").val(template.subject)
$("#envelope-sender").val(template.envelope_sender)
$("#html_editor").val(template.html) $("#html_editor").val(template.html)
$("#text_editor").val(template.text) $("#text_editor").val(template.text)
attachmentRows = [] attachmentRows = []
@ -247,6 +249,7 @@ function copy(idx) {
template = templates[idx] template = templates[idx]
$("#name").val("Copy of " + template.name) $("#name").val("Copy of " + template.name)
$("#subject").val(template.subject) $("#subject").val(template.subject)
$("#envelope-sender").val(template.envelope_sender)
$("#html_editor").val(template.html) $("#html_editor").val(template.html)
$("#text_editor").val(template.text) $("#text_editor").val(template.text)
$.each(template.attachments, function (i, file) { $.each(template.attachments, function (i, file) {

View File

@ -50,7 +50,8 @@
<input type="text" class="form-control" placeholder="Profile name" id="name" autofocus /> <input type="text" class="form-control" placeholder="Profile name" id="name" autofocus />
<label class="control-label" for="interface_type">Interface Type:</label> <label class="control-label" for="interface_type">Interface Type:</label>
<input type="text" class="form-control" value="SMTP" id="interface_type" disabled /> <input type="text" class="form-control" value="SMTP" id="interface_type" disabled />
<label class="control-label" for="from">From:</label> <label class="control-label" for="from">SMTP From: <i class="fa fa-question-circle"
data-toggle="tooltip" data-placement="right" title="Set this to an email address from your sending domain to bypass SPF-checks. You can set the Envelope Sender in Email Templates. The Envelope Sender is shown to the user."></i></label>
<input type="text" class="form-control" placeholder="First Last <test@example.com>" id="from" <input type="text" class="form-control" placeholder="First Last <test@example.com>" id="from"
required /> required />
<label class="control-label" for="host">Host:</label> <label class="control-label" for="host">Host:</label>
@ -140,4 +141,4 @@
</div> </div>
{{end}} {{define "scripts"}} {{end}} {{define "scripts"}}
<script src="/js/dist/app/sending_profiles.min.js"></script> <script src="/js/dist/app/sending_profiles.min.js"></script>
{{end}} {{end}}

View File

@ -55,6 +55,10 @@
class="fa fa-envelope"></i> class="fa fa-envelope"></i>
Import Email</button> Import Email</button>
</div> </div>
<label class="control-label" for="envelope-sender">Envelope Sender: <i class="fa fa-question-circle" data-toggle="tooltip" data-placement="right" title="This sender is shown to the user by most email clients. Defaults to the SMTP From as defined in the Sending Profile."></i></label>
<div class="form-group">
<input type="text" class="form-control" placeholder="First Last <test@example.com>" id="envelope-sender" />
</div>
<label class="control-label" for="subject">Subject:</label> <label class="control-label" for="subject">Subject:</label>
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" placeholder="Email Subject" id="subject" /> <input type="text" class="form-control" placeholder="Email Subject" id="subject" />
@ -137,4 +141,4 @@
<script src="/js/src/vendor/ckeditor/adapters/jquery.js"></script> <script src="/js/src/vendor/ckeditor/adapters/jquery.js"></script>
<script src="/js/dist/app/autocomplete.min.js"></script> <script src="/js/dist/app/autocomplete.min.js"></script>
<script src="/js/dist/app/templates.min.js"></script> <script src="/js/dist/app/templates.min.js"></script>
{{end}} {{end}}