c 20022007 python software foundation contact emailsigpython org email address parsing code lifted directly from rfc822 py this should eventually be rewritten parse a date field the timezone table does not include the military time zones defined in rfc822 other than z according to rfc1123 the description in rfc822 gets the signs wrong so we can t rely on any such time zones rfc1123 recommends that numeric timezone indicators be used instead of timezone names convert a date string to a time tuple accounts for military timezones convert date to extended time tuple the last additional element is the time zone offset in seconds except if the timezone was specified as 0000 in that case the last element is none this indicates a utc timestamp that explicitly declaims knowledge of the source timezone as opposed to a 0000 timestamp that indicates the source timezone really was utc the fws after the comma after the dayofweek is optional so search and adjust for this there s a dayname here skip it some noncompliant muas use to separate time elements check for a yy specified in twodigit format then convert it to the appropriate fourdigit format according to the posix standard rfc 822 calls for a twodigit yy but rfc 2822 which obsoletes rfc 822 mandates a 4digit yy for more information see the documentation for the time module the year is between 1969 and 1999 inclusive the year is between 2000 and 2068 inclusive convert a timezone offset into seconds 0500 18000 daylight saving time flag is set to 1 since dst is unknown convert a time string to a time tuple t parsedatetzdata if isinstancet tuple return t 9 else return t def mktimetzdata no zone info so localtime is better assumption than gmt prepare string to be used in a quoted string turns backslash and double quote characters into quoted pairs these are the only characters that need to be quoted inside a quoted string does not add the surrounding double quotes address parser class by ben escoto to understand what this class does it helps to have a copy of rfc 2822 in front of you note this class interface is deprecated and may be removed in the future use email utils addresslist instead initialize a new instance field is an unparsed address header field containing one or more addresses note that rfc 2822 now specifies as obsphrase meaning that it is obsolete syntax rfc 2822 requires that we recognize obsolete syntax so allow dots in phrases skip white space and extract comments wslist while self pos lenself field if self fieldself pos in self lws nr if self fieldself pos not in nr wslist appendself fieldself pos self pos 1 elif self fieldself pos self commentlist appendself getcomment else break return emptystring joinwslist def getaddrlistself result while self pos lenself field ad self getaddress if ad result ad else result append return result def getaddressself bad email address technically no domain email address is just an addrspec this isn t very efficient since we start over address is a group address is a phrase then a route addr parse a route address returnpath value this method just skips all the route stuff and returns the addrspec parse an rfc 2822 addrspec aslist self gotonext while self pos lenself field preservews true if self fieldself pos if aslist and not aslist1 strip aslist pop aslist append self pos 1 preservews false elif self fieldself pos aslist append s quoteself getquote elif self fieldself pos in self atomends if aslist and not aslist1 strip aslist pop break else aslist appendself getatom ws self gotonext if preservews and ws aslist appendws if self pos lenself field or self fieldself pos return emptystring joinaslist aslist append self pos 1 self gotonext domain self getdomain if not domain invalid domain return an empty address instead of returning a local part to denote failed parsing return emptystring return emptystring joinaslist domain def getdomainself bpo34155 don t parse domains with two like amalicious orgimportant com parse a header fragment delimited by special characters beginchar is the start character for the fragment if self is not looking at an instance of beginchar then getdelimited returns the empty string endchars is a sequence of allowable enddelimiting characters parsing stops when one of these is encountered if allowcomments is nonzero embedded rfc 2822 comments are allowed within the parsed fragment get a quotedelimited fragment from self s field return self getdelimited r false def getcommentself parse an rfc 2822 domainliteral return s self getdelimited r false def getatomself atomendsnone parse a sequence of rfc 2822 phrases a phrase is a sequence of words which are in turn either rfc 2822 atoms or quotedstrings phrases are canonicalized by squeezing all runs of continuous whitespace into one space an addresslist encapsulates a list of parsed rfc 2822 addresses def initself field addrlistclass initself field if field self addresslist self getaddrlist else self addresslist def lenself return lenself addresslist def addself other set union newaddr addresslistnone newaddr addresslist self addresslist for x in other addresslist if not x in self addresslist newaddr addresslist appendx return newaddr def iaddself other set union inplace for x in other addresslist if not x in self addresslist self addresslist appendx return self def subself other set difference newaddr addresslistnone for x in self addresslist if not x in other addresslist newaddr addresslist appendx return newaddr def isubself other set difference inplace for x in other addresslist if x in self addresslist self addresslist removex return self def getitemself index make indexing slices and in work return self addresslistindex c 2002 2007 python software foundation contact email sig python org email address parsing code lifted directly from rfc822 py this should eventually be rewritten parse a date field the timezone table does not include the military time zones defined in rfc822 other than z according to rfc1123 the description in rfc822 gets the signs wrong so we can t rely on any such time zones rfc1123 recommends that numeric timezone indicators be used instead of timezone names atlantic used in canada eastern central mountain pacific convert a date string to a time tuple accounts for military timezones convert date to extended time tuple the last additional element is the time zone offset in seconds except if the timezone was specified as 0000 in that case the last element is none this indicates a utc timestamp that explicitly declaims knowledge of the source timezone as opposed to a 0000 timestamp that indicates the source timezone really was utc this happens for whitespace only input the fws after the comma after the day of week is optional so search and adjust for this there s a dayname here skip it rfc 850 date deprecated dummy tz some non compliant muas use to separate time elements check for a yy specified in two digit format then convert it to the appropriate four digit format according to the posix standard rfc 822 calls for a two digit yy but rfc 2822 which obsoletes rfc 822 mandates a 4 digit yy for more information see the documentation for the time module the year is between 1969 and 1999 inclusive the year is between 2000 and 2068 inclusive convert a timezone offset into seconds 0500 18000 daylight saving time flag is set to 1 since dst is unknown convert a time string to a time tuple turn a 10 tuple as returned by parsedate_tz into a posix timestamp no zone info so localtime is better assumption than gmt prepare string to be used in a quoted string turns backslash and double quote characters into quoted pairs these are the only characters that need to be quoted inside a quoted string does not add the surrounding double quotes address parser class by ben escoto to understand what this class does it helps to have a copy of rfc 2822 in front of you note this class interface is deprecated and may be removed in the future use email utils addresslist instead initialize a new instance field is an unparsed address header field containing one or more addresses note that rfc 2822 now specifies as obs phrase meaning that it is obsolete syntax rfc 2822 requires that we recognize obsolete syntax so allow dots in phrases skip white space and extract comments parse all addresses returns a list containing all of the addresses parse the next address bad email address technically no domain email address is just an addrspec this isn t very efficient since we start over address is a group address is a phrase then a route addr parse a route address return path value this method just skips all the route stuff and returns the addrspec parse an rfc 2822 addr spec invalid domain return an empty address instead of returning a local part to denote failed parsing get the complete domain name from an address bpo 34155 don t parse domains with two like a malicious org important com parse a header fragment delimited by special characters beginchar is the start character for the fragment if self is not looking at an instance of beginchar then getdelimited returns the empty string endchars is a sequence of allowable end delimiting characters parsing stops when one of these is encountered if allowcomments is non zero embedded rfc 2822 comments are allowed within the parsed fragment have already advanced pos from getcomment get a quote delimited fragment from self s field get a parenthesis delimited fragment from self s field parse an rfc 2822 domain literal parse an rfc 2822 atom optional atomends specifies a different set of end token delimiters the default is to use self atomends this is used e g in getphraselist since phrase endings must not include the which is legal in phrases parse a sequence of rfc 2822 phrases a phrase is a sequence of words which are in turn either rfc 2822 atoms or quoted strings phrases are canonicalized by squeezing all runs of continuous whitespace into one space an addresslist encapsulates a list of parsed rfc 2822 addresses set union set union in place set difference set difference in place make indexing slices and in work
__all__ = [
    'mktime_tz',
    'parsedate',
    'parsedate_tz',
    'quote',
    ]

import time, calendar

SPACE = ' '
EMPTYSTRING = ''
COMMASPACE = ', '


_monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul',
               'aug', 'sep', 'oct', 'nov', 'dec',
               'january', 'february', 'march', 'april', 'may', 'june', 'july',
               'august', 'september', 'october', 'november', 'december']

_daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']







_timezones = {'UT':0, 'UTC':0, 'GMT':0, 'Z':0,
              'AST': -400, 'ADT': -300,  
              'EST': -500, 'EDT': -400,  
              'CST': -600, 'CDT': -500,  
              'MST': -700, 'MDT': -600,  
              'PST': -800, 'PDT': -700   
              }


def parsedate_tz(data):
    
    res = _parsedate_tz(data)
    if not res:
        return
    if res[9] is None:
        res[9] = 0
    return tuple(res)

def _parsedate_tz(data):
    
    if not data:
        return None
    data = data.split()
    if not data:  
        return None
    
    
    if data[0].endswith(',') or data[0].lower() in _daynames:
        
        del data[0]
    else:
        i = data[0].rfind(',')
        if i >= 0:
            data[0] = data[0][i+1:]
    if len(data) == 3: 
        stuff = data[0].split('-')
        if len(stuff) == 3:
            data = stuff + data[1:]
    if len(data) == 4:
        s = data[3]
        i = s.find('+')
        if i == -1:
            i = s.find('-')
        if i > 0:
            data[3:] = [s[:i], s[i:]]
        else:
            data.append('') 
    if len(data) < 5:
        return None
    data = data[:5]
    [dd, mm, yy, tm, tz] = data
    if not (dd and mm and yy):
        return None
    mm = mm.lower()
    if mm not in _monthnames:
        dd, mm = mm, dd.lower()
        if mm not in _monthnames:
            return None
    mm = _monthnames.index(mm) + 1
    if mm > 12:
        mm -= 12
    if dd[-1] == ',':
        dd = dd[:-1]
    i = yy.find(':')
    if i > 0:
        yy, tm = tm, yy
    if yy[-1] == ',':
        yy = yy[:-1]
        if not yy:
            return None
    if not yy[0].isdigit():
        yy, tz = tz, yy
    if tm[-1] == ',':
        tm = tm[:-1]
    tm = tm.split(':')
    if len(tm) == 2:
        [thh, tmm] = tm
        tss = '0'
    elif len(tm) == 3:
        [thh, tmm, tss] = tm
    elif len(tm) == 1 and '.' in tm[0]:
        
        tm = tm[0].split('.')
        if len(tm) == 2:
            [thh, tmm] = tm
            tss = 0
        elif len(tm) == 3:
            [thh, tmm, tss] = tm
        else:
            return None
    else:
        return None
    try:
        yy = int(yy)
        dd = int(dd)
        thh = int(thh)
        tmm = int(tmm)
        tss = int(tss)
    except ValueError:
        return None
    
    
    
    
    
    if yy < 100:
        
        if yy > 68:
            yy += 1900
        
        else:
            yy += 2000
    tzoffset = None
    tz = tz.upper()
    if tz in _timezones:
        tzoffset = _timezones[tz]
    else:
        try:
            tzoffset = int(tz)
        except ValueError:
            pass
        if tzoffset==0 and tz.startswith('-'):
            tzoffset = None
    
    if tzoffset:
        if tzoffset < 0:
            tzsign = -1
            tzoffset = -tzoffset
        else:
            tzsign = 1
        tzoffset = tzsign * ( (tzoffset//100)*3600 + (tzoffset % 100)*60)
    
    return [yy, mm, dd, thh, tmm, tss, 0, 1, -1, tzoffset]


def parsedate(data):
    
    t = parsedate_tz(data)
    if isinstance(t, tuple):
        return t[:9]
    else:
        return t


def mktime_tz(data):
    
    if data[9] is None:
        
        return time.mktime(data[:8] + (-1,))
    else:
        t = calendar.timegm(data)
        return t - data[9]


def quote(str):
    
    return str.replace('\\', '\\\\').replace('"', '\\"')


class AddrlistClass:
    

    def __init__(self, field):
        
        self.specials = '()<>@,:;.\"[]'
        self.pos = 0
        self.LWS = ' \t'
        self.CR = '\r\n'
        self.FWS = self.LWS + self.CR
        self.atomends = self.specials + self.LWS + self.CR
        
        
        
        self.phraseends = self.atomends.replace('.', '')
        self.field = field
        self.commentlist = []

    def gotonext(self):
        
        wslist = []
        while self.pos < len(self.field):
            if self.field[self.pos] in self.LWS + '\n\r':
                if self.field[self.pos] not in '\n\r':
                    wslist.append(self.field[self.pos])
                self.pos += 1
            elif self.field[self.pos] == '(':
                self.commentlist.append(self.getcomment())
            else:
                break
        return EMPTYSTRING.join(wslist)

    def getaddrlist(self):
        
        result = []
        while self.pos < len(self.field):
            ad = self.getaddress()
            if ad:
                result += ad
            else:
                result.append(('', ''))
        return result

    def getaddress(self):
        
        self.commentlist = []
        self.gotonext()

        oldpos = self.pos
        oldcl = self.commentlist
        plist = self.getphraselist()

        self.gotonext()
        returnlist = []

        if self.pos >= len(self.field):
            
            if plist:
                returnlist = [(SPACE.join(self.commentlist), plist[0])]

        elif self.field[self.pos] in '.@':
            
            
            self.pos = oldpos
            self.commentlist = oldcl
            addrspec = self.getaddrspec()
            returnlist = [(SPACE.join(self.commentlist), addrspec)]

        elif self.field[self.pos] == ':':
            
            returnlist = []

            fieldlen = len(self.field)
            self.pos += 1
            while self.pos < len(self.field):
                self.gotonext()
                if self.pos < fieldlen and self.field[self.pos] == ';':
                    self.pos += 1
                    break
                returnlist = returnlist + self.getaddress()

        elif self.field[self.pos] == '<':
            
            routeaddr = self.getrouteaddr()

            if self.commentlist:
                returnlist = [(SPACE.join(plist) + ' (' +
                               ' '.join(self.commentlist) + ')', routeaddr)]
            else:
                returnlist = [(SPACE.join(plist), routeaddr)]

        else:
            if plist:
                returnlist = [(SPACE.join(self.commentlist), plist[0])]
            elif self.field[self.pos] in self.specials:
                self.pos += 1

        self.gotonext()
        if self.pos < len(self.field) and self.field[self.pos] == ',':
            self.pos += 1
        return returnlist

    def getrouteaddr(self):
        
        if self.field[self.pos] != '<':
            return

        expectroute = False
        self.pos += 1
        self.gotonext()
        adlist = ''
        while self.pos < len(self.field):
            if expectroute:
                self.getdomain()
                expectroute = False
            elif self.field[self.pos] == '>':
                self.pos += 1
                break
            elif self.field[self.pos] == '@':
                self.pos += 1
                expectroute = True
            elif self.field[self.pos] == ':':
                self.pos += 1
            else:
                adlist = self.getaddrspec()
                self.pos += 1
                break
            self.gotonext()

        return adlist

    def getaddrspec(self):
        
        aslist = []

        self.gotonext()
        while self.pos < len(self.field):
            preserve_ws = True
            if self.field[self.pos] == '.':
                if aslist and not aslist[-1].strip():
                    aslist.pop()
                aslist.append('.')
                self.pos += 1
                preserve_ws = False
            elif self.field[self.pos] == '"':
                aslist.append('"%s"' % quote(self.getquote()))
            elif self.field[self.pos] in self.atomends:
                if aslist and not aslist[-1].strip():
                    aslist.pop()
                break
            else:
                aslist.append(self.getatom())
            ws = self.gotonext()
            if preserve_ws and ws:
                aslist.append(ws)

        if self.pos >= len(self.field) or self.field[self.pos] != '@':
            return EMPTYSTRING.join(aslist)

        aslist.append('@')
        self.pos += 1
        self.gotonext()
        domain = self.getdomain()
        if not domain:
            
            
            return EMPTYSTRING
        return EMPTYSTRING.join(aslist) + domain

    def getdomain(self):
        
        sdlist = []
        while self.pos < len(self.field):
            if self.field[self.pos] in self.LWS:
                self.pos += 1
            elif self.field[self.pos] == '(':
                self.commentlist.append(self.getcomment())
            elif self.field[self.pos] == '[':
                sdlist.append(self.getdomainliteral())
            elif self.field[self.pos] == '.':
                self.pos += 1
                sdlist.append('.')
            elif self.field[self.pos] == '@':
                
                
                return EMPTYSTRING
            elif self.field[self.pos] in self.atomends:
                break
            else:
                sdlist.append(self.getatom())
        return EMPTYSTRING.join(sdlist)

    def getdelimited(self, beginchar, endchars, allowcomments=True):
        
        if self.field[self.pos] != beginchar:
            return ''

        slist = ['']
        quote = False
        self.pos += 1
        while self.pos < len(self.field):
            if quote:
                slist.append(self.field[self.pos])
                quote = False
            elif self.field[self.pos] in endchars:
                self.pos += 1
                break
            elif allowcomments and self.field[self.pos] == '(':
                slist.append(self.getcomment())
                continue        
            elif self.field[self.pos] == '\\':
                quote = True
            else:
                slist.append(self.field[self.pos])
            self.pos += 1

        return EMPTYSTRING.join(slist)

    def getquote(self):
        
        return self.getdelimited('"', '"\r', False)

    def getcomment(self):
        
        return self.getdelimited('(', ')\r', True)

    def getdomainliteral(self):
        
        return '[%s]' % self.getdelimited('[', ']\r', False)

    def getatom(self, atomends=None):
        
        atomlist = ['']
        if atomends is None:
            atomends = self.atomends

        while self.pos < len(self.field):
            if self.field[self.pos] in atomends:
                break
            else:
                atomlist.append(self.field[self.pos])
            self.pos += 1

        return EMPTYSTRING.join(atomlist)

    def getphraselist(self):
        
        plist = []

        while self.pos < len(self.field):
            if self.field[self.pos] in self.FWS:
                self.pos += 1
            elif self.field[self.pos] == '"':
                plist.append(self.getquote())
            elif self.field[self.pos] == '(':
                self.commentlist.append(self.getcomment())
            elif self.field[self.pos] in self.phraseends:
                break
            else:
                plist.append(self.getatom(self.phraseends))

        return plist

class AddressList(AddrlistClass):
    
    def __init__(self, field):
        AddrlistClass.__init__(self, field)
        if field:
            self.addresslist = self.getaddrlist()
        else:
            self.addresslist = []

    def __len__(self):
        return len(self.addresslist)

    def __add__(self, other):
        
        newaddr = AddressList(None)
        newaddr.addresslist = self.addresslist[:]
        for x in other.addresslist:
            if not x in self.addresslist:
                newaddr.addresslist.append(x)
        return newaddr

    def __iadd__(self, other):
        
        for x in other.addresslist:
            if not x in self.addresslist:
                self.addresslist.append(x)
        return self

    def __sub__(self, other):
        
        newaddr = AddressList(None)
        for x in self.addresslist:
            if not x in other.addresslist:
                newaddr.addresslist.append(x)
        return newaddr

    def __isub__(self, other):
        
        for x in other.addresslist:
            if x in self.addresslist:
                self.addresslist.remove(x)
        return self

    def __getitem__(self, index):
        
        return self.addresslist[index]