Skip to content

fix(mail): log rate-limit rejections, campus-safe code limit, multipart From name, 스팸함 안내 - #443

Merged
choijunhuk merged 5 commits into
mainfrom
fix/mail-deliverability
Sep 8, 2026
Merged

fix(mail): log rate-limit rejections, campus-safe code limit, multipart From name, 스팸함 안내#443
choijunhuk merged 5 commits into
mainfrom
fix/mail-deliverability

Conversation

@choijunhuk

Copy link
Copy Markdown
Collaborator

Summary

Members reported verification emails "not arriving." Investigation found: SMTP sends succeed and are logged; two Gmail recipients requested codes but never entered one (likely spam/promotions); 429 rate-limit rejections are never logged so they're indistinguishable from real delivery failures; and the per-IP email-code limit (5 per 10 min) is easily tripped by a campus NAT.

Four bounded, independently-reviewable commits:

  1. DiagnosticsAuthService logs a WARN at each TOO_MANY_REQUESTS throw site with the limiter name and a masked client key (IP -> first two octets + x.x; studentId -> first 4 chars + ***). EmailVerificationSender logs the effective SMTP host/port and a masked username once at startup, so a misconfigured deploy is visible.
  2. Campus NATMAX_SIGNUP_EMAIL_REQUESTS_PER_WINDOW raised 5 -> 20 per 10 min per IP; the per-account 1-minute cooldown is unchanged and remains the real anti-spam guard. No existing test pinned the old value of 5, so this adds direct coverage of the new cap.
  3. Deliverability — verification/reset mail is now sent as multipart text+HTML via MimeMessageHelper, with a KW COM's display name on the From address, a matching Reply-To, and the code included in the subject ([KW COM's] 이메일 인증코드 123456). HTML is minimal: no images, no links besides the site root. MAIL_ENABLED=false still falls back to the existing log-only path.
  4. Web UX — both the signup email-verification step and the password-reset step now show a hint under the code input ("메일이 오지 않으면 스팸함·프로모션함을 확인해주세요. 1분 후 다시 요청할 수 있습니다."). Both already had a resend button that surfaces the backend's exact message (including the 429 cooldown text) via the shared apiClient error path.

Test plan

  • ./gradlew test — BUILD SUCCESSFUL (full suite, incl. new EmailVerificationSenderTest and the new signup-email rate-limit test)
  • npm test — typecheck, lint, and all 26 contract tests pass
  • Manual: sign up on a shared/campus IP with several accounts in a row and confirm codes still send under the raised cap
  • Manual: verify the multipart email renders correctly (From display name, HTML body) in Gmail

🤖 Generated with Claude Code

choijunhuk and others added 5 commits September 8, 2026 12:40
Verification emails were reported as "not arriving", but 429 rejections
from the signup-email/login/resend limiters were never logged, so a
tripped rate limit was indistinguishable from a real delivery failure.

- AuthService now logs a WARN with the limiter name and a masked client
  key (IP -> first two octets + "x.x"; studentId -> first 4 chars + "***")
  at each TOO_MANY_REQUESTS throw site.
- EmailVerificationSender logs the effective SMTP host/port and a masked
  username once at startup so a misconfigured deploy is visible in logs
  instead of only surfacing as silent delivery reports.
MAX_SIGNUP_EMAIL_REQUESTS_PER_WINDOW was 5 per 10 min per client IP.
Campus Wi-Fi and the club room sit behind one NAT IP, so a handful of
members requesting verification codes together could trip this before
any single account hit its own 1-minute resend cooldown -- indistinguishable
from "email isn't arriving" without the logging just added. Raised to 20;
the per-account cooldown remains the real anti-spam guard.

No test previously pinned the old value of 5, so this adds direct
coverage of the new cap instead of updating one.
… name

Gmail (and most inbox providers) routes a plain-text-only message with no
display name on the From address into spam/promotions far more readily
than a proper multipart message with a real sender name and a Reply-To --
the likely cause of members never seeing a code that the logs show was
sent successfully.

EmailVerificationSender now builds verification and password-reset mail
via MimeMessageHelper as multipart text+HTML, sets From to "KW COM's"
<no-reply@...> and Reply-To to the same address, and includes the code in
the subject ("[KW COM's] 이메일 인증코드 123456") alongside its existing
placement in the body. The HTML alternative is minimal: no images, no
links besides the site root. MAIL_ENABLED=false still falls back to the
existing log-only path unchanged.

Unit test mocks JavaMailSender to hand back a real MimeMessage (required
by MimeMessageHelper) and asserts both parts, the From display name, and
Reply-To.
…teps

Members reporting "no email arrived" had no in-app pointer to check spam/
promotions, and the resend cooldown message wasn't visible before they
tried. Both the signup email-verification step (EmailVerifyStep) and the
password-reset step (Login) already have a resend action that surfaces
the backend error verbatim (including the 429 cooldown message) via the
shared apiClient error path -- this adds the missing hint text under each
code input: "메일이 오지 않으면 스팸함·프로모션함을 확인해주세요. 1분 후
다시 요청할 수 있습니다."
Subjects surface on lock screens and inbox previews; the code belongs in
the body only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@choijunhuk
choijunhuk merged commit 308f0cb into main Sep 8, 2026
4 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.

1 participant