First working version of midget/midput; for now, configure IMAP server manually
[midget] / midget.py
1 #!/usr/bin/env python
2 #
3 # Kerberos login to IMAP server and extraction of provided cid: and mid: URIs.
4 #
5 # From: Rick van Rein <rick@openfortress.nl>
6
7
8 import os
9 import sys
10 from base64 import b64encode, b64decode
11 import imaplib
12 import urllib
13
14 import kerberos
15
16
17 class SASLTongue:
18
19         def __init__ (self):
20                 self.ctx = None
21                 self.complete = False
22
23         def wrap (self, plaintext):
24                 """Once a GSSAPI Context is complete, it can wrap plaintext
25                    into ciphertext.  This function operates on binary strings.
26                 """
27                 kerberos.authGSSClientWrap (self.ctx, b64encode (plaintext))
28                 cipherdata = kerberos.authGSSClientResponse (self.ctx)
29                 return (b64decode (cipherdata) if cipherdata else "")
30
31         def unwrap (self, ciphertext):
32                 """Once a GSSAPI Context is complete, it can unwrap ciphertext
33                    into plaintext.  This function operates on binary strings.
34                 """
35                 kerberos.authGSSClientUnwrap (self.ctx, b64encode (ciphertext))
36                 return b64decode (kerberos.authGSSClientResponse (self.ctx))
37
38         def processor (self, hostname):
39                 # Currying function (needed to bind 'self')
40                 def step (rcv):
41                         #DEBUG# print 'New Call with Complete:', self.complete
42                         #DEBUG# print 'Received:', '"' + b64encode (rcv) + '"'
43                         if not self.complete:
44                                 # Initiate the GSSAPI Client
45                                 #ALT# rc, self.ctx = kerberos.authGSSClientInit ('imap@' + hostname, gssflags=kerberos.GSS_C_SEQUENCE_FLAG)
46                                 #STD# rc, self.ctx = kerberos.authGSSClientInit ('imap@' + hostname)
47                                 if not self.ctx:
48                                         rc, self.ctx = kerberos.authGSSClientInit ('imap@' + hostname)
49                                 rc = kerberos.authGSSClientStep (self.ctx, b64encode (rcv))
50                                 #DEBUG# print 'ClientStep Result Code:', ['CONTINUE', 'COMPLETE'] [rc]
51                                 if rc == kerberos.AUTH_GSS_COMPLETE:
52                                         self.complete = True
53                                 # if rc != 0:
54                                 #       print 'Error making a step'
55                                 #       return None
56                                 snd = kerberos.authGSSClientResponse (self.ctx)
57                                 return (b64decode (snd) if snd else "")
58                         else:
59                                 # Unwrap and interpret the information token
60                                 rc = kerberos.authGSSClientUnwrap (self.ctx, b64encode (rcv))
61                                 # if rc != 0:
62                                 #       print 'Error unwrapping'
63                                 #       return None
64                                 token = b64decode (kerberos.authGSSClientResponse (self.ctx))
65                                 if len (token) != 4:
66                                         #DEBUG# print 'Error unwrapping token after GSSAPI handshake'
67                                         return None
68                                 flags = ord (token [0])
69                                 #DEBUG# print 'Flags:', '0x%02x' % flags
70                                 if flags & kerberos.GSS_C_INTEG_FLAG:
71                                         pass #DEBUG# print 'Integrity Supported'
72                                 if flags & kerberos.GSS_C_CONF_FLAG:
73                                         pass #DEBUG# print 'Confidentialtiy Supported'
74                                 maxlen = (ord (token [1]) << 16) | (ord (token [2]) << 8) | (ord (token [3]))
75                                 #DEBUG# print 'Maxlen:', maxlen
76                                 rettok = (chr (0) * 4) + 'ofo'
77                                 return self.wrap (rettok)
78                                 # kerberos.authGSSClientWrap (self.ctx, b64encode (rettok))
79                                 # snd = kerberos.authGSSClientResponse (self.ctx)
80                                 # return (b64decode (snd) if snd else "")
81
82                 # The Currying surroundings return the internal function
83                 # This is a strange necessity due to the IMAP assumption
84                 # that it can call a closure, or a stateless function.
85                 # What a lot of work to evade global variables... and it's
86                 # all due to an ill-designed API, I think.
87                 return step
88
89         def clientname (self):
90                 return kerberos.authGSSClientUserName (self.ctx)
91
92
93
94 #
95 # Check the commandline
96 #
97 if len (sys.argv) < 2:
98         sys.stderr.write ('Usage: ' + sys.argv [0] + ' mid:... cid:...\n\tTo retrieve the mid: and/or cid: URIs from your IMAP mailbox\nAuthentication and mailbox identities use your current Kerberos ticket\n')
99         sys.exit (1)
100
101 #
102 # Turn the commandline into (messageid,contentid) pairs
103 #
104 todo = [ ]
105 def alsodo (todo, mid=None, cid=None):
106         if mid:
107                 mid = '<' + urllib.unquote (mid) + '>'
108         if cid:
109                 cid = '<' + urllib.unquote (cid) + '>'
110         todo.append ( (mid,cid) )
111
112 for arg in sys.argv [1:]:
113         if arg [:4].lower () == 'mid:':
114                 slashpos = arg.find ('/')
115                 if slashpos > 0:
116                         alsodo (todo, mid=arg [4:slashpos], cid=arg [slashpos+1:])
117                 else:
118                         alsodo (todo, mid=arg [4:])
119         elif arg [:4].lower () == 'cid:':
120                 alsodo (todo, cid=arg [4:])
121         else:
122                 sys.stderr.write ('You should only use mid:... and cid:... arguments, see RFC 2392\n')
123                 sys.exit (1)
124         #DEBUG# print 'Searching for', todo [-1]
125
126 remote_hostname = 'popmini.opera'
127
128 im = imaplib.IMAP4 (remote_hostname, 143)
129 authctx = SASLTongue ()
130 authcpu = authctx.processor (remote_hostname)
131 #DEBUG# print 'AuthCPU:', authcpu, '::', type (authcpu)
132 im.authenticate ('GSSAPI', authcpu)
133
134 print 'Accessing IMAP as', authctx.clientname ()
135
136 ok,msgs = im.select ()
137 if ok != 'OK':
138         sys.stderr.write ('Failed to select INBOX\n')
139         sys.exit (1)
140
141 for (mid,cid) in todo:
142         #DEBUG# print 'Retrieving', (mid,cid)
143         if mid:
144                 # This is relatively quick, Content-ID is much slower, even
145                 # as an _added_ conition (huh... Dovecot?!?)
146                 cc = '(HEADER Message-ID "' + mid + '")'
147         else:
148                 # Strange... no MIME-header search facilities in IMAP4rev1?!?
149                 cc = '(TEXT "' + cid + '")'
150         #DEBUG# print 'Search criteria:', cc
151         ok,findings = im.uid ('search', None, cc)
152         if ok != 'OK':
153                 sys.stderr.write ('Failed to search\n')
154                 sys.exit (1)
155         #DEBUG# print 'Found the following:', findings
156         for uid in findings:
157                 #DEBUG# print 'Looking up UID', uid
158                 ok,data = im.uid ('fetch', uid, 'BODYSTRUCTURE')
159                 if ok != 'OK':
160                         sys.stderr.wrote ('Error fetching body structure')
161                         sys.exit (1)
162                 #DEBUG# print 'Found', data
163                 stack = [ ]
164                 parsed = [ ]
165                 if not data [0]:
166                         sys.stderr.write ('Failed to locate content\n')
167                         sys.exit (1)
168                 unquoted = data [0].split ('"')
169                 for i in range (len (unquoted)):
170                         if i & 0x0001 == 0:
171                                 # Even entries are unquoted
172                                 w = unquoted [i]
173                                 modulus = len (w) + 3
174                                 while w != '':
175                                         brapos = min (w.find ('(') % modulus, w.find (')') % modulus, w.find (' ') % modulus)
176                                         if brapos > 0:
177                                                 if w [:brapos] == 'NIL':
178                                                         parsed.append (None)
179                                                 else:
180                                                         parsed.append (w [:brapos])
181                                         if w [brapos] == '(':
182                                                 # Push on stack
183                                                 stack.append (parsed)
184                                                 parsed = [ ]
185                                         if w [brapos] == ')':
186                                                 # Pop from stack
187                                                 tail = parsed
188                                                 parsed = stack.pop ()
189                                                 parsed.append (tail)
190                                         w = w [brapos+1:]
191                         else:
192                                 # Quoted word -- pass literally
193                                 parsed.append (unquoted [i])
194                 # print 'Parsed it into', parsed
195                 bodystructure = parsed [1] [3]
196                 #DEBUG# print 'Body structure:', bodystructure
197                 def printbody (bs, indents=0):
198                         subs = True
199                         for i in range (len (bs)):
200                                 if type (bs [i]) == type ([]):
201                                         if subs:
202                                                 printbody (bs [i], indents=indents+1)
203                                         else:
204                                                 print '  ' * indents + '{%02d}' % i
205                                 else:
206                                         # subs = False
207                                         print '  ' * indents + '[%02d]' % i, bs [i]
208                 #DEBUG# printbody (bodystructure)
209                 
210                 def matchcid (bs, cid, accupar, path=[]):
211                         subs = True
212                         for i in range (len (bs)):
213                                 if type (bs [i]) == type ([]):
214                                         if subs:
215                                                 matchcid (bs [i], cid, accupar, path=path+[i])
216                                 else:
217                                         if i == 3:
218                                                 pass #DEBUG# print 'Comparing', cid, 'with', bs [i]
219                                         if i == 3 and bs [i] == cid:
220                                                 #DEBUG# print 'CID found on:', path
221                                                 accupar.append (path)
222                                         subs = False
223                 if cid:
224                         accu = []
225                         matchcid (bodystructure, cid, accu, path=[1,3])
226                         #DEBUG# print 'Result is:', accu
227                         absname = cid [1:-2]
228                 else:
229                         accu = [[1,3,1]]
230                         absname = mid [1:-2]
231                 for result in accu:
232                         here = parsed
233                         for i in result:
234                                 here = here [i]
235                         print 'MIME-Type =', here [0] + '/' + here [1]
236                         print '[attr,value,...] =', here [2]
237                         name = None
238                         for i in range (0, len (here [2]), 2):
239                                 print 'Looking for name in', here [2][i]
240                                 if here [2][i].lower () == 'name':
241                                         name = here [2][i+1]
242                         print 'Filename:', name
243                         print 'Content-ID =', here [3] if len (here) > 3 else ''
244                         print 'Description =', here [4] if len (here) > 4 else ''
245                         print 'Transfer-Encoding =', here [5] if len (here) > 5 else ''
246                         encoding = here [5] if len (here) > 5 else ''
247                         print 'Size =', here [6] if len (here) > 6 else '?'
248                         bodyspec = 'BODY'
249                         dot = '['
250                         for r in result [2:]:
251                                 bodyspec = bodyspec + dot + str (r+1)
252                                 dot = '.'
253                         bodyspec = bodyspec + ']'
254                         if bodyspec == 'BODY]':
255                                 bodyspec = 'BODY[1]'
256                         print 'Fetchable bodyspec', bodyspec, 'for UID', uid
257                         ok,data = im.uid ('fetch', uid+':'+uid, '('+bodyspec+')')
258                         if ok != 'OK':
259                                 sys.stderr.write ('Error fetching content')
260                                 sys.exit (1)
261                         #TODO# Be more subtle about encoding lists
262                         if os.path.exists (absname):
263                                 sys.stderr.write ('Fatal: file ' + absname + ' already exists\nYou probably ran the command twice; or else the sender may attempt overwriting\n')
264                                 sys.exit (1)
265                         fh = open (absname, 'wb')
266                         if encoding == 'base64':
267                                 fh.write (b64decode (data [0][1]))
268                         else:
269                                 fh.write (data [0][1])
270                         fh.close ()
271                         print 'Written to:', absname
272                         if name:
273                                 if not os.path.exists (name):
274                                         os.link (absname, name)
275                                         print 'Created a link from:', name
276                                 else:
277                                         sys.stderr.write ('Warning: file ' + name + ' already exists, not linking\n')
278
279