diff --git a/.github/workflows/close-stale-issues.yml b/.github/workflows/close-stale-issues.yml index d5bcb05..8f09675 100644 --- a/.github/workflows/close-stale-issues.yml +++ b/.github/workflows/close-stale-issues.yml @@ -10,7 +10,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@v8.0.0 + - uses: actions/stale@v9.0.0 with: days-before-issue-stale: 21 days-before-issue-close: 7 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 60c92dd..847bbea 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -69,4 +69,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 112e92b..c9a9a6e 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v4 # build the binaries - - uses: wangyoucao577/go-release-action@v1.40 + - uses: wangyoucao577/go-release-action@v1.51 with: github_token: ${{ secrets.GITHUB_TOKEN }} goos: ${{ matrix.goos }} diff --git a/cmd/mailer.go b/cmd/mailer.go index e1d3385..ee2c679 100644 --- a/cmd/mailer.go +++ b/cmd/mailer.go @@ -1,17 +1,27 @@ package cmd import ( + "bytes" "crypto/tls" "fmt" - "io" + "net/mail" "net/smtp" "os" + "regexp" + "strconv" "strings" "time" + + "github.com/lithammer/shortuuid" ) // SMTP wrapper will send and optionally log the transaction -func smtpWrapper(from string, to []string, msg []byte) error { +func smtpWrapper(from string, to []string, message []byte) error { + msg, err := injectHeaders(message) + if err != nil { + return err + } + code, response, err := smtpSend(from, to, msg) if config.LogFile != "" { @@ -21,11 +31,15 @@ func smtpWrapper(from string, to []string, msg []byte) error { if config.STARTTLS { tls = "on" } + if err != nil { + code, response = smtpErrParser(err.Error()) + } + recipients := strings.Join(to, ",") logMsg := fmt.Sprintf("%s host=%s tls=%s from=%s, recipients=%s mailsize=%d, smtpstatus=%d smtpmsg='%s'\n", ts, config.SMTPHost, tls, from, recipients, len(msg), code, response) file, err := os.OpenFile(config.LogFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666) // #nosec - // silently fail if the file cannot be opened + // silently fail if the log file cannot be opened if err == nil { defer file.Close() _, _ = file.Write([]byte(logMsg)) @@ -75,6 +89,7 @@ func smtpSend(from string, to []string, msg []byte) (int, string, error) { return 0, "", err } } + if err = c.Mail(from); err != nil { return 0, "", err } @@ -85,41 +100,79 @@ func smtpSend(from string, to []string, msg []byte) (int, string, error) { } } - w, err := c.Data() + code, message, err := dataWithResponse(c, msg) if err != nil { return 0, "", err } - if _, err := w.Write(msg); err != nil { + return code, message, c.Quit() +} + +// Go's net/smtp Data() only returns an error message. This custom function +// also returns the server's response message so it can be logged. +// +// @see https://github.com/axllent/sndmail/issues/10#issuecomment-2214807859 +func dataWithResponse(c *smtp.Client, msg []byte) (int, string, error) { + id, err := c.Text.Cmd("DATA") + if err != nil { return 0, "", err } - defer c.Quit() + c.Text.StartResponse(id) - code, message, err := closeData(c) + code, message, err := c.Text.ReadResponse(354) + if err != nil { + return code, message, err + } - return code, message, err -} + c.Text.EndResponse(id) + + w := c.Text.DotWriter() -// CloseData wil ensure the SMTP server response is returned -// @see https://stackoverflow.com/a/70925659 -func closeData(client *smtp.Client) (int, string, error) { - d := &dataCloser{ - c: client, - WriteCloser: client.Text.DotWriter(), + if _, err := w.Write(msg); err != nil { + return 0, "", err + } + + if err := w.Close(); err != nil { + return 0, "", err } - return d.Close() + return c.Text.ReadResponse(250) } -type dataCloser struct { - c *smtp.Client - io.WriteCloser +// Inject Message-Id and Date if missing +func injectHeaders(body []byte) ([]byte, error) { + msg, err := mail.ReadMessage(bytes.NewReader(body)) + if err != nil { + return nil, err + } + + // add message ID if missing + if msg.Header.Get("Message-Id") == "" { + messageID := shortuuid.New() + "@sndmail" + body = append([]byte("Message-Id: <"+messageID+">\r\n"), body...) + } + + // add date if missing + if msg.Header.Get("Date") == "" { + now := time.Now().Format("Mon, 02 Jan 2006 15:04:05 -0700 (MST)") + body = append([]byte("Date: "+now+"\r\n"), body...) + } + + return body, nil } -func (d *dataCloser) Close() (int, string, error) { - d.WriteCloser.Close() // #nosec make sure WriterCloser gets closed - code, message, err := d.c.Text.ReadResponse(250) +// error parser for SMTP response messages +func smtpErrParser(s string) (int, string) { + var re = regexp.MustCompile(`(\d\d\d) (.*)`) + if re.MatchString(s) { + matches := re.FindAllStringSubmatch(s, -1) + for _, m := range matches { + i, _ := strconv.Atoi(m[1]) + + return i, m[2] + } + } - return code, message, err + return 0, s } diff --git a/cmd/main.go b/cmd/main.go index 839b7ec..d6b8656 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -32,7 +32,3 @@ func Exec() { sendmail() } } - -// func main() { -// Cmd() -// } diff --git a/cmd/sendmail.go b/cmd/sendmail.go index b45b497..31addef 100644 --- a/cmd/sendmail.go +++ b/cmd/sendmail.go @@ -27,7 +27,7 @@ func sendmail() { msg, err := mail.ReadMessage(bytes.NewReader(body)) if err != nil { - // create blank message to lookups don't fail + // create blank message so lookups don't fail msg = &mail.Message{} // inject a new blank line below body diff --git a/go.mod b/go.mod index 9078384..df4922c 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,9 @@ module github.com/axllent/sndmail go 1.19 -require github.com/spf13/pflag v1.0.5 +require ( + github.com/lithammer/shortuuid v3.0.0+incompatible + github.com/spf13/pflag v1.0.5 +) + +require github.com/google/uuid v1.6.0 // indirect diff --git a/go.sum b/go.sum index 287f6fa..e73f3c8 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/lithammer/shortuuid v3.0.0+incompatible h1:NcD0xWW/MZYXEHa6ITy6kaXN5nwm/V115vj2YXfhS0w= +github.com/lithammer/shortuuid v3.0.0+incompatible/go.mod h1:FR74pbAuElzOUuenUHTK2Tciko1/vKuIKS9dSkDrA4w= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=