From 9b87a8a857a0d1700f426f51cf31c7f65480e292 Mon Sep 17 00:00:00 2001 From: MrMelon54 Date: Wed, 23 Aug 2023 16:17:01 +0100 Subject: [PATCH] Replace postfix config parser with query command --- .idea/sqldialects.xml | 1 - api/api.go | 39 +-- api/interfaces.go | 14 + api/send-message.go | 71 ++++++ api/send-message_test.go | 240 ++++++++++++++++++ .../comma-list-scanner/comma-list-scanner.go | 57 ----- .../comma-list-scanner_test.go | 45 ---- postfix-config/config-parser/config-parser.go | 49 ---- .../config-parser/config-parser_test.go | 39 --- postfix-config/config.go | 43 ---- postfix-config/decoder.go | 108 -------- postfix-config/decoder_test.go | 28 -- postfix-config/example.cf | 10 - postfix-config/map-provider/hash.go | 48 ---- postfix-config/map-provider/hash_example.txt | 3 - postfix-config/map-provider/hash_test.go | 23 -- postfix-config/map-provider/map-provider.go | 23 -- .../map-provider/mysql-prepared-query.go | 76 ------ .../map-provider/mysql-prepared-query_test.go | 40 --- postfix-config/map-provider/mysql.go | 112 -------- postfix-config/map-provider/mysql_example.cf | 5 - postfix-config/map-provider/variable.go | 12 - postfix-config/test-data/aliases.txt | 1 - .../test-data/mysql_sender_alias_maps.cf | 0 ...ysql_virtual_alias_domain_catchall_maps.cf | 0 ...mysql_virtual_alias_domain_mailbox_maps.cf | 0 .../mysql_virtual_alias_domain_maps.cf | 0 .../test-data/mysql_virtual_alias_maps.cf | 0 .../mysql_virtual_alias_user_catchall_maps.cf | 0 .../mysql_virtual_alias_user_mailbox_maps.cf | 0 .../mysql_virtual_alias_user_maps.cf | 0 ...l_virtual_alias_userdomain_mailbox_maps.cf | 0 .../mysql_virtual_alias_userdomain_maps.cf | 0 .../mysql_virtual_alias_wildcard_maps.cf | 0 .../test-data/mysql_virtual_domains_maps.cf | 0 .../test-data/mysql_virtual_mailbox_maps.cf | 0 postfix-lookup/lookup.sh | 5 + postfix-lookup/postfix-lookup.go | 47 ++++ postfix-lookup/postfix-lookup_test.go | 45 ++++ smtp/json.go | 40 +-- smtp/smtp.go | 10 +- smtp/smtp_test.go | 2 +- 42 files changed, 455 insertions(+), 781 deletions(-) create mode 100644 api/interfaces.go create mode 100644 api/send-message.go create mode 100644 api/send-message_test.go delete mode 100644 postfix-config/comma-list-scanner/comma-list-scanner.go delete mode 100644 postfix-config/comma-list-scanner/comma-list-scanner_test.go delete mode 100644 postfix-config/config-parser/config-parser.go delete mode 100644 postfix-config/config-parser/config-parser_test.go delete mode 100644 postfix-config/config.go delete mode 100644 postfix-config/decoder.go delete mode 100644 postfix-config/decoder_test.go delete mode 100644 postfix-config/example.cf delete mode 100644 postfix-config/map-provider/hash.go delete mode 100644 postfix-config/map-provider/hash_example.txt delete mode 100644 postfix-config/map-provider/hash_test.go delete mode 100644 postfix-config/map-provider/map-provider.go delete mode 100644 postfix-config/map-provider/mysql-prepared-query.go delete mode 100644 postfix-config/map-provider/mysql-prepared-query_test.go delete mode 100644 postfix-config/map-provider/mysql.go delete mode 100644 postfix-config/map-provider/mysql_example.cf delete mode 100644 postfix-config/map-provider/variable.go delete mode 100644 postfix-config/test-data/aliases.txt delete mode 100644 postfix-config/test-data/mysql_sender_alias_maps.cf delete mode 100644 postfix-config/test-data/mysql_virtual_alias_domain_catchall_maps.cf delete mode 100644 postfix-config/test-data/mysql_virtual_alias_domain_mailbox_maps.cf delete mode 100644 postfix-config/test-data/mysql_virtual_alias_domain_maps.cf delete mode 100644 postfix-config/test-data/mysql_virtual_alias_maps.cf delete mode 100644 postfix-config/test-data/mysql_virtual_alias_user_catchall_maps.cf delete mode 100644 postfix-config/test-data/mysql_virtual_alias_user_mailbox_maps.cf delete mode 100644 postfix-config/test-data/mysql_virtual_alias_user_maps.cf delete mode 100644 postfix-config/test-data/mysql_virtual_alias_userdomain_mailbox_maps.cf delete mode 100644 postfix-config/test-data/mysql_virtual_alias_userdomain_maps.cf delete mode 100644 postfix-config/test-data/mysql_virtual_alias_wildcard_maps.cf delete mode 100644 postfix-config/test-data/mysql_virtual_domains_maps.cf delete mode 100644 postfix-config/test-data/mysql_virtual_mailbox_maps.cf create mode 100644 postfix-lookup/lookup.sh create mode 100644 postfix-lookup/postfix-lookup.go create mode 100644 postfix-lookup/postfix-lookup_test.go diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index b7d9ab9..56782ca 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -1,7 +1,6 @@ - \ No newline at end of file diff --git a/api/api.go b/api/api.go index bdbadba..06ba934 100644 --- a/api/api.go +++ b/api/api.go @@ -3,13 +3,12 @@ package api import ( "encoding/json" "github.com/1f349/lotus/imap" - "github.com/1f349/lotus/smtp" "github.com/julienschmidt/httprouter" "net/http" "time" ) -func SetupApiServer(listen string, auth func(callback AuthCallback) httprouter.Handle, send *smtp.Smtp, recv *imap.Imap) *http.Server { +func SetupApiServer(listen string, auth func(callback AuthCallback) httprouter.Handle, send Smtp, recv Imap) *http.Server { r := httprouter.New() // === ACCOUNT === @@ -18,39 +17,7 @@ func SetupApiServer(listen string, auth func(callback AuthCallback) httprouter.H })) // === SMTP === - r.POST("/message", auth(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) { - // check body exists - if req.Body == nil { - rw.WriteHeader(http.StatusBadRequest) - return - } - - // parse json body - var j smtp.Json - err := json.NewDecoder(req.Body).Decode(&j) - if err != nil { - rw.WriteHeader(http.StatusBadRequest) - return - } - - // TODO(melon): add alias support - if j.From == b.Subject { - - } - - mail, err := j.PrepareMail() - if err != nil { - rw.WriteHeader(http.StatusBadRequest) - return - } - - if send.Send(mail) != nil { - rw.WriteHeader(http.StatusInternalServerError) - return - } - - rw.WriteHeader(http.StatusAccepted) - })) + r.POST("/message", auth(MessageSender(send))) // === IMAP === type statusJson struct { @@ -126,7 +93,7 @@ func apiError(rw http.ResponseWriter, code int, m string) { type IcCallback[T any] func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, cli *imap.Client, t T) -func imapClient[T any](recv *imap.Imap, cb IcCallback[T]) AuthCallback { +func imapClient[T any](recv Imap, cb IcCallback[T]) AuthCallback { return func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) { if req.Body == nil { rw.WriteHeader(http.StatusBadRequest) diff --git a/api/interfaces.go b/api/interfaces.go new file mode 100644 index 0000000..2ec9239 --- /dev/null +++ b/api/interfaces.go @@ -0,0 +1,14 @@ +package api + +import ( + "github.com/1f349/lotus/imap" + "github.com/1f349/lotus/smtp" +) + +type Smtp interface { + Send(mail *smtp.Mail) error +} + +type Imap interface { + MakeClient(user string) (*imap.Client, error) +} diff --git a/api/send-message.go b/api/send-message.go new file mode 100644 index 0000000..bd35c22 --- /dev/null +++ b/api/send-message.go @@ -0,0 +1,71 @@ +package api + +import ( + "encoding/json" + "errors" + postfixLookup "github.com/1f349/lotus/postfix-lookup" + "github.com/1f349/lotus/smtp" + "github.com/julienschmidt/httprouter" + "net/http" + "time" +) + +var defaultPostfixLookup = postfixLookup.NewPostfixLookup().Lookup +var timeNow = time.Now + +// MessageSender is the internal handler for `POST /message` requests +// the access token is already validated at this point +func MessageSender(send Smtp) func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) { + return func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) { + // check body exists + if req.Body == nil { + apiError(rw, http.StatusBadRequest, "Missing request body") + return + } + + // parse json body + var j smtp.Json + err := json.NewDecoder(req.Body).Decode(&j) + if err != nil { + apiError(rw, http.StatusBadRequest, "Invalid JSON body") + return + } + + // prepare the mail for sending + mail, err := j.PrepareMail(timeNow()) + if err != nil { + apiError(rw, http.StatusBadRequest, "Invalid mail message") + return + } + + // this looks up the underlying account for the sender alias + println(mail.From) + lookup, err := defaultPostfixLookup(mail.From) + + // the alias does not exist + if errors.Is(err, postfixLookup.ErrInvalidAlias) { + apiError(rw, http.StatusBadRequest, "Invalid sender alias") + return + } + + // the alias lookup failed to run + if err != nil { + apiError(rw, http.StatusInternalServerError, "Sender alias lookup failed") + return + } + + // the alias does not match the logged-in user + if lookup != b.Subject { + apiError(rw, http.StatusBadRequest, "User does not own sender alias") + return + } + + // try sending the mail + if send.Send(mail) != nil { + apiError(rw, http.StatusInternalServerError, "Failed to send mail") + return + } + + rw.WriteHeader(http.StatusAccepted) + } +} diff --git a/api/send-message_test.go b/api/send-message_test.go new file mode 100644 index 0000000..6829251 --- /dev/null +++ b/api/send-message_test.go @@ -0,0 +1,240 @@ +package api + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + postfixLookup "github.com/1f349/lotus/postfix-lookup" + "github.com/1f349/lotus/smtp" + "github.com/MrMelon54/mjwt/auth" + "github.com/MrMelon54/mjwt/claims" + "github.com/golang-jwt/jwt/v4" + "github.com/julienschmidt/httprouter" + "github.com/stretchr/testify/assert" + "io" + "net/http" + "net/http/httptest" + "slices" + "strings" + "testing" + "time" +) + +func init() { + defaultPostfixLookup = func(key string) (string, error) { + switch key { + case "noreply@example.com", "admin@example.com": + return "admin@example.com", nil + case "user@example.com": + return "user@example.com", nil + } + return "", postfixLookup.ErrInvalidAlias + } + timeNow = func() time.Time { + return time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC) + } +} + +type fakeSmtp struct { + from string + deliver []string + body []byte +} + +func (f *fakeSmtp) Send(mail *smtp.Mail) error { + if mail.From != f.from { + return fmt.Errorf("test fail: invalid from address") + } + if !slices.Equal(mail.Deliver, f.deliver) { + return fmt.Errorf("test fail: invalid deliver slice") + } + if !slices.Equal(mail.Body, f.body) { + return fmt.Errorf("test fail: invalid message body") + } + return nil +} + +type fakeFailedSmtp struct{} + +func (f *fakeFailedSmtp) Send(mail *smtp.Mail) error { + return errors.New("sending failed") +} + +var messageSenderTestData = []struct { + req func() (*http.Request, error) + smtp Smtp + claims AuthClaims + status int + output string +}{ + { + req: func() (*http.Request, error) { + return http.NewRequest(http.MethodPost, "https://api.example.com/v1/mail/message", nil) + }, + smtp: &fakeSmtp{}, + claims: makeFakeAuthClaims("admin@example.com"), + status: http.StatusBadRequest, + output: "Missing request body", + }, + { + req: func() (*http.Request, error) { + return http.NewRequest(http.MethodPost, "https://api.example.com/v1/mail/message", strings.NewReader(`{`)) + }, + smtp: &fakeSmtp{}, + claims: makeFakeAuthClaims("admin@example.com"), + status: http.StatusBadRequest, + output: "Invalid JSON body", + }, + { + req: func() (*http.Request, error) { + j, err := json.Marshal(smtp.Json{ + From: "noreply2@example.com", + ReplyTo: "admin@example.com", + To: "user@example.com", + Subject: "Test Subject", + BodyType: "plain", + Body: "Plain text", + }) + if err != nil { + return nil, err + } + return http.NewRequest(http.MethodPost, "https://api.example.com/v1/mail/message", bytes.NewReader(j)) + }, + smtp: &fakeSmtp{}, + claims: makeFakeAuthClaims("admin@example.com"), + status: http.StatusBadRequest, + output: "Invalid sender alias", + }, + { + req: func() (*http.Request, error) { + j, err := json.Marshal(smtp.Json{ + From: "user@example.com", + ReplyTo: "admin@example.com", + To: "user@example.com", + Subject: "Test Subject", + BodyType: "plain", + Body: "Plain text", + }) + if err != nil { + return nil, err + } + return http.NewRequest(http.MethodPost, "https://api.example.com/v1/mail/message", bytes.NewReader(j)) + }, + smtp: &fakeSmtp{}, + claims: makeFakeAuthClaims("admin@example.com"), + status: http.StatusBadRequest, + output: "User does not own sender alias", + }, + { + req: func() (*http.Request, error) { + j, err := json.Marshal(smtp.Json{ + From: "noreply@example.com, user2@example.com", + ReplyTo: "admin@example.com", + To: "user@example.com", + Subject: "Test Subject", + BodyType: "plain", + Body: "Plain text", + }) + if err != nil { + return nil, err + } + return http.NewRequest(http.MethodPost, "https://api.example.com/v1/mail/message", bytes.NewReader(j)) + }, + smtp: &fakeSmtp{}, + claims: makeFakeAuthClaims("admin@example.com"), + status: http.StatusBadRequest, + output: "Invalid mail message", + }, + { + req: func() (*http.Request, error) { + j, err := json.Marshal(smtp.Json{ + From: "noreply@example.com", + ReplyTo: "admin@example.com", + To: "user@example.com", + Subject: "Test Subject", + BodyType: "plain", + Body: "Plain text", + }) + if err != nil { + return nil, err + } + return http.NewRequest(http.MethodPost, "https://api.example.com/v1/mail/message", bytes.NewReader(j)) + }, + smtp: &fakeFailedSmtp{}, + claims: makeFakeAuthClaims("admin@example.com"), + status: http.StatusInternalServerError, + output: "Failed to send mail", + }, + { + req: func() (*http.Request, error) { + j, err := json.Marshal(smtp.Json{ + From: "noreply@example.com", + ReplyTo: "admin@example.com", + To: "user@example.com", + Cc: "user2@example.com", + Bcc: "user3@example.com", + Subject: "Test Subject", + BodyType: "plain", + Body: "Some plain text", + }) + if err != nil { + return nil, err + } + return http.NewRequest(http.MethodPost, "https://api.example.com/v1/mail/message", bytes.NewReader(j)) + }, + smtp: &fakeSmtp{ + from: "noreply@example.com", + deliver: []string{"user@example.com", "user2@example.com", "user3@example.com"}, + body: []byte("Mime-Version: 1.0\r\n" + + "Content-Type: text/plain; charset=utf-8\r\n" + + "Cc: \r\n" + + "To: \r\n" + + "Reply-To: \r\n" + + "From: \r\n" + + "Subject: Test Subject\r\n" + + "Date: Sat, 01 Jan 2000 00:00:00 +0000\r\n" + + "\r\n" + + "Some plain text"), + }, + claims: makeFakeAuthClaims("admin@example.com"), + status: http.StatusAccepted, + output: "", + }, +} + +func makeFakeAuthClaims(subject string) AuthClaims { + return struct { + jwt.RegisteredClaims + ClaimType string + Claims auth.AccessTokenClaims + }{ + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: "Test", + Subject: subject, + Audience: jwt.ClaimStrings{"mail.example.com"}, + }, + ClaimType: "access-token", + Claims: auth.AccessTokenClaims{Perms: claims.NewPermStorage()}, + } +} + +func TestMessageSender(t *testing.T) { + for _, i := range messageSenderTestData { + rec := httptest.NewRecorder() + req, err := i.req() + assert.NoError(t, err) + MessageSender(i.smtp)(rec, req, httprouter.Params{}, i.claims) + + res := rec.Result() + assert.Equal(t, i.status, res.StatusCode) + assert.NotNil(t, res.Body) + all, err := io.ReadAll(res.Body) + assert.NoError(t, err) + if i.output == "" { + assert.Equal(t, "", string(all)) + } else { + assert.Equal(t, "{\"error\":\""+i.output+"\"}\n", string(all)) + } + } +} diff --git a/postfix-config/comma-list-scanner/comma-list-scanner.go b/postfix-config/comma-list-scanner/comma-list-scanner.go deleted file mode 100644 index 6a3e6a1..0000000 --- a/postfix-config/comma-list-scanner/comma-list-scanner.go +++ /dev/null @@ -1,57 +0,0 @@ -package comma_list_scanner - -import ( - "bufio" - "bytes" - "fmt" - "io" -) - -type CommaListScanner struct { - r *bufio.Scanner - text string - err error -} - -func NewCommaListScanner(r io.Reader) *CommaListScanner { - s := bufio.NewScanner(r) - s.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { - if atEOF && len(data) == 0 { - return 0, nil, nil - } - println("data", fmt.Sprintf("%s", data)) - println("index", bytes.IndexAny(data, " ,")) - if i := bytes.IndexAny(data, " ,"); i >= 0 { - // consume all spaces after the comma - j := i + 1 - for j < len(data) && data[j] == ' ' { - j++ - } - return j, bytes.TrimSpace(data[0:i]), nil - } - // If we're at EOF, we have a final non-terminated line. Return it. - if atEOF { - return len(data), bytes.TrimSpace(data), nil - } - // Request more data. - return 0, nil, nil - }) - return &CommaListScanner{r: s} -} - -func (c *CommaListScanner) Scan() bool { - if c.r.Scan() { - c.text = c.r.Text() - return true - } - c.err = c.r.Err() - return false -} - -func (c *CommaListScanner) Text() string { - return c.text -} - -func (c *CommaListScanner) Err() error { - return c.err -} diff --git a/postfix-config/comma-list-scanner/comma-list-scanner_test.go b/postfix-config/comma-list-scanner/comma-list-scanner_test.go deleted file mode 100644 index a4b779d..0000000 --- a/postfix-config/comma-list-scanner/comma-list-scanner_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package comma_list_scanner - -import ( - "github.com/stretchr/testify/assert" - "strings" - "testing" -) - -var testCommaList = []string{ - "hello, wow-this-is-cool, amazing", - "hello, wow-this-is-cool", - "hello, wow-this-is-cool, ", - "hello, wow-this-is-cool,", - ",hello, wow-this-is-cool", - ",hello, wow-this-is-cool,", - "hello, wow-this-is-cool,,,", -} - -func TestNewCommaListScanner(t *testing.T) { - for _, i := range testCommaList { - t.Run(i, func(t *testing.T) { - // use comma list scanner - s := NewCommaListScanner(strings.NewReader(i)) - n := strings.Count(i, ",") - a := make([]string, 0, n+1) - for s.Scan() { - a = append(a, s.Text()) - } - assert.NoError(t, s.Err()) - - // test against splitting and trimming strings - b := strings.Split(i, ",") - for i := 0; i < len(b); i++ { - c := strings.TrimSpace(b[i]) - if c == "" { - b = append(b[0:i], b[i+1:]...) - i-- - } else { - b[i] = c - } - } - assert.Equal(t, b, a) - }) - } -} diff --git a/postfix-config/config-parser/config-parser.go b/postfix-config/config-parser/config-parser.go deleted file mode 100644 index 7738bd3..0000000 --- a/postfix-config/config-parser/config-parser.go +++ /dev/null @@ -1,49 +0,0 @@ -package config_parser - -import ( - "bufio" - "errors" - "io" - "strings" -) - -var ErrInvalidConfigLine = errors.New("invalid config line") - -type ConfigParser struct { - s *bufio.Scanner - pair [2]string - err error -} - -func NewConfigParser(r io.Reader) *ConfigParser { - return &ConfigParser{s: bufio.NewScanner(r)} -} - -func (c *ConfigParser) Scan() bool { -scanAgain: - if !c.s.Scan() { - return false - } - text := strings.TrimSpace(c.s.Text()) - if text == "" || strings.HasPrefix(text, "#") { - goto scanAgain - } - n := strings.IndexByte(text, '=') - if n < 2 || n+2 >= len(text) || text[n-1] != ' ' || text[n+1] != ' ' { - c.err = ErrInvalidConfigLine - return false - } - c.pair = [2]string{text[:n-1], text[n+2:]} - return true -} - -func (c *ConfigParser) Pair() (string, string) { - return strings.TrimSpace(c.pair[0]), strings.TrimSpace(c.pair[1]) -} - -func (c *ConfigParser) Err() error { - if c.err != nil { - return c.err - } - return c.s.Err() -} diff --git a/postfix-config/config-parser/config-parser_test.go b/postfix-config/config-parser/config-parser_test.go deleted file mode 100644 index b1762e1..0000000 --- a/postfix-config/config-parser/config-parser_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package config_parser - -import ( - "github.com/stretchr/testify/assert" - "strings" - "testing" -) - -var configParserData = []struct { - Input string - Values [][2]string -}{ - { - "a = a", - [][2]string{{"a", "a"}}, - }, - { - " a = a ", - [][2]string{{"a", "a"}}, - }, - { - " # this is a comment\n a = a, b\nb = c, d", - [][2]string{{"a", "a, b"}, {"b", "c, d"}}, - }, -} - -func TestConfigParser(t *testing.T) { - for _, i := range configParserData { - t.Run(i.Input, func(t *testing.T) { - a := NewConfigParser(strings.NewReader(i.Input)) - n := 0 - for a.Scan() { - assert.False(t, n >= len(i.Values)) - assert.Equal(t, i.Values[n], a.pair) - n++ - } - }) - } -} diff --git a/postfix-config/config.go b/postfix-config/config.go deleted file mode 100644 index 4057930..0000000 --- a/postfix-config/config.go +++ /dev/null @@ -1,43 +0,0 @@ -package postfix_config - -import mapProvider "github.com/1f349/lotus/postfix-config/map-provider" - -type Config struct { - // same - VirtualMailboxDomains mapProvider.MapProvider - VirtualAliasMaps mapProvider.MapProvider - VirtualMailboxMaps mapProvider.MapProvider - AliasMaps mapProvider.MapProvider - LocalRecipientMaps mapProvider.MapProvider - SmtpdSenderLoginMaps mapProvider.MapProvider -} - -var parseProviderData = map[string]string{ - "virtual_mailbox_domains": "comma", - "virtual_alias_maps": "comma", - "virtual_mailbox_maps": "comma", - "alias_maps": "comma", - "local_recipient_maps": "comma", - "smtpd_sender_login_maps": "union", -} - -func (c *Config) ParseProvider(k string) string { - return parseProviderData[k] -} - -func (c *Config) SetKey(k string, m mapProvider.MapProvider) { - switch k { - case "virtual_mailbox_domains": - c.VirtualMailboxDomains = m - case "virtual_alias_maps": - c.VirtualAliasMaps = m - case "virtual_mailbox_maps": - c.VirtualMailboxMaps = m - case "alias_maps": - c.AliasMaps = m - case "local_recipient_maps": - c.LocalRecipientMaps = m - case "smtpd_sender_login_maps": - c.SmtpdSenderLoginMaps = m - } -} diff --git a/postfix-config/decoder.go b/postfix-config/decoder.go deleted file mode 100644 index 0d96848..0000000 --- a/postfix-config/decoder.go +++ /dev/null @@ -1,108 +0,0 @@ -package postfix_config - -import ( - "errors" - "fmt" - commaListScanner "github.com/1f349/lotus/postfix-config/comma-list-scanner" - configParser "github.com/1f349/lotus/postfix-config/config-parser" - mapProvider "github.com/1f349/lotus/postfix-config/map-provider" - "io" - "path/filepath" - "strings" -) - -type Decoder struct { - r *configParser.ConfigParser - temp map[string]string - basePath string -} - -func NewDecoder(r io.Reader) *Decoder { - return &Decoder{r: configParser.NewConfigParser(r)} -} - -func (d *Decoder) Load() error { - for d.r.Scan() { - k, v := d.r.Pair() - d.temp[k] = v - } - if err := d.r.Err(); err != nil { - return err - } - - switch d.value.ParseProvider(k) { - case "comma": - m := mapProvider.SequenceMapProvider{} - - s := commaListScanner.NewCommaListScanner(strings.NewReader(v)) - for s.Scan() { - a := s.Text() - println("a", a) - if strings.HasPrefix(a, "$") { - m = append(m, &mapProvider.Variable{Name: a[1:]}) - continue - } - - v2, err := d.createValue(a) - if err != nil { - return err - } - m = append(m, v2) - } - if err := s.Err(); err != nil { - return err - } - d.value.SetKey(k, m) - case "union": - if !strings.HasPrefix(v, "unionmap:{") || !strings.HasSuffix(v, "}") { - return errors.New("key requires a union map") - } - v = v[len("unionmap:{") : len(v)-1] - - m := mapProvider.SequenceMapProvider{} - s := commaListScanner.NewCommaListScanner(strings.NewReader(v)) - for s.Scan() { - a := s.Text() - v2, err := d.createValue(a) - if err != nil { - return err - } - m = append(m, v2) - } - default: - return fmt.Errorf("key '%s' has no defined parse provider", k) - } - } - return d.r.Err() -} - -func (d *Decoder) createValue(a string) (mapProvider.MapProvider, error) { - n := strings.IndexByte(a, ':') - if n == -1 { - return nil, fmt.Errorf("missing prefix") - } - - namespace := a[:n] - value := a[n+1:] - switch namespace { - case "mysql": - if !filepath.IsAbs(value) { - value = filepath.Join(d.basePath, value) - } - provider, err := mapProvider.NewMySqlMapProvider(value) - if err != nil { - return nil, err - } - return provider, nil - case "hash": - if !filepath.IsAbs(value) { - value = filepath.Join(d.basePath, value) - } - provider, err := mapProvider.NewHashMapProvider(value) - if err != nil { - return nil, err - } - return provider, nil - } - return nil, errors.New("invalid provider namespace") -} diff --git a/postfix-config/decoder_test.go b/postfix-config/decoder_test.go deleted file mode 100644 index 7bc4e2e..0000000 --- a/postfix-config/decoder_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package postfix_config - -import ( - "bytes" - _ "embed" - configParser "github.com/1f349/lotus/postfix-config/config-parser" - "github.com/stretchr/testify/assert" - "os" - "path/filepath" - "testing" -) - -//go:embed example.cf -var exampleConfig []byte - -func TestDecoder_Load(t *testing.T) { - // get working directory - wd, err := os.Getwd() - assert.NoError(t, err) - - // read example config - b := bytes.NewReader(exampleConfig) - d := &Decoder{ - r: configParser.NewConfigParser(b), - basePath: filepath.Join(wd, "test-data"), - } - assert.NoError(t, d.Load()) -} diff --git a/postfix-config/example.cf b/postfix-config/example.cf deleted file mode 100644 index 359b837..0000000 --- a/postfix-config/example.cf +++ /dev/null @@ -1,10 +0,0 @@ -# this only contains the relevant config properties - -#recipient_delimiter = + - -virtual_mailbox_domains = mysql:mysql_virtual_domains_maps.cf -virtual_alias_maps = mysql:mysql_virtual_alias_maps.cf, mysql:mysql_virtual_alias_wildcard_maps.cf, mysql:mysql_virtual_alias_domain_maps.cf, mysql:mysql_virtual_alias_user_maps.cf, mysql:mysql_virtual_alias_userdomain_maps.cf, mysql:mysql_virtual_alias_domain_catchall_maps.cf, mysql:mysql_virtual_alias_user_catchall_maps.cf -virtual_mailbox_maps = mysql:mysql_virtual_mailbox_maps.cf, mysql:mysql_virtual_alias_domain_mailbox_maps.cf, mysql:mysql_virtual_alias_user_mailbox_maps.cf, mysql:mysql_virtual_alias_userdomain_mailbox_maps.cf -alias_maps = hash:aliases.txt $virtual_alias_maps -local_recipient_maps = $virtual_mailbox_maps $alias_maps -smtpd_sender_login_maps = unionmap:{ hash:aliases.txt, mysql:mysql_sender_alias_maps.cf, mysql:mysql_virtual_alias_maps.cf, mysql:mysql_virtual_alias_domain_maps.cf, mysql:mysql_virtual_alias_user_maps.cf, mysql:mysql_virtual_alias_userdomain_maps.cf } diff --git a/postfix-config/map-provider/hash.go b/postfix-config/map-provider/hash.go deleted file mode 100644 index 025e82a..0000000 --- a/postfix-config/map-provider/hash.go +++ /dev/null @@ -1,48 +0,0 @@ -package map_provider - -import ( - "bufio" - "io" - "os" - "strings" -) - -type Hash struct { - r io.Reader - v map[string]string -} - -var _ MapProvider = &Hash{} - -func NewHashMapProvider(filename string) (*Hash, error) { - open, err := os.Open(filename) - if err != nil { - return nil, err - } - return &Hash{open, make(map[string]string)}, nil -} - -func (h *Hash) Load() error { - scanner := bufio.NewScanner(h.r) - scanner.Split(bufio.ScanLines) - for scanner.Scan() { - text := strings.TrimSpace(scanner.Text()) - if strings.HasPrefix(text, "#") { - continue - } - - n := strings.IndexByte(text, ':') - key := strings.TrimSpace(text[:n]) - values := strings.Split(text[n+1:], ",") - for _, i := range values { - k := strings.TrimSpace(i) - h.v[k] = key - } - } - return scanner.Err() -} - -func (h *Hash) Find(name string) (string, bool) { - v, ok := h.v[name] - return v, ok -} diff --git a/postfix-config/map-provider/hash_example.txt b/postfix-config/map-provider/hash_example.txt deleted file mode 100644 index b8c3f94..0000000 --- a/postfix-config/map-provider/hash_example.txt +++ /dev/null @@ -1,3 +0,0 @@ -# See man 5 aliases for format -postmaster: root -test: this, is, an, example diff --git a/postfix-config/map-provider/hash_test.go b/postfix-config/map-provider/hash_test.go deleted file mode 100644 index 846b1b1..0000000 --- a/postfix-config/map-provider/hash_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package map_provider - -import ( - "bytes" - _ "embed" - "github.com/stretchr/testify/assert" - "testing" -) - -//go:embed hash_example.txt -var hashExample []byte - -func TestHash_Load(t *testing.T) { - h := &Hash{r: bytes.NewReader(hashExample), v: make(map[string]string)} - assert.NoError(t, h.Load()) - assert.Equal(t, map[string]string{ - "root": "postmaster", - "this": "test", - "is": "test", - "an": "test", - "example": "test", - }, h.v) -} diff --git a/postfix-config/map-provider/map-provider.go b/postfix-config/map-provider/map-provider.go deleted file mode 100644 index be220db..0000000 --- a/postfix-config/map-provider/map-provider.go +++ /dev/null @@ -1,23 +0,0 @@ -package map_provider - -// MapProvider is an interface to allow looking up mapped values from variables, -// hash files or mysql queries. -type MapProvider interface { - Find(name string) (string, bool) -} - -// SequenceMapProvider calls Find against each provider in a slice and outputs -// the true mapped value of the input. first mapped value found. If the input was -// not found then "", false is returned. -type SequenceMapProvider []MapProvider - -func (s SequenceMapProvider) Find(name string) (string, bool) { - for _, i := range s { - if find, ok := i.Find(name); ok { - return find, true - } - } - return "", false -} - -var _ MapProvider = SequenceMapProvider{} diff --git a/postfix-config/map-provider/mysql-prepared-query.go b/postfix-config/map-provider/mysql-prepared-query.go deleted file mode 100644 index 757fd48..0000000 --- a/postfix-config/map-provider/mysql-prepared-query.go +++ /dev/null @@ -1,76 +0,0 @@ -package map_provider - -import ( - "errors" - "sort" - "strings" - "unicode" -) - -var ( - ErrMissingArgument = errors.New("missing argument") - ErrInvalidRawQuery = errors.New("invalid raw query") -) - -type PreparedQuery struct { - raw string - params map[int]byte -} - -func NewPreparedQuery(raw string) (*PreparedQuery, error) { - var s strings.Builder - origin := 0 - params := make(map[int]byte) - for { - n := strings.IndexByte(raw[origin:], '%') - if n == -1 { - break - } - n += origin - if n+1 == len(raw) { - return nil, ErrInvalidRawQuery - } - s.WriteString(raw[origin:n]) - if raw[n+1] == '%' { - s.WriteByte('%') - origin = n + 1 - continue - } - params[s.Len()] = toLower(raw[n+1]) - origin = n + 2 - } - s.WriteString(raw[origin:]) - return &PreparedQuery{ - raw: s.String(), - params: params, - }, nil -} - -func (p *PreparedQuery) Format(args map[byte]string) (string, error) { - var s strings.Builder - keys := make([]int, 0, len(p.params)) - for k := range p.params { - keys = append(keys, k) - } - sort.Ints(keys) - origin := 0 - for _, k := range keys { - r, ok := args[p.params[k]] - if !ok { - return "", ErrMissingArgument - } - - // write up to and including the next parameter - s.WriteString(p.raw[origin:k]) - s.WriteString(strings.ReplaceAll(r, "'", "")) - origin = k - } - - // write the rest of the query - s.WriteString(p.raw[origin:]) - return s.String(), nil -} - -func toLower(a byte) byte { - return byte(unicode.ToLower(rune(a))) -} diff --git a/postfix-config/map-provider/mysql-prepared-query_test.go b/postfix-config/map-provider/mysql-prepared-query_test.go deleted file mode 100644 index e94fc5f..0000000 --- a/postfix-config/map-provider/mysql-prepared-query_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package map_provider - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -const ( - testQuery = "SELECT aliasMap.goto FROM aliasMap,aliasdomainMap WHERE aliasdomainMap.domain='%d' AND aliasMap.address = CONCAT('%u', '@', aliasdomainMap.goto) AND aliasMap.active > 0 AND aliasdomainMap.active > 0" - testQueryRaw = "SELECT aliasMap.goto FROM aliasMap,aliasdomainMap WHERE aliasdomainMap.domain='' AND aliasMap.address = CONCAT('', '@', aliasdomainMap.goto) AND aliasMap.active > 0 AND aliasdomainMap.active > 0" - testQueryFormat = "SELECT aliasMap.goto FROM aliasMap,aliasdomainMap WHERE aliasdomainMap.domain='example.com' AND aliasMap.address = CONCAT('test', '@', aliasdomainMap.goto) AND aliasMap.active > 0 AND aliasdomainMap.active > 0" -) - -func TestNewPreparedQuery(t *testing.T) { - query, err := NewPreparedQuery(testQuery) - assert.NoError(t, err) - assert.Equal(t, PreparedQuery{ - raw: testQueryRaw, - params: map[int]byte{ - 79: 'd', - 112: 'u', - }, - }, *query) -} - -func TestPreparedQuery_Format(t *testing.T) { - query := &PreparedQuery{ - raw: testQueryRaw, - params: map[int]byte{ - 79: 'd', - 112: 'u', - }, - } - format, err := query.Format(map[byte]string{ - 'd': "example.com", - 'u': "test", - }) - assert.NoError(t, err) - assert.Equal(t, testQueryFormat, format) -} diff --git a/postfix-config/map-provider/mysql.go b/postfix-config/map-provider/mysql.go deleted file mode 100644 index 937fcef..0000000 --- a/postfix-config/map-provider/mysql.go +++ /dev/null @@ -1,112 +0,0 @@ -package map_provider - -import ( - "database/sql" - configParser "github.com/1f349/lotus/postfix-config/config-parser" - "github.com/go-sql-driver/mysql" - _ "github.com/go-sql-driver/mysql" - "io" - "os" - "regexp" - "strings" -) - -var checkUatD = regexp.MustCompile("^[^@]+@[^@]+$") - -type MySql struct { - r io.Reader - db *sql.DB - query *PreparedQuery -} - -var _ MapProvider = &MySql{} - -func NewMySqlMapProvider(filename string) (*MySql, error) { - open, err := os.Open(filename) - if err != nil { - return nil, err - } - return &MySql{r: open}, nil -} - -func (m *MySql) Load() error { - p := configParser.NewConfigParser(m.r) - c := mysql.NewConfig() - var q string - for p.Scan() { - k, v := p.Pair() - switch k { - case "user": - c.User = v - case "password": - c.Passwd = v - case "hosts": - c.Net = "tcp" - c.Addr = v - case "dbname": - c.DBName = v - case "query": - q = v - } - } - if err := p.Err(); err != nil { - return err - } - - q2, err := NewPreparedQuery(q) - if err != nil { - return err - } - m.query = q2 - - // try opening connection - db, err := sql.Open("mysql", c.FormatDSN()) - if err != nil { - return err - } - m.db = db - - return db.Ping() -} - -func (m *MySql) Find(name string) (string, bool) { - format, err := m.query.Format(genQueryArgs(name)) - return format, err == nil -} - -// genQueryArgs converts an input key into the % encoded parameters -// -// %s - full input key -// %u - user part of user@domain or full input key -// %d - domain part of user@domain or missing parameter -// %[1-9] - replaced with the most significant component of the input key's domain -// for `user@mail.example.com` %1 = com, %2 = example, %3 = mail -// otherwise they are missing parameters -func genQueryArgs(name string) map[byte]string { - args := make(map[byte]string) - args['s'] = name - args['u'] = name - if checkUatD.MatchString(name) { - n := strings.IndexByte(name, '@') - args['u'] = name[:n] - args['d'] = name[n+1:] - - genDomainArgs(args, name[n+1:]) - } - return args -} - -// genDomainArgs replaces with the most significant component of the input key's -// domain for `user@mail.example.com` %1 = com, %2 = example, %3 = mail, -// otherwise they are missing parameters -func genDomainArgs(args map[byte]string, s string) { - i, l := byte(1), len(s) - for { - n := strings.LastIndexByte(s, '.') - if n == -1 { - break - } - args[(i + '0')] = s[n+1 : l] - l = n - } -} diff --git a/postfix-config/map-provider/mysql_example.cf b/postfix-config/map-provider/mysql_example.cf deleted file mode 100644 index e22ea38..0000000 --- a/postfix-config/map-provider/mysql_example.cf +++ /dev/null @@ -1,5 +0,0 @@ -user = example -password = 1234 -hosts = 127.0.0.1 -dbname = mail -query = SELECT aliasMap.goto FROM aliasMap,aliasdomainMap WHERE aliasdomainMap.domain='%d' AND aliasMap.address = CONCAT('%u', '@', aliasdomainMap.goto) AND aliasMap.active > 0 AND aliasdomainMap.active > 0 diff --git a/postfix-config/map-provider/variable.go b/postfix-config/map-provider/variable.go deleted file mode 100644 index a0dc17f..0000000 --- a/postfix-config/map-provider/variable.go +++ /dev/null @@ -1,12 +0,0 @@ -package map_provider - -type Variable struct { - Name string - Value MapProvider -} - -func (v *Variable) Find(name string) (string, bool) { - return v.Value.Find(name) -} - -var _ MapProvider = &Variable{} diff --git a/postfix-config/test-data/aliases.txt b/postfix-config/test-data/aliases.txt deleted file mode 100644 index 01c5c6a..0000000 --- a/postfix-config/test-data/aliases.txt +++ /dev/null @@ -1 +0,0 @@ -a: a b \ No newline at end of file diff --git a/postfix-config/test-data/mysql_sender_alias_maps.cf b/postfix-config/test-data/mysql_sender_alias_maps.cf deleted file mode 100644 index e69de29..0000000 diff --git a/postfix-config/test-data/mysql_virtual_alias_domain_catchall_maps.cf b/postfix-config/test-data/mysql_virtual_alias_domain_catchall_maps.cf deleted file mode 100644 index e69de29..0000000 diff --git a/postfix-config/test-data/mysql_virtual_alias_domain_mailbox_maps.cf b/postfix-config/test-data/mysql_virtual_alias_domain_mailbox_maps.cf deleted file mode 100644 index e69de29..0000000 diff --git a/postfix-config/test-data/mysql_virtual_alias_domain_maps.cf b/postfix-config/test-data/mysql_virtual_alias_domain_maps.cf deleted file mode 100644 index e69de29..0000000 diff --git a/postfix-config/test-data/mysql_virtual_alias_maps.cf b/postfix-config/test-data/mysql_virtual_alias_maps.cf deleted file mode 100644 index e69de29..0000000 diff --git a/postfix-config/test-data/mysql_virtual_alias_user_catchall_maps.cf b/postfix-config/test-data/mysql_virtual_alias_user_catchall_maps.cf deleted file mode 100644 index e69de29..0000000 diff --git a/postfix-config/test-data/mysql_virtual_alias_user_mailbox_maps.cf b/postfix-config/test-data/mysql_virtual_alias_user_mailbox_maps.cf deleted file mode 100644 index e69de29..0000000 diff --git a/postfix-config/test-data/mysql_virtual_alias_user_maps.cf b/postfix-config/test-data/mysql_virtual_alias_user_maps.cf deleted file mode 100644 index e69de29..0000000 diff --git a/postfix-config/test-data/mysql_virtual_alias_userdomain_mailbox_maps.cf b/postfix-config/test-data/mysql_virtual_alias_userdomain_mailbox_maps.cf deleted file mode 100644 index e69de29..0000000 diff --git a/postfix-config/test-data/mysql_virtual_alias_userdomain_maps.cf b/postfix-config/test-data/mysql_virtual_alias_userdomain_maps.cf deleted file mode 100644 index e69de29..0000000 diff --git a/postfix-config/test-data/mysql_virtual_alias_wildcard_maps.cf b/postfix-config/test-data/mysql_virtual_alias_wildcard_maps.cf deleted file mode 100644 index e69de29..0000000 diff --git a/postfix-config/test-data/mysql_virtual_domains_maps.cf b/postfix-config/test-data/mysql_virtual_domains_maps.cf deleted file mode 100644 index e69de29..0000000 diff --git a/postfix-config/test-data/mysql_virtual_mailbox_maps.cf b/postfix-config/test-data/mysql_virtual_mailbox_maps.cf deleted file mode 100644 index e69de29..0000000 diff --git a/postfix-lookup/lookup.sh b/postfix-lookup/lookup.sh new file mode 100644 index 0000000..001ce84 --- /dev/null +++ b/postfix-lookup/lookup.sh @@ -0,0 +1,5 @@ +#!/bin/bash +virtual_alias_maps=$(postconf -h virtual_alias_maps | tr ',' '\n') +alias_to_lookup="$1" +result=$(echo "$virtual_alias_maps" | xargs -I {} postmap -q "$alias_to_lookup" {}) +echo "result=$result" diff --git a/postfix-lookup/postfix-lookup.go b/postfix-lookup/postfix-lookup.go new file mode 100644 index 0000000..9e454f2 --- /dev/null +++ b/postfix-lookup/postfix-lookup.go @@ -0,0 +1,47 @@ +package postfix_lookup + +import ( + "bufio" + "bytes" + _ "embed" + "errors" + "os/exec" + "strings" +) + +var ErrInvalidAlias = errors.New("invalid alias") + +//go:embed lookup.sh +var lookupScript string + +type PostfixLookup struct { + execCmd func(key string) ([]byte, error) +} + +func NewPostfixLookup() *PostfixLookup { + return &PostfixLookup{ + execCmd: func(key string) ([]byte, error) { + return exec.Command("bash", "-c", lookupScript, "--", key).Output() + }, + } +} + +func (d *PostfixLookup) Lookup(key string) (string, error) { + output, err := d.execCmd(key) + if err != nil { + return "", err + } + + s := bufio.NewScanner(bytes.NewReader(output)) + for s.Scan() { + a := s.Text() + n := strings.IndexByte(a, '=') + if n != -1 && a[:n] == "result" { + return a[n+1:], nil + } + } + if err := s.Err(); err != nil { + return "", err + } + return "", ErrInvalidAlias +} diff --git a/postfix-lookup/postfix-lookup_test.go b/postfix-lookup/postfix-lookup_test.go new file mode 100644 index 0000000..198c206 --- /dev/null +++ b/postfix-lookup/postfix-lookup_test.go @@ -0,0 +1,45 @@ +package postfix_lookup + +import ( + _ "embed" + "github.com/stretchr/testify/assert" + "strings" + "testing" +) + +var postfixLookupData = []struct { + Input string + Output string +}{ + {"hi@example.com", "admin@example.com"}, + {"test@example.com", "admin@example.com"}, + {"user@example.org", "admin@example.org"}, + {"user@example.net", ""}, +} + +func TestDecoder_Load(t *testing.T) { + p := &PostfixLookup{execCmd: func(key string) ([]byte, error) { + n := strings.IndexByte(key, '@') + if n == -1 { + return []byte{}, nil + } + addr := key[n+1:] + switch addr { + case "example.com", "example.org": + return []byte("result=admin@" + addr + "\nadmin@" + addr + "\n"), nil + } + return []byte{}, nil + }} + for _, i := range postfixLookupData { + t.Run(i.Input, func(t *testing.T) { + lookup, err := p.Lookup(i.Input) + if i.Output == "" && err == nil { + t.Fatal("expected error for empty output test case") + } + if i.Output != "" && err != nil { + t.Fatal("expected no error for non-empty output test case") + } + assert.Equal(t, i.Output, lookup) + }) + } +} diff --git a/smtp/json.go b/smtp/json.go index eee11b7..856a066 100644 --- a/smtp/json.go +++ b/smtp/json.go @@ -22,7 +22,7 @@ type Json struct { Bcc string `json:"bcc"` Subject string `json:"subject"` BodyType string `json:"body_type"` - Body string `json:"body"` + Body string `json:"Body"` } func (s Json) parseAddresses() (addrFrom, addrReplyTo, addrTo, addrCc, addrBcc []*mail.Address, err error) { @@ -31,23 +31,31 @@ func (s Json) parseAddresses() (addrFrom, addrReplyTo, addrTo, addrCc, addrBcc [ if err != nil { return } - addrReplyTo, err = mail.ParseAddressList(s.ReplyTo) - if err != nil { - return + if s.ReplyTo != "" { + addrReplyTo, err = mail.ParseAddressList(s.ReplyTo) + if err != nil { + return + } } - addrTo, err = mail.ParseAddressList(s.To) - if err != nil { - return + if s.To != "" { + addrTo, err = mail.ParseAddressList(s.To) + if err != nil { + return + } } - addrCc, err = mail.ParseAddressList(s.Cc) - if err != nil { - return + if s.Cc != "" { + addrCc, err = mail.ParseAddressList(s.Cc) + if err != nil { + return + } + } + if s.Bcc != "" { + addrBcc, err = mail.ParseAddressList(s.Bcc) } - addrBcc, err = mail.ParseAddressList(s.Bcc) return } -func (s Json) PrepareMail() (*Mail, error) { +func (s Json) PrepareMail(now time.Time) (*Mail, error) { // parse addresses from json data addrFrom, addrReplyTo, addrTo, addrCc, addrBcc, err := s.parseAddresses() if err != nil { @@ -64,7 +72,7 @@ func (s Json) PrepareMail() (*Mail, error) { // set base headers var h mail.Header - h.SetDate(time.Now()) + h.SetDate(now) h.SetSubject(s.Subject) h.SetAddressList("From", addrFrom) h.SetAddressList("Reply-To", addrReplyTo) @@ -87,8 +95,8 @@ func (s Json) PrepareMail() (*Mail, error) { } m := &Mail{ - from: from, - deliver: CreateSenderSlice(addrTo, addrCc, addrBcc), + From: from, + Deliver: CreateSenderSlice(addrTo, addrCc, addrBcc), } out := new(bytes.Buffer) @@ -96,6 +104,6 @@ func (s Json) PrepareMail() (*Mail, error) { return nil, err } - m.body = out.Bytes() + m.Body = out.Bytes() return m, nil } diff --git a/smtp/smtp.go b/smtp/smtp.go index 20e3bc7..8944338 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -11,9 +11,9 @@ type Smtp struct { } type Mail struct { - from string - deliver []string - body []byte + From string + Deliver []string + Body []byte } var defaultDialer = smtp.Dial @@ -26,10 +26,10 @@ func (s *Smtp) Send(mail *Mail) error { } // use a reader to send bytes - r := bytes.NewReader(mail.body) + r := bytes.NewReader(mail.Body) // send mail - return smtpClient.SendMail(mail.from, mail.deliver, r) + return smtpClient.SendMail(mail.From, mail.Deliver, r) } func CreateSenderSlice(to, cc, bcc []*mail.Address) []string { diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 3e1fdb5..ce74fb3 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -51,7 +51,7 @@ func TestSmtp_Send(t *testing.T) { } s := &Smtp{Server: "localhost:25"} - err := s.Send(&Mail{from: "test@localhost", deliver: []string{"a@localhost", "b@localhost"}, body: sendTestMessage}) + err := s.Send(&Mail{From: "test@localhost", Deliver: []string{"a@localhost", "b@localhost"}, Body: sendTestMessage}) assert.NoError(t, err) assert.Equal(t, []byte("MAIL test@localhost\n"), <-serverData) assert.Equal(t, []byte("RCPT a@localhost\n"), <-serverData)