README: update changelog
[notmuch] / notmuch
1 #!/usr/bin/env python
2 """This is a notmuch implementation in python. It's goal is to allow running the test suite on the cnotmuch python bindings.
3
4 This "binary" honors the NOTMUCH_CONFIG environmen variable for reading a user's
5 notmuch configuration (e.g. the database path)
6
7 This code is licensed under the GNU GPL v3+."""
8 from __future__ import with_statement # This isn't required in Python 2.6
9 import sys, os, re, logging
10 from subprocess import call
11 from cnotmuch.notmuch import Database, Query
12 PREFIX=re.compile('(\w+):(.*$)')
13 #TODO Handle variable: NOTMUCH-CONFIG
14
15 #-------------------------------------------------------------------------
16 HELPTEXT="""The notmuch mail system.
17
18 Usage: notmuch <command> [args...]
19
20 Where <command> and [args...] are as follows:
21
22         setup   Interactively setup notmuch for first use.
23
24         new     [--verbose]
25
26                 Find and import new messages to the notmuch database.
27
28         search  [options...] <search-terms> [...]
29
30                 Search for messages matching the given search terms.
31
32         show    <search-terms> [...]
33
34                 Show all messages matching the search terms.
35
36         count   <search-terms> [...]
37
38                 Count messages matching the search terms.
39
40         reply   [options...] <search-terms> [...]
41
42                 Construct a reply template for a set of messages.
43
44         tag     +<tag>|-<tag> [...] [--] <search-terms> [...]
45
46                 Add/remove tags for all messages matching the search terms.
47
48         dump    [<filename>]
49
50                 Create a plain-text dump of the tags for each message.
51
52         restore <filename>
53
54                 Restore the tags from the given dump file (see 'dump').
55
56         search-tags     [<search-terms> [...] ]
57
58                 List all tags found in the database or matching messages.
59
60         help    [<command>]
61
62                 This message, or more detailed help for the named command.
63
64 Use "notmuch help <command>" for more details on each command.
65 And "notmuch help search-terms" for the common search-terms syntax.
66 """
67 #-------------------------------------------------------------------------
68 #TODO: replace the dynamic pieces
69 USAGE="""Notmuch is configured and appears to have a database. Excellent!
70
71 At this point you can start exploring the functionality of notmuch by
72 using commands such as:
73
74         notmuch search tag:inbox
75
76         notmuch search to:"Sebastian Spaeth"
77
78         notmuch search from:"Sebastian@SSpaeth.de"
79
80         notmuch search subject:"my favorite things"
81
82 See "notmuch help search" for more details.
83
84 You can also use "notmuch show" with any of the thread IDs resulting
85 from a search. Finally, you may want to explore using a more sophisticated
86 interface to notmuch such as the emacs interface implemented in notmuch.el
87 or any other interface described at http://notmuchmail.org
88
89 And don't forget to run "notmuch new" whenever new mail arrives.
90
91 Have fun, and may your inbox never have much mail.
92 """
93 #-------------------------------------------------------------------------
94 def quote_query_line(argv):
95    #mangle arguments wrapping terms with spaces in quotes
96    for i in xrange(0,len(argv)):
97       if argv[i].find(' ') >= 0:
98          #if we use prefix:termWithSpaces, put quotes around term
99          m = PREFIX.match(argv[i])
100          if m:
101             argv[i] = '%s:"%s"' % (m.group(1), m.group(2))
102          else:
103             argv[i] = '"'+argv[i]+'"'
104    return ' '.join(argv)
105
106 if __name__ == '__main__':
107
108    # Handle command line options
109    #-------------------------------------
110    # No option given, print USAGE and exit
111    if len(sys.argv) == 1:
112       print USAGE
113    #-------------------------------------
114    elif sys.argv[1] == 'setup':
115        """Interactively setup notmuch for first use."""
116        print "Not implemented."
117    #-------------------------------------
118    elif sys.argv[1] == 'new':
119        """ Interactively setup notmuch for first use. """
120        #print "Not implemented. We cheat by calling the proper notmuch"
121        call(['notmuch new'],shell=True)
122    #-------------------------------------
123    elif sys.argv[1] == 'help':
124        if len(sys.argv) == 2: print HELPTEXT
125        else: print "Not implemented"
126    #-------------------------------------
127    elif sys.argv[1] == 'search':
128       db = Database()
129       if len(sys.argv) == 2:
130          #no further search term
131          querystr=''
132       else:
133          #mangle arguments wrapping terms with spaces in quotes
134          querystr = quote_query_line(sys.argv[2:])
135       logging.debug("search "+querystr)
136       t = Query(db,querystr).search_threads()
137       for thread in t:
138          print(str(thread))
139    #-------------------------------------
140    elif sys.argv[1] == 'show':
141       db = Database()
142       if len(sys.argv) == 2:
143          #no further search term
144          querystr=''
145       else:
146          #mangle arguments wrapping terms with spaces in quotes
147          querystr = quote_query_line(sys.argv[2:])
148       logging.debug("show "+querystr)
149       m = Query(db,querystr).search_messages()
150       for msg in m:
151          print(msg.format_as_text())
152    #-------------------------------------
153    elif sys.argv[1] == 'count':
154       if len(sys.argv) == 2:
155          #no further search term, count all
156          querystr=''
157       else:
158          #mangle arguments wrapping terms with spaces in quotes
159          querystr = quote_query_line(sys.argv[2:])
160       print(Database().create_query(querystr).count_messages())
161       
162    #-------------------------------------
163    elif sys.argv[1] == 'tag':
164       #build lists of tags to be added and removed
165       add, remove = [], []
166       while not sys.argv[2]=='--' and \
167             (sys.argv[2].startswith('+') or sys.argv[2].startswith('-')):
168          if sys.argv[2].startswith('+'):
169             #append to add list without initial +
170             add.append(sys.argv.pop(2)[1:])
171          else:
172             #append to remove list without initial -
173             remove.append(sys.argv.pop(2)[1:])
174       #skip eventual '--'
175       if sys.argv[2]=='--': sys.argv.pop(2)
176       #the rest is search terms
177       querystr = quote_query_line(sys.argv[2:])
178       logging.debug("tag search-term "+querystr)
179       db = Database(mode=Database.MODE.READ_WRITE)
180       m  = Query(db,querystr).search_messages()
181       for msg in m:
182          #actually add and remove all tags
183          map(msg.add_tag, add)
184          map(msg.remove_tag, remove)
185    #-------------------------------------
186    elif sys.argv[1] == 'search-tags':
187       if len(sys.argv) == 2:
188          #no further search term
189          print("\n".join(Database().get_all_tags()))
190       else:
191          #mangle arguments wrapping terms with spaces in quotes
192          querystr = quote_query_line(sys.argv[2:])
193          logging.debug("search-term "+querystr)
194          db = Database()
195          m  = Query(db,querystr).search_messages()
196          print("\n".join([t for t in m.collect_tags()]))
197    #-------------------------------------
198    elif sys.argv[1] == 'dump':
199       #TODO: implement "dump <filename>"
200       if len(sys.argv) == 2:
201          f = sys.stdout
202       else:
203          f = open(sys.argv[2],"w")
204       db = Database()
205       q = Query(db,'')
206       q.set_sort(Query.SORT.MESSAGE_ID)
207       m = q.search_messages()
208       for msg in m:
209          f.write("%s (%s)\n" % (msg.get_message_id(), msg.get_tags()))
210    #-------------------------------------
211    elif sys.argv[1] == 'restore':
212       import re
213       if len(sys.argv) == 2:
214          print("No filename given. Reading dump from stdin.")
215          f = sys.stdin
216       else:
217          f = open(sys.argv[2],"r")
218       #split the msg id and the tags
219       MSGID_TAGS = re.compile("(\S+)\s\((.*)\)$")
220       db = Database(mode=Database.MODE.READ_WRITE)
221
222       #read each line of the dump file
223       for line in f:
224          m = MSGID_TAGS.match(line)
225          if not m:
226             sys.stderr.write("Warning: Ignoring invalid input line: %s" % 
227                              line)
228             continue
229          # split line in components and fetch message
230          msg_id = m.group(1)
231          new_tags= set(m.group(2).split())
232          msg    = db.find_message(msg_id)
233
234          if msg == None:
235             sys.stderr.write(
236                "Warning: Cannot apply tags to missing message: %s\n" % id)
237             continue
238
239          #do nothing if the old set of tags is the same as the new one
240          old_tags = set(msg.get_tags())
241          if old_tags == new_tags: continue
242
243          #set the new tags
244          msg.freeze()
245          #only remove tags if the new ones are not a superset anyway
246          if not (new_tags > old_tags): msg.remove_all_tags()
247          for tag in new_tags: msg.add_tag(tag)
248          msg.thaw()
249             
250    #-------------------------------------
251    else:
252        # unknown command
253        print "Error: Unknown command '%s' (see \"notmuch help\")" % sys.argv[1]
254
255
256    #TODO: implement
257    """
258 setup
259 new
260 search  [options...] <search-terms> [...]
261 show    <search-terms> [...]
262 reply   [options...] <search-terms> [...]
263 restore <filename>
264    """