def create_event(account_id, event_id, extra_args): with session_scope(account_id) as db_session: account = db_session.query(Account).get(account_id) event = db_session.query(Event).get(event_id) remote_create_event = module_registry[account.provider]. \ remote_create_event remote_create_event(account, event, db_session, extra_args) notify_participants = extra_args.get('notify_participants', False) cancelled_participants = extra_args.get('cancelled_participants', []) # Do we need to send an RSVP message? # We use gmail's sendNotification API for google accounts. # but we need create and send an iCalendar invite ourselves # for non-gmail accounts. if notify_participants and account.provider != 'gmail': ical_file = generate_icalendar_invite(event).to_ical() send_invite(ical_file, event, account, invite_type='request') if cancelled_participants != []: # Some people got removed from the event. Send them a # cancellation email. event.status = 'cancelled' event.participants = cancelled_participants ical_file = generate_icalendar_invite(event, invite_type='cancel').to_ical() send_invite(ical_file, event, account, invite_type='cancel')
def test_invite_generation(event, default_account): from inbox.events.ical import generate_icalendar_invite event.sequence_number = 1 event.participants = [{'email': '*****@*****.**'}, {'email': '*****@*****.**'}] cal = generate_icalendar_invite(event) assert cal['method'] == 'REQUEST' for component in cal.walk(): if component.name == "VEVENT": assert component.get('summary') == event.title assert int(component.get('sequence')) == event.sequence_number assert component.get('location') == event.location 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:] assert email in ['*****@*****.**', '*****@*****.**']
def delete_event(account_id, event_id, extra_args): with session_scope(account_id) as db_session: account = db_session.query(Account).get(account_id) event = db_session.query(Event).get(event_id) notify_participants = extra_args.get('notify_participants', False) remote_delete_event = module_registry[account.provider]. \ remote_delete_event event_uid = extra_args.pop('event_uid', None) calendar_name = extra_args.pop('calendar_name', None) # The calendar_uid argument is required for some providers, like EAS. calendar_uid = extra_args.pop('calendar_uid', None) if event.calendar == account.emailed_events_calendar: return remote_delete_event(account, event_uid, calendar_name, calendar_uid, db_session, extra_args) # Finally, update the event. event.sequence_number += 1 event.status = 'cancelled' db_session.commit() if notify_participants and account.provider != 'gmail': ical_file = generate_icalendar_invite(event, invite_type='cancel').to_ical() send_invite(ical_file, event, account, invite_type='cancel')
def update_event(account_id, event_id, extra_args): with session_scope(account_id) as db_session: account = db_session.query(Account).get(account_id) event = db_session.query(Event).get(event_id) # Update our copy of the event before sending it. if 'event_data' in extra_args: data = extra_args['event_data'] for attr in Event.API_MODIFIABLE_FIELDS: if attr in extra_args['event_data']: setattr(event, attr, data[attr]) event.sequence_number += 1 # It doesn't make sense to update or delete an event we imported from # an iCalendar file. if event.calendar == account.emailed_events_calendar: return remote_update_event = module_registry[account.provider]. \ remote_update_event remote_update_event(account, event, db_session, extra_args) notify_participants = extra_args.get('notify_participants', False) if notify_participants and account.provider != 'gmail': ical_file = generate_icalendar_invite(event).to_ical() send_invite(ical_file, event, account, invite_type='update') db_session.commit()
def test_invite_generation(event, default_account): from inbox.events.ical import generate_icalendar_invite event.sequence_number = 1 event.participants = [{ "email": "*****@*****.**" }, { "email": "*****@*****.**" }] cal = generate_icalendar_invite(event) assert cal["method"] == "REQUEST" for component in cal.walk(): if component.name == "VEVENT": assert component.get("summary") == event.title assert int(component.get("sequence")) == event.sequence_number assert component.get("location") == event.location 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 = str(attendee) # strip mailto: if it exists if email.lower().startswith("mailto:"): email = email[7:] assert email in ["*****@*****.**", "*****@*****.**"]
def update_event(account_id, event_id, db_session, extra_args): account = db_session.query(Account).get(account_id) event = db_session.query(Event).get(event_id) remote_update_event = module_registry[account.provider].remote_update_event remote_update_event(account, event, db_session, extra_args) notify_participants = extra_args.get('notify_participants', False) if notify_participants and account.provider != 'gmail': ical_file = generate_icalendar_invite(event).to_ical() send_invite(ical_file, event, account, invite_type='update')
def create_event(account_id, event_id, db_session, extra_args): account = db_session.query(Account).get(account_id) event = db_session.query(Event).get(event_id) remote_create_event = module_registry[account.provider].remote_create_event remote_create_event(account, event, db_session, extra_args) notify_participants = extra_args.get('notify_participants', False) # Do we need to send an RSVP message? # We use gmail's sendNotification API for google accounts. # but we need create and send an iCalendar invite ourselves # for non-gmail accounts. if notify_participants and account.provider != 'gmail': ical_file = generate_icalendar_invite(event).to_ical() send_invite(ical_file, event, account, invite_type='request')
def update_event(account_id, event_id, db_session, extra_args): account = db_session.query(Account).get(account_id) event = db_session.query(Event).get(event_id) # It doesn't make sense to update or delete an event we imported from # an iCalendar file. if event.calendar == account.emailed_events_calendar: return remote_update_event = module_registry[account.provider].remote_update_event remote_update_event(account, event, db_session, extra_args) notify_participants = extra_args.get('notify_participants', False) if notify_participants and account.provider != 'gmail': ical_file = generate_icalendar_invite(event).to_ical() send_invite(ical_file, event, account, invite_type='update')
def event_delete_api(public_id): g.parser.add_argument('notify_participants', type=strict_bool, location='args') args = strict_parse_args(g.parser, request.args) notify_participants = args['notify_participants'] valid_public_id(public_id) try: event = g.db_session.query(Event).filter_by( public_id=public_id, namespace_id=g.namespace.id).one() except NoResultFound: raise NotFoundError("Couldn't find event {0}".format(public_id)) if event.calendar.read_only: raise InputError( 'Cannot delete event {} from read_only calendar.'.format( public_id)) # Set the local event status to 'cancelled' rather than deleting it, # in order to be consistent with how we sync deleted events from the # remote, and consequently return them through the events, delta sync APIs event.sequence_number += 1 event.status = 'cancelled' g.db_session.commit() account = g.namespace.account # FIXME @karim: do this in the syncback thread instead. if notify_participants and account.provider != 'gmail': ical_file = generate_icalendar_invite(event, invite_type='cancel').to_ical() send_invite(ical_file, event, account, invite_type='cancel') schedule_action('delete_event', event, g.namespace.id, g.db_session, event_uid=event.uid, calendar_name=event.calendar.name, calendar_uid=event.calendar.uid, notify_participants=notify_participants) return g.encoder.jsonify(None)
def create_email(from_name, from_email, reply_to, inbox_uid, to_addr, cc_addr, bcc_addr, subject, html, in_reply_to, references, attachments, event=None): """ Creates a MIME email message (both body and sets the needed headers). Parameters ---------- from_name: string The name aka phrase of the sender. from_email: string The sender's email address. to_addr, cc_addr, bcc_addr: list of pairs (name, email_address), or None Message recipients. reply_to: tuple or None Indicates the mailbox in (name, email_address) format to which the author of the message suggests that replies be sent. subject : string a utf-8 encoded string html : string a utf-8 encoded string in_reply_to: string or None If this message is a reply, the Message-Id of the message being replied to. references: list or None If this message is a reply, the Message-Ids of prior messages in the thread. attachments: list of dicts, optional a list of dicts(filename, data, content_type) """ html = html if html else '' plaintext = html2text(html) # Create a multipart/alternative message msg = mime.create.multipart('alternative') msg.append(mime.create.text('plain', plaintext), mime.create.text('html', html)) if event: ical_txt = generate_icalendar_invite(event).to_ical() msg.append( mime.create.text('calendar; method=REQUEST', ical_txt, charset='utf8')) # Create an outer multipart/mixed message if attachments: text_msg = msg msg = mime.create.multipart('mixed') # The first part is the multipart/alternative text part msg.append(text_msg) # The subsequent parts are the attachment parts for a in attachments: # Disposition should be inline if we add Content-ID msg.append( mime.create.attachment(a['content_type'], a['data'], filename=a['filename'], disposition='attachment')) msg.headers['Subject'] = subject if subject else '' # Gmail sets the From: header to the default sending account. We can # however set our own custom phrase i.e. the name that appears next to the # email address (useful if the user has multiple aliases and wants to # specify which to send as), see: http://lee-phillips.org/gmailRewriting/ # For other providers, we simply use name = '' from_addr = address.EmailAddress(from_name, from_email) msg.headers['From'] = from_addr.full_spec() # Need to set these headers so recipients know we sent the email to them # TODO(emfree): should these really be unicode? if to_addr: full_to_specs = [ _get_full_spec_without_validation(name, spec) for name, spec in to_addr ] msg.headers['To'] = u', '.join(full_to_specs) if cc_addr: full_cc_specs = [ _get_full_spec_without_validation(name, spec) for name, spec in cc_addr ] msg.headers['Cc'] = u', '.join(full_cc_specs) if bcc_addr: full_bcc_specs = [ _get_full_spec_without_validation(name, spec) for name, spec in bcc_addr ] msg.headers['Bcc'] = u', '.join(full_bcc_specs) if reply_to: # reply_to is only ever a list with one element msg.headers['Reply-To'] = _get_full_spec_without_validation( reply_to[0][0], reply_to[0][1]) add_inbox_headers(msg, inbox_uid) if in_reply_to: msg.headers['In-Reply-To'] = in_reply_to if references: msg.headers['References'] = '\t'.join(references) rfcmsg = _rfc_transform(msg) return rfcmsg