def event_update_api(public_id): valid_public_id(public_id) try: event = g.db_session.query(Event).filter( Event.public_id == public_id, Event.namespace_id == g.namespace.id).one() except NoResultFound: raise NotFoundError("Couldn't find event {0}".format(public_id)) if event.read_only: raise InputError('Cannot update read_only event.') data = request.get_json(force=True) valid_event_update(data, g.namespace, g.db_session) if 'participants' in data: for p in data['participants']: if 'status' not in p: p['status'] = 'noreply' for attr in ['title', 'description', 'location', 'when', 'participants']: if attr in data: setattr(event, attr, data[attr]) g.db_session.commit() schedule_action('update_event', event, g.namespace.id, g.db_session, calendar_uid=event.calendar.uid) return g.encoder.jsonify(event)
def send_draft(account_id, draft_id, db_session): """Send a previously created draft.""" _send(account_id, draft_id, db_session) draft = db_session.query(Message).get(draft_id) # Schedule the deletion separately (we don't want to resend if sending # succeeds but deletion fails!) schedule_action('delete_draft', draft, draft.namespace.id, db_session)
def event_update_api(public_id): valid_public_id(public_id) data = request.get_json(force=True) valid_event_update(data, g.namespace, g.db_session) # Convert the data into our types where necessary # e.g. timestamps, participant_list if 'start' in data: data['start'] = datetime.utcfromtimestamp(int(data.get('start'))) if 'end' in data: data['end'] = datetime.utcfromtimestamp(int(data.get('end'))) if 'participants' in data: data['participant_list'] = [] for p in data['participants']: if 'status' not in p: p['status'] = 'noreply' data['participant_list'].append(p) del data['participants'] result = events.crud.update(g.namespace, g.db_session, public_id, data) if result is None: raise NotFoundError("Couldn't find event {0}".format(public_id)) schedule_action('update_event', result, g.namespace.id, g.db_session) return g.encoder.jsonify(result)
def test_action_scheduling(db, default_account): event = add_fake_event(db.session, default_account.namespace.id) schedule_action('create_event', event, default_account.namespace.id, db.session) db.session.commit() entry = db.session.query(ActionLog).filter( ActionLog.namespace_id == default_account.namespace.id, ActionLog.action == 'create_event').one() assert entry.discriminator == 'actionlog' assert entry.table_name == 'event' and entry.record_id == event.id assert not entry.extra_args schedule_action('delete_event', event, default_account.namespace.id, db.session, event_uid=event.uid, calendar_name=event.calendar.name, calendar_uid=event.calendar.uid) db.session.commit() entry = db.session.query(ActionLog).filter( ActionLog.namespace_id == default_account.namespace.id, ActionLog.action == 'delete_event').one() assert entry.discriminator == 'actionlog' assert entry.table_name == 'event' and entry.record_id == event.id assert entry.extra_args == \ dict(event_uid=event.uid, calendar_name=event.calendar.name, calendar_uid=event.calendar.uid)
def delete_draft(db_session, account, draft_public_id): """ Delete the draft with public_id = `draft_public_id`. """ draft = db_session.query(Message).filter( Message.public_id == draft_public_id).one() thread = draft.thread namespace = draft.namespace assert draft.is_draft # Delete remotely. schedule_action('delete_draft', draft, draft.namespace.id, db_session, inbox_uid=draft.inbox_uid, message_id_header=draft.message_id_header) db_session.delete(draft) # Delete the thread if it would now be empty. if not thread.messages: db_session.delete(thread) elif not thread.drafts: # Otherwise, remove the drafts tag from the thread if there are no more # drafts on it. thread.remove_tag(namespace.tags['drafts']) db_session.commit()
def delete_draft(db_session, account, draft): """ Delete the given draft. """ thread = draft.thread namespace = draft.namespace assert draft.is_draft # Delete remotely. schedule_action('delete_draft', draft, draft.namespace.id, db_session, inbox_uid=draft.inbox_uid, message_id_header=draft.message_id_header) db_session.delete(draft) # Delete the thread if it would now be empty. if not thread.messages: db_session.delete(thread) elif not thread.drafts: # Otherwise, remove the drafts tag from the thread if there are no more # drafts on it. thread.remove_tag(namespace.tags['drafts']) db_session.commit()
def draft_send_api(): if request.content_type == "message/rfc822": msg = create_draft_from_mime(g.namespace.account, request.data, g.db_session) validate_draft_recipients(msg) resp = send_raw_mime(g.namespace.account, g.db_session, msg) return resp data = request.get_json(force=True) draft_public_id = data.get('draft_id') if draft_public_id is not None: draft = get_draft(draft_public_id, data.get('version'), g.namespace.id, g.db_session) schedule_action('delete_draft', draft, draft.namespace.id, g.db_session, inbox_uid=draft.inbox_uid, message_id_header=draft.message_id_header) else: draft = create_draft(data, g.namespace, g.db_session, syncback=False) validate_draft_recipients(draft) resp = send_draft(g.namespace.account, draft, g.db_session) return resp
def update_message_labels(message, db_session, added_categories, removed_categories, optimistic): special_label_map = { 'inbox': '\\Inbox', 'important': '\\Important', 'all': '\\All', # STOPSHIP(emfree): verify 'trash': '\\Trash', 'spam': '\\Spam' } validate_labels(db_session, added_categories, removed_categories) added_labels = [] removed_labels = [] for category in added_categories: if category.name in special_label_map: added_labels.append(special_label_map[category.name]) elif category.name in ('drafts', 'sent'): raise InputError('The "{}" label cannot be changed'.format( category.name)) else: added_labels.append(category.display_name) for category in removed_categories: if category.name in special_label_map: removed_labels.append(special_label_map[category.name]) elif category.name in ('drafts', 'sent'): raise InputError('The "{}" label cannot be changed'.format( category.name)) else: removed_labels.append(category.display_name) if optimistic: # Optimistically update message state, # in a manner consistent with Gmail. for cat in added_categories: message.categories.add(cat) for cat in removed_categories: # Removing '\\All'/ \\Trash'/ '\\Spam' does not do anything on Gmail # i.e. does not move the message to a different folder, so don't # discard the corresponding category yet. # If one of these has been *added* too, apply_gmail_label_rules() # will do the right thing to ensure mutual exclusion. if cat.name not in ('all', 'trash', 'spam'): message.categories.discard(cat) apply_gmail_label_rules(db_session, message, added_categories, removed_categories) if removed_labels or added_labels: message.categories_changes = True if removed_labels or added_labels: schedule_action('change_labels', message, message.namespace_id, removed_labels=removed_labels, added_labels=added_labels, db_session=db_session)
def update_message_folder(message, db_session, category): # STOPSHIP(emfree): what about sent/inbox duality? if category not in message.categories: message.categories = [category] message.categories_changes = True schedule_action('move', message, message.namespace_id, db_session, destination=category.display_name)
def folder_label_update_api(public_id): category_type = g.namespace.account.category_type valid_public_id(public_id) try: category = g.db_session.query(Category).filter( Category.namespace_id == g.namespace.id, Category.public_id == public_id).one() except NoResultFound: raise InputError("Couldn't find {} {}".format( category_type, public_id)) if category.name is not None: raise InputError("Cannot modify a standard {}".format(category_type)) data = request.get_json(force=True) display_name = data.get('display_name') valid_display_name(g.namespace.id, category_type, display_name, g.db_session) current_name = category.display_name category.display_name = display_name g.db_session.flush() if category_type == 'folder': schedule_action('update_folder', category, g.namespace.id, g.db_session, old_name=current_name) else: schedule_action('update_label', category, g.namespace.id, g.db_session, old_name=current_name) # TODO[k]: Update corresponding folder/ label once syncback is successful, # rather than waiting for sync to pick it up? return g.encoder.jsonify(category)
def process_nylas_rsvps(db_session, message, account, rsvps): # The invite sending code generates invites with uids of the form # `[email protected]`. We couldn't use Event.uid for this because # it wouldn't work with Exchange (Exchange uids are of the form # 1:2323 and aren't guaranteed to be unique). new_uids = [ _cleanup_nylas_uid(event.uid) for event in rsvps if '@nylas.com' in event.uid ] # Drop uids which aren't base36 uids. new_uids = [uid for uid in new_uids if valid_base36(uid)] # Get the list of events which share a uid with those we received. # Note that we're excluding events from "Emailed events" because # we don't want to process RSVPs to invites we received. existing_events = db_session.query(Event).filter( Event.namespace_id == account.namespace.id, Event.calendar_id != account.emailed_events_calendar_id, Event.public_id.in_(new_uids)).all() existing_events_table = { event.public_id: event for event in existing_events } for event in rsvps: event_uid = _cleanup_nylas_uid(event.uid) if event_uid not in existing_events_table: # We've received an RSVP to an event we never heard about. Save it, # maybe we'll sync the invite later. event.message = message else: # This is an event we already have in the db. existing_event = existing_events_table[event_uid] # Is the current event an update? if existing_event.sequence_number == event.sequence_number: merged_participants = existing_event.\ _partial_participants_merge(event) # We have to do this mumbo-jumbo because MutableList does # not register changes to nested elements. # We could probably change MutableList to handle it (see: # https://groups.google.com/d/msg/sqlalchemy/i2SIkLwVYRA/mp2WJFaQxnQJ) # but it seems very brittle. existing_event.participants = [] for participant in merged_participants: existing_event.participants.append(participant) # We need to sync back changes to the event manually if existing_event.calendar != account.emailed_events_calendar: schedule_action('update_event', existing_event, existing_event.namespace.id, db_session, calendar_uid=existing_event.calendar.uid) db_session.flush()
def _send(account_id, draft_id, db_session): """Send the draft with id = `draft_id`.""" account = db_session.query(Account).get(account_id) try: sendmail_client = get_sendmail_client(account) except SendMailException: log.error('Send Error', message="Failed to create sendmail client.", account_id=account_id) raise try: draft = db_session.query(Message).filter( Message.id == draft_id).one() except NoResultFound: log.info('Send Error', message='NoResultFound for draft_id {0}'.format(draft_id), account_id=account_id) raise SendMailException('No draft with id {0}'.format(draft_id)) except MultipleResultsFound: log.info('Send Error', message='MultipleResultsFound for draft_id' '{0}'.format(draft_id), account_id=account_id) raise SendMailException('Multiple drafts with id {0}'.format( draft_id)) if not draft.is_draft or draft.is_sent: return recipients = Recipients(draft.to_addr, draft.cc_addr, draft.bcc_addr) if not draft.is_reply: sendmail_client.send_new(db_session, draft, recipients) else: sendmail_client.send_reply(db_session, draft, recipients) if account.provider == 'icloud': # Special case because iCloud doesn't save # sent messages. schedule_action('save_sent_email', draft, draft.namespace.id, db_session) # Update message draft.is_sent = True draft.is_draft = False draft.state = 'sent' # Update thread sent_tag = account.namespace.tags['sent'] draft_tag = account.namespace.tags['drafts'] draft.thread.apply_tag(sent_tag) # Remove the drafts tag from the thread if there are no more drafts. if not draft.thread.drafts: draft.thread.remove_tag(draft_tag) return draft
def update_message_labels(message, db_session, added_categories, removed_categories, optimistic): special_label_map = { 'inbox': '\\Inbox', 'important': '\\Important', 'all': '\\All', # STOPSHIP(emfree): verify 'trash': '\\Trash', 'spam': '\\Spam' } validate_labels(db_session, added_categories, removed_categories) added_labels = [] removed_labels = [] for category in added_categories: if category.name in special_label_map: added_labels.append(special_label_map[category.name]) elif category.name in ('drafts', 'sent'): raise InputError('The "{}" label cannot be changed'. format(category.name)) else: added_labels.append(category.display_name) for category in removed_categories: if category.name in special_label_map: removed_labels.append(special_label_map[category.name]) elif category.name in ('drafts', 'sent'): raise InputError('The "{}" label cannot be changed'. format(category.name)) else: removed_labels.append(category.display_name) if optimistic: # Optimistically update message state, # in a manner consistent with Gmail. for cat in added_categories: message.categories.add(cat) for cat in removed_categories: # Removing '\\All'/ \\Trash'/ '\\Spam' does not do anything on Gmail # i.e. does not move the message to a different folder, so don't # discard the corresponding category yet. # If one of these has been *added* too, apply_gmail_label_rules() # will do the right thing to ensure mutual exclusion. if cat.name not in ('all', 'trash', 'spam'): message.categories.discard(cat) apply_gmail_label_rules(db_session, message, added_categories, removed_categories) if removed_labels or added_labels: message.categories_changes = True if removed_labels or added_labels: schedule_action('change_labels', message, message.namespace_id, removed_labels=removed_labels, added_labels=added_labels, db_session=db_session)
def event_create_api(): g.parser.add_argument('notify_participants', type=strict_bool, location='args') args = strict_parse_args(g.parser, request.args) notify_participants = args['notify_participants'] data = request.get_json(force=True) calendar = get_calendar(data.get('calendar_id'), g.namespace, g.db_session) if calendar.read_only: raise InputError("Can't create events on read_only calendar.") valid_event(data) title = data.get('title', '') description = data.get('description') location = data.get('location') when = data.get('when') busy = data.get('busy') # client libraries can send explicit key = None automagically if busy is None: busy = True participants = data.get('participants') if participants is None: participants = [] for p in participants: if 'status' not in p: p['status'] = 'noreply' event = Event(calendar=calendar, namespace=g.namespace, uid=uuid.uuid4().hex, provider_name=g.namespace.account.provider, raw_data='', title=title, description=description, location=location, busy=busy, when=when, read_only=False, is_owner=True, participants=participants, sequence_number=0, source='local') g.db_session.add(event) g.db_session.flush() schedule_action('create_event', event, g.namespace.id, g.db_session, calendar_uid=event.calendar.uid, notify_participants=notify_participants) return g.encoder.jsonify(event)
def draft_send_api(): data = request.get_json(force=True) if data.get("draft_id") is None: if data.get("to") is None: return err(400, "Must specify either draft id + version or " "message recipients.") else: if data.get("version") is None: return err(400, "Must specify version to send") draft_public_id = data.get("draft_id") version = data.get("version") if draft_public_id is not None: try: valid_public_id(draft_public_id) draft = g.db_session.query(Message).filter(Message.public_id == draft_public_id).one() except InputError: return err(400, "Invalid public id {}".format(draft_public_id)) except NoResultFound: return err(404, "No draft found with id {}".format(draft_public_id)) if draft.namespace != g.namespace: return err(404, "No draft found with id {}".format(draft_public_id)) if draft.is_sent or not draft.is_draft: return err(400, "Message with id {} is not a draft".format(draft_public_id)) if not draft.to_addr: return err(400, "No 'to:' recipients specified") if draft.version != version: return err( 409, "Draft {0}.{1} has already been updated to version {2}".format(draft_public_id, version, draft.version), ) schedule_action("send_draft", draft, g.namespace.id, g.db_session) else: to = data.get("to") cc = data.get("cc") bcc = data.get("bcc") subject = data.get("subject") body = data.get("body") try: tags = get_tags(data.get("tags"), g.namespace.id, g.db_session) files = get_attachments(data.get("files"), g.namespace.id, g.db_session) replyto_thread = get_thread(data.get("reply_to_thread"), g.namespace.id, g.db_session) except InputError as e: return err(404, e.message) draft = sendmail.create_draft( g.db_session, g.namespace.account, to, subject, body, files, cc, bcc, tags, replyto_thread, syncback=False ) schedule_action("send_directly", draft, g.namespace.id, g.db_session) draft.state = "sending" return g.encoder.jsonify(draft)
def update_message_flags(message, db_session, unread=None, starred=None): if unread is not None and unread == message.is_read: message.is_read = not unread schedule_action('mark_unread', message, message.namespace_id, db_session, unread=unread) if starred is not None and starred != message.is_starred: message.is_starred = starred schedule_action('mark_starred', message, message.namespace_id, db_session, starred=starred)
def event_create_api(): g.parser.add_argument('notify_participants', type=strict_bool, location='args') args = strict_parse_args(g.parser, request.args) notify_participants = args['notify_participants'] data = request.get_json(force=True) calendar = get_calendar(data.get('calendar_id'), g.namespace, g.db_session) if calendar.read_only: raise InputError("Can't create events on read_only calendar.") valid_event(data) title = data.get('title', '') description = data.get('description') location = data.get('location') when = data.get('when') busy = data.get('busy') # client libraries can send explicit key = None automagically if busy is None: busy = True participants = data.get('participants') if participants is None: participants = [] for p in participants: if 'status' not in p: p['status'] = 'noreply' event = Event( calendar=calendar, namespace=g.namespace, uid=uuid.uuid4().hex, provider_name=g.namespace.account.provider, raw_data='', title=title, description=description, location=location, busy=busy, when=when, read_only=False, is_owner=True, participants=participants, sequence_number=0, source='local') g.db_session.add(event) g.db_session.flush() schedule_action('create_event', event, g.namespace.id, g.db_session, calendar_uid=event.calendar.uid, notify_participants=notify_participants) return g.encoder.jsonify(event)
def draft_send_api(): data = request.get_json(force=True) if data.get('draft_id') is None and data.get('to') is None: return err(400, 'Must specify either draft id or message recipients.') draft_public_id = data.get('draft_id') if draft_public_id is not None: try: valid_public_id(draft_public_id) draft = g.db_session.query(Message).filter( Message.public_id == draft_public_id).one() except InputError: return err(400, 'Invalid public id {}'.format(draft_public_id)) except NoResultFound: return err(404, 'No draft found with id {}'. format(draft_public_id)) if draft.namespace != g.namespace: return err(404, 'No draft found with id {}'. format(draft_public_id)) if draft.is_sent or not draft.is_draft: return err(400, 'Message with id {} is not a draft'. format(draft_public_id)) if not draft.to_addr: return err(400, "No 'to:' recipients specified") if not draft.is_latest: return err(409, 'Draft {} has already been updated to {}'.format( draft_public_id, g.encoder.cereal(draft.most_recent_revision))) schedule_action('send_draft', draft, g.namespace.id, g.db_session) else: to = data.get('to') cc = data.get('cc') bcc = data.get('bcc') subject = data.get('subject') body = data.get('body') try: tags = get_tags(data.get('tags'), g.namespace.id, g.db_session) files = get_attachments(data.get('files'), g.namespace.id, g.db_session) replyto_thread = get_thread(data.get('reply_to_thread'), g.namespace.id, g.db_session) except InputError as e: return err(404, e.message) draft = sendmail.create_draft(g.db_session, g.namespace.account, to, subject, body, files, cc, bcc, tags, replyto_thread, syncback=False) schedule_action('send_directly', draft, g.namespace.id, g.db_session) draft.state = 'sending' return g.encoder.jsonify(draft)
def delete_draft(db_session, account, draft_public_id): """ Delete the draft with public_id = `draft_public_id`. """ draft = db_session.query(SpoolMessage).filter(SpoolMessage.public_id == draft_public_id).one() assert draft.is_draft # Delete locally, make sure to delete all previous versions of this draft # present locally too. _delete_draft_versions(db_session, draft.id) # Delete remotely. schedule_action("delete_draft", draft, draft.namespace.id, db_session)
def process_nylas_rsvps(db_session, message, account, rsvps): # The invite sending code generates invites with uids of the form # `[email protected]`. We couldn't use Event.uid for this because # it wouldn't work with Exchange (Exchange uids are of the form # 1:2323 and aren't guaranteed to be unique). new_uids = [_cleanup_nylas_uid(event.uid) for event in rsvps if '@nylas.com' in event.uid] # Drop uids which aren't base36 uids. new_uids = [uid for uid in new_uids if valid_base36(uid)] # Get the list of events which share a uid with those we received. # Note that we're excluding events from "Emailed events" because # we don't want to process RSVPs to invites we received. existing_events = db_session.query(Event).filter( Event.namespace_id == account.namespace.id, Event.calendar_id != account.emailed_events_calendar_id, Event.public_id.in_(new_uids)).all() existing_events_table = {event.public_id: event for event in existing_events} for event in rsvps: event_uid = _cleanup_nylas_uid(event.uid) if event_uid not in existing_events_table: # We've received an RSVP to an event we never heard about. Save it, # maybe we'll sync the invite later. event.message = message else: # This is an event we already have in the db. existing_event = existing_events_table[event_uid] # Is the current event an update? if existing_event.sequence_number == event.sequence_number: merged_participants = existing_event.\ _partial_participants_merge(event) # We have to do this mumbo-jumbo because MutableList does # not register changes to nested elements. # We could probably change MutableList to handle it (see: # https://groups.google.com/d/msg/sqlalchemy/i2SIkLwVYRA/mp2WJFaQxnQJ) # but it seems very brittle. existing_event.participants = [] for participant in merged_participants: existing_event.participants.append(participant) # We need to sync back changes to the event manually if existing_event.calendar != account.emailed_events_calendar: schedule_action('update_event', existing_event, existing_event.namespace.id, db_session, calendar_uid=existing_event.calendar.uid) db_session.flush()
def delete_draft(db_session, account, draft_public_id): """ Delete the draft with public_id = `draft_public_id`. """ draft = db_session.query(Message).filter( Message.public_id == draft_public_id).one() assert draft.is_draft # Delete locally, make sure to delete all previous versions of this draft # present locally too. _delete_draft_versions(db_session, draft.id) # Delete remotely. schedule_action('delete_draft', draft, draft.namespace.id, db_session)
def draft_send_api(): data = request.get_json(force=True) if data.get('draft_id') is None and data.get('to') is None: return err(400, 'Must specify either draft id or message recipients.') draft_public_id = data.get('draft_id') if draft_public_id is not None: try: draft = g.db_session.query(Message).filter( Message.public_id == draft_public_id).one() except NoResultFound: return err(404, 'No draft found with id {}'. format(draft_public_id)) if draft.namespace != g.namespace: return err(404, 'No draft found with id {}'. format(draft_public_id)) if draft.is_sent or not draft.is_draft: return err(400, 'Message with id {} is not a draft'. format(draft_public_id)) if not draft.to_addr: return err(400, "No 'to:' recipients specified") if not draft.is_latest: return err(409, 'Draft {} has already been updated to {}'.format( draft_public_id, g.encoder.cereal(draft.most_recent_revision))) schedule_action('send_draft', draft, g.namespace.id, g.db_session) else: to = data.get('to') cc = data.get('cc') bcc = data.get('bcc') subject = data.get('subject') body = data.get('body') try: tags = get_tags(data.get('tags'), g.namespace.id, g.db_session) files = get_attachments(data.get('files'), g.namespace.id, g.db_session) replyto_thread = get_thread(data.get('reply_to_thread'), g.namespace.id, g.db_session) except InputError as e: return err(404, e.message) draft = sendmail.create_draft(g.db_session, g.namespace.account, to, subject, body, files, cc, bcc, tags, replyto_thread, syncback=False) schedule_action('send_directly', draft, g.namespace.id, g.db_session) draft.state = 'sending' return g.encoder.jsonify(draft)
def update_message_folder(message, db_session, category, optimistic): # STOPSHIP(emfree): what about sent/inbox duality? if optimistic: message.categories = [category] message.categories_changes = True schedule_action( "move", message, message.namespace_id, db_session, destination=category.display_name, )
def update_message_labels(message, db_session, label_public_ids): categories = set() for id_ in label_public_ids: try: category = db_session.query(Category).filter( Category.namespace_id == message.namespace_id, Category.public_id == id_).one() categories.add(category) except NoResultFound: raise InputError(u'Label {} does not exist'.format(id_)) added_categories = categories - set(message.categories) removed_categories = set(message.categories) - categories added_labels = [] removed_labels = [] special_label_map = { 'inbox': '\\Inbox', 'important': '\\Important', 'all': '\\All', # STOPSHIP(emfree): verify 'trash': '\\Trash', 'spam': '\\Spam' } for category in added_categories: if category.name in special_label_map: added_labels.append(special_label_map[category.name]) elif category.name in ('drafts', 'sent'): raise InputError('The "{}" label cannot be changed'.format( category.name)) else: added_labels.append(category.display_name) for category in removed_categories: if category.name in special_label_map: removed_labels.append(special_label_map[category.name]) elif category.name in ('drafts', 'sent'): raise InputError('The "{}" label cannot be changed'.format( category.name)) else: removed_labels.append(category.display_name) # Optimistically update message state. message.categories = categories if removed_labels or added_labels: schedule_action('change_labels', message, message.namespace_id, removed_labels=removed_labels, added_labels=added_labels, db_session=db_session)
def update_message_folder(message, db_session, folder_public_id): try: category = db_session.query(Category).filter( Category.namespace_id == message.namespace_id, Category.public_id == folder_public_id).one() except NoResultFound: raise InputError(u'Folder {} does not exist'. format(folder_public_id)) # STOPSHIP(emfree): what about sent/inbox duality? if category not in message.categories: message.categories = [category] schedule_action('move', message, message.namespace_id, db_session, destination=category.display_name)
def event_create_api(): # Handle ical uploads if request.headers['content-type'] == 'text/calendar': ics_str = request.data new_events = events.crud.create_from_ics(g.namespace, g.db_session, ics_str) if not new_events: return err(400, "Couldn't parse .ics file.") return g.encoder.jsonify(new_events) data = request.get_json(force=True) try: calendar = get_calendar(data.get('calendar_id'), g.namespace, g.db_session) except InputError as e: return err(404, e.message) if calendar.read_only: return err(400, "Can't create events on read_only calendar.") try: valid_event(data) except InputError as e: return err(404, e.message) title = data.get('title', '') description = data.get('description') location = data.get('location') reminders = data.get('reminders') recurrence = data.get('recurrence') when = data.get('when') participants = data.get('participants', []) for p in participants: if 'status' not in p: p['status'] = 'noreply' new_event = events.crud.create(g.namespace, g.db_session, calendar, title, description, location, reminders, recurrence, when, participants) schedule_action('create_event', new_event, g.namespace.id, g.db_session) return g.encoder.jsonify(new_event)
def test_failed_event_creation(db, patched_syncback_task, default_account, event): schedule_action("create_event", event, default_account.namespace.id, db.session) schedule_action("update_event", event, default_account.namespace.id, db.session) schedule_action("update_event", event, default_account.namespace.id, db.session) schedule_action("delete_event", event, default_account.namespace.id, db.session) db.session.commit() NUM_WORKERS = 2 service = SyncbackService( syncback_id=0, process_number=0, total_processes=NUM_WORKERS, num_workers=NUM_WORKERS, ) service._restart_workers() service._process_log() while not service.task_queue.empty(): gevent.sleep(0.1) # This has to be a separate while-loop because there's a brief moment where # the task queue is empty, but num_idle_workers hasn't been updated yet. # On slower systems, we might need to sleep a bit between the while-loops. while service.num_idle_workers != NUM_WORKERS: gevent.sleep(0.1) q = db.session.query(ActionLog).filter_by(record_id=event.id).all() assert all(a.status == "failed" for a in q)
def event_create_api(): # Handle ical uploads if request.headers.get('content-type') == 'text/calendar': ics_str = request.data new_events = events.crud.create_from_ics(g.namespace, g.db_session, ics_str) if not new_events: return err(400, "Couldn't parse .ics file.") return g.encoder.jsonify(new_events) data = request.get_json(force=True) try: calendar = get_calendar(data.get('calendar_id'), g.namespace, g.db_session) except InputError as e: return err(404, str(e)) if calendar.read_only: return err(400, "Can't create events on read_only calendar.") try: valid_event(data) except InputError as e: return err(404, str(e)) title = data.get('title', '') description = data.get('description') location = data.get('location') reminders = data.get('reminders') recurrence = data.get('recurrence') when = data.get('when') participants = data.get('participants', []) for p in participants: if 'status' not in p: p['status'] = 'noreply' new_event = events.crud.create(g.namespace, g.db_session, calendar, title, description, location, reminders, recurrence, when, participants) schedule_action('create_event', new_event, g.namespace.id, g.db_session) return g.encoder.jsonify(new_event)
def update_message_labels(message, db_session, label_public_ids): categories = set() for id_ in label_public_ids: try: category = db_session.query(Category).filter( Category.namespace_id == message.namespace_id, Category.public_id == id_).one() categories.add(category) except NoResultFound: raise InputError(u'Label {} does not exist'.format(id_)) added_categories = categories - set(message.categories) removed_categories = set(message.categories) - categories added_labels = [] removed_labels = [] special_label_map = { 'inbox': '\\Inbox', 'important': '\\Important', 'all': '\\All', # STOPSHIP(emfree): verify 'trash': '\\Trash', 'spam': '\\Spam' } for category in added_categories: if category.name in special_label_map: added_labels.append(special_label_map[category.name]) elif category.name in ('drafts', 'sent'): raise InputError('The "{}" label cannot be changed'. format(category.name)) else: added_labels.append(category.display_name) for category in removed_categories: if category.name in special_label_map: removed_labels.append(special_label_map[category.name]) elif category.name in ('drafts', 'sent'): raise InputError('The "{}" label cannot be changed'. format(category.name)) else: removed_labels.append(category.display_name) # Optimistically update message state. message.categories = categories if removed_labels or added_labels: schedule_action('change_labels', message, message.namespace_id, removed_labels=removed_labels, added_labels=added_labels, db_session=db_session)
def update_message_folder(message, db_session, folder_public_id): try: category = db_session.query(Category).filter( Category.namespace_id == message.namespace_id, Category.public_id == folder_public_id).one() except NoResultFound: raise InputError(u'Folder {} does not exist'.format(folder_public_id)) # STOPSHIP(emfree): what about sent/inbox duality? if category not in message.categories: message.categories = [category] schedule_action('move', message, message.namespace_id, db_session, destination=category.display_name)
def update_message_flags(message, db_session, optimistic, unread=None, starred=None): if unread is not None: if optimistic: message.is_read = not unread schedule_action( "mark_unread", message, message.namespace_id, db_session, unread=unread ) if starred is not None: if optimistic: message.is_starred = starred schedule_action( "mark_starred", message, message.namespace_id, db_session, starred=starred )
def send_draft(account, draft, db_session, schedule_remote_delete): """Send the draft with id = `draft_id`.""" try: sendmail_client = get_sendmail_client(account) sendmail_client.send(draft) except SendMailException as exc: kwargs = {} if exc.failures: kwargs['failures'] = exc.failures if exc.server_error: kwargs['server_error'] = exc.server_error return err(exc.http_code, exc.message, **kwargs) # We want to return success to the API client if the message was sent, even # if there are errors in post-send updating. Otherwise the client may think # the send has failed. So wrap the rest of the work in try/except. try: if account.provider == 'icloud': # Special case because iCloud doesn't save sent messages. schedule_action('save_sent_email', draft, draft.namespace.id, db_session) if schedule_remote_delete: schedule_action('delete_draft', draft, draft.namespace.id, db_session, inbox_uid=draft.inbox_uid, message_id_header=draft.message_id_header) # Update message draft.is_sent = True draft.is_draft = False draft.received_date = datetime.utcnow() # Update thread sent_tag = account.namespace.tags['sent'] draft_tag = account.namespace.tags['drafts'] thread = draft.thread thread.apply_tag(sent_tag) # Remove the drafts tag from the thread if there are no more drafts. if not draft.thread.drafts: thread.remove_tag(draft_tag) thread.update_from_message(None, draft) except Exception as e: log.error('Error in post-send processing', error=e, exc_info=True) return APIEncoder().jsonify(draft)
def event_update_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( Event.public_id == public_id, Event.namespace_id == g.namespace.id).one() except NoResultFound: raise NotFoundError("Couldn't find event {0}".format(public_id)) if event.read_only: raise InputError('Cannot update read_only event.') if (isinstance(event, RecurringEvent) or isinstance(event, RecurringEventOverride)): raise InputError('Cannot update a recurring event yet.') data = request.get_json(force=True) valid_event_update(data, g.namespace, g.db_session) if 'participants' in data: for p in data['participants']: if 'status' not in p: p['status'] = 'noreply' # Don't update an event if we don't need to. if noop_event_update(event, data): return g.encoder.jsonify(event) for attr in ['title', 'description', 'location', 'when', 'participants']: if attr in data: setattr(event, attr, data[attr]) event.sequence_number += 1 g.db_session.commit() schedule_action('update_event', event, g.namespace.id, g.db_session, calendar_uid=event.calendar.uid, notify_participants=notify_participants) return g.encoder.jsonify(event)
def delete_draft(db_session, account, draft): """ Delete the given draft. """ thread = draft.thread assert draft.is_draft # Delete remotely. schedule_action('delete_draft', draft, draft.namespace.id, db_session, inbox_uid=draft.inbox_uid, message_id_header=draft.message_id_header) db_session.delete(draft) # Delete the thread if it would now be empty. if not thread.messages: db_session.delete(thread) db_session.commit()
def schedule_test_action(db_session, account): from inbox.models.category import Category category_type = 'label' if account.provider == 'gmail' else 'folder' category = Category.find_or_create( db_session, account.namespace.id, name=None, display_name='{}-{}'.format(account.id, random.randint(1, 356)), type_=category_type) db_session.flush() if category_type == 'folder': schedule_action('create_folder', category, account.namespace.id, db_session) else: schedule_action('create_label', category, account.namespace.id, db_session) db_session.commit()
def update_draft( db_session, account, parent_draft, to=None, subject=None, body=None, blocks=None, cc=None, bcc=None, tags=None ): """ Update draft. To maintain our messages are immutable invariant, we create a new draft message object. Returns ------- SpoolMessage The new draft message object. Notes ----- Messages, including draft messages, are immutable in Inbox. So to update a draft, we create a new draft message object and return its public_id (which is different than the original's). """ to_addr = _parse_recipients(to) if to else parent_draft.to_addr cc_addr = _parse_recipients(cc) if cc else parent_draft.cc_addr bcc_addr = _parse_recipients(bcc) if bcc else parent_draft.bcc_addr subject = subject or parent_draft.subject body = body or parent_draft.sanitized_body blocks = blocks or [p for p in parent_draft.parts if p.is_attachment] new_draft = create_and_save_draft( db_session, account, to_addr, subject, body, blocks, cc_addr, bcc_addr, tags, parent_draft.thread, parent_draft.is_reply, parent_draft, ) schedule_action("delete_draft", parent_draft, parent_draft.namespace.id, db_session) return new_draft
def delete_draft(db_session, account, draft_public_id): """ Delete the draft with public_id = `draft_public_id`. """ draft = db_session.query(Message).filter( Message.public_id == draft_public_id).one() assert draft.is_draft db_session.delete(draft) # Remove the drafts tag from the thread if there are no more drafts. if not draft.thread.drafts: draft.thread.remove_tag(draft.namespace.tags['drafts']) db_session.commit() # Delete remotely. schedule_action('delete_draft', draft, draft.namespace.id, db_session, inbox_uid=draft.inbox_uid)
def event_delete_api(public_id): valid_public_id(public_id) try: event = g.db_session.query(Event).filter_by( public_id=public_id, namespace_id=g.namespace.id). \ options(subqueryload(Event.calendar)).one() except NoResultFound: raise NotFoundError("Couldn't find event {0}".format(public_id)) if event.calendar.read_only: raise NotFoundError('Cannot delete event {} from read_only ' 'calendar.'.format(public_id)) schedule_action('delete_event', event, g.namespace.id, g.db_session, event_uid=event.uid, calendar_name=event.calendar.name) events.crud.delete(g.namespace, g.db_session, public_id) return g.encoder.jsonify(None)
def update_draft(db_session, account, parent_draft, to=None, subject=None, body=None, blocks=None, cc=None, bcc=None, tags=None): """ Update draft. To maintain our messages are immutable invariant, we create a new draft message object. Returns ------- Message The new draft message object. Notes ----- Messages, including draft messages, are immutable in Inbox. So to update a draft, we create a new draft message object and return its public_id (which is different than the original's). """ to_addr = _parse_recipients(to) if to else parent_draft.to_addr cc_addr = _parse_recipients(cc) if cc else parent_draft.cc_addr bcc_addr = _parse_recipients(bcc) if bcc else parent_draft.bcc_addr subject = subject or parent_draft.subject body = body or parent_draft.sanitized_body blocks = blocks or [p for p in parent_draft.parts if p.is_attachment] new_draft = create_and_save_draft(db_session, account, to_addr, subject, body, blocks, cc_addr, bcc_addr, tags, parent_draft.thread, parent_draft.is_reply, parent_draft) schedule_action('delete_draft', parent_draft, parent_draft.namespace.id, db_session) return new_draft
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 event_delete_api(public_id): 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)) 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) g.db_session.delete(event) g.db_session.commit() return g.encoder.jsonify(None)
def folders_labels_create_api(): category_type = g.namespace.account.category_type data = request.get_json(force=True) display_name = data.get('display_name') valid_display_name(g.namespace.id, category_type, display_name, g.db_session) category = Category.find_or_create(g.db_session, g.namespace.id, name=None, display_name=display_name, type_=category_type) g.db_session.flush() if category_type == 'folder': schedule_action('create_folder', category, g.namespace.id, g.db_session) else: schedule_action('create_label', category, g.namespace.id, g.db_session) return g.encoder.jsonify(category)
def event_create_api(): data = request.get_json(force=True) calendar = get_calendar(data.get('calendar_id'), g.namespace, g.db_session) if calendar.read_only: raise InputError("Can't create events on read_only calendar.") valid_event(data) title = data.get('title', '') description = data.get('description') location = data.get('location') when = data.get('when') busy = data.get('busy', True) participants = data.get('participants', []) for p in participants: if 'status' not in p: p['status'] = 'noreply' event = Event(calendar=calendar, namespace=g.namespace, uid=uuid.uuid4().hex, provider_name=g.namespace.account.provider, raw_data='', title=title, description=description, location=location, busy=busy, when=when, read_only=False, is_owner=True, participants=participants, source='local') g.db_session.add(event) g.db_session.flush() schedule_action('create_event', event, g.namespace.id, g.db_session, calendar_uid=event.calendar.uid) return g.encoder.jsonify(event)
def event_create_api(): data = request.get_json(force=True) calendar = get_calendar(data.get('calendar_id'), g.namespace, g.db_session) if calendar.read_only: raise InputError("Can't create events on read_only calendar.") valid_event(data) title = data.get('title', '') description = data.get('description') location = data.get('location') when = data.get('when') busy = data.get('busy', True) participants = data.get('participants', []) for p in participants: if 'status' not in p: p['status'] = 'noreply' event = Event( calendar=calendar, namespace=g.namespace, uid=uuid.uuid4().hex, provider_name=g.namespace.account.provider, raw_data='', title=title, description=description, location=location, busy=busy, when=when, read_only=False, is_owner=True, participants=participants, source='local') g.db_session.add(event) g.db_session.flush() schedule_action('create_event', event, g.namespace.id, g.db_session, calendar_uid=event.calendar.uid) return g.encoder.jsonify(event)
def event_delete_api(public_id): try: valid_public_id(public_id) event = g.db_session.query(Event).filter_by( public_id=public_id). \ options(subqueryload(Event.calendar)).one() except InputError: return err(400, 'Invalid event id {}'.format(public_id)) except NoResultFound: return err(404, 'No event found with public_id {}'. format(public_id)) if event.namespace != g.namespace: return err(404, 'No event found with public_id {}'. format(public_id)) if event.calendar.read_only: return err(404, 'Cannot delete event with public_id {} from ' ' read_only calendar.'.format(public_id)) result = events.crud.delete(g.namespace, g.db_session, public_id) schedule_action('delete_event', event, g.namespace.id, g.db_session) return g.encoder.jsonify(result)
def update_message_labels(message, db_session, added_categories, removed_categories): special_label_map = { 'inbox': '\\Inbox', 'important': '\\Important', 'all': '\\All', # STOPSHIP(emfree): verify 'trash': '\\Trash', 'spam': '\\Spam' } added_labels = [] removed_labels = [] for category in added_categories: if category.name in special_label_map: added_labels.append(special_label_map[category.name]) elif category.name in ('drafts', 'sent'): raise InputError('The "{}" label cannot be changed'. format(category.name)) else: added_labels.append(category.display_name) for category in removed_categories: if category.name in special_label_map: removed_labels.append(special_label_map[category.name]) elif category.name in ('drafts', 'sent'): raise InputError('The "{}" label cannot be changed'. format(category.name)) else: removed_labels.append(category.display_name) # Optimistically update message state. for cat in added_categories: message.categories.add(cat) for cat in removed_categories: message.categories.discard(cat) if removed_labels or added_labels: message.categories_changes = True schedule_action('change_labels', message, message.namespace_id, removed_labels=removed_labels, added_labels=added_labels, db_session=db_session)