* feat: add configurable SMTP HELO hostname Allow the SMTP HELO/EHLO hostname to be configured separately from the SMTP server hostname. This is useful when the SMTP server requires clients to identify themselves with a fully qualified hostname different from the server address. * chore: remove vendored dependency changes * Bump go-pkgz/notify to v1.4.0 and document SMTP_HELO_HOST The HELOHost field lands in go-pkgz/notify v1.4.0, so the branch needs the bump to compile; v1.3.0 in master has no such field. The example module is tidied alongside, as any change to backend/go.mod requires. Documents the parameter in the parameters table and, separately, in the email setup page: what it does, that leaving it unset keeps the previous `localhost` greeting, and the case it exists for, a relay refusing the greeting under Postfix `reject_non_fqdn_helo_hostname`. Also records the current limit: verification emails for email authentication go through go-pkgz/auth's own sender, which has no equivalent setting, so the greeting there is unchanged. * Bump go-pkgz/auth to v2.2.0 and apply SMTP_HELO_HOST to verification email The verification email sender had no way to set the greeting, so a relay that refuses the HELO would accept notifications and still reject sign-in emails. EmailParams gains HELOHost in go-pkgz/auth v2.2.0, so the same SMTP_HELO_HOST now drives both paths. The example module is tidied alongside, as any change to backend/go.mod requires. --------- Co-authored-by: oli <someone@somewhere.tld> Co-authored-by: Dmitry Verkhoturov <paskal.07@gmail.com>
3.6 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
github.com/go-pkgz/email is a single-package library wrapping the stdlib net/smtp to simplify sending
messages (alerts, notifications, password-reset mails). It has no runtime dependencies — only testify for
tests. Not designed for high-volume/low-latency bulk sending. Go 1.19.
Commands
- Test:
go test -race ./...— CI runs withTZ=America/Chicago; the Date header uses RFC1123Z with an injectable clock, so timezone can affect assertions. Match CI:TZ=America/Chicago go test -race ./.... - Single test:
go test -run TestEmail_Send - Lint:
golangci-lint runfrom repo root (config is golangci-lint v2 format; CI pins v2.6) - Regenerate mocks:
go generate ./...(requiresmoq; do not hand-edit files undermocks/)
Architecture
Three source files, one package:
email.go—Sendertype,Send, and MIME message construction (buildMessage).options.go— functional options (Option = func(*Sender)), all applied inNewSender.auth.go— custom LOGIN SASL auth mechanism.
Key design points that span files:
-
Functional options.
Senderfields are unexported and set only throughOptionfuncs inoptions.go, applied inNewSenderafter defaults. Add a new config knob = newOptionfunc + field, nothing else. -
SMTPClientinterface is the seam. It's a consumer-side subset ofnet/smtp.Client(Mail/Auth/Rcpt/ Data/Quit/Close). If the caller injects one via theSMTP()option it's reused; otherwiseSendbuilds a freshnet/smtpclient per call viaem.client(). This interface is what the moq mock implements, so allSendtests run without a real SMTP server. -
em.client()handles the three transport modes: plain (dial + optional STARTTLS), and implicitTLS(dial over TLS, port 465).STARTTLSupgrades a plain connection (port 587).InsecureSkipVerifyfeeds thetls.Config. -
buildMessageis the intricate part. It assembles headers manually, then bodies:multipart/mixedfor attachments,multipart/relatedfor inline images, quoted-printable for the text body. Inline images get an autoContent-IDequal to their filename. Attachment/inline type is sniffed from the first 512 bytes viahttp.DetectContentType, then the file is re-seeked to 0 and base64-encoded. Changes here are easy to break silently — the tests assert on the exact serialized message string. -
LOGIN auth (
auth.go) exists because stdlib only ships PLAIN. Needed for Office 365 / Outlook.com. Enabled with theLoginAuth()option; it refuses to send credentials over an unencrypted, non-localhost connection.Sender.auth()picks PLAIN vs LOGIN and returns nil (no auth) when username/password are empty. -
Envelope vs. headers.
extractEmailAddress(vianet/mail) strips a display name soMAIL FROM/RCPT TOget a bare address, while theFrom/Toheaders keep the full"Name" <addr>form. Falls back to the raw string if parsing fails. -
timeNowfield is an injectable clock (func() time.Time, defaults totime.Now) so tests can pin the Date header deterministically — sets.timeNowdirectly in tests.
Testing conventions
- Mocks are moq-generated into
mocks/from//go:generatedirectives at the top ofemail.gofor theSMTPClientandLoggerinterfaces. Use the mock's*Calls()accessors to assert interactions. testdata/holds attachment fixtures (1.txt,2.txt,image.jpg,nullfilefor the empty-file path).- Coverage in CI strips
mocks/_mock.golines before submitting to coveralls.