@echo "Which can be verified with:"
@echo ""
@echo " $(RELEASE_URL)/$(SHA256_FILE)"
- @echo -n " "
- @cat releases/$(SHA256_FILE)
+ @sed "s/^/ /" releases/$(SHA256_FILE)
@echo ""
@echo " $(RELEASE_URL)/$(DETACHED_SIG_FILE)"
@echo " (signed by `getent passwd "$$USER" | cut -d: -f 5 | cut -d, -f 1`)"
.PHONY: verify-newer
verify-newer:
- @echo -n "Checking that no $(VERSION) release already exists..."
+ @printf %s "Checking that no $(VERSION) release already exists..."
@wget -q --no-check-certificate -O /dev/null $(RELEASE_URL)/$(TAR_FILE) ; \
case $$? in \
8) echo "Good." ;; \
+New add-on tool: notmuch-web
+-----------------------------
+
+The new contrib/ tool `notmuch-web` is a very thin web client. It
+supports a full search interface for one user: there is no facility
+for multiple users provided today. See the notmuch-web README file
+for more information.
+
+Be careful about running it on a network-connected system: it will
+expose a web interface that requires no authentication but exposes
+your mail store.
+
Notmuch 0.35 (2022-02-06)
=========================
typedef struct _notmuch_string_map_iterator notmuch_message_properties_t;
typedef struct _notmuch_directory notmuch_directory_t;
typedef struct _notmuch_filenames notmuch_filenames_t;
- typedef struct _notmuch_config_list notmuch_config_list_t;
+ typedef struct _notmuch_config_pairs notmuch_config_pairs_t;
typedef struct _notmuch_indexopts notmuch_indexopts_t;
const char *
notmuch_database_set_config (notmuch_database_t *db, const char *key, const char *value);
notmuch_status_t
notmuch_database_get_config (notmuch_database_t *db, const char *key, char **value);
- notmuch_status_t
- notmuch_database_get_config_list (notmuch_database_t *db, const char *prefix, notmuch_config_list_t **out);
+ notmuch_config_pairs_t *
+ notmuch_config_get_pairs (notmuch_database_t *db, const char *prefix);
notmuch_bool_t
- notmuch_config_list_valid (notmuch_config_list_t *config_list);
+ notmuch_config_pairs_valid (notmuch_config_pairs_t *config_list);
const char *
- notmuch_config_list_key (notmuch_config_list_t *config_list);
+ notmuch_config_pairs_key (notmuch_config_pairs_t *config_list);
const char *
- notmuch_config_list_value (notmuch_config_list_t *config_list);
+ notmuch_config_pairs_value (notmuch_config_pairs_t *config_list);
void
- notmuch_config_list_move_to_next (notmuch_config_list_t *config_list);
+ notmuch_config_pairs_move_to_next (notmuch_config_pairs_t *config_list);
void
- notmuch_config_list_destroy (notmuch_config_list_t *config_list);
+ notmuch_config_pairs_destroy (notmuch_config_pairs_t *config_list);
"""
)
def __init__(self, parent, iter_p):
super().__init__(
parent, iter_p,
- fn_destroy=capi.lib.notmuch_config_list_destroy,
- fn_valid=capi.lib.notmuch_config_list_valid,
- fn_get=capi.lib.notmuch_config_list_key,
- fn_next=capi.lib.notmuch_config_list_move_to_next)
+ fn_destroy=capi.lib.notmuch_config_pairs_destroy,
+ fn_valid=capi.lib.notmuch_config_pairs_valid,
+ fn_get=capi.lib.notmuch_config_pairs_key,
+ fn_next=capi.lib.notmuch_config_pairs_move_to_next)
def __next__(self):
- item = super().__next__()
- return base.BinString.from_cffi(item)
-
+ # skip pairs whose value is NULL
+ while capi.lib.notmuch_config_pairs_valid(super()._iter_p):
+ val_p = capi.lib.notmuch_config_pairs_value(super()._iter_p)
+ key_p = capi.lib.notmuch_config_pairs_key(super()._iter_p)
+ if key_p == capi.ffi.NULL:
+ # this should never happen
+ raise errors.NullPointerError
+ key = base.BinString.from_cffi(key_p)
+ capi.lib.notmuch_config_pairs_move_to_next(super()._iter_p)
+ if val_p != capi.ffi.NULL and base.BinString.from_cffi(val_p) != "":
+ return key
+ self._destroy()
+ raise StopIteration
class ConfigMapping(base.NotmuchObject, collections.abc.MutableMapping):
- """The config key/value pairs stored in the database.
+ """The config key/value pairs loaded from the database, config file,
+ and and/or defaults.
The entries are exposed as a :class:`collections.abc.MutableMapping` object.
Note that setting a value to an empty string is the same as deleting it.
+ Mutating (deleting or updating values) in the map persists only in
+ the database, which can be shadowed by config files.
+
:param parent: the parent object
:param ptr_name: the name of the attribute on the parent which will
return the memory pointer. This allows this object to
access the pointer via the parent's descriptor and thus
trigger :class:`MemoryPointer`'s memory safety.
+
"""
def __init__(self, parent, ptr_name):
:raises NullPointerError: If the iterator can not be created.
"""
- configlist_pp = capi.ffi.new('notmuch_config_list_t**')
- ret = capi.lib.notmuch_database_get_config_list(self._ptr(), b'', configlist_pp)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
- return ConfigIter(self._parent, configlist_pp[0])
+ config_pairs_p = capi.lib.notmuch_config_get_pairs(self._ptr(), b'')
+ if config_pairs_p == capi.ffi.NULL:
+ raise KeyError
+ return ConfigIter(self._parent, config_pairs_p)
def __len__(self):
return sum(1 for t in self)
print(repr(val))
def test_iter(self, db):
- assert list(db.config) == []
- db.config['spam'] = 'ham'
- db.config['eggs'] = 'bacon'
- assert set(db.config) == {'spam', 'eggs'}
- assert set(db.config.keys()) == {'spam', 'eggs'}
- assert set(db.config.values()) == {'ham', 'bacon'}
- assert set(db.config.items()) == {('spam', 'ham'), ('eggs', 'bacon')}
+ def has_prefix(x):
+ return x.startswith('TEST.')
+
+ assert [ x for x in db.config if has_prefix(x) ] == []
+ db.config['TEST.spam'] = 'TEST.ham'
+ db.config['TEST.eggs'] = 'TEST.bacon'
+ assert { x for x in db.config if has_prefix(x) } == {'TEST.spam', 'TEST.eggs'}
+ assert { x for x in db.config.keys() if has_prefix(x) } == {'TEST.spam', 'TEST.eggs'}
+ assert { x for x in db.config.values() if has_prefix(x) } == {'TEST.ham', 'TEST.bacon'}
+ assert { (x, y) for (x,y) in db.config.items() if has_prefix(x) } == \
+ {('TEST.spam', 'TEST.ham'), ('TEST.eggs', 'TEST.bacon')}
def test_len(self, db):
- assert len(db.config) == 0
+ defaults = len(db.config)
db.config['spam'] = 'ham'
- assert len(db.config) == 1
+ assert len(db.config) == defaults + 1
db.config['eggs'] = 'bacon'
- assert len(db.config) == 2
+ assert len(db.config) == defaults + 2
def test_del(self, db):
db.config['spam'] = 'ham'
-|-----------+----------------------------------------+-------------------------------------------------------+-----------------------------------------|
-| Key | Search Mode | Show Mode | Tree Mode |
-|-----------+----------------------------------------+-------------------------------------------------------+-----------------------------------------|
-| a | notmuch-search-archive-thread | notmuch-show-archive-message-then-next-or-next-thread | notmuch-tree-archive-message-then-next |
-| b | notmuch-search-scroll-down | notmuch-show-resend-message | notmuch-show-resend-message |
-| c | notmuch-search-stash-map | notmuch-show-stash-map | notmuch-show-stash-map |
-| d | | | |
-| e | | | (notmuch-tree-button-activate) |
-| f | | notmuch-show-forward-message | notmuch-show-forward-message |
-| g | | | |
-| h | | notmuch-show-toggle-visibility-headers | |
-| i | | | |
-| j | notmuch-jump-search | notmuch-jump-search | notmuch-jump-search |
-| k | notmuch-tag-jump | notmuch-tag-jump | notmuch-tag-jump |
-| l | notmuch-search-filter | notmuch-show-filter-thread | notmuch-tree-filter |
-| m | notmuch-mua-new-mail | notmuch-mua-new-mail | notmuch-mua-new-mail |
-| n | notmuch-search-next-thread | notmuch-show-next-open-message | notmuch-tree-next-matching-message |
-| o | notmuch-search-toggle-order | | notmuch-tree-toggle-order |
-| p | notmuch-search-previous-thread | notmuch-show-previous-open-message | notmuch-tree-prev-matching-message |
-| q | notmuch-bury-or-kill-this-buffer | notmuch-bury-or-kill-this-buffer | notmuch-bury-or-kill-this-buffer |
-| r | notmuch-search-reply-to-thread-sender | notmuch-show-reply-sender | notmuch-show-reply-sender |
-| s | notmuch-search | notmuch-search | notmuch-search |
-| t | notmuch-search-filter-by-tag | toggle-truncate-lines | notmuch-tree-filter-by-tag |
-| u | | | |
-| v | | | notmuch-show-view-all-mime-parts |
-| w | | notmuch-show-save-attachments | notmuch-show-save-attachments |
-| x | notmuch-bury-or-kill-this-buffer | notmuch-show-archive-message-then-next-or-exit | notmuch-tree-quit |
-| y | | | |
-| z | notmuch-tree | notmuch-tree | notmuch-tree-to-tree |
-| A | | notmuch-show-archive-thread-then-next | notmuch-tree-archive-thread |
-| F | | notmuch-show-forward-open-messages | |
-| G | notmuch-poll-and-refresh-this-buffer | notmuch-poll-and-refresh-this-buffer | notmuch-poll-and-refresh-this-buffer |
-| N | | notmuch-show-next-message | notmuch-tree-next-message |
-| O | | | |
-| P | | notmuch-show-previous-message | notmuch-tree-prev-message |
-| R | notmuch-search-reply-to-thread | notmuch-show-reply | notmuch-show-reply |
-| S | | | notmuch-search-from-tree-current-query |
-| V | | notmuch-show-view-raw-message | notmuch-show-view-raw-message |
-| X | | notmuch-show-archive-thread-then-exit | |
-| Z | notmuch-tree-from-search-current-query | notmuch-tree-from-show-current-query | |
-| =!= | | notmuch-show-toggle-elide-non-matching | |
-| =#= | | notmuch-show-print-message | |
-| =$= | | notmuch-show-toggle-process-crypto | |
-| =*= | notmuch-search-tag-all | notmuch-show-tag-all | notmuch-tree-tag-thread |
-| + | notmuch-search-add-tag | notmuch-show-add-tag | notmuch-tree-add-tag |
-| - | notmuch-search-remove-tag | notmuch-show-remove-tag | notmuch-tree-remove-tag |
-| . | | notmuch-show-part-map | |
-| < | notmuch-search-first-thread | notmuch-show-toggle-thread-indentation | |
-| <DEL> | notmuch-search-scroll-down | notmuch-show-rewind | notmuch-tree-scroll-message-window-back |
-| <RET> | notmuch-search-show-thread | notmuch-show-toggle-message | notmuch-tree-show-message |
-| <SPC> | notmuch-search-scroll-up | notmuch-show-advance | notmuch-tree-scroll-or-next |
-| <TAB> | | notmuch-show-next-button | notmuch-show-next-button |
-| <backtab> | | notmuch-show-previous-button | notmuch-show-previous-button |
-| = | notmuch-refresh-this-buffer | notmuch-refresh-this-buffer | notmuch-tree-refresh-view |
-| > | notmuch-search-last-thread | | |
-| ? | notmuch-help | notmuch-help | notmuch-help |
-| \vert | | notmuch-show-pipe-message | notmuch-show-pipe-message |
-|-----------+----------------------------------------+-------------------------------------------------------+-----------------------------------------|
+|--------------+----------------------------------------+-------------------------------------------------------+-----------------------------------------|
+| Key | Search Mode | Show Mode | Tree Mode |
+|--------------+----------------------------------------+-------------------------------------------------------+-----------------------------------------|
+| a | notmuch-search-archive-thread | notmuch-show-archive-message-then-next-or-next-thread | notmuch-tree-archive-message-then-next |
+| b | notmuch-search-scroll-down | notmuch-show-resend-message | notmuch-show-resend-message |
+| c | notmuch-search-stash-map | notmuch-show-stash-map | notmuch-show-stash-map |
+| d | | | |
+| e | | | (notmuch-tree-button-activate) |
+| f | | notmuch-show-forward-message | notmuch-show-forward-message |
+| g | | | |
+| h | | notmuch-show-toggle-visibility-headers | |
+| i | | | |
+| j | notmuch-jump-search | notmuch-jump-search | notmuch-jump-search |
+| k | notmuch-tag-jump | notmuch-tag-jump | notmuch-tag-jump |
+| l | notmuch-search-filter | notmuch-show-filter-thread | notmuch-tree-filter |
+| m | notmuch-mua-new-mail | notmuch-mua-new-mail | notmuch-mua-new-mail |
+| n | notmuch-search-next-thread | notmuch-show-next-open-message | notmuch-tree-next-matching-message |
+| o | notmuch-search-toggle-order | | notmuch-tree-toggle-order |
+| p | notmuch-search-previous-thread | notmuch-show-previous-open-message | notmuch-tree-prev-matching-message |
+| q | notmuch-bury-or-kill-this-buffer | notmuch-bury-or-kill-this-buffer | notmuch-bury-or-kill-this-buffer |
+| r | notmuch-search-reply-to-thread-sender | notmuch-show-reply-sender | notmuch-show-reply-sender |
+| s | notmuch-search | notmuch-search | notmuch-search |
+| t | notmuch-search-filter-by-tag | toggle-truncate-lines | notmuch-tree-filter-by-tag |
+| u | | | |
+| v | | | notmuch-show-view-all-mime-parts |
+| w | | notmuch-show-save-attachments | notmuch-show-save-attachments |
+| x | notmuch-bury-or-kill-this-buffer | notmuch-show-archive-message-then-next-or-exit | notmuch-tree-quit |
+| y | | | |
+| z | notmuch-tree | notmuch-tree | notmuch-tree-to-tree |
+| A | | notmuch-show-archive-thread-then-next | notmuch-tree-archive-thread |
+| F | | notmuch-show-forward-open-messages | |
+| G | notmuch-poll-and-refresh-this-buffer | notmuch-poll-and-refresh-this-buffer | notmuch-poll-and-refresh-this-buffer |
+| N | | notmuch-show-next-message | notmuch-tree-next-message |
+| O | | | |
+| P | | notmuch-show-previous-message | notmuch-tree-prev-message |
+| R | notmuch-search-reply-to-thread | notmuch-show-reply | notmuch-show-reply |
+| S | | | notmuch-search-from-tree-current-query |
+| V | | notmuch-show-view-raw-message | notmuch-show-view-raw-message |
+| X | | notmuch-show-archive-thread-then-exit | |
+| Z | notmuch-tree-from-search-current-query | notmuch-tree-from-show-current-query | |
+| =!= | | notmuch-show-toggle-elide-non-matching | |
+| =#= | | notmuch-show-print-message | |
+| =$= | | notmuch-show-toggle-process-crypto | |
+| =*= | notmuch-search-tag-all | notmuch-show-tag-all | notmuch-tree-tag-thread |
+| + | notmuch-search-add-tag | notmuch-show-add-tag | notmuch-tree-add-tag |
+| - | notmuch-search-remove-tag | notmuch-show-remove-tag | notmuch-tree-remove-tag |
+| . | | notmuch-show-part-map | |
+| < | notmuch-search-first-thread | notmuch-show-toggle-thread-indentation | |
+| <DEL> | notmuch-search-scroll-down | notmuch-show-rewind | notmuch-tree-scroll-message-window-back |
+| <RET> | notmuch-search-show-thread | notmuch-show-toggle-message | notmuch-tree-show-message |
+| <SPC> | notmuch-search-scroll-up | notmuch-show-advance | notmuch-tree-scroll-or-next |
+| <TAB> | | notmuch-show-next-button | notmuch-show-next-button |
+| <backtab> | | notmuch-show-previous-button | notmuch-show-previous-button |
+| = | notmuch-refresh-this-buffer | notmuch-refresh-this-buffer | notmuch-tree-refresh-view |
+| > | notmuch-search-last-thread | | |
+| ? | notmuch-help | notmuch-help | notmuch-help |
+| \vert | | notmuch-show-pipe-message | notmuch-show-pipe-message |
+| [remap undo] | notmuch-tag-undo | notmuch-tag-undo | notmuch-tag-undo |
+|--------------+----------------------------------------+-------------------------------------------------------+-----------------------------------------|
--- /dev/null
+#!/usr/bin/env python3
+
+# to launch nmweb from gunicorn.
+
+from nmweb import urls, index, search, show
+import web
+
+app = web.application(urls, globals())
+
+# get the wsgi app from web.py application object
+wsgiapp = app.wsgifunc()
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import absolute_import
+
+try:
+ from urllib.parse import quote_plus
+ from urllib.parse import unquote_plus
+except ImportError:
+ from urllib import quote_plus
+ from urllib import unquote_plus
+
+from datetime import datetime
+from mailbox import MaildirMessage
+import mimetypes
+import email
+import re
+import html
+import os
+
+import bleach
+import web
+from notmuch2 import Database
+from jinja2 import Environment, FileSystemLoader # FIXME to PackageLoader
+from jinja2 import Markup
+try:
+ import bjoern # from https://github.com/jonashaag/bjoern/
+ use_bjoern = True
+except:
+ use_bjoern = False
+
+# Configuration options
+safe_tags = bleach.sanitizer.ALLOWED_TAGS + \
+ [u'div', u'span', u'p', u'br', u'table', u'tr', u'td', u'th']
+linkify_plaintext = True # delays page load by about 0.02s of 0.20s budget
+show_thread_nav = True # delays page load by about 0.04s of 0.20s budget
+
+prefix = os.environ.get('NMWEB_PREFIX', "http://localhost:8080")
+webprefix = os.environ.get('NMWEB_STATIC', prefix + "/static")
+cachedir = os.environ.get('NMWEB_CACHE', "static/cache") # special for webpy server; changeable if using your own
+cachepath = os.environ.get('NMWEB_CACHE_PATH', cachedir) # location of static cache in the local filesystem
+
+if 'NMWEB_DEBUG' in os.environ:
+ web.config.debug = True
+else:
+ web.config.debug = False
+
+# End of config options
+
+env = Environment(autoescape=True,
+ loader=FileSystemLoader('templates'))
+
+urls = (
+ '/', 'index',
+ '/search/(.*)', 'search',
+ '/show/(.*)', 'show',
+)
+
+def urlencode_filter(s):
+ if type(s) == 'Markup':
+ s = s.unescape()
+ s = s.encode('utf8')
+ s = quote_plus(s)
+ return Markup(s)
+env.filters['url'] = urlencode_filter
+
+class index:
+ def GET(self):
+ web.header('Content-type', 'text/html')
+ base = env.get_template('base.html')
+ template = env.get_template('index.html')
+ db = Database()
+ tags = db.tags
+ return template.render(tags=tags,
+ title="Notmuch webmail",
+ prefix=prefix,
+ sprefix=webprefix)
+
+class search:
+ def GET(self, terms):
+ redir = False
+ if web.input(terms=None).terms:
+ redir = True
+ terms = web.input().terms
+ terms = unquote_plus (terms)
+ if web.input(afters=None).afters:
+ afters = web.input(afters=None).afters[:-3]
+ else:
+ afters = '0'
+ if web.input(befores=None).befores:
+ befores = web.input(befores=None).befores
+ else:
+ befores = '4294967296' # 2^32
+ try:
+ if int(afters) > 0 or int(befores) < 4294967296:
+ redir = True
+ terms += ' date:@%s..@%s' % (int(afters), int(befores))
+ except ValueError:
+ pass
+ if redir:
+ raise web.seeother('/search/%s' % quote_plus(terms.encode('utf8')))
+ web.header('Content-type', 'text/html')
+ db = Database()
+ ts = db.threads(query=terms, sort=Database.SORT.NEWEST_FIRST)
+ template = env.get_template('search.html')
+ return template.generate(terms=terms,
+ ts=ts,
+ title=terms,
+ prefix=prefix,
+ sprefix=webprefix)
+
+def format_time_range(start, end):
+ if end-start < (60*60*24):
+ time = datetime.fromtimestamp(start).strftime('%Y %b %d %H:%M')
+ else:
+ start = datetime.fromtimestamp(start).strftime("%Y %b %d")
+ end = datetime.fromtimestamp(end).strftime("%Y %b %d")
+ time = "%s through %s" % (start, end)
+ return time
+env.globals['format_time_range'] = format_time_range
+
+def mailto_addrs(msg,header_name):
+ try:
+ hdr = msg.header(header_name)
+ except LookupError:
+ return ''
+
+ frm = email.utils.getaddresses([hdr])
+ return ','.join(['<a href="mailto:%s">%s</a> ' % ((l, p) if p else (l, l)) for (p, l) in frm])
+env.globals['mailto_addrs'] = mailto_addrs
+
+def link_msg(msg):
+ lnk = quote_plus(msg.messageid.encode('utf8'))
+ try:
+ subj = msg.header('Subject')
+ except LookupError:
+ subj = ""
+ out = '<a href="%s/show/%s">%s</a>' % (prefix, lnk, subj)
+ return out
+env.globals['link_msg'] = link_msg
+
+def show_msgs(msgs):
+ r = '<ul>'
+ for msg in msgs:
+ red = 'color:black; font-style:normal'
+ if msg.matched:
+ red = 'color:red; font-style:italic'
+ frm = mailto_addrs(msg,'From')
+ lnk = link_msg(msg)
+ tags = ", ".join(msg.tags)
+ rs = show_msgs(msg.replies())
+ r += '<li><span style="%s">%s—%s</span> [%s] %s</li>' % (red, frm, lnk, tags, rs)
+ r += '</ul>'
+ return r
+env.globals['show_msgs'] = show_msgs
+
+# As email.message.walk, but showing close tags as well
+def mywalk(self):
+ yield self
+ if self.is_multipart():
+ for subpart in self.get_payload():
+ for subsubpart in mywalk(subpart):
+ yield subsubpart
+ yield 'close-div'
+
+class show:
+ def GET(self, mid):
+ web.header('Content-type', 'text/html')
+ db = Database()
+ try:
+ m = db.find(mid)
+ except:
+ raise web.notfound("No such message id.")
+ template = env.get_template('show.html')
+ # FIXME add reply-all link with email.urils.getaddresses
+ # FIXME add forward link using mailto with body parameter?
+ return template.render(m=m,
+ mid=mid,
+ title=m.header('Subject'),
+ prefix=prefix,
+ sprefix=webprefix)
+
+def thread_nav(m):
+ if not show_thread_nav: return
+ db = Database()
+ thread = next(db.threads('thread:'+m.threadid))
+ prv = None
+ found = False
+ nxt = None
+ for msg in thread:
+ if m == msg:
+ found = True
+ elif not found:
+ prv = msg
+ else: # found message, but not on this loop
+ nxt = msg
+ break
+ yield "<hr><ul>"
+ if prv: yield "<li>Previous message (by thread): %s</li>" % link_msg(prv)
+ if nxt: yield "<li>Next message (by thread): %s</li>" % link_msg(nxt)
+ yield "</ul><h3>Thread:</h3>"
+ # FIXME show now takes three queries instead of 1;
+ # can we yield the message body while computing the thread shape?
+ thread = next(db.threads('thread:'+m.threadid))
+ yield show_msgs(thread.toplevel())
+ return
+env.globals['thread_nav'] = thread_nav
+
+def format_message(nm_msg, mid):
+ fn = list(nm_msg.filenames())[0]
+ msg = MaildirMessage(open(fn))
+ return format_message_walk(msg, mid)
+
+def decodeAnyway(txt, charset='ascii'):
+ try:
+ out = txt.decode(charset)
+ except:
+ try:
+ out = txt.decode('utf-8')
+ except UnicodeDecodeError:
+ out = txt.decode('latin1')
+ return out
+
+def require_protocol_prefix(attrs, new=False):
+ if not new:
+ return attrs
+ link_text = attrs[u'_text']
+ if link_text.startswith(('http:', 'https:', 'mailto:', 'git:', 'id:')):
+ return attrs
+ return None
+
+# Bleach doesn't even try to linkify id:... text, so no point invoking this yet
+def modify_id_links(attrs, new=False):
+ if attrs[(None, u'href')].startswith(u'id:'):
+ attrs[(None, u'href')] = prefix + "/show/" + attrs[(None, u'href')][3:]
+ return attrs
+
+def css_part_id(content_type, parts=[]):
+ c = content_type.replace('/', '-')
+ out = "-".join(parts + [c])
+ return out
+
+def format_message_walk(msg, mid):
+ counter = 0
+ cid_refd = []
+ parts = ['main']
+ for part in mywalk(msg):
+ if part == 'close-div':
+ parts.pop()
+ yield '</div>'
+ elif part.get_content_maintype() == 'multipart':
+ yield '<div class="multipart-%s" id="%s">' % \
+ (part.get_content_subtype(), css_part_id(part.get_content_type(), parts))
+ parts.append(part.get_content_subtype())
+ if part.get_content_subtype() == 'alternative':
+ yield '<ul>'
+ for subpart in part.get_payload():
+ yield ('<li><a href="#%s">%s</a></li>' %
+ (css_part_id(subpart.get_content_type(), parts),
+ subpart.get_content_type()))
+ yield '</ul>'
+ elif part.get_content_type() == 'message/rfc822':
+ # FIXME extract subject, date, to/cc/from into a separate template and use it here
+ yield '<div class="message-rfc822">'
+ elif part.get_content_maintype() == 'text':
+ if part.get_content_subtype() == 'plain':
+ yield '<div id="%s">' % css_part_id(part.get_content_type(), parts)
+ yield '<pre>'
+ out = part.get_payload(decode=True)
+ out = decodeAnyway(out, part.get_content_charset('ascii'))
+ out = html.escape(out)
+ out = out.encode('ascii', 'xmlcharrefreplace').decode('ascii')
+ if linkify_plaintext: out = bleach.linkify(out, callbacks=[require_protocol_prefix])
+ yield out
+ yield '</pre></div>'
+ elif part.get_content_subtype() == 'html':
+ yield '<div id="%s">' % css_part_id(part.get_content_type(), parts)
+ unb64 = part.get_payload(decode=True)
+ decoded = decodeAnyway(unb64, part.get_content_charset('ascii'))
+ cid_refd += find_cids(decoded)
+ part.set_payload(bleach.clean(replace_cids(decoded, mid), tags=safe_tags).
+ encode(part.get_content_charset('ascii'), 'xmlcharrefreplace'))
+ (filename, cid) = link_to_cached_file(part, mid, counter)
+ counter += 1
+ yield '<iframe class="embedded-html" src="%s"></iframe>' % \
+ os.path.join(prefix, cachedir, mid, filename)
+ yield '</div>'
+ else:
+ yield '<div id="%s">' % css_part_id(part.get_content_type(), parts)
+ (filename, cid) = link_to_cached_file(part, mid, counter)
+ counter += 1
+ yield '<a href="%s">%s (%s)</a>' % (os.path.join(prefix,
+ cachedir,
+ mid,
+ filename),
+ filename,
+ part.get_content_type())
+ yield '</div>'
+ elif part.get_content_maintype() == 'image':
+ (filename, cid) = link_to_cached_file(part, mid, counter)
+ if cid not in cid_refd:
+ counter += 1
+ yield '<img src="%s" alt="%s">' % (os.path.join(prefix,
+ cachedir,
+ mid,
+ filename),
+ filename)
+ else:
+ (filename, cid) = link_to_cached_file(part, mid, counter)
+ counter += 1
+ yield '<a href="%s">%s (%s)</a>' % (os.path.join(prefix,
+ cachedir,
+ mid,
+ filename),
+ filename,
+ part.get_content_type())
+env.globals['format_message'] = format_message
+
+def replace_cids(body, mid):
+ return body.replace('cid:', os.path.join(prefix, cachedir, mid)+'/')
+
+def find_cids(body):
+ return re.findall(r'cid:([^ "\'>]*)', body)
+
+def link_to_cached_file(part, mid, counter):
+ filename = part.get_filename()
+ if not filename:
+ ext = mimetypes.guess_extension(part.get_content_type())
+ if not ext:
+ ext = '.bin'
+ filename = 'part-%03d%s' % (counter, ext)
+ try:
+ os.makedirs(os.path.join(cachepath, mid))
+ except OSError:
+ pass
+ fn = os.path.join(cachepath, mid, filename) # FIXME escape mid, filename
+ fp = open(fn, 'wb')
+ if part.get_content_maintype() == 'text':
+ data = part.get_payload(decode=True)
+ data = decodeAnyway(data, part.get_content_charset('ascii')).encode('utf-8')
+ else:
+ try:
+ data = part.get_payload(decode=True)
+ except:
+ data = part.get_payload(decode=False)
+ if data:
+ fp.write(data)
+ fp.close()
+ if 'Content-ID' in part:
+ cid = part['Content-ID']
+ if cid[0] == '<' and cid[-1] == '>': cid = cid[1:-1]
+ cid_fn = os.path.join(cachepath, mid, cid) # FIXME escape mid, cid
+ try:
+ os.unlink(cid_fn)
+ except OSError:
+ pass
+ os.link(fn, cid_fn)
+ return (filename, cid)
+ else:
+ return (filename, None)
+
+if __name__ == '__main__':
+ app = web.application(urls, globals())
+ if use_bjoern:
+ bjoern.run(app.wsgifunc(), "127.0.0.1", 8080)
+ else:
+ app.run()
--- /dev/null
+/usr/share/javascript/jquery-ui/themes/base/jquery-ui.min.css
\ No newline at end of file
--- /dev/null
+pre {
+ white-space: pre-wrap;
+}
+
+.message-rfc822 {
+ border: 1px solid;
+ border-radius: 25px;
+}
+
+.embedded-html {
+ frameborder: 0;
+ border: 0;
+ scrolling: no;
+ width: 100%;
+}
--- /dev/null
+/usr/share/javascript/jquery-ui/jquery-ui.min.js
\ No newline at end of file
--- /dev/null
+/usr/share/javascript/jquery/jquery.min.js
\ No newline at end of file
--- /dev/null
+$(function(){
+ $("#after").datepicker({
+ altField: "#afters",
+ altFormat: "@",
+ changeMonth: true,
+ changeYear: true,
+ defaultDate: "-7d",
+ minDate: "01/01/1970",
+ yearRange: "2000:+0",
+ onSelect: function(selectedDate) {
+ $("#before").datepicker("option","minDate",selectedDate);
+ }
+ });
+ $("#before").datepicker({
+ altField: "#befores",
+ altFormat: "@",
+ changeMonth: true,
+ changeYear: true,
+ defaultDate: "+1d",
+ maxDate: "+1d",
+ yearRange: "2000:+0",
+ onSelect: function(selectedDate) {
+ $("#after").datepicker("option","maxDate",selectedDate);
+ }
+ });
+ $(function(){
+ $('.multipart-alternative').tabs()
+ });
+ $(function(){
+ $('.embedded-html').on('load',function(){
+ this.style.height = this.contentWindow.document.body.offsetHeight + 'px';
+ });
+ });
+});
+
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<html lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"
+ />
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+<link type="text/css" href="{{sprefix}}/css/jquery-ui.css" rel="stylesheet" />
+<link type="text/css" href="{{sprefix}}/css/notmuch-0.1.css" rel="stylesheet" />
+<script type="text/javascript" src="{{sprefix}}/js/jquery.js"></script>
+<script type="text/javascript" src="{{sprefix}}/js/jquery-ui.js"></script>
+<script type="text/javascript" src="{{sprefix}}/js/notmuch-0.1.js"></script>
+<title>{{title}}</title>
+</head><body>
+<div data-role="page">
+<div data-role="header">
+{% block searchform %}
+<form action="{{prefix}}/search/" method="GET" data-ajax="false">
+<label for="terms">Terms</label><input id="terms" name="terms">
+<label for="after">After</label><input id="after"
+name="after"><input type="hidden" id="afters" name="afters">
+<label for="before">Before</label><input id="before"
+name="before"><input id="befores" type="hidden" name="befores">
+<input type="submit" name="submit" id="submit" value="Search">
+</form>
+{% endblock searchform %}
+<h2>{{title}}</h2>
+</div>
+<div data-role="content">
+{% block content %}
+<h2>Common tags</h2>
+<ul>
+{% for tag in tags %}
+ <li><a href="search/tag:{{ tag|url }}">{{ tag|e }}</a></li>
+{% endfor %}
+</ul>
+</div>
+{% endblock content %}
+</div>
+</body></html>
--- /dev/null
+{% extends "base.html" %}
+{% block content %}
+<h2>Common tags</h2>
+<ul>
+{% for tag in tags %}
+ <li><a href="search/tag:{{ tag|url }}">{{ tag|e }}</a></li>
+{% endfor %}
+</ul>
+{% endblock content %}
--- /dev/null
+{% extends "base.html" %}
+<h1>{{ terms|e }}</h1>
+{% block content %}
+{% for t in ts %}
+ <h2>{{ t.subject|e }}</h2>
+ <p><i>{{ t.authors|e }}</i></p>
+ <p><b>{{ format_time_range(t.first,t.last)|e }}</b></p>
+ {{ show_msgs(t.toplevel())|safe }}
+{% endfor %}
+{% endblock content %}
--- /dev/null
+{% extends "base.html" %}
+{% block content %}
+{% set headers = ['Subject', 'Date'] %}
+{% set addr_headers = ['To', 'Cc', 'From'] %}
+{% for header in headers: %}
+<p><b>{{header}}:</b>{{m.header(header)|e}}</p>
+{% endfor %}
+{% for header in addr_headers: %}
+<p><b>{{header}}:</b>{{mailto_addrs(m,header)|safe}}</p>
+{% endfor %}
+<hr>
+{% for part in format_message(m,mid): %}{{ part|safe }}{% endfor %}
+{% for b in thread_nav(m): %}{{b|safe}}{% endfor %}
+<hr>
+{% endblock content %}
--- /dev/null
+review escaping and safety handling mail from Bad People
+
+revise template loader---can we make this faster?
+
+add reply-all link with email.urils.getaddresses
+
+change current reply links to quote body
+
+add forward link using mailto with body parameter?
+
+unescape the current search term, including translating back dates
+
+
+later: json support, iOS app?
# In the rest of this file, tests collect list of errors to be fixed
-echo -n "Checking that git working directory is clean... "
+printf %s "Checking that git working directory is clean... "
git_status=`git status --porcelain`
if [ "$git_status" = '' ]
then
append_emsg " Please follow the instructions in RELEASING to choose a version"
}
-echo -n "Checking that '$VERSION' is good with digits and periods... "
+printf %s "Checking that '$VERSION' is good with digits and periods... "
case $VERSION in
*[!0-9.]*)
verfail "'$VERSION' contains other characters than digits and periods" ;;
*) verfail "'$VERSION' is a single number" ;;
esac
-echo -n "Checking that this is Debian package for notmuch... "
+printf %s "Checking that this is Debian package for notmuch... "
read deb_notmuch deb_version rest < debian/changelog
if [ "$deb_notmuch" = 'notmuch' ]
then
append_emsg "Package name '$deb_notmuch' is not 'notmuch' in debian/changelog"
fi
-echo -n "Checking that Debian package version is $VERSION-1... "
+printf %s "Checking that Debian package version is $VERSION-1... "
if [ "$deb_version" = "($VERSION-1)" ]
then
append_emsg "Version '$deb_version' is not '($VERSION-1)' in debian/changelog"
fi
-echo -n "Checking that python bindings version is $VERSION... "
+printf %s "Checking that python bindings version is $VERSION... "
py_version=`python3 -c "with open('$PV_FILE') as vf: exec(vf.read()); print(__VERSION__)"`
if [ "$py_version" = "$VERSION" ]
then
append_emsg "Version '$py_version' is not '$VERSION' in $PV_FILE"
fi
-echo -n "Checking that NEWS header is tidy... "
+printf %s "Checking that NEWS header is tidy... "
if [ "`exec sed 's/./=/g; 1q' NEWS`" = "`exec sed '1d; 2q' NEWS`" ]
then
echo Yes.
fi
fi
-echo -n "Checking that this is Notmuch NEWS... "
+printf %s "Checking that this is Notmuch NEWS... "
read news_notmuch news_version news_date < NEWS
if [ "$news_notmuch" = "Notmuch" ]
then
append_emsg "First word '$news_notmuch' is not 'Notmuch' in NEWS file"
fi
-echo -n "Checking that NEWS version is $VERSION... "
+printf %s "Checking that NEWS version is $VERSION... "
if [ "$news_version" = "$VERSION" ]
then
echo Yes.
#eval `date '+year=%Y mon=%m day=%d'`
today0utc=`date --date=0Z +%s` # gnu date feature
-echo -n "Checking that NEWS date is right... "
+printf %s "Checking that NEWS date is right... "
case $news_date in
'('[2-9][0-9][0-9][0-9]-[01][0-9]-[0123][0-9]')')
newsdate0utc=`nd=${news_date#\\(}; date --date="${nd%)} 0Z" +%s`
esac
year=`exec date +%Y`
-echo -n "Checking that copyright in documentation contains 2009-$year... "
+printf %s "Checking that copyright in documentation contains 2009-$year... "
# Read the value of variable `copyright' defined in 'doc/conf.py'.
copyrightline=$(grep ^copyright doc/conf.py)
case $copyrightline in
paths are presumed relative to `$HOME` for items in section
**database**.
-database.path
- Notmuch will store its database here, (in
- sub-directory named ``.notmuch`` if **database.mail\_root**
- is unset).
-
- Default: see :ref:`database`
+built_with.<name>
+ Compile time feature <name>. Current possibilities include
+ "retry_lock" (configure option, included by default).
+ (since notmuch 0.30, "compact" and "field_processor" are
+ always included.)
-database.mail_root
- The top-level directory where your mail currently exists and to
- where mail will be delivered in the future. Files should be
- individual email messages.
+database.autocommit
- History: this configuration value was introduced in notmuch 0.32.
+ How often to commit transactions to disk. `0` means wait until
+ command completes, otherwise an integer `n` specifies to commit to
+ disk after every `n` completed transactions.
- Default: For compatibility with older configurations, the value of
- database.path is used if **database.mail\_root** is unset.
+ History: this configuration value was introduced in notmuch 0.33.
database.backup_dir
Directory to store tag dumps when upgrading database.
Default: See HOOKS, below.
-database.autocommit
-
- How often to commit transactions to disk. `0` means wait until
- command completes, otherwise an integer `n` specifies to commit to
- disk after every `n` completed transactions.
-
- History: this configuration value was introduced in notmuch 0.33.
-
-user.name
- Your full name.
-
- Default: ``$NAME`` variable if set, otherwise read from
- ``/etc/passwd``.
-
-user.primary\_email
- Your primary email address.
-
- Default: ``$EMAIL`` variable if set, otherwise constructed from
- the username and hostname of the current machine.
-
-user.other\_email
- A list of other email addresses at which you receive email.
-
- Default: not set.
-
-new.tags
- A list of tags that will be added to all messages incorporated by
- **notmuch new**.
-
- Default: ``unread;inbox``.
-
-new.ignore
- A list to specify files and directories that will not be searched
- for messages by :any:`notmuch-new(1)`. Each entry in the list is either:
-
- A file or a directory name, without path, that will be ignored,
- regardless of the location in the mail store directory hierarchy.
-
- Or:
-
- A regular expression delimited with // that will be matched
- against the path of the file or directory relative to the database
- path. Matching files and directories will be ignored. The
- beginning and end of string must be explicitly anchored. For
- example, /.*/foo$/ would match "bar/foo" and "bar/baz/foo", but
- not "foo" or "bar/foobar".
-
- Default: empty list.
-
-search.exclude\_tags
- A list of tags that will be excluded from search results by
- default. Using an excluded tag in a query will override that
- exclusion.
-
- Default: empty list. Note that :any:`notmuch-setup(1)` puts
- ``deleted;spam`` here when creating new configuration file.
-
-.. _show.extra_headers:
-
-show.extra\_headers
+.. _database.mail_root:
- By default :any:`notmuch-show(1)` includes the following headers
- in structured output if they are present in the message:
- `Subject`, `From`, `To`, `Cc`, `Bcc`, `Reply-To`, `Date`. This
- option allows the specification of a list of further
- headers to output.
-
- History: This configuration value was introduced in notmuch 0.35.
-
- Default: empty list.
+database.mail_root
+ The top-level directory where your mail currently exists and to
+ where mail will be delivered in the future. Files should be
+ individual email messages.
-maildir.synchronize\_flags
- If true, then the following maildir flags (in message filenames)
- will be synchronized with the corresponding notmuch tags:
+ History: this configuration value was introduced in notmuch 0.32.
- +--------+-----------------------------------------------+
- | Flag | Tag |
- +========+===============================================+
- | D | draft |
- +--------+-----------------------------------------------+
- | F | flagged |
- +--------+-----------------------------------------------+
- | P | passed |
- +--------+-----------------------------------------------+
- | R | replied |
- +--------+-----------------------------------------------+
- | S | unread (added when 'S' flag is not present) |
- +--------+-----------------------------------------------+
+ Default: For compatibility with older configurations, the value of
+ database.path is used if **database.mail\_root** is unset.
- The :any:`notmuch-new(1)` command will notice flag changes in
- filenames and update tags, while the :any:`notmuch-tag(1)` and
- :any:`notmuch-restore(1)` commands will notice tag changes and
- update flags in filenames.
+database.path
+ Notmuch will store its database here, (in
+ sub-directory named ``.notmuch`` if **database.mail\_root**
+ is unset).
- If there have been any changes in the maildir (new messages added,
- old ones removed or renamed, maildir flags changed, etc.), it is
- advisable to run :any:`notmuch-new(1)` before
- :any:`notmuch-tag(1)` or :any:`notmuch-restore(1)` commands to
- ensure the tag changes are properly synchronized to the maildir
- flags, as the commands expect the database and maildir to be in
- sync.
+ Default: see :ref:`database`
- Default: ``true``.
+.. _index.decrypt:
index.decrypt
Policy for decrypting encrypted messages during indexing. Must be
Default: ``auto``.
+.. _index.header:
+
index.header.<prefix>
Define the query prefix <prefix>, based on a mail header. For
example ``index.header.List=List-Id`` will add a probabilistic
supported. See :any:`notmuch-search-terms(7)` for a list of existing
prefixes, and an explanation of probabilistic prefixes.
-built_with.<name>
- Compile time feature <name>. Current possibilities include
- "retry_lock" (configure option, included by default).
- (since notmuch 0.30, "compact" and "field_processor" are
- always included.)
+.. _maildir.synchronize_flags:
+
+maildir.synchronize\_flags
+ If true, then the following maildir flags (in message filenames)
+ will be synchronized with the corresponding notmuch tags:
+
+ +--------+-----------------------------------------------+
+ | Flag | Tag |
+ +========+===============================================+
+ | D | draft |
+ +--------+-----------------------------------------------+
+ | F | flagged |
+ +--------+-----------------------------------------------+
+ | P | passed |
+ +--------+-----------------------------------------------+
+ | R | replied |
+ +--------+-----------------------------------------------+
+ | S | unread (added when 'S' flag is not present) |
+ +--------+-----------------------------------------------+
+
+ The :any:`notmuch-new(1)` command will notice flag changes in
+ filenames and update tags, while the :any:`notmuch-tag(1)` and
+ :any:`notmuch-restore(1)` commands will notice tag changes and
+ update flags in filenames.
+
+ If there have been any changes in the maildir (new messages added,
+ old ones removed or renamed, maildir flags changed, etc.), it is
+ advisable to run :any:`notmuch-new(1)` before
+ :any:`notmuch-tag(1)` or :any:`notmuch-restore(1)` commands to
+ ensure the tag changes are properly synchronized to the maildir
+ flags, as the commands expect the database and maildir to be in
+ sync.
+
+ Default: ``true``.
+
+.. _new.ignore:
+
+new.ignore
+ A list to specify files and directories that will not be searched
+ for messages by :any:`notmuch-new(1)`. Each entry in the list is either:
+
+ A file or a directory name, without path, that will be ignored,
+ regardless of the location in the mail store directory hierarchy.
+
+ Or:
+
+ A regular expression delimited with // that will be matched
+ against the path of the file or directory relative to the database
+ path. Matching files and directories will be ignored. The
+ beginning and end of string must be explicitly anchored. For
+ example, /.*/foo$/ would match "bar/foo" and "bar/baz/foo", but
+ not "foo" or "bar/foobar".
+
+ Default: empty list.
+
+.. _new.tags:
+
+new.tags
+ A list of tags that will be added to all messages incorporated by
+ **notmuch new**.
+
+ Default: ``unread;inbox``.
query.<name>
Expansion for named query called <name>. See
:any:`notmuch-search-terms(7)` for more information about named
queries.
+search.exclude\_tags
+ A list of tags that will be excluded from search results by
+ default. Using an excluded tag in a query will override that
+ exclusion.
+
+ Default: empty list. Note that :any:`notmuch-setup(1)` puts
+ ``deleted;spam`` here when creating new configuration file.
+
+.. _show.extra_headers:
+
+show.extra\_headers
+
+ By default :any:`notmuch-show(1)` includes the following headers
+ in structured output if they are present in the message:
+ `Subject`, `From`, `To`, `Cc`, `Bcc`, `Reply-To`, `Date`. This
+ option allows the specification of a list of further
+ headers to output.
+
+ History: This configuration value was introduced in notmuch 0.35.
+
+ Default: empty list.
+
squery.<name>
Expansion for named query called <name>, using s-expression syntax. See
:any:`notmuch-sexp-queries(7)` for more information about s-expression
queries.
+user.name
+ Your full name.
+
+ Default: ``$NAME`` variable if set, otherwise read from
+ ``/etc/passwd``.
+
+user.other\_email
+ A list of other email addresses at which you receive email
+ (see also, :ref:`user.primary_email <user.primary_email>`).
+
+ Default: not set.
+
+.. _user.primary_email:
+
+user.primary\_email
+ Your primary email address.
+
+ Default: ``$EMAIL`` variable if set, otherwise constructed from
+ the username and hostname of the current machine.
+
FILES
=====
**notmuch insert** reads a message from standard input and delivers it
into the maildir directory given by configuration option
-**database.mail_root**, then incorporates the message into the notmuch
+:ref:`database.mail_root <database.mail_root>`, then incorporates the message into the notmuch
database. It is an alternative to using a separate tool to deliver the
message then running :any:`notmuch-new(1)` afterwards.
The new message will be tagged with the tags specified by the
-**new.tags** configuration option, then by operations specified on the
+:ref:`new.tags <new.tags>` configuration option, then by operations specified on the
command-line: tags prefixed by '+' are added while those prefixed by '-'
are removed.
``--decrypt=nostash`` without considering the security of your
index.
- See also ``index.decrypt`` in :any:`notmuch-config(1)`.
+ See also :ref:`index.decrypt <index.decrypt>` in :any:`notmuch-config(1)`.
+
+CONFIGURATION
+=============
+
+Indexing is influenced by the configuration options
+:ref:`index.decrypt <index.decrypt>` and :ref:`index.header
+<index.header>`. Tagging
+is controlled by :ref:`new.tags <new.tags>` and
+:ref:`maildir.synchronize_flags <maildir.synchronize_flags>`. See
+:any:`notmuch-config(1)` for details.
EXIT STATUS
===========
to optimize the scanning of directories for new mail. This option turns
that optimization off.
+CONFIGURATION
+=============
+
+Indexing is influenced by the configuration options
+:ref:`index.decrypt <index.decrypt>`, :ref:`index.header
+<index.header>`, and :ref:`new.ignore <new.ignore>`. Tagging
+is controlled by :ref:`new.tags <new.tags>` and
+:ref:`maildir.synchronize_flags <maildir.synchronize_flags>`. See
+:any:`notmuch-config(1)` for details.
+
EXIT STATUS
===========
post-new
This hook is invoked by the :any:`notmuch-new(1)` command after
- new messages have been imported into the database and initial tags
- have been applied. The hook will not be run if there have been any
- errors during the scan or import.
+ any new messages have been imported into the database and initial
+ tags have been applied. The hook will not be run if there have
+ been any errors during the scan or import.
Typically this hook is used to perform additional query-based
tagging on the imported messages.
- a.list.of.words
Both parenthesised lists of terms and quoted phrases are ok with
-probabilistic prefixes such as **to:**, **from:**, and **subject:**. In particular
+probabilistic prefixes such as **to:**, **from:**, and **subject:**.
+For prefixes supporting regex search, the parenthesised list should be
+quoted. In particular
::
- subject:(pizza free)
+ subject:"(pizza free)"
is equivalent to
As is the case with :ref:`notmuch-search`, the presentation of results
can be controlled by the variable ``notmuch-search-oldest-first``.
+.. _notmuch-unthreaded:
+
+notmuch-unthreaded
+------------------
+
+``notmuch-unthreaded-mode`` is similar to :any:`notmuch-tree` in that
+each line corresponds to a single message, but no thread information
+is presented.
+
+Keybindings are the same as :any:`notmuch-tree`.
Global key bindings
===================
-Several features are accessible from anywhere in notmuch through the
+Several features are accessible from most places in notmuch through the
following key bindings:
``j``
``k``
Tagging operations using :ref:`notmuch-tag-jump`
+``C-_`` ``C-/`` ``C-x u``: Undo previous tagging operation using :ref:`notmuch-tag-undo`
+
.. _notmuch-jump:
notmuch-jump
|docstring::notmuch-tagging-keys|
+.. _notmuch-tag-undo:
+
+notmuch-tag-undo
+----------------
+
+Each notmuch buffer supporting tagging operations (i.e buffers in
+:any:`notmuch-show`, :any:`notmuch-search`, :any:`notmuch-tree`, and
+:any:`notmuch-unthreaded` mode) keeps a local stack of tagging
+operations. These can be undone via ``notmuch-tag-undo``. By default
+this is bound to the usual Emacs keys for undo.
+
+:index:`notmuch-tag-undo`
+
+ |docstring::notmuch-tag-undo|
+
Buffer navigation
=================
;; that when we modify map it does not modify widget-keymap).
(let ((map (make-composed-keymap (list (make-sparse-keymap) widget-keymap))))
(set-keymap-parent map notmuch-common-keymap)
+ ;; Currently notmuch-hello-mode supports free text entry, but not
+ ;; tagging operations, so provide standard undo.
+ (define-key map [remap notmuch-tag-undo] #'undo)
map)
"Keymap for \"notmuch hello\" buffers.")
(define-key map (kbd "M-=") 'notmuch-refresh-all-buffers)
(define-key map "G" 'notmuch-poll-and-refresh-this-buffer)
(define-key map "j" 'notmuch-jump-search)
+ (define-key map [remap undo] 'notmuch-tag-undo)
map)
"Keymap shared by all notmuch modes.")
</g>
</svg>")
+;;; track history of tag operations
+(defvar-local notmuch-tag-history nil
+ "Buffer local history of `notmuch-tag' function.")
+(put 'notmuch-tag-history 'permanent-local t)
+
;;; Format Handling
(defvar notmuch-tag--format-cache (make-hash-table :test 'equal)
"Use batch tagging if the tagging query is longer than this.
This limits the length of arguments passed to the notmuch CLI to
-avoid system argument length limits and performance problems.")
+avoid system argument length limits and performance problems.
+
+NOTE: this variable is no longer used.")
+
+(make-obsolete-variable 'notmuch-tag-argument-limit nil "notmuch 0.36")
-(defun notmuch-tag (query tag-changes)
+(defun notmuch-tag (query tag-changes &optional omit-hist)
"Add/remove tags in TAG-CHANGES to messages matching QUERY.
QUERY should be a string containing the search-terms.
-TAG-CHANGES is a list of strings of the form \"+tag\" or
-\"-tag\" to add or remove tags, respectively.
+TAG-CHANGES is a list of strings of the form \"+tag\" or \"-tag\"
+to add or remove tags, respectively. OMIT-HIST disables history
+tracking if non-nil.
Note: Other code should always use this function to alter tags of
messages instead of running (notmuch-call-notmuch-process \"tag\" ..)
(notmuch-dlet ((tag-changes tag-changes)
(query query))
(run-hooks 'notmuch-before-tag-hook))
- (if (<= (length query) notmuch-tag-argument-limit)
- (apply 'notmuch-call-notmuch-process "tag"
- (append tag-changes (list "--" query)))
- ;; Use batch tag mode to avoid argument length limitations
- (let ((batch-op (concat (mapconcat #'notmuch-hex-encode tag-changes " ")
- " -- " query)))
- (notmuch-call-notmuch-process :stdin-string batch-op "tag" "--batch")))
- (notmuch-dlet ((tag-changes tag-changes)
- (query query))
- (run-hooks 'notmuch-after-tag-hook))))
+ (with-temp-buffer
+ (insert (concat (mapconcat #'notmuch-hex-encode tag-changes " ") " -- " query))
+ (unless (= 0
+ (notmuch--call-process-region
+ (point-min) (point-max) notmuch-command t t nil "tag" "--batch"))
+ (notmuch-logged-error "notmuch tag failed" (buffer-string))))
+ (unless omit-hist
+ (push (list :query query :tag-changes tag-changes) notmuch-tag-history)))
+ (notmuch-dlet ((tag-changes tag-changes)
+ (query query))
+ (run-hooks 'notmuch-after-tag-hook)))
+
+(defun notmuch-tag-undo ()
+ "Undo the previous tagging operation in the current buffer. Uses
+buffer local variable `notmuch-tag-history' to determine what
+that operation was."
+ (interactive)
+ (when (null notmuch-tag-history)
+ (error "no further notmuch undo information"))
+ (let* ((action (pop notmuch-tag-history))
+ (query (plist-get action :query))
+ (changes (notmuch-tag-change-list (plist-get action :tag-changes) t)))
+ (notmuch-tag query changes t))
+ (notmuch-refresh-this-buffer))
(defun notmuch-tag-change-list (tags &optional reverse)
"Convert TAGS into a list of tag changes.
Supported fields are: date, count, authors, subject, tags.
For example:
(setq notmuch-search-result-format
- '((\"authors\" . \"%-40s\")
+ \\='((\"authors\" . \"%-40s\")
(\"subject\" . \"%s\")))
Line breaks are permitted in format strings (though this is
* phrase parsing, when possible */
std::string query_str;
- if (*str.rbegin () != '*' || str.find (' ') != std::string::npos)
+ if ((str.at (0) != '(' || *str.rbegin () != ')') &&
+ (*str.rbegin () != '*' || str.find (' ') != std::string::npos))
query_str = '"' + str + '"';
else
query_str = str;
return fd;
}
+static bool
+write_buf (const char *buf, int fdout, ssize_t remain)
+{
+ const char *p = buf;
+
+ do {
+ ssize_t written = write (fdout, p, remain);
+ if (written < 0 && errno == EINTR)
+ continue;
+ if (written <= 0) {
+ fprintf (stderr, "Error: writing to temporary file: %s",
+ strerror (errno));
+ return false;
+ }
+ p += written;
+ remain -= written;
+ } while (remain > 0);
+ return true;
+}
+
/*
* Copy fdin to fdout, return true on success, and false on errors and
* empty input.
copy_fd (int fdout, int fdin)
{
bool empty = true;
+ bool first = true;
+ const char *header = "X-Envelope-From: ";
while (! interrupted) {
ssize_t remain;
char buf[4096];
- char *p;
+ const char *p = buf;
remain = read (fdin, buf, sizeof (buf));
if (remain == 0)
return false;
}
- p = buf;
- do {
- ssize_t written = write (fdout, p, remain);
- if (written < 0 && errno == EINTR)
- continue;
- if (written <= 0) {
- fprintf (stderr, "Error: writing to temporary file: %s",
- strerror (errno));
+ if (first && remain >= 5 && 0 == strncmp (buf, "From ", 5)) {
+ if (! write_buf (header, fdout, strlen (header)))
return false;
- }
- p += written;
- remain -= written;
- empty = false;
- } while (remain > 0);
+ p += 5;
+ remain -= 5;
+ }
+
+ first = false;
+
+ if (! write_buf (p, fdout, remain))
+ return false;
+ empty = false;
}
return (! interrupted && ! empty);
--- /dev/null
+#!/usr/bin/env bash
+
+test_description='emacs operations'
+
+. $(dirname "$0")/perf-test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
+
+test_require_emacs
+
+time_start
+
+print_emacs_header
+
+MSGS=$(notmuch search --output=messages "*" | shuf -n 50 | awk '{printf " \"%s\"",$1}')
+
+time_emacs "tag messages" \
+"(dolist (msg (list $MSGS))
+ (notmuch-tag msg (list \"+test\"))
+ (notmuch-tag msg (list \"-test\"))))"
+
+time_done
# Ensure NOTMUCH_SRCDIR and NOTMUCH_BUILDDIR are set.
. $(dirname "$0")/../test/export-dirs.sh || exit 1
+. "$NOTMUCH_SRCDIR/test/test-vars.sh" || exit 1
+
# Where to run the tests
TEST_DIRECTORY=$NOTMUCH_BUILDDIR/performance-test
printf "\t\t\tWall(s)\tUsr(s)\tSys(s)\tRes(K)\tIn/Out(512B)\n"
}
+print_emacs_header ()
+{
+ printf "\t\t\tWall(s)\tGCs\tGC time(s)\n"
+}
+
time_run ()
{
printf " %-22s" "$1"
EOF
test_expect_equal_file EXPECTED OUTPUT
+ test_begin_subtest "Config list from python ($config)"
+ test_python <<EOF > OUTPUT
+from notmuch2 import Database
+db=Database(config=Database.CONFIG.SEARCH)
+for key in list(db.config):
+ print(key)
+EOF
+ cat <<EOF > EXPECTED
+database.autocommit
+database.backup_dir
+database.hook_dir
+database.mail_root
+database.path
+maildir.synchronize_flags
+new.tags
+user.name
+user.other_email
+user.primary_email
+EOF
+ test_expect_equal_file EXPECTED OUTPUT
case $config in
XDG*)
test_begin_subtest "Set shadowed config value in database ($config)"
test_expect_code 0 "notmuch_with_shim shim-$code insert --keep < \"$gen_msg_filename\""
done
+test_begin_subtest "insert converts mboxes on delivery"
+notmuch insert +unmboxed < "${TEST_DIRECTORY}"/corpora/indexing/mbox-attachment.eml
+output=$(notmuch count tag:unmboxed)
+test_expect_equal "${output}" 1
+
test_done
test_begin_subtest "search for non-existent message prints nothing"
notmuch search "no-message-matches-this" > OUTPUT
-echo -n >EXPECTED
+: >EXPECTED
test_expect_equal_file EXPECTED OUTPUT
test_begin_subtest "search --format=json for non-existent message prints proper empty json"
test_begin_subtest "'notmuch show --part' does not corrupt a part with CRLF pair"
notmuch show --format=raw --part=3 id:base64-part-with-crlf > crlf.out
-echo -n -e "\xEF\x0D\x0A" > crlf.expected
+printf "\xEF\x0D\x0A" > crlf.expected
test_expect_equal_file crlf.out crlf.expected
(test-output)'
test_expect_equal_file $EXPECTED/notmuch-show-thread-maildir-storage OUTPUT
-test_begin_subtest "Add tag from search view"
-os_x_darwin_thread=$(notmuch search --output=threads id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com)
-test_emacs "(notmuch-search \"$os_x_darwin_thread\")
- (notmuch-test-wait)
- (execute-kbd-macro \"+tag-from-search-view\")"
-output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
-test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox tag-from-search-view unread)"
-
-test_begin_subtest "Remove tag from search view"
-test_emacs "(notmuch-search \"$os_x_darwin_thread\")
- (notmuch-test-wait)
- (execute-kbd-macro \"-tag-from-search-view\")"
-output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
-test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox unread)"
-
-test_begin_subtest "Add tag (large query)"
-# We use a long query to force us into batch mode and use a funny tag
-# that requires escaping for batch tagging.
-test_emacs "(notmuch-tag (concat \"$os_x_darwin_thread\" \" or \" (mapconcat #'identity (make-list notmuch-tag-argument-limit \"x\") \"-\")) (list \"+tag-from-%-large-query\"))"
-output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
-test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox tag-from-%-large-query unread)"
-notmuch tag -tag-from-%-large-query $os_x_darwin_thread
-
-test_begin_subtest "notmuch-show: add single tag to single message"
-test_emacs "(notmuch-show \"$os_x_darwin_thread\")
- (execute-kbd-macro \"+tag-from-show-view\")"
-output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
-test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox tag-from-show-view unread)"
-
-test_begin_subtest "notmuch-show: remove single tag from single message"
-test_emacs "(notmuch-show \"$os_x_darwin_thread\")
- (execute-kbd-macro \"-tag-from-show-view\")"
-output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
-test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox unread)"
-
-test_begin_subtest "notmuch-show: add multiple tags to single message"
-test_emacs "(notmuch-show \"$os_x_darwin_thread\")
- (execute-kbd-macro \"+tag1-from-show-view +tag2-from-show-view\")"
-output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
-test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox tag1-from-show-view tag2-from-show-view unread)"
-
-test_begin_subtest "notmuch-show: remove multiple tags from single message"
-test_emacs "(notmuch-show \"$os_x_darwin_thread\")
- (execute-kbd-macro \"-tag1-from-show-view -tag2-from-show-view\")"
-output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
-test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox unread)"
-
-test_begin_subtest "notmuch-show: before-tag-hook is run, variables are defined"
-output=$(test_emacs '(let ((notmuch-test-tag-hook-output nil)
- (notmuch-before-tag-hook (function notmuch-test-tag-hook)))
- (notmuch-show "id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com")
- (execute-kbd-macro "+activate-hook\n")
- (execute-kbd-macro "-activate-hook\n")
- notmuch-test-tag-hook-output)')
-test_expect_equal "$output" \
-'(("id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com" "-activate-hook")
- ("id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com" "+activate-hook"))'
-
-test_begin_subtest "notmuch-show: after-tag-hook is run, variables are defined"
-output=$(test_emacs '(let ((notmuch-test-tag-hook-output nil)
- (notmuch-after-tag-hook (function notmuch-test-tag-hook)))
- (notmuch-show "id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com")
- (execute-kbd-macro "+activate-hook\n")
- (execute-kbd-macro "-activate-hook\n")
- notmuch-test-tag-hook-output)')
-test_expect_equal "$output" \
-'(("id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com" "-activate-hook")
- ("id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com" "+activate-hook"))'
-
test_begin_subtest "Message with .. in Message-Id:"
add_message [id]=123..456@example '[subject]="Message with .. in Message-Id"'
test_emacs '(notmuch-search "id:\"123..456@example\"")
This is a warning
This is another warning"
-test_begin_subtest "Search thread tag operations are race-free"
-add_message '[subject]="Search race test"'
-gen_msg_id_1=$gen_msg_id
-generate_message '[in-reply-to]="<'$gen_msg_id_1'>"' \
- '[references]="<'$gen_msg_id_1'>"' \
- '[subject]="Search race test two"'
-test_emacs '(notmuch-search "subject:\"search race test\"")
- (notmuch-test-wait)
- (notmuch-poll)
- (execute-kbd-macro "+search-thread-race-tag")'
-output=$(notmuch search --output=messages 'tag:search-thread-race-tag')
-test_expect_equal "$output" "id:$gen_msg_id_1"
-
-test_begin_subtest "Search global tag operations are race-free"
-generate_message '[in-reply-to]="<'$gen_msg_id_1'>"' \
- '[references]="<'$gen_msg_id_1'>"' \
- '[subject]="Re: Search race test"'
-test_emacs '(notmuch-search "subject:\"search race test\" -subject:two")
- (notmuch-test-wait)
- (notmuch-poll)
- (execute-kbd-macro "*+search-global-race-tag")'
-output=$(notmuch search --output=messages 'tag:search-global-race-tag')
-test_expect_equal "$output" "id:$gen_msg_id_1"
-
test_begin_subtest "Term escaping"
output=$(test_emacs "(mapcar 'notmuch-escape-boolean-term (list
\"\"
--- /dev/null
+#!/usr/bin/env bash
+
+test_description="emacs interface"
+. $(dirname "$0")/test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
+
+EXPECTED=$NOTMUCH_SRCDIR/test/emacs.expected-output
+
+test_require_emacs
+add_email_corpus
+
+test_begin_subtest "Add tag from search view"
+os_x_darwin_thread=$(notmuch search --output=threads id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com)
+test_emacs "(notmuch-search \"$os_x_darwin_thread\")
+ (notmuch-test-wait)
+ (execute-kbd-macro \"+tag-from-search-view\")"
+output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox tag-from-search-view unread)"
+
+test_begin_subtest "Remove tag from search view"
+test_emacs "(notmuch-search \"$os_x_darwin_thread\")
+ (notmuch-test-wait)
+ (execute-kbd-macro \"-tag-from-search-view\")"
+output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox unread)"
+
+test_begin_subtest "Add tag (large query)"
+# We use a long query to force us into batch mode and use a funny tag
+# that requires escaping for batch tagging.
+test_emacs "(notmuch-tag (concat \"$os_x_darwin_thread\" \" or \" (mapconcat #'identity (make-list notmuch-tag-argument-limit \"x\") \"-\")) (list \"+tag-from-%-large-query\"))"
+output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox tag-from-%-large-query unread)"
+notmuch tag -tag-from-%-large-query $os_x_darwin_thread
+
+test_begin_subtest "notmuch-show: add single tag to single message"
+test_emacs "(notmuch-show \"$os_x_darwin_thread\")
+ (execute-kbd-macro \"+tag-from-show-view\")"
+output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox tag-from-show-view unread)"
+
+test_begin_subtest "notmuch-show: remove single tag from single message"
+test_emacs "(notmuch-show \"$os_x_darwin_thread\")
+ (execute-kbd-macro \"-tag-from-show-view\")"
+output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox unread)"
+
+test_begin_subtest "notmuch-show: add multiple tags to single message"
+test_emacs "(notmuch-show \"$os_x_darwin_thread\")
+ (execute-kbd-macro \"+tag1-from-show-view +tag2-from-show-view\")"
+output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox tag1-from-show-view tag2-from-show-view unread)"
+
+test_begin_subtest "notmuch-show: remove multiple tags from single message"
+test_emacs "(notmuch-show \"$os_x_darwin_thread\")
+ (execute-kbd-macro \"-tag1-from-show-view -tag2-from-show-view\")"
+output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox unread)"
+
+test_begin_subtest "notmuch-show: before-tag-hook is run, variables are defined"
+output=$(test_emacs '(let ((notmuch-test-tag-hook-output nil)
+ (notmuch-before-tag-hook (function notmuch-test-tag-hook)))
+ (notmuch-show "id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com")
+ (execute-kbd-macro "+activate-hook\n")
+ (execute-kbd-macro "-activate-hook\n")
+ notmuch-test-tag-hook-output)')
+test_expect_equal "$output" \
+'(("id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com" "-activate-hook")
+ ("id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com" "+activate-hook"))'
+
+test_begin_subtest "notmuch-show: after-tag-hook is run, variables are defined"
+output=$(test_emacs '(let ((notmuch-test-tag-hook-output nil)
+ (notmuch-after-tag-hook (function notmuch-test-tag-hook)))
+ (notmuch-show "id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com")
+ (execute-kbd-macro "+activate-hook\n")
+ (execute-kbd-macro "-activate-hook\n")
+ notmuch-test-tag-hook-output)')
+test_expect_equal "$output" \
+'(("id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com" "-activate-hook")
+ ("id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com" "+activate-hook"))'
+
+
+test_begin_subtest "Search thread tag operations are race-free"
+add_message '[subject]="Search race test"'
+gen_msg_id_1=$gen_msg_id
+generate_message '[in-reply-to]="<'$gen_msg_id_1'>"' \
+ '[references]="<'$gen_msg_id_1'>"' \
+ '[subject]="Search race test two"'
+test_emacs '(notmuch-search "subject:\"search race test\"")
+ (notmuch-test-wait)
+ (notmuch-poll)
+ (execute-kbd-macro "+search-thread-race-tag")'
+output=$(notmuch search --output=messages 'tag:search-thread-race-tag')
+test_expect_equal "$output" "id:$gen_msg_id_1"
+
+test_begin_subtest "Search global tag operations are race-free"
+generate_message '[in-reply-to]="<'$gen_msg_id_1'>"' \
+ '[references]="<'$gen_msg_id_1'>"' \
+ '[subject]="Re: Search race test"'
+test_emacs '(notmuch-search "subject:\"search race test\" -subject:two")
+ (notmuch-test-wait)
+ (notmuch-poll)
+ (execute-kbd-macro "*+search-global-race-tag")'
+output=$(notmuch search --output=messages 'tag:search-global-race-tag')
+test_expect_equal "$output" "id:$gen_msg_id_1"
+
+test_begin_subtest "undo with empty history is an error"
+test_emacs "(let ((notmuch-tag-history nil))
+ (test-log-error
+ (notmuch-tag-undo)))
+ "
+cat <<EOF > EXPECTED
+(error no further notmuch undo information)
+EOF
+test_expect_equal_file EXPECTED MESSAGES
+
+for mode in search show tree unthreaded; do
+ test_begin_subtest "undo tagging in $mode mode"
+ test_emacs "(let ((notmuch-tag-history nil))
+ (notmuch-$mode \"$os_x_darwin_thread\")
+ (notmuch-test-wait)
+ (execute-kbd-macro \"+tag-to-be-undone-$mode\")
+ (notmuch-tag-undo)
+ (notmuch-test-wait))"
+ count=$(notmuch count "tag:tag-to-be-undone-$mode")
+ test_expect_equal "$count" "0"
+
+ test_begin_subtest "undo tagging in $mode mode (multiple operations)"
+ test_emacs "(let ((notmuch-tag-history nil))
+ (notmuch-$mode \"$os_x_darwin_thread\")
+ (notmuch-test-wait)
+ (execute-kbd-macro \"+one-$mode\")
+ (execute-kbd-macro \"+two-$mode\")
+ (notmuch-tag-undo)
+ (notmuch-test-wait)
+ (execute-kbd-macro \"+three-$mode\"))"
+ output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
+ notmuch tag "-one-$mode" "-three-$mode" $os_x_darwin_thread
+ test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox one-$mode three-$mode unread)"
+
+ test_begin_subtest "undo tagging in $mode mode (multiple undo)"
+ test_emacs "(let ((notmuch-tag-history nil))
+ (notmuch-$mode \"$os_x_darwin_thread\")
+ (notmuch-test-wait)
+ (execute-kbd-macro \"+one-$mode\")
+ (execute-kbd-macro \"+two-$mode\")
+ (notmuch-tag-undo)
+ (notmuch-test-wait)
+ (notmuch-tag-undo)
+ (notmuch-test-wait)
+ (execute-kbd-macro \"+three-$mode\"))"
+ output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
+ notmuch tag "-one-$mode" "-three-$mode" $os_x_darwin_thread
+ test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox three-$mode unread)"
+
+ test_begin_subtest "undo tagging in $mode mode (via binding)"
+ test_emacs "(let ((notmuch-tag-history nil))
+ (notmuch-$mode \"$os_x_darwin_thread\")
+ (notmuch-test-wait)
+ (execute-kbd-macro \"+tag-to-be-undone-$mode\")
+ (execute-kbd-macro (kbd \"C-x u\"))
+ (notmuch-test-wait))"
+ count=$(notmuch count "tag:tag-to-be-undone-$mode")
+ test_expect_equal "$count" "0"
+done
+
+test_done
# Check output against golden output
outcount=$(cat outcount)
- echo -n > searchall
- echo -n > expectall
+ : > searchall
+ : > expectall
for ((i = 0; i < $outcount; i++)); do
if ! cmp -s search.$i expected; then
# Find the range of interruptions that match this output
EOF
test_expect_equal_file EXPECTED OUTPUT
+test_begin_subtest "bracketed subject search (with dquotes)"
+notmuch search subject:notmuch and subject:show > EXPECTED
+notmuch search 'subject:"(show notmuch)"' > OUTPUT
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "bracketed subject search (with dquotes and operator 'or')"
+notmuch search subject:notmuch or subject:show > EXPECTED
+notmuch search 'subject:"(notmuch or show)"' > OUTPUT
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "bracketed subject search (with dquotes and operator 'and')"
+notmuch search subject:notmuch and subject:show > EXPECTED
+notmuch search 'subject:"(notmuch and show)"' > OUTPUT
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "bracketed subject search (with phrase, operator 'or')"
+notmuch search 'subject:"mailing list"' or subject:FreeBSD > EXPECTED
+notmuch search 'subject:"(""mailing list"" or FreeBSD)"' > OUTPUT
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "bracketed subject search (with phrase, operator 'and')"
+notmuch search search 'subject:"notmuch show"' and subject:commands > EXPECTED
+notmuch search 'subject:"(""notmuch show"" and commands)"' > OUTPUT
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
test_begin_subtest "xapian wildcard search for from:"
notmuch search --output=messages 'from:cwo*' > OUTPUT
test_expect_equal_file cworth.msg-ids OUTPUT
--- /dev/null
+From david@tethera.net Sat Feb 5 09:19:10 2022
+From: David Bremner <david@tethera.net>
+To: David Bremner <david@tethera.net>
+Subject: Re: [RFC PATCH v2 12/12] emacs: whitespace cleanup for keybindings
+Date: Sat, 05 Feb 2022 10:19:09 -0400
+Message-ID: <87k0e9o0pu.fsf@tethera.net>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="=-=-="
+
+--=-=-=
+Content-Type: text/plain
+Content-Disposition: inline
+
+
+I figured out the race condition in the tests. The previous test was
+still running when the failing test started, the joys of using a shared
+emacs for running all of the tests in one file.
+
+The attached diff is split into the the commits that introduce the tests
+in question in my working series, but you should be able to just apply
+it on top of the posted series if you want.
+
+
+--=-=-=
+Content-Type: text/x-diff
+Content-Disposition: inline; filename=0001-test-fixups.patch
+
+From fc88cba7f1f37b9cf3b296eace2422dd0e173502 Mon Sep 17 00:00:00 2001
+From: David Bremner <david@tethera.net>
+Date: Thu, 3 Feb 2022 21:05:05 -0400
+Subject: [PATCH] test fixups
+
+---
+ test/T315-emacs-tagging.sh | 9 ++++-----
+ 1 file changed, 4 insertions(+), 5 deletions(-)
+
+diff --git a/test/T315-emacs-tagging.sh b/test/T315-emacs-tagging.sh
+index c9e3e53a..c26413ce 100755
+--- a/test/T315-emacs-tagging.sh
++++ b/test/T315-emacs-tagging.sh
+@@ -119,7 +119,8 @@ for mode in search show tree unthreaded; do
+ (notmuch-$mode \"$os_x_darwin_thread\")
+ (notmuch-test-wait)
+ (execute-kbd-macro \"+tag-to-be-undone-$mode\")
+- (notmuch-tag-undo))"
++ (notmuch-tag-undo)
++ (notmuch-test-wait))"
+ count=$(notmuch count "tag:tag-to-be-undone-$mode")
+ test_expect_equal "$count" "0"
+
+@@ -128,9 +129,7 @@ for mode in search show tree unthreaded; do
+ (notmuch-$mode \"$os_x_darwin_thread\")
+ (notmuch-test-wait)
+ (execute-kbd-macro \"+one-$mode\")
+- (notmuch-test-wait)
+ (execute-kbd-macro \"+two-$mode\")
+- (notmuch-test-wait)
+ (notmuch-tag-undo)
+ (notmuch-test-wait)
+ (execute-kbd-macro \"+three-$mode\"))"
+@@ -143,7 +142,6 @@ for mode in search show tree unthreaded; do
+ (notmuch-$mode \"$os_x_darwin_thread\")
+ (notmuch-test-wait)
+ (execute-kbd-macro \"+one-$mode\")
+- (notmuch-test-wait)
+ (execute-kbd-macro \"+two-$mode\")
+ (notmuch-tag-undo)
+ (notmuch-test-wait)
+@@ -159,7 +157,8 @@ for mode in search show tree unthreaded; do
+ (notmuch-$mode \"$os_x_darwin_thread\")
+ (notmuch-test-wait)
+ (execute-kbd-macro \"+tag-to-be-undone-$mode\")
+- (execute-kbd-macro (kbd \"C-x u\")))"
++ (execute-kbd-macro (kbd \"C-x u\"))
++ (notmuch-test-wait))"
+ count=$(notmuch count "tag:tag-to-be-undone-$mode")
+ test_expect_equal "$count" "0"
+ done
+--
+2.30.2
+
+
+--=-=-=--
exit 1
fi
+# Explicitly require external prerequisite. Useful when binary is
+# called indirectly (e.g. from emacs).
+# Returns success if dependency is available, failure otherwise.
+test_require_external_prereq () {
+ local binary
+ binary="$1"
+ if [[ ${test_missing_external_prereq_["${binary}"]} == t ]]; then
+ # dependency is missing, call the replacement function to note it
+ eval "$binary"
+ else
+ true
+ fi
+}
+
backup_database () {
test_name=$(basename $0 .sh)
rm -rf $TMP_DIRECTORY/notmuch-dir-backup."$test_name"
${TEST_EMACSCLIENT} --socket-name="$EMACS_SERVER" --eval "(notmuch-test-progn $*)"
}
+time_emacs () {
+ rm -f MESSAGES
+ printf "%s" "$1"
+ shift
+ test_emacs "(test-time $*)" > emacs.out
+ tail -n 1 MESSAGES
+}
+
emacs_generate_script
(t (message "%s" err)))
(with-current-buffer "*Messages*" (test-output "MESSAGES"))))
+(defmacro test-time (&rest body)
+ `(let ((results (mapcar (lambda (x) (/ x 5.0)) (benchmark-run 5 ,@body))))
+ (message "\t\t%0.2f\t%0.2f\t%0.2f" (nth 0 results) (nth 1 results) (nth 2 results))
+ (with-current-buffer "*Messages*" (test-output "MESSAGES"))))
+
;; For historical reasons, we hide deleted tags by default in the test
;; suite
(setq notmuch-tag-deleted-formats
BASH_XTRACEFD=7
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
-# Keep the original TERM for say_color and test_emacs
-ORIGINAL_TERM=$TERM
-
-# Set SMART_TERM to vt100 for known dumb/unknown terminal.
-# Otherwise use whatever TERM is currently used so that
-# users' actual TERM environments are being used in tests.
-case ${TERM-} in
- '' | dumb | unknown )
- SMART_TERM=vt100 ;;
- *)
- SMART_TERM=$TERM ;;
-esac
-
-# For repeatability, reset the environment to known value.
-LANG=C
-LC_ALL=C
-PAGER=cat
-TZ=UTC
-TERM=dumb
-export LANG LC_ALL PAGER TERM TZ
-GIT_TEST_CMP=${GIT_TEST_CMP:-diff -u}
-if [[ ( -n "$TEST_EMACS" && -z "$TEST_EMACSCLIENT" ) || \
- ( -z "$TEST_EMACS" && -n "$TEST_EMACSCLIENT" ) ]]; then
- echo "error: must specify both or neither of TEST_EMACS and TEST_EMACSCLIENT" >&2
- exit 1
-fi
-TEST_EMACS=${TEST_EMACS:-${EMACS:-emacs}}
-TEST_EMACSCLIENT=${TEST_EMACSCLIENT:-emacsclient}
-TEST_GDB=${TEST_GDB:-gdb}
-TEST_CC=${TEST_CC:-cc}
-TEST_CFLAGS=${TEST_CFLAGS:-"-g -O0"}
-TEST_SHIM_CFLAGS=${TEST_SHIM_CFLAGS:-"-fpic -shared"}
-TEST_SHIM_LDFLAGS=${TEST_SHIM_LDFLAGS:-"-ldl"}
-
-# Protect ourselves from common misconfiguration to export
-# CDPATH into the environment
-unset CDPATH
-
-unset GREP_OPTIONS
-
-# For lib/open.cc:_load_key_file
-unset XDG_CONFIG_HOME
-
-# For emacsclient
-unset ALTERNATE_EDITOR
-
-# for reproducibility
-unset EMAIL
-unset NAME
+. "$NOTMUCH_SRCDIR/test/test-vars.sh" || exit 1
add_gnupg_home () {
[ -e "${GNUPGHOME}/gpg.conf" ] && return
exit 1
}
-GIT_EXIT_OK=
-# Note: TEST_TMPDIR *NOT* exported!
-TEST_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/notmuch-test-$$.XXXXXX")
-# Put GNUPGHOME in TMPDIR to avoid problems with long paths.
-export GNUPGHOME="${TEST_TMPDIR}/gnupg"
trap 'trap_exit' EXIT
trap 'trap_signal' HUP INT TERM
fi
}
-# Explicitly require external prerequisite. Useful when binary is
-# called indirectly (e.g. from emacs).
-# Returns success if dependency is available, failure otherwise.
-test_require_external_prereq () {
- local binary
- binary="$1"
- if [[ ${test_missing_external_prereq_["${binary}"]} == t ]]; then
- # dependency is missing, call the replacement function to note it
- eval "$binary"
- else
- true
- fi
-}
-
# You are not expected to call test_ok_ and test_failure_ directly, use
# the text_expect_* functions instead.
--- /dev/null
+# Common variable settings for (correctness) tests and performance
+# tests.
+
+# Keep the original TERM for say_color and test_emacs
+ORIGINAL_TERM=$TERM
+
+# Set SMART_TERM to vt100 for known dumb/unknown terminal.
+# Otherwise use whatever TERM is currently used so that
+# users' actual TERM environments are being used in tests.
+case ${TERM-} in
+ '' | dumb | unknown )
+ SMART_TERM=vt100 ;;
+ *)
+ SMART_TERM=$TERM ;;
+esac
+
+# For repeatability, reset the environment to known value.
+LANG=C
+LC_ALL=C
+PAGER=cat
+TZ=UTC
+TERM=dumb
+export LANG LC_ALL PAGER TERM TZ
+GIT_TEST_CMP=${GIT_TEST_CMP:-diff -u}
+if [[ ( -n "$TEST_EMACS" && -z "$TEST_EMACSCLIENT" ) || \
+ ( -z "$TEST_EMACS" && -n "$TEST_EMACSCLIENT" ) ]]; then
+ echo "error: must specify both or neither of TEST_EMACS and TEST_EMACSCLIENT" >&2
+ exit 1
+fi
+TEST_EMACS=${TEST_EMACS:-${EMACS:-emacs}}
+TEST_EMACSCLIENT=${TEST_EMACSCLIENT:-emacsclient}
+TEST_GDB=${TEST_GDB:-gdb}
+TEST_CC=${TEST_CC:-cc}
+TEST_CFLAGS=${TEST_CFLAGS:-"-g -O0"}
+TEST_SHIM_CFLAGS=${TEST_SHIM_CFLAGS:-"-fpic -shared"}
+TEST_SHIM_LDFLAGS=${TEST_SHIM_LDFLAGS:-"-ldl"}
+
+# Protect ourselves from common misconfiguration to export
+# CDPATH into the environment
+unset CDPATH
+
+unset GREP_OPTIONS
+
+# For lib/open.cc:_load_key_file
+unset XDG_CONFIG_HOME
+
+# for lib/open.cc:_choose_database_path
+unset XDG_DATA_HOME
+unset MAILDIR
+
+# For emacsclient
+unset ALTERNATE_EDITOR
+
+# for reproducibility
+unset EMAIL
+unset NAME
+
+GIT_EXIT_OK=
+# Note: TEST_TMPDIR *NOT* exported!
+TEST_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/notmuch-test-$$.XXXXXX")
+# Put GNUPGHOME in TMPDIR to avoid problems with long paths.
+export GNUPGHOME="${TEST_TMPDIR}/gnupg"