summaryrefslogtreecommitdiff
path: root/fake-service/app
diff options
context:
space:
mode:
Diffstat (limited to 'fake-service/app')
-rw-r--r--fake-service/app/__init__.py0
-rw-r--r--fake-service/app/adapter/__init__.py16
-rw-r--r--fake-service/app/adapter/contacts.py41
-rw-r--r--fake-service/app/adapter/mail.py91
-rw-r--r--fake-service/app/adapter/mail_service.py128
-rw-r--r--fake-service/app/adapter/mailset.py69
-rw-r--r--fake-service/app/adapter/tag.py40
-rw-r--r--fake-service/app/adapter/tagsset.py57
-rw-r--r--fake-service/app/pixelated_user_agent.py196
-rw-r--r--fake-service/app/search/__init__.py16
-rw-r--r--fake-service/app/search/search_query.py86
11 files changed, 740 insertions, 0 deletions
diff --git a/fake-service/app/__init__.py b/fake-service/app/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/fake-service/app/__init__.py
diff --git a/fake-service/app/adapter/__init__.py b/fake-service/app/adapter/__init__.py
new file mode 100644
index 00000000..55f91e08
--- /dev/null
+++ b/fake-service/app/adapter/__init__.py
@@ -0,0 +1,16 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+from mail_service import MailService
diff --git a/fake-service/app/adapter/contacts.py b/fake-service/app/adapter/contacts.py
new file mode 100644
index 00000000..30ff1253
--- /dev/null
+++ b/fake-service/app/adapter/contacts.py
@@ -0,0 +1,41 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+import re
+
+
+class Contacts:
+
+ def __init__(self):
+ self.contacts = []
+
+ def add(self, mbox_mail):
+ contact = mbox_mail.get('From') or mbox_mail.from_addr
+ self.contacts.append(Contact(contact))
+
+ def search(self, query):
+ contacts_query = re.compile(query)
+ return [
+ contact.__dict__
+ for contact in self.contacts
+ if contacts_query.match(contact.addresses[0])
+ ]
+
+
+class Contact:
+
+ def __init__(self, contact):
+ self.addresses = [contact]
+ self.name = ''
diff --git a/fake-service/app/adapter/mail.py b/fake-service/app/adapter/mail.py
new file mode 100644
index 00000000..26c00277
--- /dev/null
+++ b/fake-service/app/adapter/mail.py
@@ -0,0 +1,91 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+from datetime import datetime
+import random
+import calendar
+from dateutil import parser
+
+class Mail:
+
+ NOW = calendar.timegm(
+ datetime.strptime(
+ datetime.now().isoformat(),
+ "%Y-%m-%dT%H:%M:%S.%f").timetuple())
+
+ @staticmethod
+ def from_json(mail_json):
+ mail = Mail()
+ mail.header = mail_json['header']
+ mail.header['date'] = datetime.now().isoformat()
+ mail.ident = mail_json.get('ident', 0)
+ mail.body = mail_json['body']
+ mail.tags = mail_json['tags']
+ mail.security_casing = {}
+ mail.status = []
+ mail.draft_reply_for = mail_json.get('draft_reply_for', 0)
+ return mail
+
+ def __init__(self, mbox_mail=None, ident=None):
+ if mbox_mail:
+ self.header = self._get_headers(mbox_mail)
+ self.ident = ident
+ self.body = self._get_body(mbox_mail)
+ self.tags = self._get_tags(mbox_mail)
+ self.security_casing = {}
+ self.status = self._get_status()
+ self.draft_reply_for = -1
+
+ def _get_body(self, message):
+ if message.is_multipart():
+ boundary = '--{boundary}'.format(
+ boundary=message.get_boundary().strip())
+ body_parts = [x.as_string() for x in message.get_payload()]
+
+ body = boundary + '\n'
+ body += '{boundary}\n'.format(boundary=boundary).join(body_parts)
+ body += '{boundary}--\n'.format(boundary=boundary)
+
+ return body
+ else:
+ return message.get_payload()
+
+ def _get_status(self):
+ status = []
+ if 'sent' in self.tags:
+ status.append('read')
+
+ return status
+
+ def _get_headers(self, mbox_mail):
+ headers = {}
+ headers['from'] = mbox_mail.get('From') or mbox_mail.from_addr
+ headers['to'] = [mbox_mail.get('To')]
+ headers['subject'] = mbox_mail.get('Subject')
+ headers['date'] = parser.parse(mbox_mail['Date']).isoformat()
+ headers['content_type'] = mbox_mail.get('Content-Type')
+
+ return headers
+
+ def _get_tags(self, mbox_mail):
+ return filter(len, mbox_mail.get('X-TW-Pixelated-Tags').split(', '))
+
+ @property
+ def subject(self):
+ return self.header['subject']
+
+ @property
+ def date(self):
+ return self.header['date']
diff --git a/fake-service/app/adapter/mail_service.py b/fake-service/app/adapter/mail_service.py
new file mode 100644
index 00000000..2825af9d
--- /dev/null
+++ b/fake-service/app/adapter/mail_service.py
@@ -0,0 +1,128 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+import os
+import re
+import mailbox
+
+from tagsset import TagsSet
+from mailset import MailSet
+from contacts import Contacts
+from mail import Mail
+
+
+class MailService:
+ MAILSET_PATH = os.path.join(os.environ['HOME'], 'mailsets', 'mediumtagged')
+
+ def __init__(self):
+ self.mailset = MailSet()
+ self.tagsset = TagsSet()
+ self.contacts = Contacts()
+
+ def reset(self):
+ self.mailset = MailSet()
+ self.tagsset = TagsSet()
+ self.contacts = Contacts()
+
+ def _read_file(self, filename):
+ with open(filename, 'r') as fd:
+ return fd.read()
+
+ def _create_message_from_file(self, filename):
+ data = self._read_file(filename)
+ return self.create_message_from_string(data, filename)
+
+ def create_message_from_string(self, data, filename=None):
+ if data.startswith('From '):
+ msg = mailbox.mboxMessage(data)
+ from_addr = re.sub(r"^From ", "", msg.get_unixfrom())
+ msg.from_addr = from_addr
+ msg.set_from(from_addr)
+ else:
+ msg = mailbox.Message(data)
+ msg.from_addr = msg.get('From')
+ return msg
+
+ def _create_message_from_string(self, data):
+ return mailbox.Message(data)
+
+ def load_mailset(self):
+ mbox_filenames = [
+ filename
+ for filename in os.listdir
+ (self.MAILSET_PATH) if filename.startswith('mbox')]
+ messages = (self._create_message_from_file(os.path.join(self.MAILSET_PATH, mbox))
+ for mbox in mbox_filenames)
+
+ self.index_messages(messages)
+
+ def index_messages(self, messages):
+ for message in messages:
+ self.mailset.add(message)
+ self.tagsset.add(message)
+ self.contacts.add(message)
+
+ def mails(self, query, page, window_size):
+ mails = self.mailset.values()
+ mails = [mail for mail in mails if query.test(mail)]
+ return sorted(mails, key=lambda mail: mail.date, reverse=True)
+
+ def mail(self, mail_id):
+ return self.mailset.get(mail_id)
+
+ def search_contacts(self, query):
+ return self.contacts.search(query)
+
+ def mark_as_read(self, mail_id):
+ self.mailset.mark_as_read(mail_id)
+ self.tagsset.mark_as_read(self.mail(mail_id).tags)
+
+ def delete_mail(self, mail_id):
+ purged = self.mailset.delete(mail_id)
+ if not purged:
+ self.tagsset.increment_tag_total_count('trash')
+
+ def update_tags_for(self, mail_id, new_tags):
+ mail = self.mail(mail_id)
+
+ new_tags_set = set(new_tags)
+ old_tags_set = set(mail.tags)
+
+ increment_set = new_tags_set - old_tags_set
+ decrement_set = old_tags_set - new_tags_set
+
+ map(lambda x: self.tagsset.increment_tag_total_count(x), increment_set)
+ map(lambda x: self.tagsset.decrement_tag_total_count(x), decrement_set)
+
+ mail.tags = new_tags
+
+ def send(self, mail):
+ mail = Mail.from_json(mail)
+ self.mailset.update(mail)
+ self.tagsset.increment_tag_total_count('sent')
+ self.tagsset.decrement_tag_total_count('drafts')
+ return mail.ident
+
+ def save_draft(self, mail):
+ mail = self.mailset.add_draft(Mail.from_json(mail))
+ return mail.ident
+
+ def update_draft(self, mail):
+ mail = Mail.from_json(mail)
+ self.mailset.update(mail)
+ return mail.ident
+
+ def draft_reply_for(self, mail_id):
+ return self.mailset.find(draft_reply_for=mail_id)
diff --git a/fake-service/app/adapter/mailset.py b/fake-service/app/adapter/mailset.py
new file mode 100644
index 00000000..bf7e8c67
--- /dev/null
+++ b/fake-service/app/adapter/mailset.py
@@ -0,0 +1,69 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+from mail import Mail
+
+
+class MailSet:
+
+ def __init__(self):
+ self.ident = 0
+ self.mails = {}
+
+ def add(self, mbox_mail):
+ self.mails[self.ident] = Mail(mbox_mail, self.ident)
+ self.ident += 1
+
+ def values(self):
+ return self.mails.values()
+
+ def get(self, mail_id):
+ return self.mails.get(mail_id)
+
+ def mark_as_read(self, mail_id):
+ mail = self.get(mail_id)
+ mail.status.append('read')
+
+ def delete(self, mail_id):
+ """
+ Returns True if the email got purged,
+ else returns False meaning the email got moved to trash
+ """
+
+ mail = self.get(mail_id)
+ if 'trash' in mail.tags:
+ del self.mails[mail_id]
+ return True
+ mail.tags.append('trash')
+ return False
+
+ def update(self, mail):
+ self.mails[mail.ident] = mail
+
+ def add_draft(self, mail):
+ mail.ident = self.ident
+ self.mails[mail.ident] = mail
+ self.ident += 1
+ return mail
+
+ def find(self, draft_reply_for):
+ match = [
+ mail
+ for mail in self.mails.values
+ () if mail.draft_reply_for == draft_reply_for]
+ if len(match) == 0:
+ return None
+ else:
+ return match[0]
diff --git a/fake-service/app/adapter/tag.py b/fake-service/app/adapter/tag.py
new file mode 100644
index 00000000..b866d789
--- /dev/null
+++ b/fake-service/app/adapter/tag.py
@@ -0,0 +1,40 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+
+class Tag:
+ DEFAULT_TAGS = ["inbox", "sent", "trash", "drafts"]
+
+ def __init__(self, name, ident):
+ self.counts = {
+ 'total': 0,
+ 'read': 0,
+ 'starred': 0,
+ 'reply': 0
+ }
+
+ self.ident = ident
+ self.name = name.lower()
+ self.default = name in self.DEFAULT_TAGS
+
+ def increment_count(self):
+ self.counts['total'] += 1
+
+ def increment_read(self):
+ self.counts['read'] += 1
+
+ def decrement_count(self):
+ self.counts['total'] -= 1
diff --git a/fake-service/app/adapter/tagsset.py b/fake-service/app/adapter/tagsset.py
new file mode 100644
index 00000000..8e0d7ca3
--- /dev/null
+++ b/fake-service/app/adapter/tagsset.py
@@ -0,0 +1,57 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+from tag import Tag
+
+
+class TagsSet:
+
+ DEFAULT_TAGS = ["inbox", "sent", "trash", "drafts"]
+
+ def __init__(self):
+ self.ident = 0
+ self.tags = {}
+ self.tags = {tag: self._create_new_tag(tag) for tag in self.DEFAULT_TAGS}
+
+ def add(self, mbox_mail):
+ tags = filter(len, mbox_mail.get('X-TW-Pixelated-Tags').split(', '))
+ for tag in tags:
+ tag = self._create_new_tag(tag)
+ tag.increment_count()
+
+ def all_tags(self):
+ return self.tags.values()
+
+ def mark_as_read(self, tags):
+ for tag in tags:
+ tag = tag.lower()
+ tag = self.tags.get(tag)
+ tag.increment_read()
+
+ def increment_tag_total_count(self, tagname):
+ tag = self.tags.get(tagname)
+ if tag:
+ tag.increment_count()
+ else:
+ self._create_new_tag(tagname)
+
+ def decrement_tag_total_count(self, tag):
+ self.tags.get(tag).decrement_count()
+
+ def _create_new_tag(self, tag):
+ tag = Tag(tag, self.ident)
+ tag = self.tags.setdefault(tag.name, tag)
+ self.ident += 1
+ return tag
diff --git a/fake-service/app/pixelated_user_agent.py b/fake-service/app/pixelated_user_agent.py
new file mode 100644
index 00000000..5931bce5
--- /dev/null
+++ b/fake-service/app/pixelated_user_agent.py
@@ -0,0 +1,196 @@
+# # Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+from flask import Flask, request, Response, redirect
+import os
+import csv
+import json
+import datetime
+import mailbox
+import StringIO
+import requests
+from adapter import MailService
+from search import SearchQuery
+
+app = Flask(__name__, static_url_path='', static_folder='../../web-ui/app')
+MEDIUM_TAGGED_URL = 'https://static.wazokazi.is/py-mediumtagged.tar.gz'
+client = None
+converter = None
+account = None
+autoload = os.environ.get('AUTOLOAD', False)
+mail_service = MailService()
+
+
+def respond_json(entity):
+ response = json.dumps(entity)
+ return Response(response=response, mimetype="application/json")
+
+
+@app.route('/disabled_features')
+def disabled_features():
+ return respond_json([])
+
+
+@app.route('/mails', methods=['POST'])
+def save_draft_or_send():
+ mail = request.json
+ if mail['ident']:
+ ident = mail_service.send(mail)
+ else:
+ ident = mail_service.save_draft(mail)
+
+ return respond_json({'ident': ident})
+
+
+@app.route('/mails', methods=['PUT'])
+def update_draft():
+ mail = request.json
+ ident = mail_service.update_draft(mail)
+ return respond_json({'ident': ident})
+
+
+@app.route('/mails')
+def mails():
+ query = SearchQuery.compile(request.args.get('q', ''))
+ page = request.args.get('p', '')
+ window_size = request.args.get('w', '')
+ fetched_mails = mail_service.mails(query, page, window_size)
+
+ mails = [mail.__dict__ for mail in fetched_mails]
+ response = {
+ "stats": {
+ "total": len(mails),
+ "read": 0,
+ "starred": 0,
+ "replied": 0
+ },
+ "mails": mails
+ }
+
+ return respond_json(response)
+
+
+@app.route('/mail/<int:mail_id>', methods=['DELETE'])
+def delete_mails(mail_id):
+ mail_service.delete_mail(mail_id)
+ return respond_json(None)
+
+
+@app.route('/tags')
+def tags():
+ tags = mail_service.tagsset.all_tags()
+ return respond_json([tag.__dict__ for tag in tags])
+
+
+@app.route('/mail/<int:mail_id>')
+def mail(mail_id):
+ return respond_json(mail_service.mail(mail_id).__dict__)
+
+
+@app.route('/mail/<int:mail_id>/tags', methods=['POST'])
+def mail_tags(mail_id):
+ new_tags = request.json['newtags']
+ mail_service.update_tags_for(mail_id, new_tags)
+ return respond_json(request.json['newtags'])
+
+
+@app.route('/mail/<int:mail_id>/read', methods=['POST'])
+def mark_mail_as_read(mail_id):
+ mail_service.mark_as_read(mail_id)
+ return ""
+
+
+@app.route('/contacts')
+def contacts():
+ contacts_query = request.args.get('q')
+ return respond_json(
+ {'contacts': mail_service.search_contacts(contacts_query)})
+
+
+@app.route('/draft_reply_for/<int:mail_id>')
+def draft_reply_for(mail_id):
+ mail = mail_service.draft_reply_for(mail_id)
+ if mail:
+ return respond_json(mail.__dict__)
+ else:
+ return respond_json(None)
+
+
+def utf_8_encoder(unicode_csv_data):
+ for line in unicode_csv_data:
+ yield line.encode('utf-8')
+
+
+@app.route('/control/mailset/csv/load', methods=['POST'])
+def load_mailset_from_csv():
+ csv_data = request.form.keys()[0]
+ csv_string = StringIO.StringIO(csv_data)
+ csv_reader = csv.reader(utf_8_encoder(csv_string))
+ headers = csv_reader.next()
+ messages = []
+ for row in csv_reader:
+ mail = ""
+ row[3] = ', '.join(filter(len, row[3].split(' ')))
+ for header, value in zip(headers, row):
+ if header == 'Body':
+ mail += "\n"
+ else:
+ mail += header + ": "
+ mail += value + "\n"
+ messages.append(mailbox.mboxMessage(mail))
+ mail_service.index_messages(messages)
+ return respond_json(None)
+
+
+@app.route('/control/mailset/<mailset>/load', methods=['POST'])
+def load_mailset(mailset):
+ import os
+ from tarfile import TarFile
+ from gzip import GzipFile
+ mbox_root = os.path.join(os.environ['HOME'], 'mailsets')
+ if not os.path.isdir(os.path.join(mbox_root)):
+ os.mkdir(mbox_root)
+
+ if len(os.listdir(mbox_root)) == 0:
+ response = requests.get(MEDIUM_TAGGED_URL, verify=False)
+ mbox_archive_path = os.path.join(mbox_root, 'py-mediumtagged.tar.gz')
+ mbox_archive = open(mbox_archive_path, 'w')
+ mbox_archive.write(response.content)
+ mbox_archive.close()
+ gzippedfile = GzipFile(filename=mbox_archive_path)
+ tarfile = TarFile(fileobj=gzippedfile)
+ tarfile.extractall(path=mbox_root)
+
+ mail_service.reset()
+ mail_service.load_mailset()
+
+ return respond_json(None)
+
+
+@app.route('/')
+def index():
+ global autoload
+ if autoload:
+ load_mailset('mediumtagged')
+ autoload = False
+
+ return app.send_static_file('index.html')
+
+
+def setup():
+ app.run(host="0.0.0.0", debug=True, port=4567)
+
+
+if __name__ == '__main__':
+ setup()
diff --git a/fake-service/app/search/__init__.py b/fake-service/app/search/__init__.py
new file mode 100644
index 00000000..d6d7b07c
--- /dev/null
+++ b/fake-service/app/search/__init__.py
@@ -0,0 +1,16 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+from search_query import SearchQuery
diff --git a/fake-service/app/search/search_query.py b/fake-service/app/search/search_query.py
new file mode 100644
index 00000000..34e68601
--- /dev/null
+++ b/fake-service/app/search/search_query.py
@@ -0,0 +1,86 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+from scanner import StringScanner, StringRegexp
+import re
+
+
+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": [], "general": []}
+
+ 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"].append(token)
+
+ if not first_token:
+ first_token = True
+
+ compiled["general"] = ' '.join(compiled["general"])
+ return SearchQuery(compiled)
+
+ def __init__(self, compiled):
+ self.compiled = compiled
+
+ def test(self, mail):
+ if 'all' in self.compiled.get('tags'):
+ return True
+
+ if set(self.compiled.get('not_tags')).intersection(set(mail.tags)):
+ return False
+
+ if set(self.compiled.get('tags')).intersection(set(mail.tags)):
+ return True
+
+ if self.compiled.get('general'):
+ search_terms = re.compile(
+ self.compiled['general'],
+ flags=re.IGNORECASE)
+ if search_terms.search(mail.subject+' '+mail.body):
+ return True
+
+ if not [v for v in self.compiled.values() if v]:
+ return True
+
+ return False