def get_contact_objects(db_session, account_id, addresses): """Given a list `addresses` of (name, email) pairs, return existing contacts with matching email. Create and also return contact objects for any email without a match.""" if addresses is None: return [] contacts = [] for addr in addresses: if addr is None: continue name, email = addr canonical_email = canonicalize_address(email) existing_contacts = db_session.query(Contact). \ filter(Contact.email_address == canonical_email, Contact.account_id == account_id).all() if not existing_contacts: new_contact = Contact(name=name, email_address=canonical_email, account_id=account_id, source='local', provider_name='inbox', uid=uuid.uuid4().hex) contacts.append(new_contact) db_session.add(new_contact) else: contacts.extend(existing_contacts) return contacts
def email_address(self, value): if value is not None: # Silently truncate if necessary. In practice, this may be too # long if somebody put a super-long email into their contacts by # mistake or something. value = value[:MAX_INDEXABLE_LENGTH] self._raw_address = value self._canonicalized_address = canonicalize_address(value)
def email_address(self, value): # Silently truncate if necessary. In practice, this may be too # long if somebody put a super-long email into their contacts by # mistake or something. if value is not None: value = unicode_safe_truncate(value, MAX_INDEXABLE_LENGTH) self._raw_address = value self._canonicalized_address = canonicalize_address(value)
def __eq__(self, other): return self.__clause_element__() == canonicalize_address(other)
def events_from_ics(namespace, calendar, ics_str): try: cal = iCalendar.from_ical(ics_str) except (ValueError, IndexError, KeyError): raise MalformedEventError() events = [] # See: https://tools.ietf.org/html/rfc5546#section-3.2 calendar_method = None for component in cal.walk(): if component.name == "VCALENDAR": calendar_method = component.get('method') if component.name == "VTIMEZONE": tzname = component.get('TZID') assert tzname in timezones_table,\ "Non-UTC timezone should be in table" if component.name == "VEVENT": # Make sure the times are in UTC. try: original_start = component.get('dtstart').dt original_end = component.get('dtend').dt except AttributeError: raise MalformedEventError("Event lacks start and/or end time") start = original_start end = original_end original_start_tz = None if isinstance(start, datetime) and isinstance(end, datetime): all_day = False original_start_tz = str(original_start.tzinfo) # icalendar doesn't parse Windows timezones yet # (see: https://github.com/collective/icalendar/issues/44) # so we look if the timezone isn't in our Windows-TZ # to Olson-TZ table. if original_start.tzinfo is None: tzid = component.get('dtstart').params.get('TZID', None) assert tzid in timezones_table,\ "Non-UTC timezone should be in table" corresponding_tz = timezones_table[tzid] original_start_tz = corresponding_tz local_timezone = pytz.timezone(corresponding_tz) start = local_timezone.localize(original_start) if original_end.tzinfo is None: tzid = component.get('dtend').params.get('TZID', None) assert tzid in timezones_table,\ "Non-UTC timezone should be in table" corresponding_tz = timezones_table[tzid] local_timezone = pytz.timezone(corresponding_tz) end = local_timezone.localize(original_end) elif isinstance(start, date) and isinstance(end, date): all_day = True start = arrow.get(start) end = arrow.get(end) # Get the last modification date. # Exchange uses DtStamp, iCloud and Gmail LAST-MODIFIED. last_modified_tstamp = component.get('dtstamp') last_modified = None if last_modified_tstamp is not None: # This is one surprising instance of Exchange doing # the right thing by giving us an UTC timestamp. Also note that # Google calendar also include the DtStamp field, probably to # be a good citizen. if last_modified_tstamp.dt.tzinfo is not None: last_modified = last_modified_tstamp.dt else: raise NotImplementedError("We don't support arcane Windows" " timezones in timestamps yet") else: # Try to look for a LAST-MODIFIED element instead. # Note: LAST-MODIFIED is always in UTC. # http://www.kanzaki.com/docs/ical/lastModified.html last_modified = component.get('last-modified').dt assert last_modified is not None, \ "Event should have a DtStamp or LAST-MODIFIED timestamp" title = None summaries = component.get('summary', []) if not isinstance(summaries, list): summaries = [summaries] if summaries != []: title = " - ".join(summaries) description = unicode(component.get('description')) event_status = component.get('status') if event_status is not None: event_status = event_status.lower() else: # Some providers (e.g: iCloud) don't use the status field. # Instead they use the METHOD field to signal cancellations. method = component.get('method') if method and method.lower() == 'cancel': event_status = 'cancelled' elif calendar_method and calendar_method.lower() == 'cancel': # So, this particular event was not cancelled. Maybe the # whole calendar was. event_status = 'cancelled' else: # Otherwise assume the event has been confirmed. event_status = 'confirmed' assert event_status in EVENT_STATUSES recur = component.get('rrule') if recur: recur = "RRULE:{}".format(recur.to_ical()) participants = [] organizer = component.get('organizer') if organizer: # Here's the problem. Gmail and Exchange define the organizer # field like this: # # ORGANIZER;CN="User";EMAIL="*****@*****.**":mailto:[email protected] # but iCloud does it like this: # ORGANIZER;CN=User;[email protected]:mailto: # [email protected] # so what we first try to get the EMAIL field, and only if # it's not present we use the MAILTO: link. if 'EMAIL' in organizer.params: organizer = organizer.params['EMAIL'] else: organizer = unicode(organizer) if organizer.startswith('mailto:'): organizer = organizer[7:] if (namespace.account.email_address == canonicalize_address( organizer)): is_owner = True else: is_owner = False attendees = component.get('attendee', []) # the iCalendar python module doesn't return a list when # there's only one attendee. Go figure. if not isinstance(attendees, list): attendees = [attendees] for attendee in attendees: email = unicode(attendee) # strip mailto: if it exists if email.lower().startswith('mailto:'): email = email[7:] try: name = attendee.params['CN'] except KeyError: name = None status_map = { 'NEEDS-ACTION': 'noreply', 'ACCEPTED': 'yes', 'DECLINED': 'no', 'TENTATIVE': 'maybe' } status = 'noreply' try: a_status = attendee.params['PARTSTAT'] status = status_map[a_status] except KeyError: pass notes = None try: guests = attendee.params['X-NUM-GUESTS'] notes = "Guests: {}".format(guests) except KeyError: pass participants.append({ 'email': email, 'name': name, 'status': status, 'notes': notes, 'guests': [] }) location = component.get('location') uid = str(component.get('uid')) event = Event(namespace=namespace, calendar=calendar, uid=uid, provider_name='ics', raw_data=component.to_ical(), title=title, description=description, location=location, reminders=str([]), recurrence=recur, start=start, end=end, busy=True, all_day=all_day, read_only=True, is_owner=is_owner, last_modified=last_modified, original_start_tz=original_start_tz, source='local', status=event_status, participants=participants) events.append(event) return events
def email_address(self, value): self._raw_address = value self._canonicalized_address = canonicalize_address(value)
def events_from_ics(namespace, calendar, ics_str): try: cal = iCalendar.from_ical(ics_str) except (ValueError, IndexError, KeyError): raise MalformedEventError() events = dict(invites=[], rsvps=[]) # See: https://tools.ietf.org/html/rfc5546#section-3.2 calendar_method = None for component in cal.walk(): if component.name == "VCALENDAR": calendar_method = component.get('method') if component.name == "VTIMEZONE": tzname = component.get('TZID') assert tzname in timezones_table,\ "Non-UTC timezone should be in table" if component.name == "VEVENT": # Make sure the times are in UTC. try: original_start = component.get('dtstart').dt original_end = component.get('dtend').dt except AttributeError: raise MalformedEventError("Event lacks start and/or end time") start = original_start end = original_end original_start_tz = None all_day = False if isinstance(start, datetime) and isinstance(end, datetime): tzid = str(original_start.tzinfo) if tzid in timezones_table: original_start_tz = timezones_table[tzid] if original_start.tzinfo is None: tzid = component.get('dtstart').params.get('TZID', None) assert tzid in timezones_table,\ "Non-UTC timezone should be in table" corresponding_tz = timezones_table[tzid] original_start_tz = corresponding_tz local_timezone = pytz.timezone(corresponding_tz) original_start = local_timezone.localize(original_start) if original_end.tzinfo is None: tzid = component.get('dtend').params.get('TZID', None) assert tzid in timezones_table,\ "Non-UTC timezone should be in table" corresponding_tz = timezones_table[tzid] local_timezone = pytz.timezone(corresponding_tz) original_end = local_timezone.localize(original_end) # Now that we have tz-aware datetimes, convert them to UTC start = original_start.astimezone(pytz.UTC) end = original_end.astimezone(pytz.UTC) elif isinstance(start, date) and isinstance(end, date): all_day = True start = arrow.get(start) end = arrow.get(end) assert isinstance(start, type(end)), "Start and end should be of "\ "the same type" # Get the last modification date. # Exchange uses DtStamp, iCloud and Gmail LAST-MODIFIED. last_modified_tstamp = component.get('dtstamp') last_modified = None if last_modified_tstamp is not None: # This is one surprising instance of Exchange doing # the right thing by giving us an UTC timestamp. Also note that # Google calendar also include the DtStamp field, probably to # be a good citizen. if last_modified_tstamp.dt.tzinfo is not None: last_modified = last_modified_tstamp.dt else: raise NotImplementedError("We don't support arcane Windows" " timezones in timestamps yet") else: # Try to look for a LAST-MODIFIED element instead. # Note: LAST-MODIFIED is always in UTC. # http://www.kanzaki.com/docs/ical/lastModified.html last_modified = component.get('last-modified').dt assert last_modified is not None, \ "Event should have a DtStamp or LAST-MODIFIED timestamp" title = None summaries = component.get('summary', []) if not isinstance(summaries, list): summaries = [summaries] if summaries != []: title = " - ".join(summaries) description = component.get('description') if description is not None: description = unicode(description) event_status = component.get('status') if event_status is not None: event_status = event_status.lower() else: # Some providers (e.g: iCloud) don't use the status field. # Instead they use the METHOD field to signal cancellations. method = component.get('method') if method and method.lower() == 'cancel': event_status = 'cancelled' elif calendar_method and calendar_method.lower() == 'cancel': # So, this particular event was not cancelled. Maybe the # whole calendar was. event_status = 'cancelled' else: # Otherwise assume the event has been confirmed. event_status = 'confirmed' assert event_status in EVENT_STATUSES recur = component.get('rrule') if recur: recur = "RRULE:{}".format(recur.to_ical()) participants = [] organizer = component.get('organizer') organizer_name = None organizer_email = None if organizer: organizer_email = unicode(organizer) if organizer_email.lower().startswith('mailto:'): organizer_email = organizer_email[7:] if 'CN' in organizer.params: organizer_name = organizer.params['CN'] owner = formataddr([organizer_name, organizer_email]) else: owner = None is_owner = False if owner is not None and (namespace.account.email_address == canonicalize_address(organizer_email)): is_owner = True attendees = component.get('attendee', []) # the iCalendar python module doesn't return a list when # there's only one attendee. Go figure. if not isinstance(attendees, list): attendees = [attendees] for attendee in attendees: email = unicode(attendee) # strip mailto: if it exists if email.lower().startswith('mailto:'): email = email[7:] try: name = attendee.params['CN'] except KeyError: name = None status_map = {'NEEDS-ACTION': 'noreply', 'ACCEPTED': 'yes', 'DECLINED': 'no', 'TENTATIVE': 'maybe'} status = 'noreply' try: a_status = attendee.params['PARTSTAT'] status = status_map[a_status] except KeyError: pass notes = None try: guests = attendee.params['X-NUM-GUESTS'] notes = u"Guests: {}".format(guests) except KeyError: pass participants.append({'email': email, 'name': name, 'status': status, 'notes': notes, 'guests': []}) location = component.get('location') uid = str(component.get('uid')) sequence_number = int(component.get('sequence', 0)) # Some services (I'm looking at you, http://www.foogi.me/) # don't follow the spec and generate icalendar files with # ridiculously big sequence numbers. Truncate them to fit in # our db. if sequence_number > 2147483647: sequence_number = 2147483647 event = Event( namespace=namespace, calendar=calendar, uid=uid, provider_name='ics', raw_data=component.to_ical(), title=title, description=description, location=location, reminders=str([]), recurrence=recur, start=start, end=end, busy=True, all_day=all_day, read_only=True, owner=owner, is_owner=is_owner, last_modified=last_modified, original_start_tz=original_start_tz, source='local', status=event_status, sequence_number=sequence_number, participants=participants) # We need to distinguish between invites/updates/cancellations # and RSVPs. if calendar_method == 'REQUEST' or calendar_method == 'CANCEL': events['invites'].append(event) elif calendar_method == 'REPLY': events['rsvps'].append(event) return events
def events_from_ics(namespace, calendar, ics_str): try: cal = iCalendar.from_ical(ics_str) except (ValueError, IndexError, KeyError): raise MalformedEventError() events = [] # See: https://tools.ietf.org/html/rfc5546#section-3.2 calendar_method = None for component in cal.walk(): if component.name == "VCALENDAR": calendar_method = component.get('method') if component.name == "VTIMEZONE": tzname = component.get('TZID') assert tzname in timezones_table,\ "Non-UTC timezone should be in table" if component.name == "VEVENT": # Make sure the times are in UTC. try: original_start = component.get('dtstart').dt original_end = component.get('dtend').dt except AttributeError: raise MalformedEventError("Event lacks start and/or end time") start = original_start end = original_end original_start_tz = None if isinstance(start, datetime) and isinstance(end, datetime): all_day = False original_start_tz = str(original_start.tzinfo) # icalendar doesn't parse Windows timezones yet # (see: https://github.com/collective/icalendar/issues/44) # so we look if the timezone isn't in our Windows-TZ # to Olson-TZ table. if original_start.tzinfo is None: tzid = component.get('dtstart').params.get('TZID', None) assert tzid in timezones_table,\ "Non-UTC timezone should be in table" corresponding_tz = timezones_table[tzid] original_start_tz = corresponding_tz local_timezone = pytz.timezone(corresponding_tz) start = local_timezone.localize(original_start) if original_end.tzinfo is None: tzid = component.get('dtend').params.get('TZID', None) assert tzid in timezones_table,\ "Non-UTC timezone should be in table" corresponding_tz = timezones_table[tzid] local_timezone = pytz.timezone(corresponding_tz) end = local_timezone.localize(original_end) elif isinstance(start, date) and isinstance(end, date): all_day = True start = arrow.get(start) end = arrow.get(end) # Get the last modification date. # Exchange uses DtStamp, iCloud and Gmail LAST-MODIFIED. last_modified_tstamp = component.get('dtstamp') last_modified = None if last_modified_tstamp is not None: # This is one surprising instance of Exchange doing # the right thing by giving us an UTC timestamp. Also note that # Google calendar also include the DtStamp field, probably to # be a good citizen. if last_modified_tstamp.dt.tzinfo is not None: last_modified = last_modified_tstamp.dt else: raise NotImplementedError("We don't support arcane Windows" " timezones in timestamps yet") else: # Try to look for a LAST-MODIFIED element instead. # Note: LAST-MODIFIED is always in UTC. # http://www.kanzaki.com/docs/ical/lastModified.html last_modified = component.get('last-modified').dt assert last_modified is not None, \ "Event should have a DtStamp or LAST-MODIFIED timestamp" title = None summaries = component.get('summary', []) if not isinstance(summaries, list): summaries = [summaries] if summaries != []: title = " - ".join(summaries) description = unicode(component.get('description')) event_status = component.get('status') if event_status is not None: event_status = event_status.lower() else: # Some providers (e.g: iCloud) don't use the status field. # Instead they use the METHOD field to signal cancellations. method = component.get('method') if method and method.lower() == 'cancel': event_status = 'cancelled' elif calendar_method and calendar_method.lower() == 'cancel': # So, this particular event was not cancelled. Maybe the # whole calendar was. event_status = 'cancelled' else: # Otherwise assume the event has been confirmed. event_status = 'confirmed' assert event_status in EVENT_STATUSES recur = component.get('rrule') if recur: recur = "RRULE:{}".format(recur.to_ical()) participants = [] organizer = component.get('organizer') if organizer: # Here's the problem. Gmail and Exchange define the organizer # field like this: # # ORGANIZER;CN="User";EMAIL="*****@*****.**":mailto:[email protected] # but iCloud does it like this: # ORGANIZER;CN=User;[email protected]:mailto: # [email protected] # so what we first try to get the EMAIL field, and only if # it's not present we use the MAILTO: link. if 'EMAIL' in organizer.params: organizer = organizer.params['EMAIL'] else: organizer = unicode(organizer) if organizer.startswith('mailto:'): organizer = organizer[7:] if (namespace.account.email_address == canonicalize_address(organizer)): is_owner = True else: is_owner = False attendees = component.get('attendee', []) # the iCalendar python module doesn't return a list when # there's only one attendee. Go figure. if not isinstance(attendees, list): attendees = [attendees] for attendee in attendees: email = unicode(attendee) # strip mailto: if it exists if email.lower().startswith('mailto:'): email = email[7:] try: name = attendee.params['CN'] except KeyError: name = None status_map = {'NEEDS-ACTION': 'noreply', 'ACCEPTED': 'yes', 'DECLINED': 'no', 'TENTATIVE': 'maybe'} status = 'noreply' try: a_status = attendee.params['PARTSTAT'] status = status_map[a_status] except KeyError: pass notes = None try: guests = attendee.params['X-NUM-GUESTS'] notes = "Guests: {}".format(guests) except KeyError: pass participants.append({'email': email, 'name': name, 'status': status, 'notes': notes, 'guests': []}) location = component.get('location') uid = str(component.get('uid')) event = Event( namespace=namespace, calendar=calendar, uid=uid, provider_name='ics', raw_data=component.to_ical(), title=title, description=description, location=location, reminders=str([]), recurrence=recur, start=start, end=end, busy=True, all_day=all_day, read_only=True, is_owner=is_owner, last_modified=last_modified, original_start_tz=original_start_tz, source='local', status=event_status, participants=participants) events.append(event) return events
def events_from_ics(namespace, calendar, ics_str): try: cal = iCalendar.from_ical(ics_str) except (ValueError, IndexError, KeyError): raise MalformedEventError() events = dict(invites=[], rsvps=[]) # See: https://tools.ietf.org/html/rfc5546#section-3.2 calendar_method = None for component in cal.walk(): if component.name == "VCALENDAR": calendar_method = component.get("method") if component.name == "VTIMEZONE": tzname = component.get("TZID") assert tzname in timezones_table, "Non-UTC timezone should be in table" if component.name == "VEVENT": # Make sure the times are in UTC. try: original_start = component.get("dtstart").dt original_end = component.get("dtend").dt except AttributeError: raise MalformedEventError("Event lacks start and/or end time") start = original_start end = original_end original_start_tz = None all_day = False if isinstance(start, datetime) and isinstance(end, datetime): tzid = str(original_start.tzinfo) if tzid in timezones_table: original_start_tz = timezones_table[tzid] if original_start.tzinfo is None: tzid = component.get("dtstart").params.get("TZID", None) assert (tzid in timezones_table ), "Non-UTC timezone should be in table" corresponding_tz = timezones_table[tzid] original_start_tz = corresponding_tz local_timezone = pytz.timezone(corresponding_tz) original_start = local_timezone.localize(original_start) if original_end.tzinfo is None: tzid = component.get("dtend").params.get("TZID", None) assert (tzid in timezones_table ), "Non-UTC timezone should be in table" corresponding_tz = timezones_table[tzid] local_timezone = pytz.timezone(corresponding_tz) original_end = local_timezone.localize(original_end) # Now that we have tz-aware datetimes, convert them to UTC start = original_start.astimezone(pytz.UTC) end = original_end.astimezone(pytz.UTC) elif isinstance(start, date) and isinstance(end, date): all_day = True start = arrow.get(start) end = arrow.get(end) assert isinstance(start, type(end)), ("Start and end should be of " "the same type") # Get the last modification date. # Exchange uses DtStamp, iCloud and Gmail LAST-MODIFIED. component_dtstamp = component.get("dtstamp") component_last_modified = component.get("last-modified") last_modified = None if component_dtstamp is not None: # This is one surprising instance of Exchange doing # the right thing by giving us an UTC timestamp. Also note that # Google calendar also include the DtStamp field, probably to # be a good citizen. if component_dtstamp.dt.tzinfo is not None: last_modified = component_dtstamp.dt else: raise NotImplementedError("We don't support arcane Windows" " timezones in timestamps yet") elif component_last_modified is not None: # Try to look for a LAST-MODIFIED element instead. # Note: LAST-MODIFIED is always in UTC. # http://www.kanzaki.com/docs/ical/lastModified.html last_modified = component_last_modified.dt title = None summaries = component.get("summary", []) if not isinstance(summaries, list): summaries = [summaries] if summaries != []: title = " - ".join(summaries) description = component.get("description") if description is not None: description = unicode(description) event_status = component.get("status") if event_status is not None: event_status = event_status.lower() else: # Some providers (e.g: iCloud) don't use the status field. # Instead they use the METHOD field to signal cancellations. method = component.get("method") if method and method.lower() == "cancel": event_status = "cancelled" elif calendar_method and calendar_method.lower() == "cancel": # So, this particular event was not cancelled. Maybe the # whole calendar was. event_status = "cancelled" else: # Otherwise assume the event has been confirmed. event_status = "confirmed" assert event_status in EVENT_STATUSES recur = component.get("rrule") if recur: recur = "RRULE:{}".format(recur.to_ical()) participants = [] organizer = component.get("organizer") organizer_name = None organizer_email = None if organizer: organizer_email = unicode(organizer) if organizer_email.lower().startswith("mailto:"): organizer_email = organizer_email[7:] if "CN" in organizer.params: organizer_name = organizer.params["CN"] owner = formataddr([organizer_name, organizer_email.lower()]) else: owner = None is_owner = False if owner is not None and ( namespace.account.email_address == canonicalize_address(organizer_email)): is_owner = True attendees = component.get("attendee", []) # the iCalendar python module doesn't return a list when # there's only one attendee. Go figure. if not isinstance(attendees, list): attendees = [attendees] for attendee in attendees: email = unicode(attendee) # strip mailto: if it exists if email.lower().startswith("mailto:"): email = email[7:] try: name = attendee.params["CN"] except KeyError: name = None status_map = { "NEEDS-ACTION": "noreply", "ACCEPTED": "yes", "DECLINED": "no", "TENTATIVE": "maybe", } status = "noreply" try: a_status = attendee.params["PARTSTAT"] status = status_map[a_status] except KeyError: pass notes = None try: guests = attendee.params["X-NUM-GUESTS"] notes = u"Guests: {}".format(guests) except KeyError: pass participants.append({ "email": email.lower(), "name": name, "status": status, "notes": notes, "guests": [], }) location = component.get("location") uid = str(component.get("uid")) sequence_number = int(component.get("sequence", 0)) # Some services (I'm looking at you, http://www.foogi.me/) # don't follow the spec and generate icalendar files with # ridiculously big sequence numbers. Truncate them to fit in # our db. if sequence_number > 2147483647: sequence_number = 2147483647 event = Event( namespace=namespace, calendar=calendar, uid=uid, provider_name="ics", raw_data=component.to_ical(), title=title, description=description, location=location, reminders=str([]), recurrence=recur, start=start, end=end, busy=True, all_day=all_day, read_only=True, owner=owner, is_owner=is_owner, last_modified=last_modified, original_start_tz=original_start_tz, source="local", status=event_status, sequence_number=sequence_number, participants=participants, ) # We need to distinguish between invites/updates/cancellations # and RSVPs. if calendar_method == "REQUEST" or calendar_method == "CANCEL": events["invites"].append(event) elif calendar_method == "REPLY": events["rsvps"].append(event) return events