nmhive.py: Return 400 errors for data-less POSTs
[nmhive.git] / nmhive.py
1 #!/usr/bin/env python
2
3 """Serve a JSON API for getting/setting notmuch tags with nmbug commits."""
4
5 import json
6 import mailbox
7 import os
8 import tempfile
9 import urllib.request
10
11 import flask
12 import flask_cors
13 import nmbug
14 import notmuch
15
16
17 app = flask.Flask(__name__)
18 app.config['CORS_HEADERS'] = 'Content-Type'
19 flask_cors.CORS(app)
20
21 TAG_PREFIX = os.getenv('NMBPREFIX', 'notmuch::')
22 NOTMUCH_PATH = None
23
24
25 @app.route('/tags', methods=['GET'])
26 def tags():
27     tags = set()
28     database = notmuch.Database(path=NOTMUCH_PATH)
29     try:
30         for t in database.get_all_tags():
31             if t.startswith(TAG_PREFIX):
32                 tags.add(t[len(TAG_PREFIX):])
33     finally:
34         database.close()
35     return flask.Response(
36         response=json.dumps(sorted(tags)),
37         mimetype='application/json')
38
39
40 def _message_tags(message):
41     return sorted(
42         tag[len(TAG_PREFIX):] for tag in message.get_tags()
43         if tag.startswith(TAG_PREFIX))
44
45
46 @app.route('/mid/<message_id>', methods=['GET', 'POST'])
47 def message_id_tags(message_id):
48     if flask.request.method == 'POST':
49         changes = flask.request.get_json()
50         if not changes:
51             return flask.Response(status=400)
52         database = notmuch.Database(
53             path=NOTMUCH_PATH,
54             mode=notmuch.Database.MODE.READ_WRITE)
55         try:
56             message = database.find_message(message_id)
57             if not(message):
58                 return flask.Response(status=404)
59             database.begin_atomic()
60             message.freeze()
61             for change in changes:
62                 if change.startswith('+'):
63                     message.add_tag(TAG_PREFIX + change[1:])
64                 elif change.startswith('-'):
65                     message.remove_tag(TAG_PREFIX + change[1:])
66                 else:
67                     return flask.Response(status=400)
68             message.thaw()
69             database.end_atomic()
70             tags = _message_tags(message=message)
71         finally:
72             database.close()
73         nmbug.commit(message='nmhive: {} {}'.format(
74             message_id, ' '.join(changes)))
75     elif flask.request.method == 'GET':
76         database = notmuch.Database(path=NOTMUCH_PATH)
77         try:
78             message = database.find_message(message_id)
79             if not(message):
80                 return flask.Response(status=404)
81             tags = _message_tags(message=message)
82         finally:
83             database.close()
84     return flask.Response(
85         response=json.dumps(tags),
86         mimetype='application/json')
87
88
89 @app.route('/gmane/<group>/<int:article>', methods=['GET'])
90 def gmane_message_id(group, article):
91     url = 'http://download.gmane.org/{}/{}/{}'.format(
92         group, article, article + 1)
93     response = urllib.request.urlopen(url=url, timeout=3)
94     mbox_bytes = response.read()
95     with tempfile.NamedTemporaryFile(prefix='nmbug-', suffix='.mbox') as f:
96         f.write(mbox_bytes)
97         mbox = mailbox.mbox(path=f.name)
98         _, message = mbox.popitem()
99         message_id = message['message-id']
100     return flask.Response(
101         response=message_id.lstrip('<').rstrip('>'),
102         mimetype='text/plain')
103
104
105 if __name__ == '__main__':
106     import argparse
107
108     parser = argparse.ArgumentParser(description=__doc__)
109     parser.add_argument(
110         '-H', '--host', default='127.0.0.1',
111         help='The hostname to listen on.')
112     parser.add_argument(
113         '-p', '--port', type=int, default=5000,
114         help='The port to listen on.')
115
116     args = parser.parse_args()
117
118     app.run(host=args.host, port=args.port)