Skip to content

smtplib.quoteaddr() returns malformed angle-bracket address for input '<' #145552

Open

Description

@stefanzetzsche

Bug report

Description

smtplib.quoteaddr() formats addresses for SMTP MAIL FROM: and RCPT TO: commands per RFC 821, which requires angle-bracket format (<addr>). It first delegates to email.utils.parseaddr(); when parsing succeeds, the result is correctly wrapped in <...>.

When parseaddr fails — which happens for inputs that don't resemble any recognizable email address format, like '<', '< ', or '@' — a fallback path kicks in. The fallback checks if the input starts with < and returns it verbatim, without verifying it also ends with >. This produces structurally invalid output (e.g. MAIL FROM:< instead of MAIL FROM:<>).

The existing code intentionally chooses to be lenient with unparseable input rather than raising an exception (the comment reads "use it as is and hope for the best"). Given that design choice, the output should at least be structurally valid — a half-open < with no > is neither rejecting bad input nor producing well-formed protocol output.

Reproducer

from smtplib import quoteaddr

print(repr(quoteaddr('<')))    # '<'  — missing closing >
print(repr(quoteaddr('< ')))   # '< ' — missing closing >
print(repr(quoteaddr('<user@example.com')))  # '<user@example.com' — missing >

CPython versions tested on:

CPython main branch

Operating systems tested on:

No response

Linked PRs

Activity

  1. added
    stdlibStandard Library Python modules in the Lib/ directory
    on Mar 5, 2026
  2. bitdancer commented on Mar 6, 2026

    @bitdancer
    Member

    What should happen to a string like <<foo@? Do you know how typical smtp servers respond to such malformed addresses? Out of curiosity, did you encounter this in a "real" situation, or is this a "code inspection" or fuzzing type discovery?

    Looking at the code, I think the current logic is: if it starts with a '<' then assume it is a valid address. If not, assume we must quote it with '<' (and hope for the best). I wonder if the better response to there not being a trailing '>' would be to either do the '<' quoting anyway or raise an error. Raising an error if parseaddr can't handle it might have been the best choice to begin with, but that ship sailed long ago.

    After a little history checking, I see I am the one that added that '<' check...the older version of the code would just have wrapped the unparseable address in <> unconditionally. Therefore I think we should return to that behavior, as it looks like I introduced a bug with that commit.

  3. stefanzetzsche commented on Mar 10, 2026

    @stefanzetzsche
    ContributorAuthor

    Not a real-world scenario — found via fuzzing. Happy to restore the original unconditional wrapping. I'll update the PR.

  4. stefanzetzsche commented on Mar 10, 2026

    @stefanzetzsche
    ContributorAuthor

    Turns out unconditional wrapping doesn't quite work — parseaddr('<>') returns ('', ''), so the null sender <> would get double-wrapped to <<>>, producing MAIL FROM:<<>> instead of MAIL FROM:<>. This breaks testSendNullSender in CI.

    Updated the fix to keep the startswith('<') check but add an endswith('>') guard. Already-bracketed input like <> passes through, half-open input like < falls through to wrapping.

  5. khalidsaidi commented on Mar 25, 2026

    @khalidsaidi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytopic-emailtype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions