1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/imip_agent.py Sun Sep 21 23:37:24 2014 +0200
1.3 @@ -0,0 +1,102 @@
1.4 +#!/usr/bin/env python
1.5 +
1.6 +from email import message_from_file
1.7 +from vCalendar import parse
1.8 +import sys
1.9 +
1.10 +try:
1.11 + from cStringIO import StringIO
1.12 +except ImportError:
1.13 + from StringIO import StringIO
1.14 +
1.15 +# Postfix exit codes.
1.16 +
1.17 +EX_USAGE = 64
1.18 +EX_DATAERR = 65
1.19 +EX_NOINPUT = 66
1.20 +EX_NOUSER = 67
1.21 +EX_NOHOST = 68
1.22 +EX_UNAVAILABLE = 69
1.23 +EX_SOFTWARE = 70
1.24 +EX_OSERR = 71
1.25 +EX_OSFILE = 72
1.26 +EX_CANTCREAT = 73
1.27 +EX_IOERR = 74
1.28 +EX_TEMPFAIL = 75
1.29 +EX_PROTOCOL = 76
1.30 +EX_NOPERM = 77
1.31 +EX_CONFIG = 78
1.32 +
1.33 +# Permitted iTIP content types.
1.34 +
1.35 +itip_content_types = [
1.36 + "text/calendar", # from RFC 6047
1.37 + "text/x-vcalendar", "application/ics", # other possibilities
1.38 + ]
1.39 +
1.40 +def process(f):
1.41 + msg = message_from_file(f)
1.42 +
1.43 + # Handle messages with iTIP parts.
1.44 +
1.45 + for part in msg.walk():
1.46 + if part.get_content_type() in itip_content_types and \
1.47 + part.get_param("method"):
1.48 +
1.49 + handle_itip_part(part)
1.50 +
1.51 +def get_itip_elements(elements):
1.52 + d = {}
1.53 + for name, attr, value in elements:
1.54 + if isinstance(value, list):
1.55 + d[name] = attr, get_itip_elements(value)
1.56 + else:
1.57 + d[name] = attr, value
1.58 + return d
1.59 +
1.60 +def get_value(d, name):
1.61 + if d.has_key(name):
1.62 + attr, value = d[name]
1.63 + return value
1.64 + else:
1.65 + return None
1.66 +
1.67 +def handle_itip_part(part):
1.68 + method = part.get_param("method")
1.69 +
1.70 + f = StringIO(part.get_payload(decode=True))
1.71 + doctype, attrs, elements = parse(f, encoding=part.get_content_charset())
1.72 +
1.73 + if doctype == "VCALENDAR":
1.74 + itip = get_itip_elements(elements)
1.75 +
1.76 + if get_value(itip, "METHOD") == method:
1.77 + for name, handler in [
1.78 + ("VFREEBUSY", handle_itip_freebusy),
1.79 + ("VEVENT", handle_itip_event),
1.80 + ("VTODO", handle_itip_todo),
1.81 + ("VJOURNAL", handle_itip_journal),
1.82 + ]:
1.83 +
1.84 + obj = get_value(itip, name)
1.85 + if obj:
1.86 + uid = get_value(obj, "UID")
1.87 + if uid:
1.88 + handler(obj, method)
1.89 +
1.90 +def handle_itip_freebusy(freebusy, method):
1.91 + print >>open("/tmp/imip.txt", "a"), freebusy
1.92 +
1.93 +def handle_itip_event(event, method):
1.94 + print >>open("/tmp/imip.txt", "a"), event
1.95 +
1.96 +def handle_itip_todo(todo, method):
1.97 + print >>open("/tmp/imip.txt", "a"), todo
1.98 +
1.99 +def handle_itip_journal(journal, method):
1.100 + print >>open("/tmp/imip.txt", "a"), journal
1.101 +
1.102 +if __name__ == "__main__":
1.103 + process(sys.stdin)
1.104 +
1.105 +# vim: tabstop=4 expandtab shiftwidth=4
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2.2 +++ b/vCalendar.py Sun Sep 21 23:37:24 2014 +0200
2.3 @@ -0,0 +1,259 @@
2.4 +#!/usr/bin/env python
2.5 +
2.6 +"""
2.7 +Parsing of vCalendar and iCalendar files.
2.8 +
2.9 +Copyright (C) 2008, 2009, 2011, 2013 Paul Boddie <paul@boddie.org.uk>
2.10 +
2.11 +This program is free software; you can redistribute it and/or modify it under
2.12 +the terms of the GNU General Public License as published by the Free Software
2.13 +Foundation; either version 3 of the License, or (at your option) any later
2.14 +version.
2.15 +
2.16 +This program is distributed in the hope that it will be useful, but WITHOUT
2.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
2.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
2.19 +details.
2.20 +
2.21 +You should have received a copy of the GNU General Public License along with
2.22 +this program. If not, see <http://www.gnu.org/licenses/>.
2.23 +
2.24 +--------
2.25 +
2.26 +References:
2.27 +
2.28 +RFC 5545: Internet Calendaring and Scheduling Core Object Specification
2.29 + (iCalendar)
2.30 + http://tools.ietf.org/html/rfc5545
2.31 +
2.32 +RFC 2445: Internet Calendaring and Scheduling Core Object Specification
2.33 + (iCalendar)
2.34 + http://tools.ietf.org/html/rfc2445
2.35 +"""
2.36 +
2.37 +import vContent
2.38 +import re
2.39 +
2.40 +try:
2.41 + set
2.42 +except NameError:
2.43 + from sets import Set as set
2.44 +
2.45 +# Format details.
2.46 +
2.47 +QUOTED_PARAMETERS = set([
2.48 + "ALTREP", "DELEGATED-FROM", "DELEGATED-TO", "DIR", "MEMBER", "SENT-BY"
2.49 + ])
2.50 +MULTIVALUED_PARAMETERS = set([
2.51 + "DELEGATED-FROM", "DELEGATED-TO", "MEMBER"
2.52 + ])
2.53 +QUOTED_TYPES = set(["URI"])
2.54 +
2.55 +unquoted_separator_regexp = re.compile(r"(?<!\\)([,;])")
2.56 +
2.57 +# Parser classes.
2.58 +
2.59 +class vCalendarStreamParser(vContent.StreamParser):
2.60 +
2.61 + "A stream parser specifically for vCalendar/iCalendar."
2.62 +
2.63 + def next(self):
2.64 +
2.65 + """
2.66 + Return the next content item in the file as a tuple of the form
2.67 + (name, parameters, value).
2.68 + """
2.69 +
2.70 + name, parameters, value = vContent.StreamParser.next(self)
2.71 + return name, self.decode_parameters(parameters), value
2.72 +
2.73 + def decode_content(self, value):
2.74 +
2.75 + """
2.76 + Decode the given 'value' (which may represent a collection of distinct
2.77 + values), replacing quoted separator characters.
2.78 + """
2.79 +
2.80 + sep = None
2.81 + values = []
2.82 +
2.83 + for i, s in enumerate(unquoted_separator_regexp.split(value)):
2.84 + if i % 2 != 0:
2.85 + if not sep:
2.86 + sep = s
2.87 + continue
2.88 + values.append(self.decode_content_value(s))
2.89 +
2.90 + if sep == ",":
2.91 + return values
2.92 + elif sep == ";":
2.93 + return tuple(values)
2.94 + else:
2.95 + return values[0]
2.96 +
2.97 + def decode_content_value(self, value):
2.98 +
2.99 + "Decode the given 'value', replacing quoted separator characters."
2.100 +
2.101 + # Replace quoted characters (see 4.3.11 in RFC 2445).
2.102 +
2.103 + value = vContent.StreamParser.decode_content(self, value)
2.104 + return value.replace(r"\,", ",").replace(r"\;", ";")
2.105 +
2.106 + # Internal methods.
2.107 +
2.108 + def decode_quoted_value(self, value):
2.109 +
2.110 + "Decode the given 'value', returning a list of decoded values."
2.111 +
2.112 + if value[0] == '"' and value[-1] == '"':
2.113 + return value[1:-1]
2.114 + else:
2.115 + return value
2.116 +
2.117 + def decode_parameters(self, parameters):
2.118 +
2.119 + """
2.120 + Decode the given 'parameters' according to the vCalendar specification.
2.121 + """
2.122 +
2.123 + decoded_parameters = {}
2.124 +
2.125 + for param_name, param_value in parameters.items():
2.126 + if param_name in QUOTED_PARAMETERS:
2.127 + param_value = self.decode_quoted_value(param_value)
2.128 + separator = '","'
2.129 + else:
2.130 + separator = ","
2.131 + if param_name in MULTIVALUED_PARAMETERS:
2.132 + param_value = param_value.split(separator)
2.133 + decoded_parameters[param_name] = param_value
2.134 +
2.135 + return decoded_parameters
2.136 +
2.137 +class vCalendarParser(vContent.Parser):
2.138 +
2.139 + "A parser specifically for vCalendar/iCalendar."
2.140 +
2.141 + def parse(self, f, parser_cls=None):
2.142 + return vContent.Parser.parse(self, f, (parser_cls or vCalendarStreamParser))
2.143 +
2.144 +# Writer classes.
2.145 +
2.146 +class vCalendarStreamWriter(vContent.StreamWriter):
2.147 +
2.148 + "A stream writer specifically for vCalendar."
2.149 +
2.150 + # Overridden methods.
2.151 +
2.152 + def encode_parameters(self, parameters):
2.153 +
2.154 + """
2.155 + Encode the given 'parameters' according to the vCalendar specification.
2.156 + """
2.157 +
2.158 + encoded_parameters = {}
2.159 +
2.160 + for param_name, param_value in parameters.items():
2.161 + if param_name in QUOTED_PARAMETERS:
2.162 + param_value = self.encode_quoted_parameter_value(param_value)
2.163 + separator = '","'
2.164 + else:
2.165 + separator = ","
2.166 + if param_name in MULTIVALUED_PARAMETERS:
2.167 + param_value = separator.join(param_value)
2.168 + encoded_parameters[param_name] = param_value
2.169 +
2.170 + return encoded_parameters
2.171 +
2.172 + def encode_content(self, value):
2.173 +
2.174 + """
2.175 + Encode the given 'value' (which may be a list or tuple of separate
2.176 + values), quoting characters and separating collections of values.
2.177 + """
2.178 +
2.179 + if isinstance(value, list):
2.180 + sep = ","
2.181 + elif isinstance(value, tuple):
2.182 + sep = ";"
2.183 + else:
2.184 + value = [value]
2.185 + sep = ""
2.186 +
2.187 + return sep.join([self.encode_content_value(v) for v in value])
2.188 +
2.189 + def encode_content_value(self, value):
2.190 +
2.191 + "Encode the given 'value', quoting characters."
2.192 +
2.193 + # Replace quoted characters (see 4.3.11 in RFC 2445).
2.194 +
2.195 + value = vContent.StreamWriter.encode_content(self, value)
2.196 + return value.replace(";", r"\;").replace(",", r"\,")
2.197 +
2.198 +# Public functions.
2.199 +
2.200 +def parse(stream_or_string, encoding=None, non_standard_newline=0):
2.201 +
2.202 + """
2.203 + Parse the resource data found through the use of the 'stream_or_string',
2.204 + which is either a stream providing Unicode data (the codecs module can be
2.205 + used to open files or to wrap streams in order to provide Unicode data) or a
2.206 + filename identifying a file to be parsed.
2.207 +
2.208 + The optional 'encoding' can be used to specify the character encoding used
2.209 + by the file to be parsed.
2.210 +
2.211 + The optional 'non_standard_newline' can be set to a true value (unlike the
2.212 + default) in order to attempt to process files with CR as the end of line
2.213 + character.
2.214 +
2.215 + As a result of parsing the resource, the root node of the imported resource
2.216 + is returned.
2.217 + """
2.218 +
2.219 + return vContent.parse(stream_or_string, encoding, non_standard_newline, vCalendarParser)
2.220 +
2.221 +def iterparse(stream_or_string, encoding=None, non_standard_newline=0):
2.222 +
2.223 + """
2.224 + Parse the resource data found through the use of the 'stream_or_string',
2.225 + which is either a stream providing Unicode data (the codecs module can be
2.226 + used to open files or to wrap streams in order to provide Unicode data) or a
2.227 + filename identifying a file to be parsed.
2.228 +
2.229 + The optional 'encoding' can be used to specify the character encoding used
2.230 + by the file to be parsed.
2.231 +
2.232 + The optional 'non_standard_newline' can be set to a true value (unlike the
2.233 + default) in order to attempt to process files with CR as the end of line
2.234 + character.
2.235 +
2.236 + An iterator is returned which provides event tuples describing parsing
2.237 + events of the form (name, parameters, value).
2.238 + """
2.239 +
2.240 + return vContent.iterparse(stream_or_string, encoding, non_standard_newline, vCalendarStreamParser)
2.241 +
2.242 +def iterwrite(stream_or_string=None, write=None, encoding=None, line_length=None):
2.243 +
2.244 + """
2.245 + Return a writer which will either send data to the resource found through
2.246 + the use of 'stream_or_string' or using the given 'write' operation.
2.247 +
2.248 + The 'stream_or_string' parameter may be either a stream accepting Unicode
2.249 + data (the codecs module can be used to open files or to wrap streams in
2.250 + order to accept Unicode data) or a filename identifying a file to be
2.251 + written.
2.252 +
2.253 + The optional 'encoding' can be used to specify the character encoding used
2.254 + by the file to be written.
2.255 +
2.256 + The optional 'line_length' can be used to specify how long lines should be
2.257 + in the resulting data.
2.258 + """
2.259 +
2.260 + return vContent.iterwrite(stream_or_string, write, encoding, line_length, vCalendarStreamWriter)
2.261 +
2.262 +# vim: tabstop=4 expandtab shiftwidth=4
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
3.2 +++ b/vContent.py Sun Sep 21 23:37:24 2014 +0200
3.3 @@ -0,0 +1,701 @@
3.4 +#!/usr/bin/env python
3.5 +
3.6 +"""
3.7 +Parsing of vCard, vCalendar and iCalendar files.
3.8 +
3.9 +Copyright (C) 2005, 2006, 2007, 2008, 2009, 2011, 2013 Paul Boddie <paul@boddie.org.uk>
3.10 +
3.11 +This program is free software; you can redistribute it and/or modify it under
3.12 +the terms of the GNU General Public License as published by the Free Software
3.13 +Foundation; either version 3 of the License, or (at your option) any later
3.14 +version.
3.15 +
3.16 +This program is distributed in the hope that it will be useful, but WITHOUT
3.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
3.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
3.19 +details.
3.20 +
3.21 +You should have received a copy of the GNU General Public License along with
3.22 +this program. If not, see <http://www.gnu.org/licenses/>.
3.23 +
3.24 +--------
3.25 +
3.26 +References:
3.27 +
3.28 +RFC 5545: Internet Calendaring and Scheduling Core Object Specification
3.29 + (iCalendar)
3.30 + http://tools.ietf.org/html/rfc5545
3.31 +
3.32 +RFC 2445: Internet Calendaring and Scheduling Core Object Specification
3.33 + (iCalendar)
3.34 + http://tools.ietf.org/html/rfc2445
3.35 +
3.36 +RFC 2425: A MIME Content-Type for Directory Information
3.37 + http://tools.ietf.org/html/rfc2425
3.38 +
3.39 +RFC 2426: vCard MIME Directory Profile
3.40 + http://tools.ietf.org/html/rfc2426
3.41 +"""
3.42 +
3.43 +try:
3.44 + set
3.45 +except NameError:
3.46 + from sets import Set as set
3.47 +
3.48 +# Encoding-related imports.
3.49 +
3.50 +import base64, quopri
3.51 +import codecs
3.52 +
3.53 +# Tokenisation help.
3.54 +
3.55 +import re
3.56 +
3.57 +# Configuration.
3.58 +
3.59 +default_encoding = "utf-8"
3.60 +
3.61 +# Reader and parser classes.
3.62 +
3.63 +class Reader:
3.64 +
3.65 + "A simple class wrapping a file, providing simple pushback capabilities."
3.66 +
3.67 + def __init__(self, f, non_standard_newline=0):
3.68 +
3.69 + """
3.70 + Initialise the object with the file 'f'. If 'non_standard_newline' is
3.71 + set to a true value (unlike the default), lines ending with CR will be
3.72 + treated as complete lines.
3.73 + """
3.74 +
3.75 + self.f = f
3.76 + self.non_standard_newline = non_standard_newline
3.77 + self.lines = []
3.78 + self.line_number = 1 # about to read line 1
3.79 +
3.80 + def close(self):
3.81 +
3.82 + "Close the reader."
3.83 +
3.84 + self.f.close()
3.85 +
3.86 + def pushback(self, line):
3.87 +
3.88 + """
3.89 + Push the given 'line' back so that the next line read is actually the
3.90 + given 'line' and not the next line from the underlying file.
3.91 + """
3.92 +
3.93 + self.lines.append(line)
3.94 + self.line_number -= 1
3.95 +
3.96 + def readline(self):
3.97 +
3.98 + """
3.99 + If no pushed-back lines exist, read a line directly from the file.
3.100 + Otherwise, read from the list of pushed-back lines.
3.101 + """
3.102 +
3.103 + self.line_number += 1
3.104 + if self.lines:
3.105 + return self.lines.pop()
3.106 + else:
3.107 + # Sanity check for broken lines (\r instead of \r\n or \n).
3.108 + line = self.f.readline()
3.109 + while line.endswith("\r") and not self.non_standard_newline:
3.110 + s = self.f.readline()
3.111 + if not s:
3.112 + break
3.113 + line += s
3.114 + if line.endswith("\r") and self.non_standard_newline:
3.115 + return line + "\n"
3.116 + else:
3.117 + return line
3.118 +
3.119 + def read_content_line(self):
3.120 +
3.121 + """
3.122 + Read an entire content line, itself potentially consisting of many
3.123 + physical lines of text, returning a string.
3.124 + """
3.125 +
3.126 + # Skip blank lines.
3.127 +
3.128 + line = self.readline()
3.129 + while line:
3.130 + line_stripped = line.rstrip("\r\n")
3.131 + if not line_stripped:
3.132 + line = self.readline()
3.133 + else:
3.134 + break
3.135 + else:
3.136 + return ""
3.137 +
3.138 + # Strip all appropriate whitespace from the right end of each line.
3.139 + # For subsequent lines, remove the first whitespace character.
3.140 + # See section 4.1 of the iCalendar specification.
3.141 +
3.142 + lines = [line_stripped]
3.143 +
3.144 + line = self.readline()
3.145 + while line.startswith(" ") or line.startswith("\t"):
3.146 + lines.append(line[1:].rstrip("\r\n"))
3.147 + line = self.readline()
3.148 +
3.149 + # Since one line too many will have been read, push the line back into
3.150 + # the file.
3.151 +
3.152 + if line:
3.153 + self.pushback(line)
3.154 +
3.155 + return "".join(lines)
3.156 +
3.157 + def get_content_line(self):
3.158 +
3.159 + "Return a content line object for the current line."
3.160 +
3.161 + return ContentLine(self.read_content_line())
3.162 +
3.163 +class ContentLine:
3.164 +
3.165 + "A content line which can be searched."
3.166 +
3.167 + SEPARATORS = re.compile('[;:"]')
3.168 + SEPARATORS_PLUS_EQUALS = re.compile('[=;:"]')
3.169 +
3.170 + def __init__(self, text):
3.171 + self.text = text
3.172 + self.start = 0
3.173 +
3.174 + def __repr__(self):
3.175 + return "ContentLine(%r)" % self.text
3.176 +
3.177 + def get_remaining(self):
3.178 +
3.179 + "Get the remaining text from the content line."
3.180 +
3.181 + return self.text[self.start:]
3.182 +
3.183 + def search(self, targets):
3.184 +
3.185 + """
3.186 + Find one of the 'targets' in the text, returning the string from the
3.187 + current position up to the target found, along with the target string,
3.188 + using a tuple of the form (string, target). If no target was found,
3.189 + return the entire string together with a target of None.
3.190 +
3.191 + The 'targets' parameter must be a regular expression object or an object
3.192 + compatible with the API of such objects.
3.193 + """
3.194 +
3.195 + text = self.text
3.196 + start = pos = self.start
3.197 + length = len(text)
3.198 +
3.199 + # Remember the first target.
3.200 +
3.201 + first = None
3.202 + first_pos = None
3.203 + in_quoted_region = 0
3.204 +
3.205 + # Process the text, looking for the targets.
3.206 +
3.207 + while pos < length:
3.208 + match = targets.search(text, pos)
3.209 +
3.210 + # Where nothing matches, end the search.
3.211 +
3.212 + if match is None:
3.213 + pos = length
3.214 +
3.215 + # Where a double quote matches, toggle the region state.
3.216 +
3.217 + elif match.group() == '"':
3.218 + in_quoted_region = not in_quoted_region
3.219 + pos = match.end()
3.220 +
3.221 + # Where something else matches outside a region, stop searching.
3.222 +
3.223 + elif not in_quoted_region:
3.224 + first = match.group()
3.225 + first_pos = match.start()
3.226 + break
3.227 +
3.228 + # Otherwise, keep looking for the end of the region.
3.229 +
3.230 + else:
3.231 + pos = match.end()
3.232 +
3.233 + # Where no more input can provide the targets, return a special result.
3.234 +
3.235 + else:
3.236 + self.start = length
3.237 + return text[start:], None
3.238 +
3.239 + self.start = match.end()
3.240 + return text[start:first_pos], first
3.241 +
3.242 +class StreamParser:
3.243 +
3.244 + "A stream parser for content in vCard/vCalendar/iCalendar-like formats."
3.245 +
3.246 + def __init__(self, f):
3.247 +
3.248 + "Initialise the parser for the given file 'f'."
3.249 +
3.250 + self.f = f
3.251 +
3.252 + def close(self):
3.253 +
3.254 + "Close the reader."
3.255 +
3.256 + self.f.close()
3.257 +
3.258 + def __iter__(self):
3.259 +
3.260 + "Return self as the iterator."
3.261 +
3.262 + return self
3.263 +
3.264 + def next(self):
3.265 +
3.266 + """
3.267 + Return the next content item in the file as a tuple of the form
3.268 + (name, parameters, values).
3.269 + """
3.270 +
3.271 + return self.parse_content_line()
3.272 +
3.273 + def decode_content(self, value):
3.274 +
3.275 + "Decode the given 'value', replacing quoted characters."
3.276 +
3.277 + return value.replace("\r", "").replace("\\N", "\n").replace("\\n", "\n")
3.278 +
3.279 + # Internal methods.
3.280 +
3.281 + def parse_content_line(self):
3.282 +
3.283 + """
3.284 + Return the name, parameters and value information for the current
3.285 + content line in the file being parsed.
3.286 + """
3.287 +
3.288 + f = self.f
3.289 + line_number = f.line_number
3.290 + line = f.get_content_line()
3.291 +
3.292 + # Read the property name.
3.293 +
3.294 + name, sep = line.search(line.SEPARATORS)
3.295 + name = name.strip()
3.296 +
3.297 + if not name and sep is None:
3.298 + raise StopIteration
3.299 +
3.300 + # Read the parameters.
3.301 +
3.302 + parameters = {}
3.303 +
3.304 + while sep == ";":
3.305 +
3.306 + # Find the actual modifier.
3.307 +
3.308 + parameter_name, sep = line.search(line.SEPARATORS_PLUS_EQUALS)
3.309 + parameter_name = parameter_name.strip()
3.310 +
3.311 + if sep == "=":
3.312 + parameter_value, sep = line.search(line.SEPARATORS)
3.313 + parameter_value = parameter_value.strip()
3.314 + else:
3.315 + parameter_value = None
3.316 +
3.317 + # Append a key, value tuple to the parameters list.
3.318 +
3.319 + parameters[parameter_name] = parameter_value
3.320 +
3.321 + # Get the value content.
3.322 +
3.323 + if sep != ":":
3.324 + raise ValueError, (line_number, line)
3.325 +
3.326 + # Obtain and decode the value.
3.327 +
3.328 + value = self.decode(name, parameters, line.get_remaining())
3.329 +
3.330 + return name, parameters, value
3.331 +
3.332 + def decode(self, name, parameters, value):
3.333 +
3.334 + "Decode using 'name' and 'parameters' the given 'value'."
3.335 +
3.336 + encoding = parameters.get("ENCODING")
3.337 + charset = parameters.get("CHARSET")
3.338 +
3.339 + value = self.decode_content(value)
3.340 +
3.341 + if encoding == "QUOTED-PRINTABLE":
3.342 + return unicode(quopri.decodestring(value), charset or "iso-8859-1")
3.343 + elif encoding == "BASE64":
3.344 + return base64.decodestring(value)
3.345 + else:
3.346 + return value
3.347 +
3.348 +class ParserBase:
3.349 +
3.350 + "An abstract parser for content in vCard/vCalendar/iCalendar-like formats."
3.351 +
3.352 + def __init__(self):
3.353 +
3.354 + "Initialise the parser."
3.355 +
3.356 + self.names = []
3.357 +
3.358 + def parse(self, f, parser_cls=None):
3.359 +
3.360 + "Parse the contents of the file 'f'."
3.361 +
3.362 + parser = (parser_cls or StreamParser)(f)
3.363 +
3.364 + for name, parameters, value in parser:
3.365 +
3.366 + if name == "BEGIN":
3.367 + self.names.append(value)
3.368 + self.startComponent(value, parameters)
3.369 +
3.370 + elif name == "END":
3.371 + start_name = self.names.pop()
3.372 + if start_name != value:
3.373 + raise ParseError, "Mismatch in BEGIN and END declarations (%r and %r) at line %d." % (
3.374 + start_name, value, f.line_number)
3.375 +
3.376 + self.endComponent(value)
3.377 +
3.378 + else:
3.379 + self.handleProperty(name, parameters, value)
3.380 +
3.381 +class Parser(ParserBase):
3.382 +
3.383 + "A SAX-like parser for vCard/vCalendar/iCalendar-like formats."
3.384 +
3.385 + def __init__(self):
3.386 + ParserBase.__init__(self)
3.387 + self.components = []
3.388 +
3.389 + def startComponent(self, name, parameters):
3.390 +
3.391 + """
3.392 + Add the component with the given 'name' and 'parameters', recording an
3.393 + empty list of children as part of the component's content.
3.394 + """
3.395 +
3.396 + component = self.handleProperty(name, parameters)
3.397 + self.components.append(component)
3.398 + return component
3.399 +
3.400 + def endComponent(self, name):
3.401 +
3.402 + """
3.403 + End the component with the given 'name' by removing it from the active
3.404 + component stack. If only one component exists on the stack, retain it
3.405 + for later inspection.
3.406 + """
3.407 +
3.408 + if len(self.components) > 1:
3.409 + return self.components.pop()
3.410 +
3.411 + # Or return the only element.
3.412 +
3.413 + elif self.components:
3.414 + return self.components[0]
3.415 +
3.416 + def handleProperty(self, name, parameters, value=None):
3.417 +
3.418 + """
3.419 + Record the property with the given 'name', 'parameters' and optional
3.420 + 'value' as part of the current component's children.
3.421 + """
3.422 +
3.423 + component = self.makeComponent(name, parameters, value)
3.424 + self.attachComponent(component)
3.425 + return component
3.426 +
3.427 + # Component object construction/manipulation methods.
3.428 +
3.429 + def attachComponent(self, component):
3.430 +
3.431 + "Attach the given 'component' to its parent."
3.432 +
3.433 + if self.components:
3.434 + component_name, component_parameters, component_children = self.components[-1]
3.435 + component_children.append(component)
3.436 +
3.437 + def makeComponent(self, name, parameters, value=None):
3.438 +
3.439 + """
3.440 + Make a component object from the given 'name', 'parameters' and optional
3.441 + 'value'.
3.442 + """
3.443 +
3.444 + return (name, parameters, value or [])
3.445 +
3.446 + # Public methods.
3.447 +
3.448 + def parse(self, f, parser_cls=None):
3.449 +
3.450 + "Parse the contents of the file 'f'."
3.451 +
3.452 + ParserBase.parse(self, f, parser_cls)
3.453 + return self.components[0]
3.454 +
3.455 +# Writer classes.
3.456 +
3.457 +class Writer:
3.458 +
3.459 + "A simple class wrapping a file, providing simple output capabilities."
3.460 +
3.461 + default_line_length = 76
3.462 +
3.463 + def __init__(self, write, line_length=None):
3.464 +
3.465 + """
3.466 + Initialise the object with the given 'write' operation. If 'line_length'
3.467 + is set, the length of written lines will conform to the specified value
3.468 + instead of the default value.
3.469 + """
3.470 +
3.471 + self._write = write
3.472 + self.line_length = line_length or self.default_line_length
3.473 + self.char_offset = 0
3.474 +
3.475 + def write(self, text):
3.476 +
3.477 + "Write the 'text' to the file."
3.478 +
3.479 + write = self._write
3.480 + line_length = self.line_length
3.481 +
3.482 + i = 0
3.483 + remaining = len(text)
3.484 +
3.485 + while remaining:
3.486 + space = line_length - self.char_offset
3.487 + if remaining > space:
3.488 + write(text[i:i + space])
3.489 + write("\r\n ")
3.490 + self.char_offset = 1
3.491 + i += space
3.492 + remaining -= space
3.493 + else:
3.494 + write(text[i:])
3.495 + self.char_offset += remaining
3.496 + i += remaining
3.497 + remaining = 0
3.498 +
3.499 + def end_line(self):
3.500 +
3.501 + "End the current content line."
3.502 +
3.503 + if self.char_offset > 0:
3.504 + self.char_offset = 0
3.505 + self._write("\r\n")
3.506 +
3.507 +class StreamWriter:
3.508 +
3.509 + "A stream writer for content in vCard/vCalendar/iCalendar-like formats."
3.510 +
3.511 + def __init__(self, f):
3.512 +
3.513 + "Initialise the stream writer with the given 'f' stream object."
3.514 +
3.515 + self.f = f
3.516 +
3.517 + def write(self, name, parameters, value):
3.518 +
3.519 + """
3.520 + Write a content line, serialising the given 'name', 'parameters' and
3.521 + 'value' information.
3.522 + """
3.523 +
3.524 + self.write_content_line(name, self.encode_parameters(parameters), self.encode_value(name, parameters, value))
3.525 +
3.526 + # Internal methods.
3.527 +
3.528 + def write_content_line(self, name, encoded_parameters, encoded_value):
3.529 +
3.530 + """
3.531 + Write a content line for the given 'name', 'encoded_parameters' and
3.532 + 'encoded_value' information.
3.533 + """
3.534 +
3.535 + f = self.f
3.536 +
3.537 + f.write(name)
3.538 + for param_name, param_value in encoded_parameters.items():
3.539 + f.write(";")
3.540 + f.write(param_name)
3.541 + f.write("=")
3.542 + f.write(param_value)
3.543 + f.write(":")
3.544 + f.write(encoded_value)
3.545 + f.end_line()
3.546 +
3.547 + def encode_quoted_parameter_value(self, value):
3.548 +
3.549 + "Encode the given 'value'."
3.550 +
3.551 + return '"%s"' % value
3.552 +
3.553 + def encode_value(self, name, parameters, value):
3.554 +
3.555 + """
3.556 + Encode using 'name' and 'parameters' the given 'value' so that the
3.557 + resulting encoded form employs any specified character encodings.
3.558 + """
3.559 +
3.560 + encoding = parameters.get("ENCODING")
3.561 + charset = parameters.get("CHARSET")
3.562 +
3.563 + if encoding == "QUOTED-PRINTABLE":
3.564 + value = quopri.encodestring(value.encode(charset or "iso-8859-1"))
3.565 + elif encoding == "BASE64":
3.566 + value = base64.encodestring(value)
3.567 +
3.568 + return self.encode_content(value)
3.569 +
3.570 + # Overrideable methods.
3.571 +
3.572 + def encode_parameters(self, parameters):
3.573 +
3.574 + """
3.575 + Encode the given 'parameters' according to the vCalendar specification.
3.576 + """
3.577 +
3.578 + encoded_parameters = {}
3.579 +
3.580 + for param_name, param_value in parameters.items():
3.581 +
3.582 + # Basic format support merely involves quoting values which seem to
3.583 + # need it. Other more specific formats may define exactly which
3.584 + # parameters should be quoted.
3.585 +
3.586 + if ContentLine.SEPARATORS.search(param_value):
3.587 + param_value = self.encode_quoted_parameter_value(param_value)
3.588 +
3.589 + encoded_parameters[param_name] = param_value
3.590 +
3.591 + return encoded_parameters
3.592 +
3.593 + def encode_content(self, value):
3.594 +
3.595 + "Encode the given 'value', quoting characters."
3.596 +
3.597 + return value.replace("\n", "\\n")
3.598 +
3.599 +# Utility functions.
3.600 +
3.601 +def is_input_stream(stream_or_string):
3.602 + return hasattr(stream_or_string, "read")
3.603 +
3.604 +def get_input_stream(stream_or_string, encoding=None):
3.605 + if is_input_stream(stream_or_string):
3.606 + return stream_or_string
3.607 + else:
3.608 + return codecs.open(stream_or_string, encoding=(encoding or default_encoding))
3.609 +
3.610 +def get_output_stream(stream_or_string, encoding=None):
3.611 + if hasattr(stream_or_string, "write"):
3.612 + return stream_or_string
3.613 + else:
3.614 + return codecs.open(stream_or_string, "w", encoding=(encoding or default_encoding))
3.615 +
3.616 +# Public functions.
3.617 +
3.618 +def parse(stream_or_string, encoding=None, non_standard_newline=0, parser_cls=None):
3.619 +
3.620 + """
3.621 + Parse the resource data found through the use of the 'stream_or_string',
3.622 + which is either a stream providing Unicode data (the codecs module can be
3.623 + used to open files or to wrap streams in order to provide Unicode data) or a
3.624 + filename identifying a file to be parsed.
3.625 +
3.626 + The optional 'encoding' can be used to specify the character encoding used
3.627 + by the file to be parsed.
3.628 +
3.629 + The optional 'non_standard_newline' can be set to a true value (unlike the
3.630 + default) in order to attempt to process files with CR as the end of line
3.631 + character.
3.632 +
3.633 + As a result of parsing the resource, the root node of the imported resource
3.634 + is returned.
3.635 + """
3.636 +
3.637 + stream = get_input_stream(stream_or_string, encoding)
3.638 + reader = Reader(stream, non_standard_newline)
3.639 +
3.640 + # Parse using the reader.
3.641 +
3.642 + try:
3.643 + parser = (parser_cls or Parser)()
3.644 + return parser.parse(reader)
3.645 +
3.646 + # Close any opened streams.
3.647 +
3.648 + finally:
3.649 + if not is_input_stream(stream_or_string):
3.650 + reader.close()
3.651 +
3.652 +def iterparse(stream_or_string, encoding=None, non_standard_newline=0, parser_cls=None):
3.653 +
3.654 + """
3.655 + Parse the resource data found through the use of the 'stream_or_string',
3.656 + which is either a stream providing Unicode data (the codecs module can be
3.657 + used to open files or to wrap streams in order to provide Unicode data) or a
3.658 + filename identifying a file to be parsed.
3.659 +
3.660 + The optional 'encoding' can be used to specify the character encoding used
3.661 + by the file to be parsed.
3.662 +
3.663 + The optional 'non_standard_newline' can be set to a true value (unlike the
3.664 + default) in order to attempt to process files with CR as the end of line
3.665 + character.
3.666 +
3.667 + An iterator is returned which provides event tuples describing parsing
3.668 + events of the form (name, parameters, value).
3.669 + """
3.670 +
3.671 + stream = get_input_stream(stream_or_string, encoding)
3.672 + reader = Reader(stream, non_standard_newline)
3.673 + parser = (parser_cls or StreamParser)(reader)
3.674 + return parser
3.675 +
3.676 +def iterwrite(stream_or_string=None, write=None, encoding=None, line_length=None, writer_cls=None):
3.677 +
3.678 + """
3.679 + Return a writer which will either send data to the resource found through
3.680 + the use of 'stream_or_string' or using the given 'write' operation.
3.681 +
3.682 + The 'stream_or_string' parameter may be either a stream accepting Unicode
3.683 + data (the codecs module can be used to open files or to wrap streams in
3.684 + order to accept Unicode data) or a filename identifying a file to be
3.685 + written.
3.686 +
3.687 + The optional 'encoding' can be used to specify the character encoding used
3.688 + by the file to be written.
3.689 +
3.690 + The optional 'line_length' can be used to specify how long lines should be
3.691 + in the resulting data.
3.692 + """
3.693 +
3.694 + if stream_or_string:
3.695 + stream = get_output_stream(stream_or_string, encoding)
3.696 + _writer = Writer(stream.write, line_length)
3.697 + elif write:
3.698 + _writer = Writer(write, line_length)
3.699 + else:
3.700 + raise IOError, "No stream, filename or write operation specified."
3.701 +
3.702 + return (writer_cls or StreamWriter)(_writer)
3.703 +
3.704 +# vim: tabstop=4 expandtab shiftwidth=4