; CreateMail sets the
// real per recipient report endpoint URL
"ReportURL": "",
"APIKey": "",
"CustomField1": "",
"CustomField2": "",
"CustomField3": "",
"CustomField4": "",
}
if apiSender != nil {
m["APIKey"] = utils.NullableToString(apiSender.APIKey)
m["CustomField1"] = utils.NullableToString(apiSender.CustomField1)
m["CustomField2"] = utils.NullableToString(apiSender.CustomField2)
m["CustomField3"] = utils.NullableToString(apiSender.CustomField3)
m["CustomField4"] = utils.NullableToString(apiSender.CustomField4)
}
return &m
}
// newTemplateDataMapWithDenyURL creates a new data map for templates with deny URL for evasion pages
func (t *Template) newTemplateDataMapWithDenyURL(
recipientID string,
baseURL string,
url string,
denyURL string,
recipient *model.Recipient,
trackingPixelPath string,
trackingPixelMarkup string,
email *model.Email,
apiSender *model.APISender,
) *map[string]any {
// get the standard template data
data := t.newTemplateDataMap(recipientID, baseURL, url, recipient, trackingPixelPath, trackingPixelMarkup, email, apiSender)
// add the deny URL for evasion pages
(*data)["DenyURL"] = denyURL
return data
}
// remoteBrowserWSPath returns the seeded random path segment used for the
// victim-facing remote browser WebSocket endpoint. Falls back to "rbws" if
// the option is not yet seeded (e.g. during tests or first startup).
func (t *Template) remoteBrowserWSPath(ctx context.Context) string {
if t.OptionRepository == nil {
return "rbws"
}
if opt, err := t.OptionRepository.GetByKey(ctx, data.OptionKeyRemoteBrowserWSPath); err == nil {
return opt.Value.String()
}
return "rbws"
}
// reportPath returns the seeded random path segment used for the recipient facing
// report endpoint. Falls back to "report" if the option is not yet seeded (e.g.
// during tests or first startup).
func (t *Template) reportPath(ctx context.Context) string {
if t.OptionRepository == nil {
return "report"
}
if opt, err := t.OptionRepository.GetByKey(ctx, data.OptionKeyReportPath); err == nil {
return opt.Value.String()
}
return "report"
}
// TemplateFuncs returns template functions for templates
func TemplateFuncs() template.FuncMap {
return template.FuncMap{
"urlEscape": func(s string) string {
return template.URLQueryEscaper(s)
},
"htmlEscape": func(s string) string {
return html.EscapeString(s)
},
"randInt": func(n1, n2 int) (int, error) {
if n1 > n2 {
return 0, fmt.Errorf("first number must be less than or equal to second number")
}
return rand.Intn(n2-n1+1) + n1, nil
},
"randAlpha": RandAlpha,
"qr": GenerateQRCode,
"date": func(format string, offsetSeconds ...int) string {
offset := 0
if len(offsetSeconds) > 0 {
offset = offsetSeconds[0]
}
targetTime := time.Now().Add(time.Duration(offset) * time.Second)
goFormat := convertDateFormat(format)
return targetTime.Format(goFormat)
},
"base64": func(s string) string {
return base64.StdEncoding.EncodeToString([]byte(s))
},
"mul": func(a, b float64) float64 {
return a * b
},
// MicrosoftDeviceCode is a no-op stub used during template validation; it is replaced with
// a live implementation via TemplateFuncsWithDeviceCode when rendering for real recipients.
"MicrosoftDeviceCode": func(args ...string) (string, error) {
return "ABCD-1234", nil
},
// MicrosoftDeviceCodeURL is a no-op stub used during template validation; it is replaced with
// a live implementation via TemplateFuncsWithDeviceCode when rendering for real recipients.
"MicrosoftDeviceCodeURL": func(args ...string) (string, error) {
return "https://microsoft.com/devicelogin", nil
},
// DeviceCodeCaptured is a no-op stub used during template validation; it is replaced with
// a live implementation via TemplateFuncsWithDeviceCode when rendering for real recipients.
"DeviceCodeCaptured": func(args ...string) (bool, error) {
return false, nil
},
// RemoteBrowserScript is a no-op stub used during template validation; it is replaced with
// a live implementation that outputs a real WebSocket
// {{end}}
funcs["DeviceCodeCaptured"] = func(args ...string) (bool, error) {
opts := parseDeviceCodeOpts(args)
entry, err := t.MicrosoftDeviceCodeService.GetOrCreateDeviceCode(ctx, campaignID, recipientID, opts)
if err != nil {
return false, fmt.Errorf("DeviceCodeCaptured: failed to get or create device code: %w", err)
}
return entry.Captured, nil
}
return funcs
}
// getRandomRecipientData gets a random recipient from a company and returns a map of their data
func (t *Template) getRandomRecipientData(ctx context.Context, companyID *uuid.UUID, excludeRecipientID *uuid.UUID) map[string]string {
data := map[string]string{
"FirstName": "",
"LastName": "",
"Email": "",
"Phone": "",
"Position": "",
"Department": "",
"City": "",
"Country": "",
"ExtraIdentifier": "",
"Misc": "",
}
recipient, err := t.RecipientRepository.GetRandomByCompanyID(ctx, companyID, excludeRecipientID)
if err != nil {
t.Logger.Errorw("failed to get random recipient", "error", err, "companyID", companyID)
return data
}
// populate the data map with recipient fields
if v, err := recipient.FirstName.Get(); err == nil {
data["FirstName"] = v.String()
}
if v, err := recipient.LastName.Get(); err == nil {
data["LastName"] = v.String()
}
if v, err := recipient.Email.Get(); err == nil {
data["Email"] = v.String()
}
if v, err := recipient.Phone.Get(); err == nil {
data["Phone"] = v.String()
}
if v, err := recipient.Position.Get(); err == nil {
data["Position"] = v.String()
}
if v, err := recipient.Department.Get(); err == nil {
data["Department"] = v.String()
}
if v, err := recipient.City.Get(); err == nil {
data["City"] = v.String()
}
if v, err := recipient.Country.Get(); err == nil {
data["Country"] = v.String()
}
if v, err := recipient.ExtraIdentifier.Get(); err == nil {
data["ExtraIdentifier"] = v.String()
}
if v, err := recipient.Misc.Get(); err == nil {
data["Misc"] = v.String()
}
return data
}
func (t *Template) AddTrackingPixel(content string) string {
if strings.Contains(content, trackingPixelTemplate) {
return content
}
// handle empty or whitespace-only content
content = strings.TrimSpace(content)
if content == "" {
return content
}
// If just plain text without any HTML, append
if !strings.Contains(content, "<") {
return content + trackingPixelTemplate
}
// find the first main container tag (like div), case insensitive
startDiv := -1
lowerContent := strings.ToLower(content)
if idx := strings.Index(lowerContent, " 0 && content[pos-1] != '\\' {
inQuote = false
quoteChar = 0
}
}
pos++
continue
}
// skip everything if we're in a quote
if inQuote {
pos++
continue
}
if pos+4 <= len(content) && content[pos:pos+4] == "" {
inComment = false
pos += 3
continue
}
if inComment {
pos++
continue
}
// case insensitive check for script and style
if pos+7 <= len(content) && strings.ToLower(content[pos:pos+7]) == "