Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
Comment: | First pass, iMap web interface |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | descendants | master | trunk |
Files: | files | file ages | folders |
SHA3-256: |
781ec6c7e901242319457f07b7f63b41 |
User & Date: | ajv-899-334-8894@vsta.org 2016-11-26 23:23:36 |
Context
2016-11-27
| ||
01:17 | Forward and back in message lists. Fix noop check of server status. Emphasis on date, don't use space for message index. check-in: 17e03cf724 user: ajv-899-334-8894@vsta.org tags: master, trunk | |
2016-11-26
| ||
23:23 | First pass, iMap web interface check-in: 781ec6c7e9 user: ajv-899-334-8894@vsta.org tags: master, trunk | |
Changes
Added .gitignore.
> > > |
1 2 3 |
*.pyc certs chore |
Added etc/.gitignore.
> |
1 |
*.real
|
Added etc/config.
> > > > > > > > > > > |
1 2 3 4 5 6 7 8 9 10 11 |
service "iMap Reader" # Plain old web interface serve http port 8084 iface eth0 # Crypto to the world serve https port 8094 publicCert certs/signed.crt privateKey certs/domain.key |
Added get.py.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# # get.py # Mixin to implement HTML GET operations # # Structure of paths in the server: # / # Main UI. Top part is playlist, bottom is file/dir browser # /media/<prefix>/... # For each prefix of files, its contents is served by # way of its path under here. import sys, urllib, cgi, time from email.utils import parseaddr import imap # How many messages to list at a time NMSG = 20 # The GET part of our handling class GET_mixin(object): # Configure our WPlayer GET treatment def __init__(self): # Server connection self.srv = None # GET handlers self.dispatchers.append( ("GET", self.list_folder) ) self.dispatchers.append( ("GET", self.read_msg) ) # Get server connection if needed def get_server(self): server = self.server approot = server.approot srv = self.srv if srv is not None: return srv user = self.user sys.stderr.write("uconfig %r\n" % (approot.uconfig,)) self.srv = srv = imap.get(user, approot.uconfig.get(user), approot.imaps) return srv # "/": list folders def send_top(self): # Get an imap server connection. It could be cached, # or it might be set up right now. srv = self.get_server() if srv is None: # No imap connection available self.send_error(503) return True,None buf = self.build_header("Folders") buf += '<h2>Select folder:</h2>\n' for f in srv.folders(): buf += ' <a href="/%s">%s</a><br>\n' % \ (urllib.quote_plus(f), cgi.escape(f)) buf = self.build_tailer(buf) return self.send_result(buf, "text/html") # Valid folder name. # Starts with alpha, and then alnum or '/', and with alnum def valid_fname(self, fn): # Give me a break if not fn: return False # Starts with letter, ands with alnum if (not fn[0].isalpha()) or (not fn[-1].isalnum()): return False # No empty elements between slashes tup = fn.split('/') if any((not s) for s in tup): return False # All alnum if any((not s.isalnum()) for s in tup): return False return True # See if this names a folder, then list contents def list_folder(self): pp = self.paths if len(pp) != 1: return False,None # Which folder? fn = urllib.unquote_plus(pp[0]) if not self.valid_fname(fn): return False,None # Start at which index number? if self.vals and ("from" in self.vals): try: msgidx = int(self.vals["from"]) except: return False,None else: msgidx = 1 # Ask our library to pull them together srv = self.get_server() msgs = srv.messages(fn, msgidx, NMSG) if msgs is None: return False,None # Here's your messages buf = self.build_header("Messages %d-%d" % (msgidx, msgidx+len(msgs)-1)) buf += '<table>\n' for msgidx,flags,fields in msgs: buf += ' <tr>\n' # Message #, emphasized if not marked as read if "Seen" not in flags: buf += ' <td><em>%d</em></td>\n' % (msgidx,) else: buf += ' <td>%d</td>\n' % (msgidx,) # Time if today # mm/dd if this year # mm/yy if previous years today = time.localtime() tm = srv.dt_parse(fields["date"]) # This year if tm.tm_year == today.tm_year: # Today if (tm.tm_mon == today.tm_mon) and \ (tm.tm_mday == today.tm_mday): buf += ' <td>%02d:%02d</td>\n' % \ (tm.tm_hour, tm.tm_min) else: # Some month of this year buf += ' <td>%d/%d</td>\n' % \ (tm.tm_mon, tm.tm_mday) else: # Some month of that year buf += ' <td>%d/%04d</td>\n' % \ (tm.tm_mon, tm.tm_year) # Who if "from" in fields: tup = parseaddr(fields["from"]) s = tup[0].strip() or tup[1].strip() or "--" s = s[:20] buf += ' <td>%s</td>\n' % (cgi.escape(s),) else: buf += ' <td>--</td>\n' # Subject buf += ' <td>%s</td>\n' % \ (cgi.escape(fields.get("subject", "--")),) buf += ' </tr>\n' buf += '</table>\n' buf = self.build_tailer(buf) return True,self.send_result(buf, "text/html") # Message number under folder, render contents def read_msg(self): return None |
Added imap.py.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# # imap.py # imaplib services # import imaplib, time, sys from email.header import decode_header # Wrapper around imaplib connection class Server(object): def __init__(self, user, srv): self.user = user self.srv = srv # Get list of folders def folders(self): srv = self.srv tup = srv.list() if tup[0] != "OK": sys.stderr.write("%s list failed: %s\n" % (self.user, tup[1])) return () folders = tup[1] names = set() for s in folders: # '(flags...) "/" <name>' tup = s.split('"/"') if len(tup) != 2: sys.stderr.write("Unexpected folder: %s\n" % (s,)) continue if "Noselect" in tup[0]: # Not an actual folder with messages continue names.add(tup[1].strip()) return sorted(names) # Parse mail file style fields, where continuations # are done with leading tabs of following lines # Return a list of lines, one entire field per entry def collect_fields(self, fields): res = [] for f in fields.replace('\r', '').split('\n'): # Empty, ignore if not f: continue # New field if f[0] != '\t': res.append(f) continue # Continuation assert res res[-1] += f[1:] return res # Turn this selection of message fields into a dict def field_dict(self, fields): res = {} fields = self.collect_fields(fields) for f in fields: if not f: continue if ": " not in f: continue idx = f.index(": ") v = f[idx+2:] # For encoded (typically "q code") headers dv = decode_header(v) if not dv: dv = v else: dv = dv[0][0] res[f[:idx]] = res[f[:idx].lower()] = dv return res # Convert imap message flags to set def flags_set(self, flags): if "FLAGS (" not in flags: return () # "FLAGS (\\Flag1 \\Flag2...) ..." # -> "\\Flag1 \\Flag2.." idx = flags.index("FLAGS (") flags = flags[idx+7:] if ')' not in flags: return () idx = flags.index(')') flags = flags[:idx] # Burst flags, insert simple name in set res = set() for f in flags.split(): res.add(f[1:]) return res # Parse an email Date: field as a timeval def dt_parse(self, t): # Normalize whitespace tup = t.split() base = " ".join(tup[:5]) # Try to interpret by the RFC try: tm = time.strptime(base, "%a, %d %b %Y %H:%M:%S") tmf = time.mktime(tm) hrs = tup[5] if hrs[0].isalpha(): # TBD, decode timezone name like "EST" and get # an offset. But standard Python doesn't # appear to have this?!? pass else: deltaval = 60*60*int(hrs[1:3]) + 60*int(hrs[3:5]) if hrs.startswith('+'): tmf += deltaval elif hrs.startswith('-'): tmf -= deltaval # Just sigh and use "now" if it's malformed except: tmf = time.time() return time.localtime(tmf) # Return list of metadata on messages starting at the # given index for @nmsg def messages(self, fn, msgidx, maxmsg): srv = self.srv # Select folder, get message count tup = srv.select(fn, readonly=True) if tup[0] != "OK": return None nmsg = int(tup[1][0]) # Walk messages, generating metadata for each message res = [] msgcount = 0 while msgidx <= nmsg: # Message header tup = srv.fetch(msgidx, '(FLAGS BODY[HEADER.FIELDS (FROM TO DATE STATUS SUBJECT)])') if tup[0] != "OK": sys.stderr.write("%s msg %d failed: %s\n" % (self.user, msgidx, tup[1])) break # Encode it for our convenience flags = self.flags_set(tup[1][0][0]) fields = self.field_dict(tup[1][0][1]) # Add to results res.append( (msgidx, flags, fields) ) # On to next msgidx += 1 msgcount += 1 if msgcount >= maxmsg: break return res # Get or start an imap server connection def get(user, uconfig, imaps): # Existing connection? srv = imaps.get(user) if srv is not None: # Make sure it's still alive import pdb pdb.set_trace() tup = srv.noop() if tup[0] == "OK": return srv srv.logout() del imaps[user] # New connection server = uconfig.get("server") passw = uconfig.get("pass") if "user" in uconfig: uname = uconfig["user"] else: uname = user if (not server) or (not passw): return None try: srv = imaplib.IMAP4_SSL(server) except: srv = None if srv is None: # Can't reach server return None # Authenticate tup = srv.login(uname, passw) if tup[0] != "OK": sys.stderr.write("%s imap login failed: %s\n" % (user, tup[1])) srv.logout() return None # We're on. Wrap the imaplib connection in our own state srv = Server(user, srv) imaps[user] = srv return srv |
Added main.py.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# # main.py # Main driver for ePub web reader # import sys, time import chore from get import GET_mixin from chore.authen import Authen_mixin, Authen_Server_mixin # Tie our various handlers together class App_Handler(chore.handlers.Chore_Handler, GET_mixin, Authen_mixin): def __init__(self, conn, tup, approot): chore.handlers.Chore_Handler.__init__(self, conn, tup, approot, (GET_mixin.__init__, Authen_mixin.__init__)) # Load our configuration file def load_cfg(fn): # A configurator c = chore.config.Config() # Let the web network side add its config entries chore.www.add_config(c) # Parse the input return c.load_cfg(fn) # Root of our app server class App(chore.server.Server, Authen_Server_mixin): def __init__(self, config): # Let Chore handle most things chore.server.Server.__init__(self, config, App_Handler); # Cached imap server connections self.imaps = {} # Use authentication server Authen_Server_mixin.__init__(self) self.authentication.append(Authen_mixin.auth_server) self.init_acct_server() if __name__ == "__main__": if len(sys.argv) != 2: sys.stderr.write("Usage is: %s <config-file>\n" % (sys.argv[0],)) sys.exit(1) # Create the server with config app = App(load_cfg(sys.argv[1])) # It's an HTTP service app.start_http() # HTTP servers each get their own thread. sys.exit(0) |