diff options
41 files changed, 837 insertions, 1267 deletions
| diff --git a/changes/bug-7480_extract_attach_and_openpgp b/changes/bug-7480_extract_attach_and_openpgp new file mode 100644 index 0000000..27f668a --- /dev/null +++ b/changes/bug-7480_extract_attach_and_openpgp @@ -0,0 +1 @@ +- don't extract openpgp header if valid attached key (Closes: #7480) diff --git a/changes/feature-7471_disable-local-bind-for-docker b/changes/feature-7471_disable-local-bind-for-docker new file mode 100644 index 0000000..a1ccb67 --- /dev/null +++ b/changes/feature-7471_disable-local-bind-for-docker @@ -0,0 +1 @@ +- disable local only tcp bind on docker containers to allow access to IMAP and SMTP. Related to #7471. diff --git a/docs/api/Makefile b/docs/api/Makefile deleted file mode 100644 index ebcd0f4..0000000 --- a/docs/api/Makefile +++ /dev/null @@ -1,177 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS    = -SPHINXBUILD   = sphinx-build -PAPER         = -BUILDDIR      = _build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4     = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: -	@echo "Please use \`make <target>' where <target> is one of" -	@echo "  html       to make standalone HTML files" -	@echo "  dirhtml    to make HTML files named index.html in directories" -	@echo "  singlehtml to make a single large HTML file" -	@echo "  pickle     to make pickle files" -	@echo "  json       to make JSON files" -	@echo "  htmlhelp   to make HTML files and a HTML help project" -	@echo "  qthelp     to make HTML files and a qthelp project" -	@echo "  devhelp    to make HTML files and a Devhelp project" -	@echo "  epub       to make an epub" -	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter" -	@echo "  latexpdf   to make LaTeX files and run them through pdflatex" -	@echo "  latexpdfja to make LaTeX files and run them through platex/dvipdfmx" -	@echo "  text       to make text files" -	@echo "  man        to make manual pages" -	@echo "  texinfo    to make Texinfo files" -	@echo "  info       to make Texinfo files and run them through makeinfo" -	@echo "  gettext    to make PO message catalogs" -	@echo "  changes    to make an overview of all changed/added/deprecated items" -	@echo "  xml        to make Docutils-native XML files" -	@echo "  pseudoxml  to make pseudoxml-XML files for display purposes" -	@echo "  linkcheck  to check all external links for integrity" -	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)" - -clean: -	rm -rf $(BUILDDIR)/* - -html: -	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html -	@echo -	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: -	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml -	@echo -	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: -	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml -	@echo -	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: -	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle -	@echo -	@echo "Build finished; now you can process the pickle files." - -json: -	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json -	@echo -	@echo "Build finished; now you can process the JSON files." - -htmlhelp: -	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp -	@echo -	@echo "Build finished; now you can run HTML Help Workshop with the" \ -	      ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: -	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp -	@echo -	@echo "Build finished; now you can run "qcollectiongenerator" with the" \ -	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:" -	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/mail.qhcp" -	@echo "To view the help file:" -	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/mail.qhc" - -devhelp: -	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp -	@echo -	@echo "Build finished." -	@echo "To view the help file:" -	@echo "# mkdir -p $$HOME/.local/share/devhelp/mail" -	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/mail" -	@echo "# devhelp" - -epub: -	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub -	@echo -	@echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: -	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex -	@echo -	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." -	@echo "Run \`make' in that directory to run these through (pdf)latex" \ -	      "(use \`make latexpdf' here to do that automatically)." - -latexpdf: -	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex -	@echo "Running LaTeX files through pdflatex..." -	$(MAKE) -C $(BUILDDIR)/latex all-pdf -	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: -	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex -	@echo "Running LaTeX files through platex and dvipdfmx..." -	$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja -	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: -	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text -	@echo -	@echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: -	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man -	@echo -	@echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: -	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo -	@echo -	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." -	@echo "Run \`make' in that directory to run these through makeinfo" \ -	      "(use \`make info' here to do that automatically)." - -info: -	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo -	@echo "Running Texinfo files through makeinfo..." -	make -C $(BUILDDIR)/texinfo info -	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: -	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale -	@echo -	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: -	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes -	@echo -	@echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: -	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -	@echo -	@echo "Link check complete; look for any errors in the above output " \ -	      "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: -	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest -	@echo "Testing of doctests in the sources finished, look at the " \ -	      "results in $(BUILDDIR)/doctest/output.txt." - -xml: -	$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml -	@echo -	@echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: -	$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml -	@echo -	@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/api/conf.py b/docs/api/conf.py deleted file mode 100644 index 2199c2f..0000000 --- a/docs/api/conf.py +++ /dev/null @@ -1,331 +0,0 @@ -# -*- coding: utf-8 -*- -# -# mail documentation build configuration file, created by -# sphinx-quickstart on Mon Aug 25 19:47:12 2014. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ -    'sphinx.ext.autodoc', -    'sphinx.ext.viewcode', -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'mail' -copyright = u'2014, Author' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '' -# The full version, including alpha/beta/rc tags. -release = '' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages.  See the documentation for -# a list of builtin themes. -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further.  For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents.  If None, it defaults to -# "<project> v<release> documentation". -#html_title = None - -# A shorter title for the navigation bar.  Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a <link> tag referring to it.  The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'maildoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -#  author, documentclass [howto, manual, or own class]). -latex_documents = [ -  ('index', 'mail.tex', u'mail Documentation', -   u'Author', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ -    ('index', 'mail', u'mail Documentation', -     [u'Author'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -#  dir menu entry, description, category) -texinfo_documents = [ -  ('index', 'mail', u'mail Documentation', -   u'Author', 'mail', 'One line description of project.', -   'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False - - -# -- Options for Epub output ---------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = u'mail' -epub_author = u'Author' -epub_publisher = u'Author' -epub_copyright = u'2014, Author' - -# The basename for the epub file. It defaults to the project name. -#epub_basename = u'mail' - -# The HTML theme for the epub output. Since the default themes are not optimized -# for small screen space, using the same theme for HTML and epub output is -# usually not wise. This defaults to 'epub', a theme designed to save visual -# space. -#epub_theme = 'epub' - -# The language of the text. It defaults to the language option -# or en if the language is not set. -#epub_language = '' - -# The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -#epub_identifier = '' - -# A unique identification for the text. -#epub_uid = '' - -# A tuple containing the cover image and cover page html template filenames. -#epub_cover = () - -# A sequence of (type, uri, title) tuples for the guide element of content.opf. -#epub_guide = () - -# HTML files that should be inserted before the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_pre_files = [] - -# HTML files shat should be inserted after the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_post_files = [] - -# A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] - -# The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 - -# Allow duplicate toc entries. -#epub_tocdup = True - -# Choose between 'default' and 'includehidden'. -#epub_tocscope = 'default' - -# Fix unsupported image types using the PIL. -#epub_fix_images = False - -# Scale large images. -#epub_max_image_width = 0 - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#epub_show_urls = 'inline' - -# If false, no index is generated. -#epub_use_index = True diff --git a/docs/api/index.rst b/docs/api/index.rst deleted file mode 100644 index f5531df..0000000 --- a/docs/api/index.rst +++ /dev/null @@ -1,23 +0,0 @@ -.. mail documentation master file, created by -   sphinx-quickstart on Mon Aug 25 19:47:12 2014. -   You can adapt this file completely to your liking, but it should at least -   contain the root `toctree` directive. - -Welcome to mail's documentation! -================================ - -Contents: - -.. toctree:: -   :maxdepth: 4 - -   mail - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/docs/api/leap.mail.adaptors.rst b/docs/api/leap.mail.adaptors.rst new file mode 100644 index 0000000..472cade --- /dev/null +++ b/docs/api/leap.mail.adaptors.rst @@ -0,0 +1,43 @@ +mail.adaptors package +===================== + +.. automodule:: leap.mail.adaptors +    :members: +    :undoc-members: +    :show-inheritance: + +Subpackages +----------- + +.. toctree:: + +    leap.mail.adaptors.tests + +Submodules +---------- + +mail.adaptors.models module +--------------------------- + +.. automodule:: leap.mail.adaptors.models +    :members: +    :undoc-members: +    :show-inheritance: + +mail.adaptors.soledad module +---------------------------- + +.. automodule:: leap.mail.adaptors.soledad +    :members: +    :undoc-members: +    :show-inheritance: + +mail.adaptors.soledad_indexes module +------------------------------------ + +.. automodule:: leap.mail.adaptors.soledad_indexes +    :members: +    :undoc-members: +    :show-inheritance: + + diff --git a/docs/api/leap.mail.adaptors.tests.rst b/docs/api/leap.mail.adaptors.tests.rst new file mode 100644 index 0000000..2ae76e8 --- /dev/null +++ b/docs/api/leap.mail.adaptors.tests.rst @@ -0,0 +1,28 @@ +mail.adaptors.tests package +=========================== + +.. automodule:: leap.mail.adaptors.tests +    :members: +    :undoc-members: +    :show-inheritance: + +Submodules +---------- + +mail.adaptors.tests.test_models module +-------------------------------------- + +.. automodule:: leap.mail.adaptors.tests.test_models +    :members: +    :undoc-members: +    :show-inheritance: + +mail.adaptors.tests.test_soledad_adaptor module +----------------------------------------------- + +.. automodule:: leap.mail.adaptors.tests.test_soledad_adaptor +    :members: +    :undoc-members: +    :show-inheritance: + + diff --git a/docs/api/leap.mail.imap.rst b/docs/api/leap.mail.imap.rst new file mode 100644 index 0000000..bfaa3fd --- /dev/null +++ b/docs/api/leap.mail.imap.rst @@ -0,0 +1,52 @@ +leap.mail.imap package +======================= + +.. automodule:: leap.mail.imap +    :members: +    :undoc-members: +    :show-inheritance: + +Subpackages +----------- + +.. toctree:: + +    leap.mail.imap.service +    leap.mail.imap.tests + +Submodules +---------- + +leap.mail.imap.account module +------------------------------ + +.. automodule:: leap.mail.imap.account +    :members: +    :undoc-members: +    :show-inheritance: + +leap.mail.imap.mailbox module +------------------------------ + +.. automodule:: leap.mail.imap.mailbox +    :members: +    :undoc-members: +    :show-inheritance: + +leap.mail.imap.messages module +------------------------------ + +.. automodule:: leap.mail.imap.messages +    :members: +    :undoc-members: +    :show-inheritance: + +leap.mail.imap.server module +----------------------------- + +.. automodule:: leap.mail.imap.server +    :members: +    :undoc-members: +    :show-inheritance: + + diff --git a/docs/api/leap.mail.imap.service.rst b/docs/api/leap.mail.imap.service.rst new file mode 100644 index 0000000..2f3ed4b --- /dev/null +++ b/docs/api/leap.mail.imap.service.rst @@ -0,0 +1,9 @@ +leap.mail.imap.service package +=============================== + +.. automodule:: leap.mail.imap.service +    :members: +    :undoc-members: +    :show-inheritance: + + diff --git a/docs/api/leap.mail.incoming.rst b/docs/api/leap.mail.incoming.rst new file mode 100644 index 0000000..4bd1614 --- /dev/null +++ b/docs/api/leap.mail.incoming.rst @@ -0,0 +1,20 @@ +leap.mail.incoming package +========================== + +.. automodule:: leap.mail.incoming +    :members: +    :undoc-members: +    :show-inheritance: + +Submodules +---------- + +leap.mail.incoming.service module +---------------------------------- + +.. automodule:: leap.mail.incoming.service +    :members: +    :undoc-members: +    :show-inheritance: + + diff --git a/docs/api/leap.mail.outgoing.rst b/docs/api/leap.mail.outgoing.rst new file mode 100644 index 0000000..af8c173 --- /dev/null +++ b/docs/api/leap.mail.outgoing.rst @@ -0,0 +1,21 @@ +leap.mail.outgoing package +========================== + +.. automodule:: leap.mail.outgoing +    :members: +    :undoc-members: +    :show-inheritance: + + +Submodules +---------- + +mail.outgoing.service module +---------------------------- + +.. automodule:: leap.mail.outgoing.service +    :members: +    :undoc-members: +    :show-inheritance: + + diff --git a/docs/api/leap.mail.plugins.rst b/docs/api/leap.mail.plugins.rst new file mode 100644 index 0000000..7a5d6b4 --- /dev/null +++ b/docs/api/leap.mail.plugins.rst @@ -0,0 +1,20 @@ +leap.mail.plugins package +========================== + +.. automodule:: leap.mail.plugins +    :members: +    :undoc-members: +    :show-inheritance: + +Submodules +---------- + +leap.mail.plugins.soledad_sync_hooks module +------------------------------------------- + +.. automodule:: leap.mail.plugins.soledad_sync_hooks +    :members: +    :undoc-members: +    :show-inheritance: + + diff --git a/docs/api/leap.mail.rst b/docs/api/leap.mail.rst new file mode 100644 index 0000000..686e648 --- /dev/null +++ b/docs/api/leap.mail.rst @@ -0,0 +1,105 @@ +leap.mail package +================= + +.. automodule:: leap.mail +    :members: +    :undoc-members: +    :show-inheritance: + +Subpackages +----------- + +.. toctree:: + +    leap.mail.adaptors +    leap.mail.imap +    leap.mail.incoming +    leap.mail.outgoing +    leap.mail.plugins +    leap.mail.smtp +    leap.mail.tests + +Submodules +---------- + +leap.mail.constants module +--------------------------- + +.. automodule:: leap.mail.constants +    :members: +    :undoc-members: +    :show-inheritance: + +leap.mail.decorators module +--------------------------- + +.. automodule:: leap.mail.decorators +    :members: +    :undoc-members: +    :show-inheritance: + +leap.mail.interfaces module +---------------------------- + +.. automodule:: leap.mail.interfaces +    :members: +    :undoc-members: +    :show-inheritance: + +leap.mail.load_tests module +---------------------------- + +.. automodule:: leap.mail.load_tests +    :members: +    :undoc-members: +    :show-inheritance: + +leap.mail.mail module +--------------------- + +.. automodule:: leap.mail.mail +    :members: +    :undoc-members: +    :show-inheritance: + +leap.mail.mailbox_indexer module +--------------------------------- + +.. automodule:: leap.mail.mailbox_indexer +    :members: +    :undoc-members: +    :show-inheritance: + +leap.mail.size module +---------------------- + +.. automodule:: leap.mail.size +    :members: +    :undoc-members: +    :show-inheritance: + +leap.mail.sync_hooks module +---------------------------- + +.. automodule:: leap.mail.sync_hooks +    :members: +    :undoc-members: +    :show-inheritance: + +leap.mail.utils module +----------------------- + +.. automodule:: leap.mail.utils +    :members: +    :undoc-members: +    :show-inheritance: + +leap.mail.walk module +---------------------- + +.. automodule:: leap.mail.walk +    :members: +    :undoc-members: +    :show-inheritance: + + diff --git a/docs/api/leap.mail.smtp.rst b/docs/api/leap.mail.smtp.rst new file mode 100644 index 0000000..f35d3f9 --- /dev/null +++ b/docs/api/leap.mail.smtp.rst @@ -0,0 +1,19 @@ +leap.mail.smtp package +======================= + +.. automodule:: leap.mail.smtp +    :members: +    :undoc-members: +    :show-inheritance: + +Submodules +---------- + +leap.mail.smtp.gateway module +----------------------------- + +.. automodule:: leap.mail.smtp.gateway +    :members: +    :undoc-members: +    :show-inheritance: + diff --git a/docs/api/mail.imap.rst b/docs/api/mail.imap.rst deleted file mode 100644 index 051ded6..0000000 --- a/docs/api/mail.imap.rst +++ /dev/null @@ -1,118 +0,0 @@ -mail.imap package -================= - -Subpackages ------------ - -.. toctree:: - -    mail.imap.service -    mail.imap.tests - -Submodules ----------- - -mail.imap.account module ------------------------- - -.. automodule:: mail.imap.account -    :members: -    :undoc-members: -    :show-inheritance: - -mail.imap.fetch module ----------------------- - -.. automodule:: mail.imap.fetch -    :members: -    :undoc-members: -    :show-inheritance: - -mail.imap.fields module ------------------------ - -.. automodule:: mail.imap.fields -    :members: -    :undoc-members: -    :show-inheritance: - -mail.imap.index module ----------------------- - -.. automodule:: mail.imap.index -    :members: -    :undoc-members: -    :show-inheritance: - -mail.imap.interfaces module ---------------------------- - -.. automodule:: mail.imap.interfaces -    :members: -    :undoc-members: -    :show-inheritance: - -mail.imap.mailbox module ------------------------- - -.. automodule:: mail.imap.mailbox -    :members: -    :undoc-members: -    :show-inheritance: - -mail.imap.memorystore module ----------------------------- - -.. automodule:: mail.imap.memorystore -    :members: -    :undoc-members: -    :show-inheritance: - -mail.imap.messageparts module ------------------------------ - -.. automodule:: mail.imap.messageparts -    :members: -    :undoc-members: -    :show-inheritance: - -mail.imap.messages module -------------------------- - -.. automodule:: mail.imap.messages -    :members: -    :undoc-members: -    :show-inheritance: - -mail.imap.parser module ------------------------ - -.. automodule:: mail.imap.parser -    :members: -    :undoc-members: -    :show-inheritance: - -mail.imap.server module ------------------------ - -.. automodule:: mail.imap.server -    :members: -    :undoc-members: -    :show-inheritance: - -mail.imap.soledadstore module ------------------------------ - -.. automodule:: mail.imap.soledadstore -    :members: -    :undoc-members: -    :show-inheritance: - - -Module contents ---------------- - -.. automodule:: mail.imap -    :members: -    :undoc-members: -    :show-inheritance: diff --git a/docs/api/mail.imap.service.rst b/docs/api/mail.imap.service.rst deleted file mode 100644 index c288813..0000000 --- a/docs/api/mail.imap.service.rst +++ /dev/null @@ -1,30 +0,0 @@ -mail.imap.service package -========================= - -Submodules ----------- - -mail.imap.service.imap module ------------------------------ - -.. automodule:: mail.imap.service.imap -    :members: -    :undoc-members: -    :show-inheritance: - -mail.imap.service.manhole module --------------------------------- - -.. automodule:: mail.imap.service.manhole -    :members: -    :undoc-members: -    :show-inheritance: - - -Module contents ---------------- - -.. automodule:: mail.imap.service -    :members: -    :undoc-members: -    :show-inheritance: diff --git a/docs/api/mail.imap.tests.rst b/docs/api/mail.imap.tests.rst deleted file mode 100644 index b6717a3..0000000 --- a/docs/api/mail.imap.tests.rst +++ /dev/null @@ -1,38 +0,0 @@ -mail.imap.tests package -======================= - -Submodules ----------- - -mail.imap.tests.imapclient module ---------------------------------- - -.. automodule:: mail.imap.tests.imapclient -    :members: -    :undoc-members: -    :show-inheritance: - -mail.imap.tests.test_imap module --------------------------------- - -.. automodule:: mail.imap.tests.test_imap -    :members: -    :undoc-members: -    :show-inheritance: - -mail.imap.tests.walktree module -------------------------------- - -.. automodule:: mail.imap.tests.walktree -    :members: -    :undoc-members: -    :show-inheritance: - - -Module contents ---------------- - -.. automodule:: mail.imap.tests -    :members: -    :undoc-members: -    :show-inheritance: diff --git a/docs/api/mail.rst b/docs/api/mail.rst deleted file mode 100644 index 2713207..0000000 --- a/docs/api/mail.rst +++ /dev/null @@ -1,70 +0,0 @@ -mail package -============ - -Subpackages ------------ - -.. toctree:: - -    mail.imap -    mail.smtp - -Submodules ----------- - -mail.decorators module ----------------------- - -.. automodule:: mail.decorators -    :members: -    :undoc-members: -    :show-inheritance: - -mail.load_tests module ----------------------- - -.. automodule:: mail.load_tests -    :members: -    :undoc-members: -    :show-inheritance: - -mail.messageflow module ------------------------ - -.. automodule:: mail.messageflow -    :members: -    :undoc-members: -    :show-inheritance: - -mail.size module ----------------- - -.. automodule:: mail.size -    :members: -    :undoc-members: -    :show-inheritance: - -mail.utils module ------------------ - -.. automodule:: mail.utils -    :members: -    :undoc-members: -    :show-inheritance: - -mail.walk module ----------------- - -.. automodule:: mail.walk -    :members: -    :undoc-members: -    :show-inheritance: - - -Module contents ---------------- - -.. automodule:: mail -    :members: -    :undoc-members: -    :show-inheritance: diff --git a/docs/api/mail.smtp.rst b/docs/api/mail.smtp.rst deleted file mode 100644 index da67279..0000000 --- a/docs/api/mail.smtp.rst +++ /dev/null @@ -1,37 +0,0 @@ -mail.smtp package -================= - -Subpackages ------------ - -.. toctree:: - -    mail.smtp.tests - -Submodules ----------- - -mail.smtp.gateway module ------------------------- - -.. automodule:: mail.smtp.gateway -    :members: -    :undoc-members: -    :show-inheritance: - -mail.smtp.rfc3156 module ------------------------- - -.. automodule:: mail.smtp.rfc3156 -    :members: -    :undoc-members: -    :show-inheritance: - - -Module contents ---------------- - -.. automodule:: mail.smtp -    :members: -    :undoc-members: -    :show-inheritance: diff --git a/docs/api/mail.smtp.tests.rst b/docs/api/mail.smtp.tests.rst deleted file mode 100644 index c313fb3..0000000 --- a/docs/api/mail.smtp.tests.rst +++ /dev/null @@ -1,22 +0,0 @@ -mail.smtp.tests package -======================= - -Submodules ----------- - -mail.smtp.tests.test_gateway module ------------------------------------ - -.. automodule:: mail.smtp.tests.test_gateway -    :members: -    :undoc-members: -    :show-inheritance: - - -Module contents ---------------- - -.. automodule:: mail.smtp.tests -    :members: -    :undoc-members: -    :show-inheritance: diff --git a/docs/api/make.bat b/docs/api/make.bat deleted file mode 100644 index 63cd17f..0000000 --- a/docs/api/make.bat +++ /dev/null @@ -1,242 +0,0 @@ -@ECHO OFF
 -
 -REM Command file for Sphinx documentation
 -
 -if "%SPHINXBUILD%" == "" (
 -	set SPHINXBUILD=sphinx-build
 -)
 -set BUILDDIR=_build
 -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
 -set I18NSPHINXOPTS=%SPHINXOPTS% .
 -if NOT "%PAPER%" == "" (
 -	set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
 -	set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
 -)
 -
 -if "%1" == "" goto help
 -
 -if "%1" == "help" (
 -	:help
 -	echo.Please use `make ^<target^>` where ^<target^> is one of
 -	echo.  html       to make standalone HTML files
 -	echo.  dirhtml    to make HTML files named index.html in directories
 -	echo.  singlehtml to make a single large HTML file
 -	echo.  pickle     to make pickle files
 -	echo.  json       to make JSON files
 -	echo.  htmlhelp   to make HTML files and a HTML help project
 -	echo.  qthelp     to make HTML files and a qthelp project
 -	echo.  devhelp    to make HTML files and a Devhelp project
 -	echo.  epub       to make an epub
 -	echo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter
 -	echo.  text       to make text files
 -	echo.  man        to make manual pages
 -	echo.  texinfo    to make Texinfo files
 -	echo.  gettext    to make PO message catalogs
 -	echo.  changes    to make an overview over all changed/added/deprecated items
 -	echo.  xml        to make Docutils-native XML files
 -	echo.  pseudoxml  to make pseudoxml-XML files for display purposes
 -	echo.  linkcheck  to check all external links for integrity
 -	echo.  doctest    to run all doctests embedded in the documentation if enabled
 -	goto end
 -)
 -
 -if "%1" == "clean" (
 -	for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
 -	del /q /s %BUILDDIR%\*
 -	goto end
 -)
 -
 -
 -%SPHINXBUILD% 2> nul
 -if errorlevel 9009 (
 -	echo.
 -	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
 -	echo.installed, then set the SPHINXBUILD environment variable to point
 -	echo.to the full path of the 'sphinx-build' executable. Alternatively you
 -	echo.may add the Sphinx directory to PATH.
 -	echo.
 -	echo.If you don't have Sphinx installed, grab it from
 -	echo.http://sphinx-doc.org/
 -	exit /b 1
 -)
 -
 -if "%1" == "html" (
 -	%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.Build finished. The HTML pages are in %BUILDDIR%/html.
 -	goto end
 -)
 -
 -if "%1" == "dirhtml" (
 -	%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
 -	goto end
 -)
 -
 -if "%1" == "singlehtml" (
 -	%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
 -	goto end
 -)
 -
 -if "%1" == "pickle" (
 -	%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.Build finished; now you can process the pickle files.
 -	goto end
 -)
 -
 -if "%1" == "json" (
 -	%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.Build finished; now you can process the JSON files.
 -	goto end
 -)
 -
 -if "%1" == "htmlhelp" (
 -	%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.Build finished; now you can run HTML Help Workshop with the ^
 -.hhp project file in %BUILDDIR%/htmlhelp.
 -	goto end
 -)
 -
 -if "%1" == "qthelp" (
 -	%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.Build finished; now you can run "qcollectiongenerator" with the ^
 -.qhcp project file in %BUILDDIR%/qthelp, like this:
 -	echo.^> qcollectiongenerator %BUILDDIR%\qthelp\mail.qhcp
 -	echo.To view the help file:
 -	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\mail.ghc
 -	goto end
 -)
 -
 -if "%1" == "devhelp" (
 -	%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.Build finished.
 -	goto end
 -)
 -
 -if "%1" == "epub" (
 -	%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.Build finished. The epub file is in %BUILDDIR%/epub.
 -	goto end
 -)
 -
 -if "%1" == "latex" (
 -	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
 -	goto end
 -)
 -
 -if "%1" == "latexpdf" (
 -	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
 -	cd %BUILDDIR%/latex
 -	make all-pdf
 -	cd %BUILDDIR%/..
 -	echo.
 -	echo.Build finished; the PDF files are in %BUILDDIR%/latex.
 -	goto end
 -)
 -
 -if "%1" == "latexpdfja" (
 -	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
 -	cd %BUILDDIR%/latex
 -	make all-pdf-ja
 -	cd %BUILDDIR%/..
 -	echo.
 -	echo.Build finished; the PDF files are in %BUILDDIR%/latex.
 -	goto end
 -)
 -
 -if "%1" == "text" (
 -	%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.Build finished. The text files are in %BUILDDIR%/text.
 -	goto end
 -)
 -
 -if "%1" == "man" (
 -	%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.Build finished. The manual pages are in %BUILDDIR%/man.
 -	goto end
 -)
 -
 -if "%1" == "texinfo" (
 -	%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
 -	goto end
 -)
 -
 -if "%1" == "gettext" (
 -	%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
 -	goto end
 -)
 -
 -if "%1" == "changes" (
 -	%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.The overview file is in %BUILDDIR%/changes.
 -	goto end
 -)
 -
 -if "%1" == "linkcheck" (
 -	%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.Link check complete; look for any errors in the above output ^
 -or in %BUILDDIR%/linkcheck/output.txt.
 -	goto end
 -)
 -
 -if "%1" == "doctest" (
 -	%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.Testing of doctests in the sources finished, look at the ^
 -results in %BUILDDIR%/doctest/output.txt.
 -	goto end
 -)
 -
 -if "%1" == "xml" (
 -	%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.Build finished. The XML files are in %BUILDDIR%/xml.
 -	goto end
 -)
 -
 -if "%1" == "pseudoxml" (
 -	%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
 -	if errorlevel 1 exit /b 1
 -	echo.
 -	echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
 -	goto end
 -)
 -
 -:end
 diff --git a/docs/conf.py b/docs/conf.py index 95d919b..746f57c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,9 +18,16 @@ import os  # If extensions (or modules to document with autodoc) are in another directory,  # add these directories to sys.path here. If the directory is relative to the  # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../src')) -sys.path.insert(0, os.path.abspath('../src/leap')) -sys.path.insert(0, os.path.abspath('../src/leap/mail')) +#sys.path.insert(0, os.path.abspath('../src')) +#sys.path.insert(0, os.path.abspath('../src/leap')) +#sys.path.insert(0, os.path.abspath('../src/leap/mail')) +#sys.path.insert(0, os.path.abspath('../../leap_common/src/leap/common')) +#sys.path.insert(0, os.path.abspath('../../soledad/client/src/leap/soledad/client')) + +VENV_PATH = os.environ.get('VIRTUAL_ENV') +if VENV_PATH: +    sys.path.insert(0, os.path.abspath(VENV_PATH + 'lib/python2.7/site-packages')) +  # -- General configuration ------------------------------------------------ @@ -57,7 +64,7 @@ copyright = u'2014-2015, The LEAP Encryption Access Project'  # built documents.  #  # The short X.Y version. -version = '0.4.0alpha1' +version = '0.4.0rc2'  # The full version, including alpha/beta/rc tags.  release = '0.4.0' @@ -104,7 +111,7 @@ pygments_style = 'sphinx'  # The theme to use for HTML and HTML Help pages.  See the documentation for  # a list of builtin themes. -html_theme = 'default' +html_theme = 'alabaster'  # Theme options are theme-specific and customize the look and feel of a theme  # further.  For a list of options available for each theme, see the diff --git a/docs/index.rst b/docs/index.rst index 8bacc51..a2133f4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,21 +3,50 @@     You can adapt this file completely to your liking, but it should at least     contain the root `toctree` directive. -Welcome to leap.mail's documentation! -===================================== +leap.mail +========= -This is the documentation for the ``leap.mail`` module. It is a twisted package -that exposes two services, ``smtp`` and ``imap``, that run local proxies and interact -with a remote ``LEAP`` provider that offers *a soledad syncronization endpoint* -and receive the outgoing email. +*decentralized and secure mail delivery and synchronization* + +This is the documentation for the ``leap.mail`` module. It is a `twisted`_ +package that allows to receive, process, send and access existing messages using +the `LEAP`_ platform. + +One way to use this library is to let it launch two standard mail services, +``smtp`` and ``imap``, that run as local proxies and interact with a remote +``LEAP`` provider that offers *a soledad syncronization endpoint* and receives  +the outgoing email. This is what `Bitmask`_ client does. + +From the release 0.4.0 on, it's also possible to use a protocol-agnostic email +public API, so that third party mail clients can manipulate the data layer. This +is what the awesome MUA in the `Pixelated`_ project is using. + +.. _`twisted`: https://twistedmatrix.com/trac/ +.. _`LEAP`: https://leap.se/en/docs +.. _`Bitmask`: https://bitmask.net/en/features#email +.. _`Pixelated`: https://pixelated-project.org/ + +How does this all work? +----------------------- + +All the underlying data storage and sync is handled by a library called +`soledad`_, which handles encryption, storage and sync. Based on `u1db`_, +documents are stored locally as local ``sqlcipher`` tables, and syncs against +the soledad sync service in the provider. + +OpenPGP key generation and keyring management are handled by another leap +python library: `keymanager`_.  See :ref:`the life cycle of a leap email <mail_journey>` for an overview of the life cycle  of an email through ``LEAP`` providers. -``Soledad`` stores its documents as local ``sqlcipher`` tables, and syncs -against the soledad sync service in the provider. +.. _`Soledad`: https://leap.se/en/docs/design/soledad +.. _`u1db`: https://en.wikipedia.org/wiki/U1DB +.. _`keymanager`: https://github.com/leapcode/keymanager/ +Data model +----------  .. TODO clear document types documentation.  The data model at the present moment consists of several *document types* that split email into @@ -25,13 +54,8 @@ different documents that are stored in ``Soledad``. The idea behind this is to  keep clear the separation between *mutable* and *inmutable* parts, and still being able to  reconstruct arbitrarily nested email structures easily. -In the coming releases we are going to be working towards the goal of exposing -a protocol-agnostic email public API, so that third party mail clients can -manipulate the data layer without having to resort to handling the sql tables or -doing direct u1db calls. The code will be transitioning towards a LEAPMail -public API that we can stabilize as soon as possible, and leaving the IMAP -server as another code entity that uses this lower layer. - +Documentation index +===================  ..  .. Contents: @@ -48,14 +72,18 @@ API documentation  -----------------  If you were looking for the documentation of the ``leap.mail`` module, you will -find it here. Beware that the public API will still be unstable for the next -development cycles. +find it here. + +Of special interest is the `public mail api`_, which should remain relatively +stable across the next few releases. + +.. _`public mail api`: api/mail.html#module-mail +  .. toctree::     :maxdepth: 2 -   api/mail - +   api/leap.mail diff --git a/docs/intro.rst b/docs/intro.rst deleted file mode 100644 index 6090a90..0000000 --- a/docs/intro.rst +++ /dev/null @@ -1,4 +0,0 @@ -Introduction -============ - -leap.mail intro diff --git a/docs/recreate_apidocs.sh b/docs/recreate_apidocs.sh new file mode 100755 index 0000000..9a79d09 --- /dev/null +++ b/docs/recreate_apidocs.sh @@ -0,0 +1,4 @@ +#!/bin/sh +# Watchout! this will need much manual touches +# to the generated apidocs. Mainly: s/mail/leap.mail/g +sphinx-apidoc -M -o api ../src/leap/mail diff --git a/pkg/requirements-leap.pip b/pkg/requirements-leap.pip index f50487e..feb9f37 100644 --- a/pkg/requirements-leap.pip +++ b/pkg/requirements-leap.pip @@ -1,3 +1,3 @@ -leap.common>=0.4.0 +leap.common>=0.4.3  leap.soledad.client>=0.7.0  leap.keymanager>=0.4.0 diff --git a/src/leap/mail/imap/mailbox.py b/src/leap/mail/imap/mailbox.py index c52a2e3..c7accbb 100644 --- a/src/leap/mail/imap/mailbox.py +++ b/src/leap/mail/imap/mailbox.py @@ -215,11 +215,13 @@ class IMAPMailbox(object):          but in the future will be useful to get absolute UIDs from          message sequence numbers. +          :param message: the message sequence number.          :type message: int          :rtype: int          :return: the UID of the message. +          """          # TODO support relative sequences. The (imap) message should          # receive a sequence number attribute: a deferred is not expected @@ -558,7 +560,8 @@ class IMAPMailbox(object):              def _get_imap_msg(messages):                  d_imapmsg = [] -                for msg in messages: +                # just in case we got bad data in here +                for msg in filter(None, messages):                      d_imapmsg.append(getimapmsg(msg))                  return defer.gatherResults(d_imapmsg, consumeErrors=True) diff --git a/src/leap/mail/imap/server.py b/src/leap/mail/imap/server.py index 39f483f..8f14936 100644 --- a/src/leap/mail/imap/server.py +++ b/src/leap/mail/imap/server.py @@ -27,7 +27,7 @@ from twisted.mail import imap4  from twisted.python import log  from leap.common.check import leap_assert, leap_assert_type -from leap.common.events import emit, catalog +from leap.common.events import emit_async, catalog  from leap.soledad.client import Soledad  # imports for LITERAL+ patch @@ -224,7 +224,7 @@ class LEAPIMAPServer(imap4.IMAP4Server):              # bad username, reject.              raise cred.error.UnauthorizedLogin()          # any dummy password is allowed so far. use realm instead! -        emit(catalog.IMAP_CLIENT_LOGIN, "1") +        emit_async(catalog.IMAP_CLIENT_LOGIN, "1")          return imap4.IAccount, self.theAccount, lambda: None      def do_FETCH(self, tag, messages, query, uid=0): diff --git a/src/leap/mail/imap/service/imap.py b/src/leap/mail/imap/service/imap.py index c3ae59a..a50611b 100644 --- a/src/leap/mail/imap/service/imap.py +++ b/src/leap/mail/imap/service/imap.py @@ -28,7 +28,7 @@ from twisted.internet.protocol import ServerFactory  from twisted.mail import imap4  from twisted.python import log -from leap.common.events import emit, catalog +from leap.common.events import emit_async, catalog  from leap.common.check import leap_check  from leap.mail.imap.account import IMAPAccount  from leap.mail.imap.server import LEAPIMAPServer @@ -158,8 +158,14 @@ def run_service(store, **kwargs):      factory = LeapIMAPFactory(uuid, userid, store)      try: +        interface = "localhost" +        # don't bind just to localhost if we are running on docker since we +        # won't be able to access imap from the host +        if os.environ.get("LEAP_DOCKERIZED"): +            interface = '' +          tport = reactor.listenTCP(port, factory, -                                  interface="localhost") +                                  interface=interface)      except CannotListenError:          logger.error("IMAP Service failed to start: "                       "cannot listen in port %s" % (port,)) @@ -178,10 +184,10 @@ def run_service(store, **kwargs):              reactor.listenTCP(manhole.MANHOLE_PORT, manhole_factory,                                interface="127.0.0.1")          logger.debug("IMAP4 Server is RUNNING in port  %s" % (port,)) -        emit(catalog.IMAP_SERVICE_STARTED, str(port)) +        emit_async(catalog.IMAP_SERVICE_STARTED, str(port))          # FIXME -- change service signature          return tport, factory      # not ok, signal error. -    emit(catalog.IMAP_SERVICE_FAILED_TO_START, str(port)) +    emit_async(catalog.IMAP_SERVICE_FAILED_TO_START, str(port)) diff --git a/src/leap/mail/imap/tests/getmail b/src/leap/mail/imap/tests/getmail index 0fb00d2..dd3fa0b 100755 --- a/src/leap/mail/imap/tests/getmail +++ b/src/leap/mail/imap/tests/getmail @@ -10,6 +10,7 @@ Simple IMAP4 client which displays the subjects of all messages in a  particular mailbox.  """ +import os  import sys  from twisted.internet import protocol @@ -20,6 +21,9 @@ from twisted.mail import imap4  from twisted.protocols import basic  from twisted.python import log +# Global options stored here from main +_opts = {} +  class TrivialPrompter(basic.LineReceiver):      from os import linesep as delimiter @@ -70,9 +74,7 @@ class SimpleIMAP4ClientFactory(protocol.ClientFactory):          """          Initiate the protocol instance. Since we are building a simple IMAP          client, we don't bother checking what capabilities the server has. We -        just add all the authenticators twisted.mail has.  Note: Gmail no -        longer uses any of the methods below, it's been using XOAUTH since -        2010. +        just add all the authenticators twisted.mail has.          """          assert not self.usedUp          self.usedUp = True @@ -159,14 +161,24 @@ def InsecureLogin(proto, username, password):  def cbMailboxList(result, proto):      """      Callback invoked when a list of mailboxes has been retrieved. +    If we have a selected mailbox in the global options, we directly pick it. +    Otherwise, we offer a prompt to let user choose one.      """ -    result = [e[2] for e in result] -    s = '\n'.join(['%d. %s' % (n + 1, m) for (n, m) in zip(range(len(result)), result)]) +    all_mbox_list = [e[2] for e in result] +    s = '\n'.join(['%d. %s' % (n + 1, m) for (n, m) in zip(range(len(all_mbox_list)), all_mbox_list)])      if not s:          return defer.fail(Exception("No mailboxes exist on server!")) -    return proto.prompt(s + "\nWhich mailbox? [1] " -        ).addCallback(cbPickMailbox, proto, result -        ) + +    selected_mailbox = _opts.get('mailbox') + +    if not selected_mailbox: +        return proto.prompt(s + "\nWhich mailbox? [1] " +            ).addCallback(cbPickMailbox, proto, all_mbox_list +            ) +    else: +        mboxes_lower = map(lambda s: s.lower(), all_mbox_list) +        index = mboxes_lower.index(selected_mailbox.lower()) + 1 +        return cbPickMailbox(index, proto, all_mbox_list)  def cbPickMailbox(result, proto, mboxes): @@ -194,18 +206,34 @@ def cbExamineMbox(result, proto):  def cbFetch(result, proto):      """ -    Display headers. +    Display a listing of the messages in the mailbox, based on the collected +    headers.      """ +    selected_subject = _opts.get('subject', None) +    index = None +      if result:          keys = result.keys()          keys.sort() -        for k in keys: -            proto.display('%s %s' % (k, result[k][0][2])) + +        if selected_subject: +            for k in keys: +                # remove 'Subject: ' preffix plus eol +                subject = result[k][0][2][9:].rstrip('\r\n') +                if subject.lower() == selected_subject.lower(): +                    index = k +                    break +        else: +            for k in keys: +                proto.display('%s %s' % (k, result[k][0][2]))      else:          print "Hey, an empty mailbox!" -    return proto.prompt("\nWhich message? [1] (Q quits) " -                        ).addCallback(cbPickMessage, proto) +    if not index: +        return proto.prompt("\nWhich message? [1] (Q quits) " +                            ).addCallback(cbPickMessage, proto) +    else: +        return cbPickMessage(index, proto)  def cbPickMessage(result, proto): @@ -247,16 +275,53 @@ def cbClose(result):  def main(): +    import argparse +    import ConfigParser      import sys +    from twisted.internet import reactor + +    description = ( +        'Get messages from a LEAP IMAP Proxy.\nThis is a ' +        'debugging tool, do not use this to retrieve any sensitive ' +        'information, or we will send ninjas to your house!') +    epilog = ( +        'In case you want to automate the usage of this utility ' +        'you can place your credentials in a file pointed by ' +        'BITMASK_CREDENTIALS. You need to have a [Credentials] ' +        'section, with username=<user@provider> and password fields')  + +    parser = argparse.ArgumentParser(description=description, epilog=epilog) +    credentials = os.environ.get('BITMASK_CREDENTIALS') + +    if credentials: +        try: +            config = ConfigParser.ConfigParser() +            config.read(credentials) +            username = config.get('Credentials', 'username') +            password = config.get('Credentials', 'password') +        except Exception, e: +            print "Error reading credentials file: {0}".format(e) +            sys.exit() +    else: +        parser.add_argument('username', type=str) +        parser.add_argument('password', type=str) -    if len(sys.argv) != 3: -        print "Usage: getmail <user> <pass>" -        sys.exit() +    parser.add_argument('--mailbox', dest='mailbox', default=None, +        help='Which mailbox to retrieve. Empty for interactive prompt.') +    parser.add_argument('--subject', dest='subject', default=None, +        help='A subject for retrieve a mail that matches. Empty for interactive prompt.') + +    ns = parser.parse_args() + +    if not credentials: +        username = ns.username +        password = ns.password + +    _opts['mailbox'] = ns.mailbox +    _opts['subject'] = ns.subject      hostname = "localhost"      port = "1984" -    username = sys.argv[1] -    password = sys.argv[2]      onConn = defer.Deferred(          ).addCallback(cbServerGreeting, username, password @@ -265,7 +330,6 @@ def main():      factory = SimpleIMAP4ClientFactory(username, onConn) -    from twisted.internet import reactor      if port == '993':          reactor.connectSSL(              hostname, int(port), factory, ssl.ClientContextFactory()) diff --git a/src/leap/mail/incoming/service.py b/src/leap/mail/incoming/service.py index 2bc6751..d554c51 100644 --- a/src/leap/mail/incoming/service.py +++ b/src/leap/mail/incoming/service.py @@ -21,7 +21,6 @@ import copy  import logging  import shlex  import time -import traceback  import warnings  from email.parser import Parser @@ -33,18 +32,18 @@ from urlparse import urlparse  from twisted.application.service import Service  from twisted.python import log +from twisted.python.failure import Failure  from twisted.internet import defer, reactor  from twisted.internet.task import LoopingCall  from twisted.internet.task import deferLater -from u1db import errors as u1db_errors -from leap.common.events import emit, catalog +from leap.common.events import emit_async, catalog  from leap.common.check import leap_assert, leap_assert_type  from leap.common.mail import get_email_charset  from leap.keymanager import errors as keymanager_errors  from leap.keymanager.openpgp import OpenPGPKey  from leap.mail.adaptors import soledad_indexes as fields -from leap.mail.utils import json_loads, empty, first +from leap.mail.utils import json_loads, empty  from leap.soledad.client import Soledad  from leap.soledad.common.crypto import ENC_SCHEME_KEY, ENC_JSON_KEY  from leap.soledad.common.errors import InvalidAuthTokenError @@ -90,6 +89,7 @@ class IncomingMail(Service):      CONTENT_KEY = "content"      LEAP_SIGNATURE_HEADER = 'X-Leap-Signature' +    LEAP_ENCRYPTION_HEADER = 'X-Leap-Encryption'      """      Header added to messages when they are decrypted by the fetcher,      which states the validity of an eventual signature that might be included @@ -99,6 +99,8 @@ class IncomingMail(Service):      LEAP_SIGNATURE_INVALID = 'invalid'      LEAP_SIGNATURE_COULD_NOT_VERIFY = 'could not verify' +    LEAP_ENCRYPTION_DECRYPTED = 'decrypted' +      def __init__(self, keymanager, soledad, inbox, userid,                   check_period=INCOMING_CHECK_PERIOD): @@ -182,13 +184,21 @@ class IncomingMail(Service):      def startService(self):          """          Starts a loop to fetch mail. + +        :returns: A Deferred whose callback will be invoked with +                  the LoopingCall instance when loop.stop is called, or +                  whose errback will be invoked when the function raises an +                  exception or returned a deferred that has its errback +                  invoked.          """          Service.startService(self)          if self._loop is None:              self._loop = LoopingCall(self.fetch) -            return self._loop.start(self._check_period) +            stop_deferred = self._loop.start(self._check_period) +            return stop_deferred          else:              logger.warning("Tried to start an already running fetching loop.") +            return defer.fail(Failure('Already running loop.'))      def stopService(self):          """ @@ -228,7 +238,7 @@ class IncomingMail(Service):          except InvalidAuthTokenError:              # if the token is invalid, send an event so the GUI can              # disable mail and show an error message. -            emit(catalog.SOLEDAD_INVALID_AUTH_TOKEN) +            emit_async(catalog.SOLEDAD_INVALID_AUTH_TOKEN)      def _signal_fetch_to_ui(self, doclist):          """ @@ -244,16 +254,16 @@ class IncomingMail(Service):              num_mails = len(doclist) if doclist is not None else 0              if num_mails != 0:                  log.msg("there are %s mails" % (num_mails,)) -            emit(catalog.MAIL_FETCHED_INCOMING, -                 str(num_mails), str(fetched_ts)) +            emit_async(catalog.MAIL_FETCHED_INCOMING, +                       str(num_mails), str(fetched_ts))              return doclist      def _signal_unread_to_ui(self, *args):          """          Sends unread event to ui.          """ -        emit(catalog.MAIL_UNREAD_MESSAGES, -             str(self._inbox_collection.count_unseen())) +        emit_async(catalog.MAIL_UNREAD_MESSAGES, +                   str(self._inbox_collection.count_unseen()))      # process incoming mail. @@ -276,8 +286,8 @@ class IncomingMail(Service):          deferreds = []          for index, doc in enumerate(doclist):              logger.debug("processing doc %d of %d" % (index + 1, num_mails)) -            emit(catalog.MAIL_MSG_PROCESSING, -                 str(index), str(num_mails)) +            emit_async(catalog.MAIL_MSG_PROCESSING, +                       str(index), str(num_mails))              keys = doc.content.keys() @@ -294,7 +304,7 @@ class IncomingMail(Service):                  logger.debug("skipping msg with decrypting errors...")              elif self._is_msg(keys):                  d = self._decrypt_doc(doc) -                d.addCallback(self._extract_keys) +                d.addCallback(self._maybe_extract_keys)                  d.addCallbacks(self._add_message_locally, self._errback)                  deferreds.append(d)          d = defer.gatherResults(deferreds, consumeErrors=True) @@ -326,7 +336,7 @@ class IncomingMail(Service):                  decrdata = ""                  success = False -            emit(catalog.MAIL_MSG_DECRYPTED, "1" if success else "0") +            emit_async(catalog.MAIL_MSG_DECRYPTED, "1" if success else "0")              return self._process_decrypted_doc(doc, decrdata)          d = self._keymanager.decrypt( @@ -461,6 +471,10 @@ class IncomingMail(Service):          d.addCallback(add_leap_header)          return d +    def _add_decrypted_header(self, msg): +        msg.add_header(self.LEAP_ENCRYPTION_HEADER, +                       self.LEAP_ENCRYPTION_DECRYPTED) +      def _decrypt_multipart_encrypted_msg(self, msg, encoding, senderAddress):          """          Decrypt a message with content-type 'multipart/encrypted'. @@ -503,6 +517,7 @@ class IncomingMail(Service):              # all ok, replace payload by unencrypted payload              msg.set_payload(decrmsg.get_payload()) +            self._add_decrypted_header(msg)              return (msg, signkey)          d = self._keymanager.decrypt( @@ -537,7 +552,9 @@ class IncomingMail(Service):          def decrypted_data(res):              decrdata, signkey = res -            return data.replace(pgp_message, decrdata), signkey +            replaced_data = data.replace(pgp_message, decrdata) +            self._add_decrypted_header(origmsg) +            return replaced_data, signkey          def encode_and_return(res):              data, signkey = res @@ -577,7 +594,8 @@ class IncomingMail(Service):          else:              return failure -    def _extract_keys(self, msgtuple): +    @defer.inlineCallbacks +    def _maybe_extract_keys(self, msgtuple):          """          Retrieve attached keys to the mesage and parse message headers for an          *OpenPGP* header as described on the `IETF draft @@ -604,20 +622,19 @@ class IncomingMail(Service):          msg = self._parser.parsestr(data)          _, fromAddress = parseaddr(msg['from']) -        header = msg.get(OpenPGP_HEADER, None) -        dh = defer.succeed(None) -        if header is not None: -            dh = self._extract_openpgp_header(header, fromAddress) - -        da = defer.succeed(None) +        valid_attachment = False          if msg.is_multipart(): -            da = self._extract_attached_key(msg.get_payload(), fromAddress) +            valid_attachment = yield self._maybe_extract_attached_key( +                msg.get_payload(), fromAddress) -        d = defer.gatherResults([dh, da]) -        d.addCallback(lambda _: msgtuple) -        return d +        if not valid_attachment: +            header = msg.get(OpenPGP_HEADER, None) +            if header is not None: +                yield self._maybe_extract_openpgp_header(header, fromAddress) -    def _extract_openpgp_header(self, header, address): +        defer.returnValue(msgtuple) + +    def _maybe_extract_openpgp_header(self, header, address):          """          Import keys from the OpenPGP header @@ -662,7 +679,7 @@ class IncomingMail(Service):                           % (header,))          return d -    def _extract_attached_key(self, attachments, address): +    def _maybe_extract_attached_key(self, attachments, address):          """          Import keys from the attachments @@ -672,20 +689,33 @@ class IncomingMail(Service):          :type address: str          :return: A Deferred that will be fired when all the keys are stored +                 with a boolean: True if there was a valid key attached, or +                 False otherwise.          :rtype: Deferred          """          MIME_KEY = "application/pgp-keys" +        def log_key_added(ignored): +            logger.debug('Added key found in attachment for %s' % address) +            return True + +        def failed_put_key(failure): +            logger.info("An error has ocurred adding attached key for %s: %s" +                        % (address, failure.getErrorMessage())) +            return False +          deferreds = []          for attachment in attachments:              if MIME_KEY == attachment.get_content_type(): -                logger.debug("Add key from attachment")                  d = self._keymanager.put_raw_key(                      attachment.get_payload(),                      OpenPGPKey,                      address=address) +                d.addCallbacks(log_key_added, failed_put_key)                  deferreds.append(d) -        return defer.gatherResults(deferreds) +        d = defer.gatherResults(deferreds) +        d.addCallback(lambda result: any(result)) +        return d      def _add_message_locally(self, msgtuple):          """ @@ -713,10 +743,10 @@ class IncomingMail(Service):                  listener(result)              def signal_deleted(doc_id): -                emit(catalog.MAIL_MSG_DELETED_INCOMING) +                emit_async(catalog.MAIL_MSG_DELETED_INCOMING)                  return doc_id -            emit(catalog.MAIL_MSG_SAVED_LOCALLY) +            emit_async(catalog.MAIL_MSG_SAVED_LOCALLY)              d = self._delete_incoming_message(doc)              d.addCallback(signal_deleted)              return d diff --git a/src/leap/mail/incoming/tests/test_incoming_mail.py b/src/leap/mail/incoming/tests/test_incoming_mail.py index f43f746..6880496 100644 --- a/src/leap/mail/incoming/tests/test_incoming_mail.py +++ b/src/leap/mail/incoming/tests/test_incoming_mail.py @@ -30,12 +30,13 @@ from email.parser import Parser  from mock import Mock  from twisted.internet import defer +from leap.keymanager.errors import KeyAddressMismatch  from leap.keymanager.openpgp import OpenPGPKey  from leap.mail.adaptors import soledad_indexes as fields  from leap.mail.constants import INBOX_NAME  from leap.mail.imap.account import IMAPAccount  from leap.mail.incoming.service import IncomingMail -from leap.mail.smtp.rfc3156 import MultipartEncrypted, PGPEncrypted +from leap.mail.rfc3156 import MultipartEncrypted, PGPEncrypted  from leap.mail.tests import (      TestCaseWithKeyManager,      ADDRESS, @@ -154,9 +155,6 @@ subject: independence of cyberspace          return d      def testExtractAttachedKey(self): -        """ -        Test the OpenPGP header key extraction -        """          KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..."          message = MIMEMultipart() @@ -166,18 +164,101 @@ subject: independence of cyberspace          message.attach(key)          self.fetcher._keymanager.put_raw_key = Mock(              return_value=defer.succeed(None)) + +        def put_raw_key_called(_): +            self.fetcher._keymanager.put_raw_key.assert_called_once_with( +                KEY, OpenPGPKey, address=ADDRESS_2) + +        d = self._do_fetch(message.as_string()) +        d.addCallback(put_raw_key_called) +        return d + +    def testExtractInvalidAttachedKey(self): +        KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..." + +        message = MIMEMultipart() +        message.add_header("from", ADDRESS_2) +        key = MIMEApplication("", "pgp-keys") +        key.set_payload(KEY) +        message.attach(key) +        self.fetcher._keymanager.put_raw_key = Mock( +            return_value=defer.fail(KeyAddressMismatch())) + +        def put_raw_key_called(_): +            self.fetcher._keymanager.put_raw_key.assert_called_once_with( +                KEY, OpenPGPKey, address=ADDRESS_2) + +        d = self._do_fetch(message.as_string()) +        d.addCallback(put_raw_key_called) +        return d + +    def testExtractAttachedKeyAndNotOpenPGPHeader(self): +        KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..." +        KEYURL = "https://leap.se/key.txt" +        OpenPGP = "id=12345678; url=\"%s\"; preference=signencrypt" % (KEYURL,) + +        message = MIMEMultipart() +        message.add_header("from", ADDRESS_2) +        message.add_header("OpenPGP", OpenPGP) +        key = MIMEApplication("", "pgp-keys") +        key.set_payload(KEY) +        message.attach(key) + +        self.fetcher._keymanager.put_raw_key = Mock( +            return_value=defer.succeed(None)) +        self.fetcher._keymanager.fetch_key = Mock() + +        def put_raw_key_called(_): +            self.fetcher._keymanager.put_raw_key.assert_called_once_with( +                KEY, OpenPGPKey, address=ADDRESS_2) +            self.assertFalse(self.fetcher._keymanager.fetch_key.called) + +        d = self._do_fetch(message.as_string()) +        d.addCallback(put_raw_key_called) +        return d + +    def testExtractOpenPGPHeaderIfInvalidAttachedKey(self): +        KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..." +        KEYURL = "https://leap.se/key.txt" +        OpenPGP = "id=12345678; url=\"%s\"; preference=signencrypt" % (KEYURL,) + +        message = MIMEMultipart() +        message.add_header("from", ADDRESS_2) +        message.add_header("OpenPGP", OpenPGP) +        key = MIMEApplication("", "pgp-keys") +        key.set_payload(KEY) +        message.attach(key) + +        self.fetcher._keymanager.put_raw_key = Mock( +            return_value=defer.fail(KeyAddressMismatch()))          self.fetcher._keymanager.fetch_key = Mock()          def put_raw_key_called(_):              self.fetcher._keymanager.put_raw_key.assert_called_once_with(                  KEY, OpenPGPKey, address=ADDRESS_2) +            self.fetcher._keymanager.fetch_key.assert_called_once_with( +                ADDRESS_2, KEYURL, OpenPGPKey)          d = self._do_fetch(message.as_string())          d.addCallback(put_raw_key_called)          return d +    def testAddDecryptedHeader(self): +        class DummyMsg(): +            def __init__(self): +                self.headers = {} + +            def add_header(self, k, v): +                self.headers[k] = v + +        msg = DummyMsg() +        self.fetcher._add_decrypted_header(msg) + +        self.assertEquals(msg.headers['X-Leap-Encryption'], 'decrypted') +      def testDecryptEmail(self):          self.fetcher._decryption_error = Mock() +        self.fetcher._add_decrypted_header = Mock()          def create_encrypted_message(encstr):              message = Parser().parsestr(self.EMAIL) @@ -198,9 +279,13 @@ subject: independence of cyberspace              return newmsg          def decryption_error_not_called(_): -            self.assertFalse(self.fetcher._decyption_error.called, +            self.assertFalse(self.fetcher._decryption_error.called,                               "There was some errors with decryption") +        def add_decrypted_header_called(_): +            self.assertTrue(self.fetcher._add_decrypted_header.called, +                            "There was some errors with decryption") +          d = self._km.encrypt(              self.EMAIL,              ADDRESS, OpenPGPKey, sign=ADDRESS_2) diff --git a/src/leap/mail/interfaces.py b/src/leap/mail/interfaces.py index 899400f..10f5123 100644 --- a/src/leap/mail/interfaces.py +++ b/src/leap/mail/interfaces.py @@ -1,6 +1,6 @@  # -*- coding: utf-8 -*-  # interfaces.py -# Copyright (C) 2014 LEAP +# Copyright (C) 2014,2015 LEAP  #  # This program is free software: you can redistribute it and/or modify  # it under the terms of the GNU General Public License as published by @@ -24,17 +24,71 @@ class IMessageWrapper(Interface):      """      I know how to access the different parts into which a given message is      splitted into. + +    :ivar fdoc: dict with flag document. +    :ivar hdoc: dict with flag document. +    :ivar cdocs: dict with content-documents, one-indexed.      """      fdoc = Attribute('A dictionaly-like containing the flags document '                       '(mutable)') -    hdoc = Attribute('A dictionary-like containing the headers docuemnt ' +    hdoc = Attribute('A dictionary-like containing the headers document '                       '(immutable)')      cdocs = Attribute('A dictionary with the content-docs, one-indexed') +    def create(self, store, notify_just_mdoc=False, pending_inserts_dict={}): +        """ +        Create the underlying wrapper. +        """ + +    def update(self, store): +        """ +        Update the only mutable parts, which are within the flags document. +        """ + +    def delete(self, store): +        """ +        Delete the parts for this wrapper that are not referenced from anywhere +        else. +        """ + +    def copy(self, store, new_mbox_uuid): +        """ +        Return a copy of this IMessageWrapper in a new mailbox. +        """ + +    def set_mbox_uuid(self, mbox_uuid): +        """ +        Set the mailbox for this wrapper. +        """ + +    def set_flags(self, flags): +        """ +        """ + +    def set_tags(self, tags): +        """ +        """ + +    def set_date(self, date): +        """ +        """ + +    def get_subpart_dict(self, index): +        """ +        :param index: the part to lookup, 1-indexed +        """ + +    def get_subpart_indexes(self): +        """ +        """ + +    def get_body(self, store): +        """ +        """ + -# TODO [ ] Catch up with the actual implementation! -#          Lot of stuff added there ... +# TODO -- split into smaller interfaces? separate mailbox interface at least?  class IMailAdaptor(Interface):      """ @@ -53,64 +107,109 @@ class IMailAdaptor(Interface):          :rtype: deferred          """ -    # TODO is staticmethod valid with an interface? -    # @staticmethod      def get_msg_from_string(self, MessageClass, raw_msg):          """ -        Return IMessageWrapper implementor from a raw mail string +        Get an instance of a MessageClass initialized with a MessageWrapper +        that contains all the parts obtained from parsing the raw string for +        the message.          :param MessageClass: an implementor of IMessage          :type raw_msg: str          :rtype: implementor of leap.mail.IMessage          """ -    # TODO is staticmethod valid with an interface? -    # @staticmethod -    def get_msg_from_docs(self, MessageClass, msg_wrapper): +    def get_msg_from_docs(self, MessageClass, mdoc, fdoc, hdoc, cdocs=None, +                          uid=None):          """ -        Return an IMessage implementor from its parts. +        Get an instance of a MessageClass initialized with a MessageWrapper +        that contains the passed part documents. -        :param MessageClass: an implementor of IMessage -        :param msg_wrapper: an implementor of IMessageWrapper -        :rtype: implementor of leap.mail.IMessage +        This is not the recommended way of obtaining a message, unless you know +        how to take care of ensuring the internal consistency between the part +        documents, or unless you are glueing together the part documents that +        have been previously generated by `get_msg_from_string`. +        """ + +    def get_flags_from_mdoc_id(self, store, mdoc_id): +        """ +        """ + +    def create_msg(self, store, msg): +        """ +        :param store: an instance of soledad, or anything that behaves alike +        :param msg: a Message object. + +        :return: a Deferred that is fired when all the underlying documents +                 have been created. +        :rtype: defer.Deferred +        """ + +    def update_msg(self, store, msg):          """ +        :param msg: a Message object. +        :param store: an instance of soledad, or anything that behaves alike +        :return: a Deferred that is fired when all the underlying documents +                 have been updated (actually, it's only the fdoc that's allowed +                 to update). +        :rtype: defer.Deferred +        """ + +    def get_count_unseen(self, store, mbox_uuid): +        """ +        Get the number of unseen messages for a given mailbox. -    # ------------------------------------------------------------------- -    # XXX unsure about the following part yet ........................... +        :param store: instance of Soledad. +        :param mbox_uuid: the uuid for this mailbox. +        :rtype: int +        """ -    # the idea behind these three methods is that the adaptor also offers a -    # fixed interface to create the documents the first time (using -    # soledad.create_docs or whatever method maps to it in a similar store, and -    # also allows to update flags and tags, hiding the actual implementation of -    # where the flags/tags live in behind the concrete MailWrapper in use -    # by this particular adaptor. In our impl it will be put_doc(fdoc) after -    # locking the getting + updating of that fdoc for atomicity. +    def get_count_recent(self, store, mbox_uuid): +        """ +        Get the number of recent messages for a given mailbox. -    # 'store' must be an instance of something that offers a minimal subset of -    # the document API that Soledad currently implements (create_doc, put_doc) -    # I *think* store should belong to Account/Collection and be passed as -    # param here instead of relying on it being an attribute of the instance. +        :param store: instance of Soledad. +        :param mbox_uuid: the uuid for this mailbox. +        :rtype: int +        """ -    def create_msg_docs(self, store, msg_wrapper): +    def get_mdoc_id_from_msgid(self, store, mbox_uuid, msgid):          """ -        :param store: The documents store -        :type store: -        :param msg_wrapper: -        :type msg_wrapper: IMessageWrapper implementor +        Get the UID for a message with the passed msgid (the one in the headers +        msg-id). +        This is used by the MUA to retrieve the recently saved draft.          """ -    def update_msg_flags(self, store, msg_wrapper): +    # mbox handling + +    def get_or_create_mbox(self, store, name):          """ -        :param store: The documents store -        :type store: -        :param msg_wrapper: -        :type msg_wrapper: IMessageWrapper implementor +        Get the mailbox with the given name, or create one if it does not +        exist. + +        :param store: instance of Soledad +        :param name: the name of the mailbox +        :type name: str          """ -    def update_msg_tags(self, store, msg_wrapper): +    def update_mbox(self, store, mbox_wrapper):          """ -        :param store: The documents store -        :type store: -        :param msg_wrapper: -        :type msg_wrapper: IMessageWrapper implementor +        Update the documents for a given mailbox. +        :param mbox_wrapper: MailboxWrapper instance +        :type mbox_wrapper: MailboxWrapper +        :return: a Deferred that will be fired when the mailbox documents +                 have been updated. +        :rtype: defer.Deferred +        """ + +    def delete_mbox(self, store, mbox_wrapper): +        """ +        """ + +    def get_all_mboxes(self, store): +        """ +        Retrieve a list with wrappers for all the mailboxes. + +        :return: a deferred that will be fired with a list of all the +                 MailboxWrappers found. +        :rtype: defer.Deferred          """ diff --git a/src/leap/mail/mail.py b/src/leap/mail/mail.py index 540a493..c0e16a6 100644 --- a/src/leap/mail/mail.py +++ b/src/leap/mail/mail.py @@ -1,6 +1,6 @@  # -*- coding: utf-8 -*-  # mail.py -# Copyright (C) 2014 LEAP +# Copyright (C) 2014,2015 LEAP  #  # This program is free software: you can redistribute it and/or modify  # it under the terms of the GNU General Public License as published by @@ -15,7 +15,12 @@  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  """ -Generic Access to Mail objects: Public LEAP Mail API. +Generic Access to Mail objects. + +This module holds the public LEAP Mail API, which should be viewed as the main +entry point for message and account manipulation, in a protocol-agnostic way. + +In the future, pluggable transports will expose this generic API.  """  import itertools  import uuid @@ -28,7 +33,7 @@ from twisted.internet import defer  from twisted.python import log  from leap.common.check import leap_assert_type -from leap.common.events import emit, catalog +from leap.common.events import emit_async, catalog  from leap.common.mail import get_email_charset  from leap.mail.adaptors.soledad import SoledadMailAdaptor @@ -148,6 +153,9 @@ class MessagePart(object):      # TODO This class should be better abstracted from the data model.      # TODO support arbitrarily nested multiparts (right now we only support      #      the trivial case) +    """ +    Represents a part of a multipart MIME Message. +    """      def __init__(self, part_map, cdocs={}, nested=False):          """ @@ -736,8 +744,7 @@ class MessageCollection(object):          :param unseen: number of unseen messages.          :type unseen: int          """ -        # TODO change name of the signal, independent from imap now. -        emit(catalog.MAIL_UNREAD_MESSAGES, str(unseen)) +        emit_async(catalog.MAIL_UNREAD_MESSAGES, str(unseen))      def copy_msg(self, msg, new_mbox_uuid):          """ @@ -917,17 +924,19 @@ class Account(object):      adaptor_class = SoledadMailAdaptor -    # This is a mapping to collection instances so that we always -    # return a reference to them instead of creating new ones. However, being a -    # dictionary of weakrefs values, they automagically vanish from the dict -    # when no hard refs is left to them (so they can be garbage collected) -    # This is important because the different wrappers rely on several -    # kinds of deferredLocks that are kept as class or instance variables -    _collection_mapping = weakref.WeakValueDictionary() -      def __init__(self, store, ready_cb=None):          self.store = store          self.adaptor = self.adaptor_class() + +        # this is a mapping to collection instances so that we always +        # return a reference to them instead of creating new ones. however, +        # being a dictionary of weakrefs values, they automagically vanish +        # from the dict when no hard refs is left to them (so they can be +        # garbage collected) this is important because the different wrappers +        # rely on several kinds of deferredlocks that are kept as class or +        # instance variables +        self._collection_mapping = weakref.WeakValueDictionary() +          self.mbox_indexer = MailboxIndexer(self.store)          # This flag is only used from the imap service for the moment. diff --git a/src/leap/mail/mailbox_indexer.py b/src/leap/mail/mailbox_indexer.py index 08e5f10..c49f808 100644 --- a/src/leap/mail/mailbox_indexer.py +++ b/src/leap/mail/mailbox_indexer.py @@ -15,6 +15,8 @@  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  """ +.. :py:module::mailbox_indexer +  Local tables to store the message Unique Identifiers for a given mailbox.  """  import re @@ -72,10 +74,11 @@ class MailboxIndexer(object):      These indexes are Message Attributes needed for the IMAP specification (rfc      3501), although they can be useful for other non-imap store      implementations. +      """      # The uids are expected to be 32-bits values, but the ROWIDs in sqlite      # are 64-bit values. I *don't* think it really matters for any -    # practical use, but it's good to remmeber we've got that difference going +    # practical use, but it's good to remember we've got that difference going      # on.      store = None diff --git a/src/leap/mail/outgoing/service.py b/src/leap/mail/outgoing/service.py index 838a908..7cc5a24 100644 --- a/src/leap/mail/outgoing/service.py +++ b/src/leap/mail/outgoing/service.py @@ -31,17 +31,17 @@ from twisted.protocols.amp import ssl  from twisted.python import log  from leap.common.check import leap_assert_type, leap_assert -from leap.common.events import emit, catalog +from leap.common.events import emit_async, catalog  from leap.keymanager.openpgp import OpenPGPKey  from leap.keymanager.errors import KeyNotFound, KeyAddressMismatch  from leap.mail import __version__  from leap.mail.utils import validate_address -from leap.mail.smtp.rfc3156 import MultipartEncrypted -from leap.mail.smtp.rfc3156 import MultipartSigned -from leap.mail.smtp.rfc3156 import encode_base64_rec -from leap.mail.smtp.rfc3156 import RFC3156CompliantGenerator -from leap.mail.smtp.rfc3156 import PGPSignature -from leap.mail.smtp.rfc3156 import PGPEncrypted +from leap.mail.rfc3156 import MultipartEncrypted +from leap.mail.rfc3156 import MultipartSigned +from leap.mail.rfc3156 import encode_base64_rec +from leap.mail.rfc3156 import RFC3156CompliantGenerator +from leap.mail.rfc3156 import PGPSignature +from leap.mail.rfc3156 import PGPEncrypted  # TODO  # [ ] rename this module to something else, service should be the implementor @@ -135,7 +135,7 @@ class OutgoingMail:          """          dest_addrstr = smtp_sender_result[1][0][0]          log.msg('Message sent to %s' % dest_addrstr) -        emit(catalog.SMTP_SEND_MESSAGE_SUCCESS, dest_addrstr) +        emit_async(catalog.SMTP_SEND_MESSAGE_SUCCESS, dest_addrstr)      def sendError(self, failure):          """ @@ -145,7 +145,7 @@ class OutgoingMail:          :type e: anything          """          # XXX: need to get the address from the exception to send signal -        # emit(catalog.SMTP_SEND_MESSAGE_ERROR, self._user.dest.addrstr) +        # emit_async(catalog.SMTP_SEND_MESSAGE_ERROR, self._user.dest.addrstr)          err = failure.value          log.err(err)          raise err @@ -178,7 +178,7 @@ class OutgoingMail:              requireAuthentication=False,              requireTransportSecurity=True)          factory.domain = __version__ -        emit(catalog.SMTP_SEND_MESSAGE_START, recipient.dest.addrstr) +        emit_async(catalog.SMTP_SEND_MESSAGE_START, recipient.dest.addrstr)          reactor.connectSSL(              self._host, self._port, factory,              contextFactory=SSLContextFactory(self._cert, self._key)) @@ -240,27 +240,27 @@ class OutgoingMail:              return d          def signal_encrypt_sign(newmsg): -            emit(catalog.SMTP_END_ENCRYPT_AND_SIGN, -                 "%s,%s" % (self._from_address, to_address)) +            emit_async(catalog.SMTP_END_ENCRYPT_AND_SIGN, +                       "%s,%s" % (self._from_address, to_address))              return newmsg, recipient          def if_key_not_found_send_unencrypted(failure, message):              failure.trap(KeyNotFound, KeyAddressMismatch)              log.msg('Will send unencrypted message to %s.' % to_address) -            emit(catalog.SMTP_START_SIGN, self._from_address) +            emit_async(catalog.SMTP_START_SIGN, self._from_address)              d = self._sign(message, from_address)              d.addCallback(signal_sign)              return d          def signal_sign(newmsg): -            emit(catalog.SMTP_END_SIGN, self._from_address) +            emit_async(catalog.SMTP_END_SIGN, self._from_address)              return newmsg, recipient          log.msg("Will encrypt the message with %s and sign with %s."                  % (to_address, from_address)) -        emit(catalog.SMTP_START_ENCRYPT_AND_SIGN, -             "%s,%s" % (self._from_address, to_address)) +        emit_async(catalog.SMTP_START_ENCRYPT_AND_SIGN, +                   "%s,%s" % (self._from_address, to_address))          d = self._maybe_attach_key(origmsg, from_address, to_address)          d.addCallback(maybe_encrypt_and_sign)          return d diff --git a/src/leap/mail/outgoing/tests/test_outgoing.py b/src/leap/mail/outgoing/tests/test_outgoing.py index 2376da9..5518b33 100644 --- a/src/leap/mail/outgoing/tests/test_outgoing.py +++ b/src/leap/mail/outgoing/tests/test_outgoing.py @@ -30,7 +30,7 @@ from twisted.mail.smtp import User  from mock import Mock  from leap.mail.smtp.gateway import SMTPFactory -from leap.mail.smtp.rfc3156 import RFC3156CompliantGenerator +from leap.mail.rfc3156 import RFC3156CompliantGenerator  from leap.mail.outgoing.service import OutgoingMail  from leap.mail.tests import (      TestCaseWithKeyManager, diff --git a/src/leap/mail/smtp/rfc3156.py b/src/leap/mail/rfc3156.py index 7d7bc0f..7d7bc0f 100644 --- a/src/leap/mail/smtp/rfc3156.py +++ b/src/leap/mail/rfc3156.py diff --git a/src/leap/mail/smtp/__init__.py b/src/leap/mail/smtp/__init__.py index 2ff14d7..7b62808 100644 --- a/src/leap/mail/smtp/__init__.py +++ b/src/leap/mail/smtp/__init__.py @@ -19,12 +19,13 @@  SMTP gateway helper function.  """  import logging +import os  from twisted.internet import reactor  from twisted.internet.error import CannotListenError  from leap.mail.outgoing.service import OutgoingMail -from leap.common.events import emit, catalog +from leap.common.events import emit_async, catalog  from leap.mail.smtp.gateway import SMTPFactory  logger = logging.getLogger(__name__) @@ -64,13 +65,19 @@ def setup_smtp_gateway(port, userid, keymanager, smtp_host, smtp_port,          userid, keymanager, smtp_cert, smtp_key, smtp_host, smtp_port)      factory = SMTPFactory(userid, keymanager, encrypted_only, outgoing_mail)      try: -        tport = reactor.listenTCP(port, factory, interface="localhost") -        emit(catalog.SMTP_SERVICE_STARTED, str(port)) +        interface = "localhost" +        # don't bind just to localhost if we are running on docker since we +        # won't be able to access smtp from the host +        if os.environ.get("LEAP_DOCKERIZED"): +            interface = '' + +        tport = reactor.listenTCP(port, factory, interface=interface) +        emit_async(catalog.SMTP_SERVICE_STARTED, str(port))          return factory, tport      except CannotListenError:          logger.error("STMP Service failed to start: "                       "cannot listen in port %s" % port) -        emit(catalog.SMTP_SERVICE_FAILED_TO_START, str(port)) +        emit_async(catalog.SMTP_SERVICE_FAILED_TO_START, str(port))      except Exception as exc:          logger.error("Unhandled error while launching smtp gateway service")          logger.exception(exc) diff --git a/src/leap/mail/smtp/gateway.py b/src/leap/mail/smtp/gateway.py index 7dae907..45560bf 100644 --- a/src/leap/mail/smtp/gateway.py +++ b/src/leap/mail/smtp/gateway.py @@ -37,14 +37,11 @@ from twisted.python import log  from email.Header import Header  from leap.common.check import leap_assert_type -from leap.common.events import emit, catalog +from leap.common.events import emit_async, catalog  from leap.keymanager.openpgp import OpenPGPKey  from leap.keymanager.errors import KeyNotFound  from leap.mail.utils import validate_address - -from leap.mail.smtp.rfc3156 import ( -    RFC3156CompliantGenerator, -) +from leap.mail.rfc3156 import RFC3156CompliantGenerator  # replace email generator with a RFC 3156 compliant one.  from email import generator @@ -204,18 +201,19 @@ class SMTPDelivery(object):          # verify if recipient key is available in keyring          def found(_):              log.msg("Accepting mail for %s..." % user.dest.addrstr) -            emit(catalog.SMTP_RECIPIENT_ACCEPTED_ENCRYPTED, user.dest.addrstr) +            emit_async(catalog.SMTP_RECIPIENT_ACCEPTED_ENCRYPTED, +                       user.dest.addrstr)          def not_found(failure):              failure.trap(KeyNotFound)              # if key was not found, check config to see if will send anyway              if self._encrypted_only: -                emit(catalog.SMTP_RECIPIENT_REJECTED, user.dest.addrstr) +                emit_async(catalog.SMTP_RECIPIENT_REJECTED, user.dest.addrstr)                  raise smtp.SMTPBadRcpt(user.dest.addrstr)              log.msg("Warning: will send an unencrypted message (because "                      "encrypted_only' is set to False).") -            emit( +            emit_async(                  catalog.SMTP_RECIPIENT_ACCEPTED_UNENCRYPTED,                  user.dest.addrstr) @@ -309,7 +307,7 @@ class EncryptedMessage(object):          """          log.msg("Connection lost unexpectedly!")          log.err() -        emit(catalog.SMTP_CONNECTION_LOST, self._user.dest.addrstr) +        emit_async(catalog.SMTP_CONNECTION_LOST, self._user.dest.addrstr)          # unexpected loss of connection; don't save          self._lines = [] diff --git a/src/leap/mail/tests/__init__.py b/src/leap/mail/tests/__init__.py index de0088f..71452d2 100644 --- a/src/leap/mail/tests/__init__.py +++ b/src/leap/mail/tests/__init__.py @@ -91,7 +91,7 @@ class TestCaseWithKeyManager(unittest.TestCase, BaseLeapTest):          nickserver_url = ''  # the url of the nickserver          self._km = KeyManager(address, nickserver_url, self._soledad, -                              ca_cert_path='', gpgbinary=self.GPG_BINARY_PATH) +                              gpgbinary=self.GPG_BINARY_PATH)          self._km._fetcher.put = Mock()          self._km._fetcher.get = Mock(return_value=Response()) | 
