Skip to content

Processing breaks if a "headers:" email header exists #139

@opsdisk

Description

@opsdisk

Describe the bug
If an email contains a headers: header, like headers: hello-world, processing breaks resulting in a RecursionError: maximum recursion depth exceeded. It only happens if the case is exactly headers...Headers is not affected. The issue happens because of https://github.com/SpamScope/mail-parser/blob/develop/src/mailparser/core.py#L647 where getattr() will continue to call the headers property of the class.

To Reproduce
Steps to reproduce the behavior:

import mailparser

mail = mailparser.parse_from_file("gtube_with_headers_header.txt")

Expected behavior
Email should gracefully handle a headers: header, even though it is not a valid email header.

Raw mail

Subject: Test spam mail (GTUBE)
Message-ID: <GTUBE1.1010101@example.net>
Date: Wed, 23 Jul 2003 23:30:00 +0200
From: Sender <sender@example.net>
To: Recipient <recipient@example.net>
Precedence: junk
MIME-Version: 1.0
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
headers: hello-world

This is the GTUBE, the
	Generic
	Test for
	Unsolicited
	Bulk
	Email

If your spam filter supports it, the GTUBE provides a test by which you
can verify that the filter is installed correctly and is detecting incoming
spam. You can send yourself a test mail containing the following string of
characters (in upper case and with no white spaces and line breaks):

XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X

You should send this test mail from an account outside of your network.

Environment:

  • OS: Linux
  • Docker: yes
  • mail-parser version 4.1.3

Additional context

---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
Cell In[2], line 1
----> 1 mail = mailparser.parse_from_file("gtube_with_headers_header.txt")

File /usr/local/lib/python3.13/site-packages/mailparser/core.py:76, in parse_from_file(fp)
     66 def parse_from_file(fp):
     67     """
     68     Parsing email from file.
     69 
   (...)     74         Instance of MailParser with raw email parsed
     75     """
---> 76     return MailParser.from_file(fp)

File /usr/local/lib/python3.13/site-packages/mailparser/core.py:187, in MailParser.from_file(cls, fp, is_outlook)
    184     log.debug("Removing temp converted Outlook email {!r}".format(fp))
    185     os.remove(fp)
--> 187 return cls(message)

File /usr/local/lib/python3.13/site-packages/mailparser/core.py:134, in MailParser.__init__(self, message)
    132 self._message = message
    133 log.debug("All headers of emails: {}".format(", ".join(message.keys())))
--> 134 self.parse()

File /usr/local/lib/python3.13/site-packages/mailparser/core.py:493, in MailParser.parse(self)
    490                     self._text_not_managed.append(payload)
    492 # Parsed object mail with all parts
--> 493 self._mail = self._make_mail()
    495 # Parsed object mail with mains parts
    496 self._mail_partial = self._make_mail(complete=False)

File /usr/local/lib/python3.13/site-packages/mailparser/core.py:299, in MailParser._make_mail(self, complete)
    297 for i in keys:
    298     log.debug("Getting header or part {!r}".format(i))
--> 299     value = getattr(self, i)
    300     if value:
    301         mail[i] = value

File /usr/local/lib/python3.13/site-packages/mailparser/core.py:647, in MailParser.headers(self)
    645 d = {}
    646 for i in self.message.keys():
--> 647     d[i] = getattr(self, i)
    648 return d

File /usr/local/lib/python3.13/site-packages/mailparser/core.py:647, in MailParser.headers(self)
    645 d = {}
    646 for i in self.message.keys():
--> 647     d[i] = getattr(self, i)
    648 return d

    [... skipping similar frames: MailParser.headers at line 647 (2971 times)]

File /usr/local/lib/python3.13/site-packages/mailparser/core.py:647, in MailParser.headers(self)
    645 d = {}
    646 for i in self.message.keys():
--> 647     d[i] = getattr(self, i)
    648 return d

File /usr/local/lib/python3.13/site-packages/mailparser/core.py:592, in MailParser.__getattr__(self, name)
    590 elif name_header in ADDRESSES_HEADERS:
    591     h = decode_header_part(self.message.get(name_header, six.text_type()))
--> 592     return email.utils.getaddresses([h])
    594 # others headers
    595 else:
    596     return get_header(self.message, name_header)

File /usr/local/lib/python3.13/email/utils.py:175, in getaddresses(fieldvalues, strict)
    173 fieldvalues = _pre_parse_validation(fieldvalues)
    174 addr = COMMASPACE.join(fieldvalues)
--> 175 a = _AddressList(addr)
    176 result = _post_parse_validation(a.addresslist)
    178 # Treat output as invalid if the number of addresses is not equal to the
    179 # expected number of addresses.

File /usr/local/lib/python3.13/email/_parseaddr.py:520, in AddressList.__init__(self, field)
    518 AddrlistClass.__init__(self, field)
    519 if field:
--> 520     self.addresslist = self.getaddrlist()
    521 else:
    522     self.addresslist = []

File /usr/local/lib/python3.13/email/_parseaddr.py:264, in AddrlistClass.getaddrlist(self)
    262 result = []
    263 while self.pos < len(self.field):
--> 264     ad = self.getaddress()
    265     if ad:
    266         result += ad

File /usr/local/lib/python3.13/email/_parseaddr.py:311, in AddrlistClass.getaddress(self)
    307         returnlist = returnlist + self.getaddress()
    309 elif self.field[self.pos] == '<':
    310     # Address is a phrase then a route addr
--> 311     routeaddr = self.getrouteaddr()
    313     if self.commentlist:
    314         returnlist = [(SPACE.join(plist) + ' (' +
    315                        ' '.join(self.commentlist) + ')', routeaddr)]

File /usr/local/lib/python3.13/email/_parseaddr.py:355, in AddrlistClass.getrouteaddr(self)
    353     self.pos += 1
    354 else:
--> 355     adlist = self.getaddrspec()
    356     self.pos += 1
    357     break

File /usr/local/lib/python3.13/email/_parseaddr.py:366, in AddrlistClass.getaddrspec(self)
    363 """Parse an RFC 2822 addr-spec."""
    364 aslist = []
--> 366 self.gotonext()
    367 while self.pos < len(self.field):
    368     preserve_ws = True

RecursionError: maximum recursion depth exceeded

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions