1 """RFC 2822 message manipulation.
3 Note: This is only a very rough sketch of a full RFC-822 parser; in particular
4 the tokenizing of addresses does not adhere to all the quoting rules.
6 Note: RFC 2822 is a long awaited update to RFC 822. This module should
7 conform to RFC 2822, and is thus mis-named (it's not worth renaming it). Some
8 effort at RFC 2822 updates have been made, but a thorough audit has not been
9 performed. Consider any RFC 2822 non-conformance to be a bug.
11 RFC 2822: http://www.faqs.org/rfcs/rfc2822.html
12 RFC 822 : http://www.faqs.org/rfcs/rfc822.html (obsolete)
16 To create a Message object: first open a file, e.g.:
20 You can use any other legal way of getting an open file object, e.g. use
21 sys.stdin or call os.popen(). Then pass the open file object to the Message()
26 This class can work with any input object that supports a readline method. If
27 the input object has seek and tell capability, the rewindbody method will
28 work; also illegal lines will be pushed back onto the input stream. If the
29 input object lacks seek but has an `unread' method that can push back a line
30 of input, Message will use that to push back illegal lines. Thus this class
31 can be used to parse messages coming from a buffered stream.
33 The optional `seekable' argument is provided as a workaround for certain stdio
34 libraries in which tell() discards buffered data before discovering that the
35 lseek() system call doesn't work. For maximum portability, you should set the
36 seekable argument to zero to prevent that initial \code{tell} when passing in
37 an unseekable object such as a a file object created from a socket object. If
38 it is 1 on entry -- which it is by default -- the tell() method of the open
39 file object is called once; if this raises an exception, seekable is reset to
40 0. For other nonzero values of seekable, this test is not made.
42 To get the text of a particular header there are several methods:
44 str = m.getheader(name)
45 str = m.getrawheader(name)
47 where name is the name of the header, e.g. 'Subject'. The difference is that
48 getheader() strips the leading and trailing whitespace, while getrawheader()
49 doesn't. Both functions retain embedded whitespace (including newlines)
50 exactly as they are specified in the header, and leave the case of the text
53 For addresses and address lists there are functions
55 realname, mailaddress = m.getaddr(name)
56 list = m.getaddrlist(name)
58 where the latter returns a list of (realname, mailaddr) tuples.
60 There is also a method
62 time = m.getdate(name)
64 which parses a Date-like field and returns a time-compatible tuple,
65 i.e. a tuple such as returned by time.localtime() or accepted by
68 See the class definition for lower level access methods.
70 There are also some utility functions here.
72 # Cleanup and extensions by Eric S. Raymond <esr@thyrsus.com>
76 from warnings
import warnpy3k
77 warnpy3k("in 3.x, rfc822 has been removed in favor of the email package",
80 __all__
= ["Message","AddressList","parsedate","parsedate_tz","mktime_tz"]
82 _blanklines
= ('\r\n', '\n') # Optimization for islast()
86 """Represents a single RFC 2822-compliant message."""
88 def __init__(self
, fp
, seekable
= 1):
89 """Initialize the class instance and read the headers."""
91 # Exercise tell() to make sure it works
92 # (and then assume seek() works, too)
95 except (AttributeError, IOError):
98 self
.seekable
= seekable
99 self
.startofheaders
= None
100 self
.startofbody
= None
104 self
.startofheaders
= self
.fp
.tell()
112 self
.startofbody
= self
.fp
.tell()
116 def rewindbody(self
):
117 """Rewind the file to the start of the body (if seekable)."""
118 if not self
.seekable
:
119 raise IOError, "unseekable file"
120 self
.fp
.seek(self
.startofbody
)
122 def readheaders(self
):
123 """Read header lines.
125 Read header lines up to the entirely blank line that terminates them.
126 The (normally blank) line that ends the headers is skipped, but not
127 included in the returned list. If a non-header line ends the headers,
128 (which is an error), an attempt is made to backspace over it; it is
129 never included in the returned list.
131 The variable self.status is set to the empty string if all went well,
132 otherwise it is an error message. The variable self.headers is a
133 completely uninterpreted list of lines contained in the header (so
134 printing them will reproduce the header exactly as it appears in the
139 self
.headers
= lst
= []
143 startofline
= unread
= tell
= None
144 if hasattr(self
.fp
, 'unread'):
145 unread
= self
.fp
.unread
153 startofline
= tell
= None
155 line
= self
.fp
.readline()
157 self
.status
= 'EOF in headers'
159 # Skip unix From name time lines
160 if firstline
and line
.startswith('From '):
161 self
.unixfrom
= self
.unixfrom
+ line
164 if headerseen
and line
[0] in ' \t':
165 # It's a continuation line.
167 x
= (self
.dict[headerseen
] + "\n " + line
.strip())
168 self
.dict[headerseen
] = x
.strip()
170 elif self
.iscomment(line
):
171 # It's a comment. Ignore it.
173 elif self
.islast(line
):
174 # Note! No pushback here! The delimiter line gets eaten.
176 headerseen
= self
.isheader(line
)
178 # It's a legal header line, save it.
180 self
.dict[headerseen
] = line
[len(headerseen
)+1:].strip()
183 # It's not a header line; throw it back and stop here.
185 self
.status
= 'No headers'
187 self
.status
= 'Non-header line where header expected'
188 # Try to undo the read.
192 self
.fp
.seek(startofline
)
194 self
.status
= self
.status
+ '; bad seek'
197 def isheader(self
, line
):
198 """Determine whether a given line is a legal header.
200 This method should return the header name, suitably canonicalized.
201 You may override this method in order to use Message parsing on tagged
202 data in RFC 2822-like formats with special header formats.
206 return line
[:i
].lower()
209 def islast(self
, line
):
210 """Determine whether a line is a legal end of RFC 2822 headers.
212 You may override this method if your application wants to bend the
213 rules, e.g. to strip trailing whitespace, or to recognize MH template
214 separators ('--------'). For convenience (e.g. for code reading from
215 sockets) a line consisting of \r\n also matches.
217 return line
in _blanklines
219 def iscomment(self
, line
):
220 """Determine whether a line should be skipped entirely.
222 You may override this method in order to use Message parsing on tagged
223 data in RFC 2822-like formats that support embedded comments or
228 def getallmatchingheaders(self
, name
):
229 """Find all header lines matching a given header name.
231 Look through the list of headers and find all lines matching a given
232 header name (and their continuation lines). A list of the lines is
233 returned, without interpretation. If the header does not occur, an
234 empty list is returned. If the header occurs multiple times, all
235 occurrences are returned. Case is not important in the header name.
237 name
= name
.lower() + ':'
241 for line
in self
.headers
:
242 if line
[:n
].lower() == name
:
244 elif not line
[:1].isspace():
250 def getfirstmatchingheader(self
, name
):
251 """Get the first header line matching name.
253 This is similar to getallmatchingheaders, but it returns only the
254 first matching header (and its continuation lines).
256 name
= name
.lower() + ':'
260 for line
in self
.headers
:
262 if not line
[:1].isspace():
264 elif line
[:n
].lower() == name
:
270 def getrawheader(self
, name
):
271 """A higher-level interface to getfirstmatchingheader().
273 Return a string containing the literal text of the header but with the
274 keyword stripped. All leading, trailing and embedded whitespace is
275 kept in the string, however. Return None if the header does not
279 lst
= self
.getfirstmatchingheader(name
)
282 lst
[0] = lst
[0][len(name
) + 1:]
285 def getheader(self
, name
, default
=None):
286 """Get the header value for a name.
288 This is the normal interface: it returns a stripped version of the
289 header value for a given header name, or None if it doesn't exist.
290 This uses the dictionary version which finds the *last* such header.
292 return self
.dict.get(name
.lower(), default
)
295 def getheaders(self
, name
):
296 """Get all values for a header.
298 This returns a list of values for headers given more than once; each
299 value in the result list is stripped in the same way as the result of
300 getheader(). If the header is not given, return an empty list.
305 for s
in self
.getallmatchingheaders(name
):
308 current
= "%s\n %s" % (current
, s
.strip())
313 result
.append(current
)
314 current
= s
[s
.find(":") + 1:].strip()
317 result
.append(current
)
320 def getaddr(self
, name
):
321 """Get a single address from a header, as a tuple.
323 An example return value:
324 ('Guido van Rossum', 'guido@cwi.nl')
327 alist
= self
.getaddrlist(name
)
333 def getaddrlist(self
, name
):
334 """Get a list of addresses from a header.
336 Retrieves a list of addresses from a header, where each address is a
337 tuple as returned by getaddr(). Scans all named headers, so it works
338 properly with multiple To: or Cc: headers for example.
341 for h
in self
.getallmatchingheaders(name
):
351 alladdrs
= ''.join(raw
)
352 a
= AddressList(alladdrs
)
355 def getdate(self
, name
):
356 """Retrieve a date field from a header.
358 Retrieves a date field from the named header, returning a tuple
359 compatible with time.mktime().
365 return parsedate(data
)
367 def getdate_tz(self
, name
):
368 """Retrieve a date field from a header as a 10-tuple.
370 The first 9 elements make up a tuple compatible with time.mktime(),
371 and the 10th is the offset of the poster's time zone from GMT/UTC.
377 return parsedate_tz(data
)
380 # Access as a dictionary (only finds *last* header of each type):
383 """Get the number of headers in a message."""
384 return len(self
.dict)
386 def __getitem__(self
, name
):
387 """Get a specific header, as from a dictionary."""
388 return self
.dict[name
.lower()]
390 def __setitem__(self
, name
, value
):
391 """Set the value of a header.
393 Note: This is not a perfect inversion of __getitem__, because any
394 changed headers get stuck at the end of the raw-headers list rather
395 than where the altered header was.
397 del self
[name
] # Won't fail if it doesn't exist
398 self
.dict[name
.lower()] = value
399 text
= name
+ ": " + value
400 for line
in text
.split("\n"):
401 self
.headers
.append(line
+ "\n")
403 def __delitem__(self
, name
):
404 """Delete all occurrences of a specific header, if it is present."""
406 if not name
in self
.dict:
413 for i
in range(len(self
.headers
)):
414 line
= self
.headers
[i
]
415 if line
[:n
].lower() == name
:
417 elif not line
[:1].isspace():
421 for i
in reversed(lst
):
424 def setdefault(self
, name
, default
=""):
425 lowername
= name
.lower()
426 if lowername
in self
.dict:
427 return self
.dict[lowername
]
429 text
= name
+ ": " + default
430 for line
in text
.split("\n"):
431 self
.headers
.append(line
+ "\n")
432 self
.dict[lowername
] = default
435 def has_key(self
, name
):
436 """Determine whether a message contains the named header."""
437 return name
.lower() in self
.dict
439 def __contains__(self
, name
):
440 """Determine whether a message contains the named header."""
441 return name
.lower() in self
.dict
444 return iter(self
.dict)
447 """Get all of a message's header field names."""
448 return self
.dict.keys()
451 """Get all of a message's header field values."""
452 return self
.dict.values()
455 """Get all of a message's headers.
457 Returns a list of name, value tuples.
459 return self
.dict.items()
462 return ''.join(self
.headers
)
468 # XXX Should fix unquote() and quote() to be really conformant.
469 # XXX The inverses of the parse functions may also be useful.
473 """Remove quotes from a string."""
475 if s
.startswith('"') and s
.endswith('"'):
476 return s
[1:-1].replace('\\\\', '\\').replace('\\"', '"')
477 if s
.startswith('<') and s
.endswith('>'):
483 """Add quotes around a string."""
484 return s
.replace('\\', '\\\\').replace('"', '\\"')
487 def parseaddr(address
):
488 """Parse an address into a (realname, mailaddr) tuple."""
489 a
= AddressList(address
)
497 """Address parser class by Ben Escoto.
499 To understand what this class does, it helps to have a copy of
500 RFC 2822 in front of you.
502 http://www.faqs.org/rfcs/rfc2822.html
504 Note: this class interface is deprecated and may be removed in the future.
505 Use rfc822.AddressList instead.
508 def __init__(self
, field
):
509 """Initialize a new instance.
511 `field' is an unparsed address header field, containing one or more
514 self
.specials
= '()<>@,:;.\"[]'
518 self
.atomends
= self
.specials
+ self
.LWS
+ self
.CR
519 # Note that RFC 2822 now specifies `.' as obs-phrase, meaning that it
520 # is obsolete syntax. RFC 2822 requires that we recognize obsolete
521 # syntax, so allow dots in phrases.
522 self
.phraseends
= self
.atomends
.replace('.', '')
524 self
.commentlist
= []
527 """Parse up to the start of the next address."""
528 while self
.pos
< len(self
.field
):
529 if self
.field
[self
.pos
] in self
.LWS
+ '\n\r':
530 self
.pos
= self
.pos
+ 1
531 elif self
.field
[self
.pos
] == '(':
532 self
.commentlist
.append(self
.getcomment())
535 def getaddrlist(self
):
536 """Parse all addresses.
538 Returns a list containing all of the addresses.
541 ad
= self
.getaddress()
544 ad
= self
.getaddress()
547 def getaddress(self
):
548 """Parse the next address."""
549 self
.commentlist
= []
553 oldcl
= self
.commentlist
554 plist
= self
.getphraselist()
559 if self
.pos
>= len(self
.field
):
560 # Bad email address technically, no domain.
562 returnlist
= [(' '.join(self
.commentlist
), plist
[0])]
564 elif self
.field
[self
.pos
] in '.@':
565 # email address is just an addrspec
566 # this isn't very efficient since we start over
568 self
.commentlist
= oldcl
569 addrspec
= self
.getaddrspec()
570 returnlist
= [(' '.join(self
.commentlist
), addrspec
)]
572 elif self
.field
[self
.pos
] == ':':
576 fieldlen
= len(self
.field
)
578 while self
.pos
< len(self
.field
):
580 if self
.pos
< fieldlen
and self
.field
[self
.pos
] == ';':
583 returnlist
= returnlist
+ self
.getaddress()
585 elif self
.field
[self
.pos
] == '<':
586 # Address is a phrase then a route addr
587 routeaddr
= self
.getrouteaddr()
590 returnlist
= [(' '.join(plist
) + ' (' + \
591 ' '.join(self
.commentlist
) + ')', routeaddr
)]
592 else: returnlist
= [(' '.join(plist
), routeaddr
)]
596 returnlist
= [(' '.join(self
.commentlist
), plist
[0])]
597 elif self
.field
[self
.pos
] in self
.specials
:
601 if self
.pos
< len(self
.field
) and self
.field
[self
.pos
] == ',':
605 def getrouteaddr(self
):
606 """Parse a route address (Return-path value).
608 This method just skips all the route stuff and returns the addrspec.
610 if self
.field
[self
.pos
] != '<':
617 while self
.pos
< len(self
.field
):
621 elif self
.field
[self
.pos
] == '>':
624 elif self
.field
[self
.pos
] == '@':
627 elif self
.field
[self
.pos
] == ':':
630 adlist
= self
.getaddrspec()
637 def getaddrspec(self
):
638 """Parse an RFC 2822 addr-spec."""
642 while self
.pos
< len(self
.field
):
643 if self
.field
[self
.pos
] == '.':
646 elif self
.field
[self
.pos
] == '"':
647 aslist
.append('"%s"' % self
.getquote())
648 elif self
.field
[self
.pos
] in self
.atomends
:
650 else: aslist
.append(self
.getatom())
653 if self
.pos
>= len(self
.field
) or self
.field
[self
.pos
] != '@':
654 return ''.join(aslist
)
659 return ''.join(aslist
) + self
.getdomain()
662 """Get the complete domain name from an address."""
664 while self
.pos
< len(self
.field
):
665 if self
.field
[self
.pos
] in self
.LWS
:
667 elif self
.field
[self
.pos
] == '(':
668 self
.commentlist
.append(self
.getcomment())
669 elif self
.field
[self
.pos
] == '[':
670 sdlist
.append(self
.getdomainliteral())
671 elif self
.field
[self
.pos
] == '.':
674 elif self
.field
[self
.pos
] in self
.atomends
:
676 else: sdlist
.append(self
.getatom())
677 return ''.join(sdlist
)
679 def getdelimited(self
, beginchar
, endchars
, allowcomments
= 1):
680 """Parse a header fragment delimited by special characters.
682 `beginchar' is the start character for the fragment. If self is not
683 looking at an instance of `beginchar' then getdelimited returns the
686 `endchars' is a sequence of allowable end-delimiting characters.
687 Parsing stops when one of these is encountered.
689 If `allowcomments' is non-zero, embedded RFC 2822 comments are allowed
690 within the parsed fragment.
692 if self
.field
[self
.pos
] != beginchar
:
698 while self
.pos
< len(self
.field
):
700 slist
.append(self
.field
[self
.pos
])
702 elif self
.field
[self
.pos
] in endchars
:
705 elif allowcomments
and self
.field
[self
.pos
] == '(':
706 slist
.append(self
.getcomment())
707 continue # have already advanced pos from getcomment
708 elif self
.field
[self
.pos
] == '\\':
711 slist
.append(self
.field
[self
.pos
])
714 return ''.join(slist
)
717 """Get a quote-delimited fragment from self's field."""
718 return self
.getdelimited('"', '"\r', 0)
720 def getcomment(self
):
721 """Get a parenthesis-delimited fragment from self's field."""
722 return self
.getdelimited('(', ')\r', 1)
724 def getdomainliteral(self
):
725 """Parse an RFC 2822 domain-literal."""
726 return '[%s]' % self
.getdelimited('[', ']\r', 0)
728 def getatom(self
, atomends
=None):
729 """Parse an RFC 2822 atom.
731 Optional atomends specifies a different set of end token delimiters
732 (the default is to use self.atomends). This is used e.g. in
733 getphraselist() since phrase endings must not include the `.' (which
734 is legal in phrases)."""
737 atomends
= self
.atomends
739 while self
.pos
< len(self
.field
):
740 if self
.field
[self
.pos
] in atomends
:
742 else: atomlist
.append(self
.field
[self
.pos
])
745 return ''.join(atomlist
)
747 def getphraselist(self
):
748 """Parse a sequence of RFC 2822 phrases.
750 A phrase is a sequence of words, which are in turn either RFC 2822
751 atoms or quoted-strings. Phrases are canonicalized by squeezing all
752 runs of continuous whitespace into one space.
756 while self
.pos
< len(self
.field
):
757 if self
.field
[self
.pos
] in self
.LWS
:
759 elif self
.field
[self
.pos
] == '"':
760 plist
.append(self
.getquote())
761 elif self
.field
[self
.pos
] == '(':
762 self
.commentlist
.append(self
.getcomment())
763 elif self
.field
[self
.pos
] in self
.phraseends
:
766 plist
.append(self
.getatom(self
.phraseends
))
770 class AddressList(AddrlistClass
):
771 """An AddressList encapsulates a list of parsed RFC 2822 addresses."""
772 def __init__(self
, field
):
773 AddrlistClass
.__init
__(self
, field
)
775 self
.addresslist
= self
.getaddrlist()
777 self
.addresslist
= []
780 return len(self
.addresslist
)
783 return ", ".join(map(dump_address_pair
, self
.addresslist
))
785 def __add__(self
, other
):
787 newaddr
= AddressList(None)
788 newaddr
.addresslist
= self
.addresslist
[:]
789 for x
in other
.addresslist
:
790 if not x
in self
.addresslist
:
791 newaddr
.addresslist
.append(x
)
794 def __iadd__(self
, other
):
795 # Set union, in-place
796 for x
in other
.addresslist
:
797 if not x
in self
.addresslist
:
798 self
.addresslist
.append(x
)
801 def __sub__(self
, other
):
803 newaddr
= AddressList(None)
804 for x
in self
.addresslist
:
805 if not x
in other
.addresslist
:
806 newaddr
.addresslist
.append(x
)
809 def __isub__(self
, other
):
810 # Set difference, in-place
811 for x
in other
.addresslist
:
812 if x
in self
.addresslist
:
813 self
.addresslist
.remove(x
)
816 def __getitem__(self
, index
):
817 # Make indexing, slices, and 'in' work
818 return self
.addresslist
[index
]
820 def dump_address_pair(pair
):
821 """Dump a (name, address) pair in a canonicalized form."""
823 return '"' + pair
[0] + '" <' + pair
[1] + '>'
829 _monthnames
= ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul',
830 'aug', 'sep', 'oct', 'nov', 'dec',
831 'january', 'february', 'march', 'april', 'may', 'june', 'july',
832 'august', 'september', 'october', 'november', 'december']
833 _daynames
= ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
835 # The timezone table does not include the military time zones defined
836 # in RFC822, other than Z. According to RFC1123, the description in
837 # RFC822 gets the signs wrong, so we can't rely on any such time
838 # zones. RFC1123 recommends that numeric timezone indicators be used
839 # instead of timezone names.
841 _timezones
= {'UT':0, 'UTC':0, 'GMT':0, 'Z':0,
842 'AST': -400, 'ADT': -300, # Atlantic (used in Canada)
843 'EST': -500, 'EDT': -400, # Eastern
844 'CST': -600, 'CDT': -500, # Central
845 'MST': -700, 'MDT': -600, # Mountain
846 'PST': -800, 'PDT': -700 # Pacific
850 def parsedate_tz(data
):
851 """Convert a date string to a time tuple.
853 Accounts for military timezones.
858 if data
[0][-1] in (',', '.') or data
[0].lower() in _daynames
:
859 # There's a dayname here. Skip it
862 # no space after the "weekday,"?
863 i
= data
[0].rfind(',')
865 data
[0] = data
[0][i
+1:]
866 if len(data
) == 3: # RFC 850 date, deprecated
867 stuff
= data
[0].split('-')
869 data
= stuff
+ data
[1:]
874 data
[3:] = [s
[:i
], s
[i
+1:]]
876 data
.append('') # Dummy tz
880 [dd
, mm
, yy
, tm
, tz
] = data
882 if not mm
in _monthnames
:
883 dd
, mm
= mm
, dd
.lower()
884 if not mm
in _monthnames
:
886 mm
= _monthnames
.index(mm
)+1
887 if mm
> 12: mm
= mm
- 12
895 if not yy
[0].isdigit():
918 tzoffset
= _timezones
[tz
]
924 # Convert a timezone offset into seconds ; -0500 -> -18000
931 tzoffset
= tzsign
* ( (tzoffset
//100)*3600 + (tzoffset
% 100)*60)
932 return (yy
, mm
, dd
, thh
, tmm
, tss
, 0, 1, 0, tzoffset
)
936 """Convert a time string to a time tuple."""
937 t
= parsedate_tz(data
)
944 """Turn a 10-tuple as returned by parsedate_tz() into a UTC timestamp."""
946 # No zone info, so localtime is better assumption than GMT
947 return time
.mktime(data
[:8] + (-1,))
949 t
= time
.mktime(data
[:8] + (0,))
950 return t
- data
[9] - time
.timezone
952 def formatdate(timeval
=None):
953 """Returns time format preferred for Internet standards.
955 Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123
957 According to RFC 1123, day and month names must always be in
958 English. If not for that, this code could use strftime(). It
959 can't because strftime() honors the locale and could generated
963 timeval
= time
.time()
964 timeval
= time
.gmtime(timeval
)
965 return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (
966 ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")[timeval
[6]],
968 ("Jan", "Feb", "Mar", "Apr", "May", "Jun",
969 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")[timeval
[1]-1],
970 timeval
[0], timeval
[3], timeval
[4], timeval
[5])
973 # When used as script, run a small test program.
974 # The first command line argument must be a filename containing one
975 # message in RFC-822 format.
977 if __name__
== '__main__':
979 file = os
.path
.join(os
.environ
['HOME'], 'Mail/inbox/1')
980 if sys
.argv
[1:]: file = sys
.argv
[1]
983 print 'From:', m
.getaddr('from')
984 print 'To:', m
.getaddrlist('to')
985 print 'Subject:', m
.getheader('subject')
986 print 'Date:', m
.getheader('date')
987 date
= m
.getdate_tz('date')
989 date
= time
.localtime(mktime_tz(date
))
991 print 'ParsedDate:', time
.asctime(date
),
993 hhmm
, ss
= divmod(hhmmss
, 60)
994 hh
, mm
= divmod(hhmm
, 60)
995 print "%+03d%02d" % (hh
, mm
),
996 if ss
: print ".%02d" % ss
,
999 print 'ParsedDate:', None
1006 print 'len =', len(m
)
1007 if 'Date' in m
: print 'Date =', m
['Date']
1008 if 'X-Nonsense' in m
: pass
1009 print 'keys =', m
.keys()
1010 print 'values =', m
.values()
1011 print 'items =', m
.items()