diff options
Diffstat (limited to 'inboxapp-service/app')
-rw-r--r-- | inboxapp-service/app/factory/__init__.py | 25 | ||||
-rw-r--r-- | inboxapp-service/app/inboxapp/__init__.py | 3 | ||||
-rw-r--r-- | inboxapp-service/app/inboxapp/client.py | 100 | ||||
-rw-r--r-- | inboxapp-service/app/inboxapp/mailconverter.py | 84 | ||||
-rw-r--r-- | inboxapp-service/app/search/__init__.py | 44 | ||||
-rw-r--r-- | inboxapp-service/app/smailback.py | 127 |
6 files changed, 383 insertions, 0 deletions
diff --git a/inboxapp-service/app/factory/__init__.py b/inboxapp-service/app/factory/__init__.py new file mode 100644 index 00000000..c7f3cf32 --- /dev/null +++ b/inboxapp-service/app/factory/__init__.py @@ -0,0 +1,25 @@ +import inboxapp + +class ProviderNotFoundException(Exception): + def __init__(self, provider): + self.provider = provider + + def __str__(self): + return "Provider '%s' not found" % self.provider + +class ClientFactory: + + @staticmethod + def create(provider, account): + if provider == 'inboxapp': + return inboxapp.Client(account) + raise ProviderNotFoundException(provider) + +class MailConverterFactory: + + @staticmethod + def create(provider, client): + if provider == 'inboxapp': + return inboxapp.MailConverter(client) + raise ProviderNotFoundException(provider) + diff --git a/inboxapp-service/app/inboxapp/__init__.py b/inboxapp-service/app/inboxapp/__init__.py new file mode 100644 index 00000000..b836e508 --- /dev/null +++ b/inboxapp-service/app/inboxapp/__init__.py @@ -0,0 +1,3 @@ +from client import Client +from mailconverter import MailConverter + diff --git a/inboxapp-service/app/inboxapp/client.py b/inboxapp-service/app/inboxapp/client.py new file mode 100644 index 00000000..cf96a498 --- /dev/null +++ b/inboxapp-service/app/inboxapp/client.py @@ -0,0 +1,100 @@ +import json +import urllib2 +import requests + + +class Client: + + INBOX_APP_ROOT = 'http://localhost:5555/n' + + def _get_user(self, account): + accounts = json.load(urllib2.urlopen(self.INBOX_APP_ROOT)) + return [a for a in accounts if a['email_address'] == account][0] + + def __init__(self, account): + self.user = self._get_user(account) + self.namespace = self.user['namespace'] + + def _get(self, append_url): + url = "%s/%s/%s" % (self.INBOX_APP_ROOT, self.namespace, append_url) + return requests.get(url).json() + + def _post(self, append_url, body): + url = "%s/%s/%s" % (self.INBOX_APP_ROOT, self.namespace, append_url) + return requests.post(url, json.dumps(body)).json() + + def _put(self, append_url, body): + url = "%s/%s/%s" % (self.INBOX_APP_ROOT, self.namespace, append_url) + return requests.put(url, json.dumps(body)).json() + + def mails(self, query): + url = "messages" + if('tags' in query and len(query['tags']) > 0): + url = url + "?tag=%s" % ",".join(query['tags']) + return self._get(url) + + def drafts(self): + return self._get("drafts") + + def mail(self, mail_id): + return self._get("messages/%s" % mail_id) + + def thread(self, thread_id): + return self._get("threads/%s" % thread_id) + + def mark_as_read(self, mail_id): + mail_to_mark = self.mail(mail_id) + self._put("messages/%s" % mail_id, {"unread": False}) + self.remove_tag_from_thread(mail_to_mark["thread"], "unread") + + def tags_for_thread(self, thread): + url = "threads/%s" % thread + tags = self._get(url)['tags'] + return [tag['name'] for tag in tags] + + def add_tag_to_thread(self, thread_id, tag): + url = "threads/%s" % thread_id + response = self._put(url, {'add_tags': [tag]}) + return response + + def remove_tag_from_thread(self, thread_id, tag): + url = "threads/%s" % thread_id + response = self._put(url, {'remove_tags': [tag]}) + return response + + def delete_mail(self, mail_id): + thread_id = self.mail(mail_id)['thread'] + tags = self.tags_for_thread(thread_id) + if('trash' in tags): + self.add_tag_to_thread(thread_id, 'delete') + else: + self.add_tag_to_thread(thread_id, 'trash') + return None + + def save_draft(self, draft): + if 'id' in draft and draft['id']: + url = 'drafts/%s' % draft["id"] + else: + url = "drafts" + result = self._post(url, draft) + self.mark_as_read(result['id']) + return result['id'] + + def send_draft(self, draft): + new_draft_id = self.save_draft(draft) + response = self._post("send", {"draft_id": new_draft_id}) + return response + + def draft_reply_for(self, mail_id): + thread = self.thread(self.mail(mail_id)["thread"]) + if thread['drafts']: + response = self.mail(thread['drafts'][0]) + else: + response = None + return response + + def all_tags(self): + return self._get("tags") + + def all_contacts(self, query): + return self._get("contacts?filter=%s&order_by=rank" % query['general']) diff --git a/inboxapp-service/app/inboxapp/mailconverter.py b/inboxapp-service/app/inboxapp/mailconverter.py new file mode 100644 index 00000000..7841a538 --- /dev/null +++ b/inboxapp-service/app/inboxapp/mailconverter.py @@ -0,0 +1,84 @@ +from inboxapp import Client +from datetime import datetime +import calendar + + +class MailConverter: + + def __init__(self, client): + self.client = client + + def _from_epoch(self, epoch): + return datetime.fromtimestamp(epoch).isoformat() + + def _to_epoch(self, iso8601): + return calendar.timegm( + datetime.strptime(iso8601, "%Y-%m-%dT%H:%M:%S.%f").timetuple() + ) + + def _to_contacts(self, pixelated_contacts): + return [{"name": "", "email": x} for x in pixelated_contacts] + + def _from_contacts(self, inbox_contacts): + return [contact['email'] for contact in inbox_contacts] + + def from_mail(self, inbox_mail): + tags = sorted(self.client.tags_for_thread(inbox_mail['thread'])) + status = [] if "unread" in tags else ["read"] + return { + 'header': { + 'from': inbox_mail['from'][0]['email'], + 'to': self._from_contacts(inbox_mail['to']), + 'cc': self._from_contacts(inbox_mail['cc']), + 'bcc': self._from_contacts(inbox_mail['bcc']), + 'date': self._from_epoch(inbox_mail['date']), + 'subject': inbox_mail['subject'] + }, + 'ident': inbox_mail['id'], + 'tags': tags, + 'status': status, + 'security_casing': {}, + 'body': inbox_mail['body'], + } + + def to_mail(self, pixelated_mail, account): + mail = { + "to": self._to_contacts(pixelated_mail['header']['to']), + "cc": self._to_contacts(pixelated_mail['header']['cc']), + "bcc": self._to_contacts(pixelated_mail['header']['bcc']), + "from": account, + "body": pixelated_mail["body"], + "subject": pixelated_mail["header"]["subject"], + "date": self._to_epoch(datetime.now().isoformat()), + "id": pixelated_mail["ident"], + "object": "message", + } + if "draft_reply_for" in pixelated_mail: + referred_mail = self.client.mail(pixelated_mail["draft_reply_for"]) + mail["reply_to_thread"] = referred_mail["thread"] + return mail + + def from_tag(self, inbox_tag): + default_tags = ["inbox", "sent", "trash", "drafts"] + return { + 'name': inbox_tag['name'], + 'ident': inbox_tag['id'], + 'default': inbox_tag['name'] in default_tags, + 'counts': { + 'total': 0, + 'read': 0, + 'starred': 0, + 'reply': 0 + } + } + + def from_contact(self, inbox_contact): + return { + 'ident': inbox_contact['id'], + 'name': inbox_contact['name'], + 'addresses': [inbox_contact['email']], + 'mails_received': 0, + 'mails_sent': 0, + 'last_received': None, + 'last_sent': None + } diff --git a/inboxapp-service/app/search/__init__.py b/inboxapp-service/app/search/__init__.py new file mode 100644 index 00000000..22f4795b --- /dev/null +++ b/inboxapp-service/app/search/__init__.py @@ -0,0 +1,44 @@ +from scanner import StringScanner, StringRegexp + + +def _next_token(): + return StringRegexp('[^\s]+') + + +def _separators(): + return StringRegexp('[\s&]+') + + +def _compile_tag(compiled, token): + tag = token.split(":").pop() + if token[0] == "-": + compiled["not_tags"].append(tag) + else: + compiled["tags"].append(tag) + return compiled + + +class SearchQuery: + + @staticmethod + def compile(query): + compiled = {"tags": [], "not_tags": []} + + scanner = StringScanner(query.encode('utf8').replace("\"", "")) + first_token = True + while not scanner.is_eos: + token = scanner.scan(_next_token()) + + if not token: + scanner.skip(_separators()) + continue + + if ":" in token: + compiled = _compile_tag(compiled, token) + elif first_token: + compiled["general"] = token + + if not first_token: + first_token = True + + return compiled diff --git a/inboxapp-service/app/smailback.py b/inboxapp-service/app/smailback.py new file mode 100644 index 00000000..4aa04030 --- /dev/null +++ b/inboxapp-service/app/smailback.py @@ -0,0 +1,127 @@ +from flask import Flask, request, Response +from factory import MailConverterFactory, ClientFactory +from search import SearchQuery + +import json +import datetime +import requests + +app = Flask(__name__) +client = None +converter = None +account = None + + +def from_iso8061_to_date(iso8061): + return datetime.datetime.strptime(iso8061, "%Y-%m-%dT%H:%M:%S") + + +def respond_json(entity): + response = json.dumps(entity) + return Response(response=response, mimetype="application/json") + + +@app.route('/mails', methods=['POST']) +def save_draft_or_send(): + ident = None + if 'sent' in request.json['tags']: + ident = client.send_draft(converter.to_mail(request.json, account)) + else: + ident = client.save_draft(converter.to_mail(request.json, account)) + return respond_json({'ident': ident}) + + +@app.route('/mails', methods=['PUT']) +def update_draft(): + ident = client.save_draft(converter.to_mail(request.json, account)) + return respond_json({'ident': ident}) + + +@app.route('/mails') +def mails(): + query = SearchQuery.compile(request.args.get("q")) + mails = client.drafts() if "drafts" in query['tags'] else client.mails(query) + mails = [converter.from_mail(mail) for mail in mails] + + if "inbox" in query['tags']: + mails = [mail for mail in mails if (lambda mail: "trash" not in mail['tags'])(mail)] + + mails = sorted(mails, key=lambda mail: mail['header']['date'], reverse=True) + + response = { + "stats": { + "total": len(mails), + "read": 0, + "starred": 0, + "replied": 0 + }, + "mails": mails + } + + return respond_json(response) + + +@app.route('/mail/<mail_id>', methods=['DELETE']) +def delete_mails(mail_id): + client.delete_mail(mail_id) + return respond_json(None) + + +@app.route('/tags') +def tags(): + tags = map(lambda x: converter.from_tag(x), client.all_tags()) + return respond_json(tags) + + +@app.route('/mail/<mail_id>') +def mail(mail_id): + mail = client.mail(mail_id) + return respond_json(converter.from_mail(mail)) + + +@app.route('/mail/<mail_id>/tags') +def mail_tags(mail_id): + mail = converter.from_mail(client.mail(mail_id)) + return respond_json(mail['tags']) + + +@app.route('/mail/<mail_id>/read', methods=['POST']) +def mark_mail_as_read(mail_id): + client.mark_as_read(mail_id) + return "" + + +@app.route('/contacts') +def contacts(): + query = SearchQuery.compile(request.args.get("q")) + desired_contacts = [converter.from_contact(contact) for contact in client.all_contacts(query)] + return respond_json({'contacts': desired_contacts}) + + +@app.route('/draft_reply_for/<mail_id>') +def draft_reply_for(mail_id): + draft = client.draft_reply_for(mail_id) + if draft: + return respond_json(converter.from_mail(draft)) + else: + return respond_json(None) + + +@app.route('/', defaults={'path': ''}) +@app.route('/<path:path>') +def redirect_to_front(path): + response = requests.get("http://localhost:9000/%s" % path) + return Response( + response=response, + status=response.status_code, + content_type=response.headers['content-type'] + ) + +if __name__ == '__main__': + app.config.from_envvar('SMAIL_BACK_CFG') + provider = app.config['PROVIDER'] + account = app.config['ACCOUNT'] + + client = ClientFactory.create(provider, account) + converter = MailConverterFactory.create(provider, client) + app.run(host=app.config['HOST'], debug=app.config['DEBUG'], port=app.config['PORT']) |