summaryrefslogtreecommitdiff
path: root/service
diff options
context:
space:
mode:
authorDuda Dornelles <ddornell@thoughtworks.com>2014-08-01 13:47:28 -0300
committerDuda Dornelles <ddornell@thoughtworks.com>2014-08-01 13:47:28 -0300
commitdbbbdc638d140b6b861a4ec386aaabf6bc7e1bdd (patch)
treea115c5e8bff3c1f5026aee28ecd4bbbe5ce75d25 /service
parent0666ea46470a10334178a355a3895423f9906908 (diff)
Pixelated user agent REST api without implementation
Diffstat (limited to 'service')
-rw-r--r--service/README.md37
-rw-r--r--service/app/leap/__init__.py3
-rw-r--r--service/app/leap/client.py63
-rw-r--r--service/app/leap/mailconverter.py21
-rw-r--r--service/app/pixelated_user_agent.py127
-rw-r--r--service/app/search/__init__.py44
-rw-r--r--service/config/pixelated_ua.cfg6
-rwxr-xr-xservice/go4
-rw-r--r--service/requirements.txt4
-rwxr-xr-xservice/runtests1
-rw-r--r--service/test/search/test_search_query.py22
11 files changed, 295 insertions, 37 deletions
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/<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'])
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"]