{"record":{"id":"47d08eed7d22d600","repo":"Billionmail/BillionMail","slug":"smtp-close-writer-w","errorCode":null,"errorMessage":"SMTP close writer: %w","messagePattern":"SMTP close writer: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/mail_service/sending.go","lineNumber":439,"sourceCode":"\t\t\treturn fmt.Errorf(\"SMTP rcpt: %w\", err)\n\t\t}\n\t}\n\n\t// Get a writer for the message body\n\tvar w io.WriteCloser\n\tw, err = e.client.Data()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"SMTP data: %w\", err)\n\t}\n\n\t// Write the message\n\tif _, err = w.Write(msg); err != nil {\n\t\treturn fmt.Errorf(\"SMTP write: %w\", err)\n\t}\n\n\t// Close the writer\n\tif err = w.Close(); err != nil {\n\t\treturn fmt.Errorf(\"SMTP close writer: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// isSecure check if TLS connection is needed\nfunc (e *EmailSender) isSecure() bool {\n\t// usually,\n\t// port 465 is used for SMTP with SSL (implicit TLS)\n\t// port 587 is used for SMTP with STARTTLS (explicit TLS)\n\tif e.Port == \"465\" {\n\t\treturn true\n\t} else if e.Port == \"587\" {\n\t\t// We will use STARTTLS if the port is 587\n\t\treturn false\n\t}\n\treturn false\n}","sourceCodeStart":421,"sourceCodeEnd":457,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/mail_service/sending.go#L421-L457","documentation":"This error wraps a failure from w.Close() on the DATA-phase writer. Closing the writer sends the terminating '.' to end the DATA command and awaits the server's final delivery status (e.g. '250 Ok: queued'). A failure here means the server rejected the message at end-of-data or the connection failed before the final reply.","triggerScenarios":"w.Close() returned an error because the server replied with a non-2xx code at end-of-DATA (e.g. '554 5.7.1 message content rejected' by rspamd), or the connection dropped before the final reply arrived.","commonSituations":"Spam/content filters rejecting the finished message; oversized message rejected after full upload; server crash or timeout during queueing; connection killed by firewall after long DATA transfer.","solutions":["Inspect the wrapped error and mail server logs for the final SMTP reply code (content-rejected vs timeout)","Reduce message size or remove content triggering the spam filter; check rspamd scores","Retry the send; end-of-data 4xx replies (greylisting/throttling) are often transient","Confirm firewall/load-balancer idle timeouts are longer than the DATA transfer duration"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// No pre-call check possible: end-of-data verdict depends on server content policy.\n// Pre-screen content with rspamd client if available:\nif spamScore, _ := rspamd.Check(msg); spamScore > threshold {\n    return errors.New(\"message likely to be rejected at end-of-data\")\n}","typeGuard":null,"tryCatchPattern":"err := sender.Send(msg, recipients)\nvar smtpErr *textproto.Error\nif errors.As(err, &smtpErr) && strings.Contains(err.Error(), \"SMTP close writer\") {\n    switch {\n    case smtpErr.Code == 451 || smtpErr.Code == 452: requeueLater(msg)\n    case smtpErr.Code >= 500: quarantine(msg, smtpErr.Error())\n    }\n}","preventionTips":["Check rspamd/spam-filter scores before sending content-heavy campaigns","Treat 4xx end-of-data replies as transient and retry with backoff","Ensure firewall idle timeouts exceed longest expected DATA transfer","Log the wrapped textproto.Error code for every close-writer failure"],"tags":["smtp","end-of-data","email-delivery"],"backgroundTag":"smtp-transaction-failed","analyzedSha":"fc36c76c050c3775c5e899faf7403cf0262d2744","analyzedAt":"2026-09-05T21:28:54.019Z","contentChangedAt":"2026-09-05T21:28:54.019Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}