From dbbbdc638d140b6b861a4ec386aaabf6bc7e1bdd Mon Sep 17 00:00:00 2001 From: Duda Dornelles Date: Fri, 1 Aug 2014 13:47:28 -0300 Subject: Pixelated user agent REST api without implementation --- service/README.md | 37 --------- service/app/leap/__init__.py | 3 + service/app/leap/client.py | 63 +++++++++++++++ service/app/leap/mailconverter.py | 21 +++++ service/app/pixelated_user_agent.py | 127 +++++++++++++++++++++++++++++++ service/app/search/__init__.py | 44 +++++++++++ service/config/pixelated_ua.cfg | 6 ++ service/go | 4 + service/requirements.txt | 4 + service/runtests | 1 + service/test/search/test_search_query.py | 22 ++++++ 11 files changed, 295 insertions(+), 37 deletions(-) delete mode 100644 service/README.md create mode 100644 service/app/leap/__init__.py create mode 100644 service/app/leap/client.py create mode 100644 service/app/leap/mailconverter.py create mode 100644 service/app/pixelated_user_agent.py create mode 100644 service/app/search/__init__.py create mode 100644 service/config/pixelated_ua.cfg create mode 100755 service/go create mode 100644 service/requirements.txt create mode 100755 service/runtests create mode 100644 service/test/search/test_search_query.py (limited to 'service') diff --git a/service/README.md b/service/README.md deleted file mode 100644 index 971b1494..00000000 --- a/service/README.md +++ /dev/null @@ -1,37 +0,0 @@ -Pixelated User Agent Service -============================ - -This is the service for the Pixelated User Agent. The primary purpose of this is to integrate well with the Pixelated Provider and provide all the capabilities necessary for the UI to work well. - -The aim is to support these resources/endpoints: - -``` -GET /mails -DELETE /mails -POST /mails -PUT /mails -POST /mails/read - -GET /mail/:id -DELETE /mail/:id -POST /mail/:id/star -POST /mail/:id/unstar -POST /mail/:id/replied -POST /mail/:id/unreplied -POST /mail/:id/read -POST /mail/:id/unread -GET /mail/:id/tags -POST /mail/:id/tags - -GET /draft_reply_for/:id - -GET /contacts -GET /contact/:id - -GET /stats - -GET /tags -POST /tags -``` - -The implementation of the User Agent Service will be in Python, in order to better work together with LEAP. Another goal of the User Agent Service will be to run well on all major client platforms. Finally, there will be a lot of support for search and indexing, and also for encryption and signing. However, we want to push most of these features back to LEAP so that Bitmask can leverage them as well. diff --git a/service/app/leap/__init__.py b/service/app/leap/__init__.py new file mode 100644 index 00000000..b836e508 --- /dev/null +++ b/service/app/leap/__init__.py @@ -0,0 +1,3 @@ +from client import Client +from mailconverter import MailConverter + diff --git a/service/app/leap/client.py b/service/app/leap/client.py new file mode 100644 index 00000000..5f9020fd --- /dev/null +++ b/service/app/leap/client.py @@ -0,0 +1,63 @@ +class Client: + + def __init__(self, account): + pass + + + def mails(self, query): + raise NotImplementedError() + + + def drafts(self): + raise NotImplementedError() + + + def mail(self, mail_id): + raise NotImplementedError() + + + def thread(self, thread_id): + raise NotImplementedError() + + + def mark_as_read(self, mail_id): + raise NotImplementedError() + + + def tags_for_thread(self, thread): + raise NotImplementedError() + + + def add_tag_to_thread(self, thread_id, tag): + raise NotImplementedError() + + + def remove_tag_from_thread(self, thread_id, tag): + raise NotImplementedError() + + + def delete_mail(self, mail_id): + raise NotImplementedError() + + + def save_draft(self, draft): + raise NotImplementedError() + + + def send_draft(self, draft): + raise NotImplementedError() + + + def draft_reply_for(self, mail_id): + raise NotImplementedError() + + + def all_tags(self): + raise NotImplementedError() + + + def all_contacts(self, query): + raise NotImplementedError() + + + diff --git a/service/app/leap/mailconverter.py b/service/app/leap/mailconverter.py new file mode 100644 index 00000000..e05b2c30 --- /dev/null +++ b/service/app/leap/mailconverter.py @@ -0,0 +1,21 @@ +class MailConverter: + + def __init__(self, client): + pass + + def from_mail(self, inbox_mail): + raise NotImplementedError() + + + def to_mail(self, pixelated_mail, account): + raise NotImplementedError() + + + def from_tag(self, inbox_tag): + raise NotImplementedError() + + + def from_contact(self, inbox_contact): + raise NotImplementedError() + + diff --git a/service/app/pixelated_user_agent.py b/service/app/pixelated_user_agent.py new file mode 100644 index 00000000..4aa04030 --- /dev/null +++ b/service/app/pixelated_user_agent.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/', 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/') +def mail(mail_id): + mail = client.mail(mail_id) + return respond_json(converter.from_mail(mail)) + + +@app.route('/mail//tags') +def mail_tags(mail_id): + mail = converter.from_mail(client.mail(mail_id)) + return respond_json(mail['tags']) + + +@app.route('/mail//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/') +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('/') +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']) diff --git a/service/app/search/__init__.py b/service/app/search/__init__.py new file mode 100644 index 00000000..22f4795b --- /dev/null +++ b/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/service/config/pixelated_ua.cfg b/service/config/pixelated_ua.cfg new file mode 100644 index 00000000..083e1dd4 --- /dev/null +++ b/service/config/pixelated_ua.cfg @@ -0,0 +1,6 @@ +DEBUG=True +HOST="0.0.0.0" +ACCOUNT="pixelatedinboxapp@gmail.com" +PROVIDER="inboxapp" +PORT=3333 + diff --git a/service/go b/service/go new file mode 100755 index 00000000..b4de3424 --- /dev/null +++ b/service/go @@ -0,0 +1,4 @@ +#!/bin/bash + +export PIXELATED_UA_CFG=../config/pixelated_ua.cfg +python app/pixelated_usar_agent.py diff --git a/service/requirements.txt b/service/requirements.txt new file mode 100644 index 00000000..e1ec3242 --- /dev/null +++ b/service/requirements.txt @@ -0,0 +1,4 @@ +Twisted==12.2.0 +flask==0.10.1 +scanner==0.0.5 +requests==2.3.0 diff --git a/service/runtests b/service/runtests new file mode 100755 index 00000000..64316a29 --- /dev/null +++ b/service/runtests @@ -0,0 +1 @@ +APP_ROOT=`pwd`/app py.test test/ diff --git a/service/test/search/test_search_query.py b/service/test/search/test_search_query.py new file mode 100644 index 00000000..d980c3f0 --- /dev/null +++ b/service/test/search/test_search_query.py @@ -0,0 +1,22 @@ +import sys, os +sys.path.insert(0, os.environ['APP_ROOT']) + +from search import SearchQuery + +def test_one_tag(): + assert SearchQuery.compile(u"in:inbox")["tags"] == ["inbox"] + assert SearchQuery.compile(u"in:trash")["tags"] == ["trash"] + + +def test_two_tags_or(): + assert SearchQuery.compile(u"in:inbox or in:trash")["tags"] == ["inbox", "trash"] + + +def test_tag_negate(): + assert SearchQuery.compile(u"-in:trash")["not_tags"] == ["trash"] + +def test_general_search(): + assert SearchQuery.compile(u"searching")["general"] == "searching" + +def test_tags_with_quotes(): + assert SearchQuery.compile(u"in:\"inbox\"")["tags"] == ["inbox"] -- cgit v1.2.3