Skip to content

Alex/join form emails #243

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions server/src/international_application_processor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Please keep in sync with [Google Docs](https://docs.google.com/document/d/1tgtxGONu86XN0KBvOx9-OXmh3mDKpvzkAyQGFoHOdkM/edit?tab=t.0#heading=h.qsu6qdtc3163) for email authors to see the emails that are sent and suggest changes.

Further documentation in [Coda](https://coda.io/d/Tech-Team_dR-UIgVShEf/ADB-Forms_suuKCpXS).
104 changes: 104 additions & 0 deletions server/src/international_application_processor/mailer_common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package international_application_processor

import (
"net/mail"
"strings"
"unicode"

"github.com/dxe/adb/model"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)

type email struct {
name string
email string
}

var (
sfBayCoordinator = email{
name: "Antonelle Racelis",
email: "[email protected]",
}
californiaCoordinator = email{
name: "Almira Tanner",
email: "[email protected]",
}
globalCoordinator = email{
name: "Michelle Del Cueto",
email: "[email protected]",
}
)

func isCalifornia(state string) bool {
return state == "CA"
}

func getChapterEmailFallback(state string) string {
if isCalifornia(state) {
return californiaCoordinator.email
} else {
return globalCoordinator.email
}
}

func getChapterEmailsWithFallback(chapter *model.ChapterWithToken, fallback string) []string {
emails := getChapterEmails(chapter)
if len(emails) == 0 {
return []string{fallback}
}
return emails
}

func getChapterEmails(chapter *model.ChapterWithToken) []string {
var emails []string

if chapter.Email != "" {
emails = append(emails, chapter.Email)
}

emails = append(emails, getChapterOrganizerEmails(chapter)...)

return emails
}

func getChapterOrganizerEmails(chapter *model.ChapterWithToken) []string {
organizers := chapter.Organizers

emails := make([]string, 0, len(organizers))
if len(organizers) > 0 {
for _, o := range organizers {
if o.Email != "" {
emails = append(emails, o.Email)
}
}
}

return emails
}

func sanitizeAndFormatName(name string) string {
sanitized := strings.Map(func(r rune) rune {
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == ' ' {
return r
}
return -1
}, name)

return strings.TrimSpace(
cases.Title(language.AmericanEnglish).String(sanitized))
}

func validateEmail(str string) error {
_, err := mail.ParseAddress(str)
return err
}

func sanitizeState(state string) string {
return strings.Map(func(r rune) rune {
if unicode.IsLetter(r) {
return r
}
return -1
}, state)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package international_application_processor

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestSanitizeAndFormatName(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"John Doe", "John Doe"},
{"Jóhn Doe", "Jóhn Doe"},
{" John Doe ", "John Doe"},
{"john <doe>\t", "John Doe"},
{"!@#John123", "John123"},
{"jane doe the 3rd", "Jane Doe The 3Rd"},
{"", ""},
}

for _, test := range tests {
result := sanitizeAndFormatName(test.input)
assert.Equal(t, test.expected, result, "Expected sanitized and formatted name to match")
}
}

func TestValidateEmail(t *testing.T) {
tests := []struct {
input string
hasError bool
}{
{"[email protected]", false},
{"[email protected]", false},
{"user@مثال.com", false},
{"user@tld", false},
{"user@", true},
{"invalid-email", true},
{"", true},
}

for _, test := range tests {
err := validateEmail(test.input)
if test.hasError {
assert.Error(t, err, "Expected an error for invalid email")
} else {
assert.NoError(t, err, "Expected no error for valid email")
}
}
}

func TestSanitizeState(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"CA", "CA"},
{"California123", "California"},
{"!@#CA", "CA"},
{"", ""},
}

for _, test := range tests {
result := sanitizeState(test.input)
assert.Equal(t, test.expected, result, "Expected sanitized state to match")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Logic for notifying existing organizers that someone submitted the
// international application form.

package international_application_processor

import (
"fmt"
"html"
"log"
"strings"

"github.com/dxe/adb/mailer"
"github.com/dxe/adb/model"
"github.com/pkg/errors"
)

func sendNotificationEmail(formData model.InternationalFormData, chapter *model.ChapterWithToken) error {
msg, err := buildNotificationEmail(formData, chapter)
if err != nil {
return errors.Wrap(err, "error building int'l alert email")
}

err = mailer.Send(*msg)
if err != nil {
return errors.Wrap(err, "error sending int'l alert email")
}

log.Printf("Int'l mailer notification email sent to %v", formData.Email)

return err
}

func buildNotificationEmail(formData model.InternationalFormData, chapter *model.ChapterWithToken) (*mailer.Message, error) {
recipients, err := getNotificationRecipients(formData, chapter)
if err != nil {
return nil, err
}

fullName := sanitizeAndFormatName(formData.FirstName + " " + formData.LastName)

msg := mailer.Message{
FromName: "DxE Join Form",
FromAddress: "[email protected]",
ToAddress: recipients[0],
CC: recipients[1:],
Subject: fmt.Sprintf("%v signed up to join your chapter", fullName),
}

var bodyBuilder strings.Builder
fmt.Fprintf(&bodyBuilder, "<p>Name: %s</p>", html.EscapeString(fullName))
fmt.Fprintf(&bodyBuilder, "<p>Email: %s</p>", html.EscapeString(formData.Email))
fmt.Fprintf(&bodyBuilder, "<p>Phone: %s</p>", html.EscapeString(formData.Phone))
fmt.Fprintf(&bodyBuilder, "<p>City: %s</p>", html.EscapeString(formData.City))
fmt.Fprintf(&bodyBuilder, "<p>Involvement: %s</p>", html.EscapeString(formData.Involvement))
fmt.Fprintf(&bodyBuilder, "<p>Skills: %s</p>", html.EscapeString(formData.Skills))
msg.BodyHTML = bodyBuilder.String()

return &msg, nil
}

func getNotificationRecipients(formData model.InternationalFormData, chapter *model.ChapterWithToken) ([]string, error) {
if chapter != nil {
if chapter.ChapterID == model.SFBayChapterId {
return []string{sfBayCoordinator.email}, nil
}

return getChapterEmailsWithFallback(chapter,
getChapterEmailFallback(formData.State)), nil
}

if isCalifornia(formData.State) {
return []string{californiaCoordinator.email}, nil
}

return []string{globalCoordinator.email}, nil
}
Loading