README.rst: Unwind 'If ..., returns a 404' -> 'Returns a 404 if ...'
[nmhive.git] / nmhive.py
1 #!/usr/bin/env python
2 #
3 # Copyright (C) 2014 W. Trevor King <wking@tremily.us>
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are met:
7 #
8 # * Redistributions of source code must retain the above copyright notice, this
9 # list of conditions and the following disclaimer.
10 #
11 # * Redistributions in binary form must reproduce the above copyright notice,
12 # this list of conditions and the following disclaimer in the documentation
13 # and/or other materials provided with the distribution.
14 #
15 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25 # POSSIBILITY OF SUCH DAMAGE.
26
27 """Serve a JSON API for getting/setting notmuch tags with nmbug commits."""
28
29 import json
30 import mailbox
31 import os
32 import tempfile
33 import urllib.request
34
35 import flask
36 import flask_cors
37 import nmbug
38 import notmuch
39
40
41 app = flask.Flask(__name__)
42 app.config['CORS_HEADERS'] = 'Content-Type'
43 flask_cors.CORS(app)
44
45 TAG_PREFIX = os.getenv('NMBPREFIX', 'notmuch::')
46 NOTMUCH_PATH = None
47
48
49 @app.route('/tags', methods=['GET'])
50 def tags():
51     tags = set()
52     database = notmuch.Database(path=NOTMUCH_PATH)
53     try:
54         for t in database.get_all_tags():
55             if t.startswith(TAG_PREFIX):
56                 tags.add(t[len(TAG_PREFIX):])
57     finally:
58         database.close()
59     return flask.Response(
60         response=json.dumps(sorted(tags)),
61         mimetype='application/json')
62
63
64 def _message_tags(message):
65     return sorted(
66         tag[len(TAG_PREFIX):] for tag in message.get_tags()
67         if tag.startswith(TAG_PREFIX))
68
69
70 @app.route('/mid/<message_id>', methods=['GET', 'POST'])
71 def message_id_tags(message_id):
72     if flask.request.method == 'POST':
73         changes = flask.request.get_json()
74         if not changes:
75             return flask.Response(status=400)
76         database = notmuch.Database(
77             path=NOTMUCH_PATH,
78             mode=notmuch.Database.MODE.READ_WRITE)
79         try:
80             message = database.find_message(message_id)
81             if not(message):
82                 return flask.Response(status=404)
83             database.begin_atomic()
84             message.freeze()
85             for change in changes:
86                 if change.startswith('+'):
87                     message.add_tag(TAG_PREFIX + change[1:])
88                 elif change.startswith('-'):
89                     message.remove_tag(TAG_PREFIX + change[1:])
90                 else:
91                     return flask.Response(status=400)
92             message.thaw()
93             database.end_atomic()
94             tags = _message_tags(message=message)
95         finally:
96             database.close()
97         nmbug.commit(message='nmhive: {} {}'.format(
98             message_id, ' '.join(changes)))
99     elif flask.request.method == 'GET':
100         database = notmuch.Database(path=NOTMUCH_PATH)
101         try:
102             message = database.find_message(message_id)
103             if not(message):
104                 return flask.Response(status=404)
105             tags = _message_tags(message=message)
106         finally:
107             database.close()
108     return flask.Response(
109         response=json.dumps(tags),
110         mimetype='application/json')
111
112
113 @app.route('/gmane/<group>/<int:article>', methods=['GET'])
114 def gmane_message_id(group, article):
115     url = 'http://download.gmane.org/{}/{}/{}'.format(
116         group, article, article + 1)
117     response = urllib.request.urlopen(url=url, timeout=3)
118     mbox_bytes = response.read()
119     with tempfile.NamedTemporaryFile(prefix='nmbug-', suffix='.mbox') as f:
120         f.write(mbox_bytes)
121         mbox = mailbox.mbox(path=f.name)
122         _, message = mbox.popitem()
123         message_id = message['message-id']
124     return flask.Response(
125         response=message_id.lstrip('<').rstrip('>'),
126         mimetype='text/plain')
127
128
129 if __name__ == '__main__':
130     import argparse
131
132     parser = argparse.ArgumentParser(description=__doc__)
133     parser.add_argument(
134         '-H', '--host', default='127.0.0.1',
135         help='The hostname to listen on.')
136     parser.add_argument(
137         '-p', '--port', type=int, default=5000,
138         help='The port to listen on.')
139     parser.add_argument(
140         '-d', '--debug', type=bool, default=False,
141         help='Run Flask in debug mode (e.g. show errors).')
142
143     args = parser.parse_args()
144
145     app.debug = args.debug
146     app.run(host=args.host, port=args.port)