From 3c51d28567ce43e98bc74e13d21b067c5c21fbca Mon Sep 17 00:00:00 2001 From: pazz Date: Sat, 9 Jul 2011 11:52:41 +0100 Subject: cleanup of db.Message --- TODO | 13 +++- alot/db.py | 196 ++++++++++++++++++++++++++++++++++++++------------------ alot/widgets.py | 54 +++++++++++----- 3 files changed, 184 insertions(+), 79 deletions(-) diff --git a/TODO b/TODO index d5886162..acbbc790 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,15 @@ +rewrite widgets: + * write example howto use treewidgets + * attachments as TW + * rewrite messagewidget + +remove cmdline interface in favour of simple commands -> interpret args -> instanciate alot.command.Command + Commands * how should we do mappings? via settings, 'set' in the commandline interpreter or both? + use global commandmap to map urwid actions: 'activate/cursor down etc'. only those are interpreted by widgets! + all other will be read from buffer.bindings: in b.keypress, look for local, then global mappings, then return + widget.keypress (interprets urwid actions) * help strings in commands and help buffer * undo for commands @@ -34,6 +44,7 @@ db: * db.Message.get_replies() needs fixing. see http://notmuch.198994.n3.nabble.com/python-get-all-messages-of-a-thread-tt2996080.html * relay matched-flag to msg objects (to open only matches messages in thread buffer) + * all interpretation of the mail shoud happen in db.Message ui.commandline: possible cmds: @@ -47,7 +58,7 @@ ui: the benefit is that switching to notmuch's mimepart api later will be painless general - * gg, g5j.. combos like in vim + * gg, g5j.. combos like in vim: input->stack; rm timeoutalarm; try to interpret stack; if this fails, wait for more; on alarm remove stack * pydoc/sphinx * update dependencies in setup.py. Do we have to include all these? cmd diff --git a/alot/db.py b/alot/db.py index bee7aee0..2c92d960 100644 --- a/alot/db.py +++ b/alot/db.py @@ -110,25 +110,36 @@ class DBManager: threads = self.query(querystring).search_threads() return [thread.get_thread_id() for thread in threads] - def find_message(self, mid, writeable=False): - db = Database(path=self.path) - if writeable: - query = self.query('id:' + mid, writeable=writeable) - #TODO raise exceptions here in 01 - msg = query.search_messages().next() - else: - msg = db.find_message(mid) - return msg - - def get_message(self, mid): - """returns the message with the given id and wrapps it in a Message - - :param mid: the message id of the message to look up - :type mid: str. - :returns: Message -- the message. - - """ - return Message(self, self.find_message(mid)) + #def find_message(self, mid): + # db = Database(path=self.path) + # query = self.query('id:' + mid) + # try: + # thread = query.search_threads().next() + # def search_msg_in_replies(mid, msg): + # if msg.get_message_id() == mid: + # return msg + # else: + # replies = msg.get_replies() + # if replies is not None: + # for r in replies: + # return search_msg_in_replies(mid, r) + + # for m in thread.get_toplevel_messages(): + # return searchformsg(mid, msg) + # except: + # return None + # #TODO raise exceptions here in 01 + # msg = query.search_messages().next() + + #def get_message(self, mid): + # """returns the message with the given id and wrapps it in a Message + + # :param mid: the message id of the message to look up + # :type mid: str. + # :returns: Message -- the message. + + # """ + # return Message(self, self.find_message(mid)) def get_thread(self, tid): query = self.query('thread:' + tid) @@ -165,6 +176,7 @@ class Thread: self.oldest = datetime.fromtimestamp(thread.get_oldest_date()) self.newest = datetime.fromtimestamp(thread.get_newest_date()) self.tags = set([str(tag).decode(DB_ENC) for tag in thread.get_tags()]) + self._messages = None # will be read on demand def get_thread_id(self): return self.tid @@ -195,7 +207,7 @@ class Thread: return self.subject def _build_messages(self, acc, msg): - M = Message(self.dbman, msg) + M = Message(self.dbman, msg, thread=self) acc[M] = {} r = msg.get_replies() if r is not None: @@ -203,13 +215,14 @@ class Thread: self._build_messages(acc[M], m) def get_messages(self): - query = self.dbman.query('thread:' + self.tid) - thread = query.search_threads().next() + if not self._messages: + query = self.dbman.query('thread:' + self.tid) + thread = query.search_threads().next() - messages = {} - for m in thread.get_toplevel_messages(): - self._build_messages(messages, m) - return messages + self._messages = {} + for m in thread.get_toplevel_messages(): + self._build_messages(self._messages, m) + return self._messages def get_newest_date(self): return self.newest @@ -220,53 +233,112 @@ class Thread: def get_total_messages(self): return self.total_messages + def get_replies_to(self, msg): + msgs = self.get_messages() + if msg in msgs: + return msgs[msg] + else: + return None + class Message: - def __init__(self, dbman, msg): - self.dbman = dbman - self.mid = msg.get_message_id() - self.datetime = datetime.fromtimestamp(msg.get_date()) - self.sender = str(msg.get_header('From')).decode(DB_ENC) - self.strrep = str(msg).decode(DB_ENC) - self.email = None # will be read upon first use - self.tags = set([str(tag).decode(DB_ENC) for tag in msg.get_tags()]) + def __init__(self, dbman, msg, thread=None): + self._dbman = dbman + self._message_id = msg.get_message_id() + self._thread_id = msg.get_thread_id() + self._thread = thread + self._datetime = datetime.fromtimestamp(msg.get_date()) + self._filename = str(msg.get_filename()).decode(DB_ENC) + self._from = str(msg.get_header('From')).decode(DB_ENC) + self._email = None # will be read upon first use + self._tags = set([str(tag).decode(DB_ENC) for tag in msg.get_tags()]) def __str__(self): - return self.strrep + """prettyprint the message""" + aname, aaddress = self.get_author() + if not aname: + aname = aaddress + #tags = ','.join(self.get_tags()) + return "%s (%s)" % (aname, self.get_datestring()) + + def __hash__(self): + """Implement hash(), so we can use Message() sets""" + return hash(self._message_id) + + def __cmp__(self, other): + """Implement cmp(), so we can compare Message()s""" + res = cmp(self.get_message_id(), other.get_message_id()) + return res - def get_replies(self): - #this doesn't work. see Note in doc -> more work here. - return [self.dbman.find_message(mid) for mid in self.replies] + def get_email(self): + """returns email.Message representing this message""" + if not self._email: + f_mail = open(self.get_filename()) + self._email = email.message_from_file(f_mail) + f_mail.close() + return self._email - def get_author(self): - return helper.parse_addr(self.sender) + def get_date(self): + """returns date as datetime obj""" + return self._datetime + + def get_filename(self): + """returns absolute path of messages location""" + return self._filename + + def get_message_id(self): + """returns messages id (a string)""" + return self._message_id + + def get_thread_id(self): + """returns id of messages thread (a string)""" + return self._thread_id + + def get_message_parts(self): + """returns a list of all body parts of this message""" + out = [] + for msg in self._get_email().walk(): + if not msg.is_multipart(): + out.append(msg) + return out def get_tags(self): - return list(self.tags) + """returns tags attached to this message as list of stings""" + return list(self._tags) - def get_email(self): - if not self.email: - self.email = self.read_mail(self.get_filename()) - return self.email - - def read_mail(self, filename): - try: - f_mail = open(filename) - except EnvironmentError: - eml = email.message_from_string('Unable to open the file') - else: - eml = email.message_from_file(f_mail) - f_mail.close() - return eml + def get_thread(self): + """returns the thread this msg belongs to as alot.db.Thread object""" + if not self._thread: + self._thread = seld._dbman.get_thread(self._thread_id) + return self._thread + + def get_replies(self): + """returns a list of replies to this msg""" + t = self.get_tread() + return t.get_replies_to(self) + + def get_datestring(self, pretty=True): + """returns formated datestring in sup-style, eg: 'Yest.3pm'""" + return helper.pretty_datetime(self._datetime) + + def get_author(self): + """returns realname and address pair of this messages author""" + return email.Utils.parseaddr(self._from) def add_tags(self, tags): - self.dbman.tag('id:' + self.mid, tags) - self.tags = self.tags.union(tags) + """adds tags from message + + :param tags: tags to add + :type tags: list of str + """ + self._dbman.tag('id:' + self._message_id, tags) + self._tags = self._tags.union(tags) def remove_tags(self, tags): - self.dbman.untag('id:' + self.mid, tags) - self.tags = self.tags.difference(tags) + """remove tags from message - def get_filename(self): - m = self.dbman.find_message(self.mid) - return m.get_filename() + :param tags: tags to remove + :type tags: list of str + """ + self._dbman.untag('id:' + self._message_id, tags) + self._tags = self._tags.difference(tags) diff --git a/alot/widgets.py b/alot/widgets.py index 059eda30..9915b93f 100644 --- a/alot/widgets.py +++ b/alot/widgets.py @@ -293,6 +293,11 @@ class MessageWidget(urwid.WidgetWrap): self.displayed_list.insert(1, hw) self.rebuild() + def toggle_full_header(self): + """toggles if message headers are shown""" + hw = self._get_header_widget().widget_list[-1] + hw.toggle_all() + def toggle_body(self): """toggles if message body is shown""" bw = self._get_body_widget() @@ -306,12 +311,15 @@ class MessageWidget(urwid.WidgetWrap): def selectable(self): return True + #TODO: this needs to go in favour of a binding in the buffer! def keypress(self, size, key): if key == 'h': self.toggle_header() elif key == 'enter': self.toggle_header() self.toggle_body() + elif key == 'H': + self.toggle_full_header() else: return self.pile.keypress(size, key) @@ -343,11 +351,7 @@ class MessageSummaryWidget(urwid.WidgetWrap): prefix = "- " if self.folded: prefix = '+ ' - aname, aaddress = self.message.get_author() - if not aname: - aname = aaddress - return "%s%s (%s)" % (prefix, aname, - pretty_datetime(self.message.datetime)) + return "%s%s" % (prefix, str(self.message)) def toggle_folded(self): self.folded = not self.folded @@ -374,18 +378,33 @@ class MessageHeaderWidget(urwid.AttrMap): :type state: list(str) """ self.eml = eml - headerlines = [] + self.display_all = False + self.displayed_headers = displayed_headers + headerlines = self._build_lines(displayed_headers) + urwid.AttrMap.__init__(self, urwid.Pile(headerlines), 'message_header') + + def toggle_all(self): + if self.display_all: + self.display_all = False + headerlines = self._build_lines(self.displayed_headers) + else: + self.display_all = True + headerlines = self._build_lines(None) + self._w = urwid.Pile(headerlines) + + def _build_lines(self, displayed): max_key_len = 1 - if not displayed_headers: - displayed_headers = eml.keys() - for key in displayed_headers: - if key in eml: + headerlines = [] + if not displayed: + displayed = self.eml.keys() + for key in displayed: + if key in self.eml: if len(key) > max_key_len: max_key_len = len(key) - for key in displayed_headers: + for key in displayed: #todo: parse from,cc,bcc seperately into name-addr-widgets - if key in eml: - valuelist = email.header.decode_header(eml[key]) + if key in self.eml: + valuelist = email.header.decode_header(self.eml[key]) value = '' for v, enc in valuelist: if enc: @@ -400,7 +419,7 @@ class MessageHeaderWidget(urwid.AttrMap): valuew = urwid.Text(('message_header_value', value)) line = urwid.Columns([keyw, valuew]) headerlines.append(line) - urwid.AttrMap.__init__(self, urwid.Pile(headerlines), 'message_header') + return headerlines def selectable(self): return True @@ -439,7 +458,10 @@ class MessageBodyWidget(urwid.AttrMap): tmpfile = tempfile.NamedTemporaryFile(delete=False, suffix='.html') #write payload to tmpfile - tmpfile.write(raw_payload.encode('utf8')) + if part.get_content_maintype() == 'text': + tmpfile.write(raw_payload.encode('utf8')) + else: + tmpfile.write(raw_payload) #create and call external command cmd = handler % tmpfile.name rendered_payload = cmd_output(cmd) @@ -448,7 +470,7 @@ class MessageBodyWidget(urwid.AttrMap): os.unlink(tmpfile.name) if rendered_payload: # handler had output bodytxt += rendered_payload.decode('utf8').strip() - elif part.get_content_maintype() == 'text': # revert to plaintext + elif part.get_content_maintype() == 'text': bodytxt += raw_payload # else drop urwid.AttrMap.__init__(self, urwid.Text(bodytxt), 'message_body') -- cgit v1.2.3