Skip to content

fix(plugin): negotiate SMTP AUTH so LOGIN-only servers work - #630

Merged
pikann merged 2 commits into
Paca-AI:masterfrom
kayahk:fix/smtp-auth-login
Oct 24, 2026
Merged

pikann merged 2 commits into
Paca-AI:masterfrom
kayahk:fix/smtp-auth-login

Conversation

@kayahk

@kayahk kayahk commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Problem

paca.send_email hard-codes smtp.PlainAuth, which sends AUTH PLAIN unconditionally. Servers that accept only AUTH LOGIN — Azure Communication Services is one, so are several Exchange relays — reject that handshake with:

send_email: auth: 504 "5.7.4 Unrecognized authentication type"

The credential is valid; the mechanism is not one the server offers. SMTP email is therefore impossible against ACS from a Paca plugin today.

Change

  • Read the mechanisms the server advertises via smtp.Client.Extension("AUTH") after STARTTLS.
  • Try PLAIN first, fall back to AUTH LOGIN (implemented here, since net/smtp ships no LOGIN auth), and fail naming the advertised set when nothing supported is on offer — instead of the opaque 504.
  • loginAuth.Start mirrors PlainAuth's protection: it refuses ("unencrypted connection") unless the connection is already encrypted or the host is loopback, so opportunistic STARTTLS failing cannot leak the password.
  • The LOGIN handler answers only the username and password prompts it expects; any other challenge fails closed.

No config or plugin-manifest change: existing host/port/username/password/use_tls fields are unchanged, so PLAIN servers behave exactly as before.

Verification

  • go test ./internal/platform/plugin/ passes. New tests cover PLAIN+LOGIN (PLAIN chosen even when LOGIN is advertised first), LOGIN only, XOAUTH2 only (error), no AUTH advertisement (error), the LOGIN challenge order including an unexpected challenge, and the unencrypted-connection refusal with its loopback exception.
  • End-to-end against smtp.azurecomm.net:587 with a real ACS credential and a plugin-shaped message: AUTH LOGIN accepted, message accepted for delivery (250). Before the change the same server/credential pair returned the 504 above.

Scoped to services/api/internal/platform/plugin/email.go and its test.

send_email hard-coded PLAIN. Azure Communication Services (and some Exchange
relays) advertise only LOGIN/XOAUTH2, so every send failed with
"504 5.7.4 Unrecognized authentication type" even though the credential was
valid — net/smtp's PlainAuth refuses to transmit when PLAIN is not offered.

Read the server's advertised mechanisms after STARTTLS, prefer PLAIN, fall
back to AUTH LOGIN, and fail with the advertised list when nothing supported
is on offer. The LOGIN challenge handler answers only the two prompts it
expects, so a server that asks again cannot harvest the password twice.

Verified against smtp.azurecomm.net:587 with a real ACS credential (LOGIN
accepted, message accepted for delivery) and by unit tests covering
PLAIN+LOGIN, LOGIN only, XOAUTH2 only, and no AUTH advertisement.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The new AUTH LOGIN path drops the unencrypted-connection guard that smtp.PlainAuth gave us, so credentials can now be transmitted in cleartext on a connection where TLS was never negotiated. Details inline.

Reviewed changes

  • SMTP AUTH negotiation — dialAndSendSMTP now reads the mechanisms from client.Extension("AUTH") after the implicit-TLS/STARTTLS step and picks PLAIN or LOGIN via smtpAuthFor, instead of always assuming PLAIN; when neither is offered it errors naming the advertised set.
  • loginAuth — new smtp.Auth implementing the AUTH LOGIN username/password challenge handshake, failing closed on an unexpected challenge so the password is never echoed twice.
  • Tests — table test for mechanism selection (PLAIN preferred, LOGIN-only, XOAUTH2-only and empty both error) plus a unit test for the LOGIN challenge order including the unexpected-challenge case.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread services/api/internal/platform/plugin/email.go Outdated
Comment thread services/api/internal/platform/plugin/email.go Outdated
Comment thread services/api/internal/platform/plugin/email.go
Review caught that loginAuth.Start ignored ServerInfo, while smtp.PlainAuth
refuses to send credentials when the connection was never encrypted and the
host is not loopback. dialAndSendSMTP only upgrades opportunistically, so a
LOGIN-only server without STARTTLS would have received the password in the
clear. Mirror the guard.

Also: scan PLAIN across all advertised mechanisms before LOGIN (the loop
returned whichever came first, so "LOGIN PLAIN" picked LOGIN), and correct
two comments that claimed PlainAuth refuses to send when PLAIN is unadvertised
— it sends AUTH PLAIN regardless and the server rejects it with 504.
@kayahk

kayahk commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

All three review points addressed in 7ea0d0f:

  • loginAuth.Start now mirrors smtp.PlainAuth: "unencrypted connection" unless server.TLS or the host is loopback. That was a real regression — dialAndSendSMTP only upgrades via STARTTLS when advertised, so a LOGIN-only server without TLS would have received the password in the clear. Test covers both the refusal and the loopback exception.
  • smtpAuthFor now scans PLAIN across all mechanisms before LOGIN, so the code matches the stated preference instead of taking advertisement order.
  • Both comments corrected: PlainAuth sends AUTH PLAIN unconditionally; ACS/Exchange reject it with 504. PR description says so too.

go test ./internal/platform/plugin/ passes.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

This run reviewed the delta since the prior pullfrog review (84f494b), which resolves every item that review raised.

  • Cleartext guard restored on AUTH LOGIN — loginAuth.Start now returns "unencrypted connection" when !server.TLS && !isLocalhost(server.Name), mirroring plainAuth.Start so opportunistic STARTTLS failing can no longer leak the password.
  • Comment corrected — the inline comment now states that PlainAuth sends AUTH PLAIN unconditionally and the server rejected it, instead of claiming a client-side refusal.
  • PLAIN-first selection implemented — smtpAuthFor now scans PLAIN before LOGIN regardless of the server's advertisement order, matching its doc comment.
  • Tests extended — the LOGIN test now asserts the unencrypted-connection refusal and the loopback exception.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pikann pikann left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you for the contribution! 🚀

@pikann
pikann merged commit d1d9e4e into Paca-AI:master Oct 24, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants