[
  {
    "path": ".coveragerc",
    "content": "# =========================================================================\n# COVERAGE CONFIGURATION FILE: .coveragerc\n# =========================================================================\n# LANGUAGE: Python\n# SEE ALSO:\n#  * http://nedbatchelder.com/code/coverage/\n#  * http://nedbatchelder.com/code/coverage/config.html\n# =========================================================================\n\n[coverage:run]\nappend  = .coverage\ninclude = mailpile*\nomit = nose\nbranch  = True\n#parallel = True\n\n[coverage:report]\n# Regexes for lines to exclude from consideration\nexclude_lines =\n    # Have to re-enable the standard pragma\n    pragma: no cover\n\n    # Don't complain about missing debug-only code:\n    def __repr__\n    if self\\.debug\n\n    # Don't complain if tests don't hit defensive assertion code:\n    raise AssertionError\n    raise NotImplementedError\n\n    # Don't complain if non-runnable code isn't run:\n    if 0:\n    if False:\n    if __name__ == .__main__.:\n\nignore_errors = True\n\n[coverage:html]\ndirectory = build/coverage.html\n\n[coverage:xml]\noutfile = build/coverage.xml\n\n"
  },
  {
    "path": ".dockerignore",
    "content": "Dockerfile\nVagrantfile\n"
  },
  {
    "path": ".gitignore",
    "content": "*.pyc\n*.pyo\n*~\n*.po\n*.debhelper\n*.iml\n*.ipr\n*.swp\n*.swo\nmp-virtualenv\n.idea\n.*deps\n.tox\nmailpile.egg-info/\nmailpile-tmp.py\nstatic/default/plugins/\nmailpile/tests/data/private-test-data.mbx\nmailpile/tests/data/tmp\nmailpile/tests/data/gpg-keyring/random_seed\nscripts/less-compiler.mk\n.DS_Store\n.vagrant\n.coverage\nghostdriver.log\nbuild/\ndist/\n.eggs/\ncover/\nbower_components/\nnode_modules/\ntesting/\nmailpile.tar.gz\nAUTHORS\nChangeLog\nshared-data/multipile/www/admin.cgi\nsetup-tmp/\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"doc\"]\n\tpath = doc\n\turl = https://github.com/pagekite/Mailpile.wiki.git\n\tbranch = master\n[submodule \"shared-data/contrib/print\"]\n\tpath = shared-data/contrib/print\n\turl = https://github.com/mailpile/Mailpile-print.git\n[submodule \"submodules/gui-o-matic\"]\n\tpath = submodules/gui-o-matic\n\turl = https://github.com/mailpile/gui-o-matic\n[submodule \"submodules/gui-o-mac-tic\"]\n\tpath = submodules/gui-o-mac-tic\n\turl = https://github.com/mailpile/gui-o-mac-tic\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: python\npython: 2.7\nenv:\n  - TOX_ENV=py27\n\naddons:\n  apt:\n    packages:\n      - gnupg\n\ninstall:\n  - pip install tox coveralls\n\nscript:\n  - tox -e $TOX_ENV\n\nafter_success:\n  - coveralls\n"
  },
  {
    "path": ".tx/config",
    "content": "[main]\nhost = https://www.transifex.com\n\n[mailpile.mailpilepot]\nsource_file = shared-data/locale/mailpile.pot\nsource_lang = en\ntype = PO\nfile_filter = shared-data/locale/<lang>/LC_MESSAGES/mailpile.po\n"
  },
  {
    "path": "AGPLv3.txt",
    "content": "NOTE: Please see the file COPYING.md for details on Mailpile licensing.\n\n\n                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<http://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# The Mailpile Code of Conduct\n\nThe Mailpile project depends on its community of volunteers, supporters and of\ncourse users. We want people to feel welcome and respected. To this end,\nproject maintainers have plegded to adhere to and enforce the Contrbitor\nCovenant Code of Conduct, in their Mailpile related activities, both online and\nin person.\n\nWe respectfully ask that other contributors and participants in our community\ndo so as well.\n\nThank you for your understanding, and welcome!\n\n\n# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, gender identity and expression, level of experience,\nnationality, personal appearance, race, religion, or sexual identity and\norientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n  advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at <mailto:team@mailpile.is>. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## How to contribute to Mailpile\n\nFirst of all: Thank you! :heart:\n\n\n#### Further Reading\n\nSecond of all, please adhere to our Code of Conduct when you participate\nin our community. Be kind, be respectful. [The full Code of Conduct can\nbe found here](CODE_OF_CONDUCT.md).\n\nPlease also be sure you are comfortable with [our license](COPYING.md)\nbefore contributing any code to Mailpile.\n\nNote that Mailpile is a slightly unsual app; the design is both political\nand opinionated. If you are confused or unsure whether something is a bug or a\nfeature, [our developer FAQ might have answers](DEV_FAQ.md)! Please read it\nbefore filing issues about design choices or attempting to reorganize the code\nin any major way. The FAQ also contains a quick introduction Mailpile internals\nand debugging techniques.\n\nAlso, have you seen [the main Mailpile website](https://www.mailpile.is/) and\n[the community Discourse](https://community.mailpile.is/categories)?\n\n\n#### Are you new to Mailpile and/or FOSS?\n\n* Welcome! The first step is probably to get Mailpile up and running, play with it, get familiar.\n\n* Read the links above, and [check out our website](https://www.mailpile.is/) if you haven't already.\n\n* Not sure what to work on? Find inspiration in one of our [Low Hanging Fruit](https://github.com/mailpile/Mailpile/issues?q=is%3Aissue+is%3Aopen+label%3A%22Low+Hanging+Fruit%22) issues!\n\n* Otherwise, read on...\n\n\n#### Did you find a bug?\n\n* **Ensure the bug was not already reported** by searching on GitHub\n  under [Issues](https://github.com/mailpile/Mailpile/issues).\n\n* If you're unable to find an open issue addressing the problem,\n  [open a new one](https://github.com/mailpile/Mailpile/issues/new). Be\n  sure to include a **title and clear description**, as much relevant\n  information as possible.\n\n* Remember, we cannot read your mind or see your screen, so you'll\n  probably need to answer all of these:\n\n   1. What were you doing?\n   2. What did you expect would happen?\n   3. What actually happened?\n   4. Operating system? GnuPG version? Mailpile version?\n\n* Reproducability is key. If you cannot reliably trigger the bug or\n  cannot describe how to do so, then unfortunately it's less likely that\n  the Mailpile team will be able to do anything about it. It may still\n  be useful to file a report in case others are having the same issue, but\n  bugs that can be reproduced will in general get fixed much faster!\n\n* The [Developer FAQ](DEV_FAQ.md) has a section on debugging techniques.\n\n* If it's not bug, but you still need help: [visit our Discourse support\n  forum](https://community.mailpile.is/c/support).\n\n\n#### Did you write a patch that fixes a bug?\n\n* Open a new GitHub pull request with the patch.\n\n* Ensure the PR description clearly describes the problem and solution.\n  Include the relevant issue number if applicable.\n\n\n#### Do you plan write a patch that adds a feature?\n\n* Please be sure you have read the [Developer FAQ](DEV_FAQ.md) to ensure\n  your feature is compatible with our high-level goals and design decisions.\n\n* If you are unsure or would like some guidance, join the #mailpile\n  channel on IRC (Freenode) and discuss your plans there.\n\n* Open a new GitHub pull request with the patch.\n\n* Ensure the PR description clearly describes what your feature does.\n  Include the relevant issue number if applicable.\n\n\n#### Did you fix whitespace, format code, or make a purely cosmetic patch?\n\nChanges that are cosmetic in nature and do not add anything substantial to the\nstability, functionality, or testability of Mailpile will generally not be\naccepted.\n\nAll patches to Mailpile are reviewed by a human and our resources (time,\npeople) are very limited. Cosmetic patches are no easier to review than other\npatches and we would rather focus our efforts on functional improvements to the\nsoftware.\n\n\n#### Do you want help other users?\n\nOur [community Discourse](https://community.mailpile.is/categories) has a\nsupport forum and needs friendly people to help out and steer conversations\nin friendly and productive directions. Please join the conversation!\n\n\n#### Do you want to translate Mailpile to your language?\n\nPlease feel free to join our translation community\n[on Transifex](https://www.transifex.com/otf/mailpile/).\n\n\n#### Do you plan to write documentation?\n\n* Please feel free to contribute documentation to\n  [our wiki](https://github.com/mailpile/Mailpile/wiki).\n\n* If you would like some guidance, join the #mailpile channel on IRC\n  (Freenode) or file an issue with your questions.\n\n* If you added a new page or made major major changes to an existing page,\n  please file [a new issue requesting a review](https://github.com/mailpile/Mailpile/issues/new)\n  when you are done. Be sure to include a link to the wiki page!\n\n\n#### Credits\n\nThis document borrows some language and structure from the [Ruby on Rails\ncontributor guidelines](https://raw.githubusercontent.com/rails/rails/master/CONTRIBUTING.md).\n\n"
  },
  {
    "path": "COPYING.md",
    "content": "# Mailpile - a program for doing stuff with e-mail\n\nCopyright (C) 2011-2015, Bjarni R. Einarsson, Mailpile ehf and friends.\n\nThis program is free software: you can redistribute it and/or modify it\nunder the terms of the GNU Affero General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or (at\nyour option) any later version.\n\nThis program is distributed in the hope that it will be useful, but\nWITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero\nGeneral Public License more details.\n\nYou should have received a copy of the GNU Affero General Public License\nalong with this program. If not, see <http://www.gnu.org/licenses/>.\n\n\n## What happened to the Apache license?\n\nThis software used to be dual-licensed under AGPLv3 and the Apache\nLicense 2.0. If you feel an uncontrollable urge to fork and carry on\nwithout the AGPL, the last dual-licensed code point is tagged as\n\"Gunsmoke--TheLastApacheTag\".  Have fun!\n\nFor more details on our licensing choices, see these blog posts:\n\n* <https://www.mailpile.is/blog/2015-05-08_Choosing_a_License.html>\n* <https://www.mailpile.is/blog/2015-06-15_Community_License_Feedback.html> \n* <https://www.mailpile.is/blog/2015-07-02_Licensing_Decision.html>\n\nThanks!\n"
  },
  {
    "path": "DEV_FAQ.md",
    "content": "## Mailpile Developer FAQ\n\nThis document contain a collection of frequently asked questions (with\nanswers) about Mailpile development. Please familiarize yourself with\nthe contents before attempting any deep hacking on Mailpile.\n\nYou don't have to agree with all of our priorities to take part or make\nuse of Mailpile, but we do feel it helps if most of the community is\nrowing in roughly the same general direction!\n\nNote: If you are just looking for debugging tips and tricks, you can skip\nto the end.\n\n\n### Why Mailpile?\n\nThe long-term goal of Mailpile is to help *non-technical people* become\n*more independent* and *more private* online, in particular when it\ncomes to e-mail.\n\nBy **more independent**, we mean people should be in control of their\ne-mail and the software used to manage it. This is why Mailpile is Free\nSoftware, and this is why we *don't* promote Mailpile as a tool for\nbuilding \"cloud services\".\n\nBy **more private**, we mean people should have more control over who has\naccess to their e-mail, when and how. This is why Mailpile tries to make\ne-mail encryption more accessible, and this is why Mailpile tries to\nmake it convenient *and secure* for people to store their e-mail on their\nown devices.\n\nOur focus on **non-technical people** implies, amongst other things, that\nwe cannot exclude people who are using non-free platforms such as\nMicrosoft Windows or Mac OS X, and we cannot require our users learn a\nradically new, unfamiliar user interface or understand highly technical\nconcepts such as public key cryptography.\n\n\n### Are All of These Decisions Awesome?\n\nNo. Some of these decisions were almost certainly mistakes. But that's\npart of innovating, you try new things and see how they go!\n\nSo we kindly request that our contributors and collaboratores refrain\nfrom picking arguments with us over these decisions. Making forward\nprogress is much more fun than rehashing the past over and over again.\n\n\n### Why Do You Reinvent So Many Wheels?\n\nUsually, the answer is one or more of the following:\n\n   * We didn't know a solution already existed\n   * We evaluated solutions X, Y and Z, but didn't like them\n   * We felt the problem was simple enough to Just Do It\n\nIn general, we are reluctant to take on new external dependencies unless\nthey are stable and widely available: if they exist in pre-packaged form\nfor Windows, Mac OS X and the most common Linux distributions, they are\nprobably fine!\n\nIf not, the benefits need to outweigh the added complexity posed for our\ncross-platform packaging efforts.\n\nFor this reason we also tend to prefer pure-Python (or Javascript, for\nthe UI) libraries over native code, which also avoids certain classes of\nbugs and security vulnerabilities which are simply not present in a\nmemory-safe, managed language.\n\nWe have broken this rule more often than we'd like in our user-interface\ncode, due to cultural differences between web developers and Python\nfolks. *We would like to gradually reduce our front-end dependencies.*\n\n\n### Why a Search Engine?\n\nMailpile started as an experimental search engine, a hobby project.\nEverything else came later.\n\n*OK, so why not replace it with a standard component?*\n\nBecause it works! Replacing it at this point would be *more work, not less*.\n\nAlso, we are unaware of any \"off the shelf\" search engines that let us\nencrypt their data stores and we feel encrypting the search index is an\nabsolute requirement since we by default allow the user to search inside\nencrypted messages.\n\n*I see. Why not remove it, to simplify the code?*\n\nThe entire app is built around the search metaphor, much like Google's\nGMail. This is a fundamentally different way to build an e-mail client,\nfrom the traditional \"messages and mailboxes\" model.\n\nThe search engine also makes it easy for Mailpile to do some pretty cool\nthings. For example:\n\n   * Mailpile can evaluate the trustworthiness of a message by asking the\n     search engine about the past behaviour of the sender\n   * Mailpile can postpone processing of things like attached PGP keys\n     until it actually intends to use the key: the search engine makes the\n     keys easy to find later on\n\n\n### Why doesn't Mailpile have a Native User Interface?\n\nFor now, because the team is very small and we would like to reach users\non many different platforms, we have bet entirely on the web as our\nprimary user interface:\n\n   * This decision allows us to target any operating system for which\n     Python 2.7 has been made available.\n\n   * It also allows us to get help from the enormous community of web\n     developers; this is a much larger talent pool than the pool of\n     developers that know how to write native apps.\n\n   * Last but not least, the web interface is key to our plan to allow\n     users to access their Mailpile remotely over the network. Remote\n     access is critical if we want to get people to store their e-mail\n     on their own devices, because most people read their mail on\n     multiple devices (laptop, tablet, mobile, work computer, etc).\n\nThat said: we do want to have a minimal native user interface, on all\nthe major desktop operating systems. [That is what the gui-o-matic\nspin-off project is about](https://github.com/mailpile/gui-o-matic).\n\nNative mobile apps on Android and/or iOS would also be nice to have!\n\n\n### How Does Mailpile Handle Security?\n\nThis is a huge topic! Please consult our [Security\nRoadmap](https://github.com/mailpile/Mailpile/wiki/Security-roadmap).\n\nSecurity is complex and means different things to different people.\n\nThe most controversial questions relating to security, have to do with\nmass surveillance and law enforcement. Mailpile's stance is that we\nbelieve that people have an innate right to privacy we believe that mass\nsurveillance is *wrong*. If the government wants to read your e-mail,\nwe feel they should present you with a search warrant.\n\nThwarting other adversaries (criminals, jealous partners, etc.) is also\nvery much something we care about, but is probably less divisive.\n\n\n### Why GnuPG? Why not GPGME? Why not PGPy?\n\nGnuPG is mature and stable. Although the user interfaces may leave\nsomething to be desired, it has a rich ecosystem of powerful tools built\naround it and a wealth of documentation and support to be found online.\nIf we didn't use GnuPG, we would have to reinvent a lot of wheels that\naren't central to Mailpile's mission.\n\nOur issue tracker contains [further discussions on the topic of why\nGnuPG and not something else, such as\nPGPy](https://github.com/mailpile/Mailpile/issues/1743).\n\n[Use of GPGME is also being\ndiscussed](https://github.com/mailpile/Mailpile/issues/1742). Currently,\nwe don't feel GPGME provides enough benefit to justify the additional\ndependency and the additional (hypothetical) risk posed by solving the\nGnuPG integration problem with a large amount of code written in C (as\nopposed to Python, which is memory-safe and less likely to contain\ncertain classes of security vulnerabilities).\n\n\n### Why not Python 3?\n\nWe depend on some libraries - spambayes in particular - which were not\navailable for Python 3 when we started this project.\n\nWe don't think Python 2 is going away in the near future.\n\n[There is a Github issue discussing\nthis.](https://github.com/mailpile/Mailpile/issues/160)\n\n\n### Why Not Django? Or Flask?\n\nReasons!\n\nOur way may be unusual, but it's kinda awesome once you get used to it\nand it wasn't obvious to us how we could get this kind of behaviour from\none of the standard frameworks.\n\nPlease read the next section for details.\n\n\n### How Does Mailpile's Web UI Work?\n\nThe text-based command-line interface is an important part of Mailpile's\nuser interface. Our home-brewed framework allows us to generate web API\nend-points, text commands and command-line arguments *at the same time*.\n\nOur internal framework also has the concept of commands supporting\nmultiple output formats; so the same API endpoint can generate text,\ntemplated HTML, JSON and XML-RPC interfaces with relatively little\nadditional code. Some endpoints also generate XML, CSV, CSS or\nJavascript.\n\nYou can try this yourself, simply by editing the URL in your browser:\n\n    # The default, rendered as HTML\n    http://localhost:33411/in/inbox/\n    http://localhost:33411/in/inbox/as.html\n    http://localhost:33411/search/?q=in:inbox\n\n    # Same thing, as JSON\n    http://localhost:33411/in/inbox/as.json\n    http://localhost:33411/api/0/search/?q=in:inbox\n\n    # Same thing, as text\n    http://localhost:33411/in/inbox/as.text\n    http://localhost:33411/api/0/search/as.text?q=in:inbox\n\nThe filename part of the URL is used to select output formats. All\nendpoints support `as.json`, most support HTML and/or text.\n\nThe HTML output of each command is generated using Jinja2 templates\nthat are found in `shared-data/default-theme/html/...`. The directory\nstructure generally matches the URL paths seen in the browser, with\nthe main template for each command named `index.html`.\n\nAlternate templates for the same API endpoint can have other names, for\nexample a template named `.../html/search/social.html` would be\naccessible using URLs like so:\n\n    http://localhost:33411/in/inbox/social.html\n    http://localhost:33411/search/social.html?q=from:person@foo.com\n\n\n### Can I Develop Plugins For Mailpile?\n\nSort-of!\n\nInternally, the app is quite modular and there are methods which allow\ncode to register classes or functions that perform various functions.\n\nHowever, the plugin API is not considered stable, it is incomplete and\nit is not very well documented. It may also not be a very nice API, and\nwe rather expect it to develop and change rapidly post-1.0.\n\nIf you are interested in Mailpile's plugin APIs, take a look in\n`shared-data/contrib/` for some examples of \"external plugins\" and\n`mailpile/plugins/` for \"internal plugins\".\n\n\n### How Do I Debug Mailpile?\n\nDevelopers should learn to use the Mailpile CLI. The `mailpile>` prompt is\nwhere all of the low-level magic happens. Future versions of Mailpile will\nexpose this functionality to the web interface itself, but for now you will\nneed to use your shell.\n\nPossibly the most important command for Mailpile hackers, is to know\nhow to enable debugging. An example:\n\n    # Enable verbose debugging of HTTP requests and GnuPG integration\n    # Note: HTTP debugging disables all sorts of internal caches!\n    mailpile> set sys.debug = log http gnupg\n    ...\n\nMany other subsystems can have debugging enabled.  At the time of writing,\nthe `sys.debug` can include the following terms to make various parts of\nthe app more verbose:\n\n    log http compose cryptostate autotag rescan keywords cache connbroker\n    vcard pop3 gnupg keylookup imap sources jinja timing sendmail httpdata\n\nThere are also a few other ways to examine the app state:\n\n    # Watch logging and debug messages fly by\n    mailpile> eventlog/watch\n    ...\n    [CTRL+C]\n    ...\n\n    # Examine event log (piped through less)\n    mailpile> pipe less eventlog\n    ...\n    mailpile> pipe less eventlog incomplete\n    ...\n\n    # Get an overview of what threads are running and what they are doing\n    mailpile> ps\n    ...\n\nLow-level changes and exploration of the configuration are also best done\nfrom the CLI:\n\n    # Explore the configuration; see also mailpile/config/defaults.py\n    mailpile> print -short sys\n    ...\n    mailpile> print -flat sources\n    ...\n    mailpile> print -secrets secrets\n    ...\n \n    # Change things (dangerous)\n    mailpile> set sys.gpg_binary = /bin/false\n    ...\n\n    # Reset something to its default setting\n    mailpile> unset sys.gpg_binary\n    ...\n\nThere is also a help command, and you can use tab completion to try and\n\"guess\" what commands exist.\n\n    mailpile> help\n    ...\n    mailpile> help tags\n    ...\n\nFinally, the app ships with a `hacks` plugin which is disabled by default.\nIf you load it, that will add a few more low-level commands, including an\nembedded Python interpretor:\n\n    mailpile> plugins/load hacks\n    ...\n\n    mailpile> hacks/pycli\n    ...\n\nThere's sure to be more; please feel free to file a pull request against\nthis document to add your favourite tricks or clarify these.\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM debian:stretch-slim\n\nENV GID 33411\nENV UID 33411\n\nRUN apt-get update && \\\n    apt-get install -y curl apt-transport-https gnupg && \\\n    curl -s https://packages.mailpile.is/deb/key.asc | apt-key add - && \\\n    echo \"deb https://packages.mailpile.is/deb release main\" | tee /etc/apt/sources.list.d/000-mailpile.list && \\\n    apt-get update && \\\n    apt-get install -y mailpile && \\\n    # TODO Enable apache for multi users\n    # apt-get install -y mailpile-apache2\n    update-rc.d tor defaults && \\\n    service tor start && \\\n    groupadd -g $GID mailpile && \\\n    useradd -u $UID -g $GID -m mailpile && \\\n    su - mailpile -c 'mailpile setup' && \\\n    apt-get clean\n\nWORKDIR /home/mailpile\nUSER mailpile\n\nVOLUME /home/mailpile/.local/share/Mailpile\nEXPOSE 33411\n\nCMD mailpile --www=0.0.0.0:33411/ --wait\n"
  },
  {
    "path": "Dockerfile.dev",
    "content": "FROM mailpile\n\n# Install C compiler for python deps w/ native extensions\nRUN apk --no-cache add \\\n  gcc \\\n  libc-dev \\\n  python-dev \\\n  shadow\n\n# Workaround: Setting mailpile users uid to 1000 to have write permissions\n# from the docker host the to the shared volumes /Mailpile and /mailpile-data.\n# Mounted volumes seem to be configured w/ uid/guid = 1000.\n# Learn more here:\n# - https://github.com/docker/docker/issues/7198\n# - https://denibertovic.com/posts/handling-permissions-with-docker-volumes/\nRUN usermod -u 1000 mailpile\n\nRUN pip install -r requirements-dev.txt\n\nRUN chmod +x /entrypoint.sh\n\nCMD [\"./mp\"]\n"
  },
  {
    "path": "Gruntfile.js",
    "content": "module.exports = function(grunt) {\n\n  grunt.registerTask('watch', [ 'watch' ]);\n\n  grunt.initConfig({\n    concat: {\n      js: {\n        options: {\n          separator: ';'\n        },\n        src: [\n          'bower_components/jquery/dist/jquery.min.js',\n          'bower_components/underscore/underscore-min.js',\n          'bower_components/jquery-timer/jquery.timer.js',\n          'bower_components/autosize/dist/autosize.js',\n          'bower_components/mousetrap/mousetrap.js',\n          'shared-data/default-theme/js/mousetrap.global.bind.js',\n          'bower_components/jquery.ui/ui/jquery.ui.core.js',\n          'bower_components/jquery.ui/ui/jquery.ui.widget.js',\n          'bower_components/jquery.ui/ui/jquery.ui.mouse.js',\n          'bower_components/jquery.ui/ui/jquery.ui.draggable.js',\n          'bower_components/jquery.ui/ui/jquery.ui.droppable.js',\n          'bower_components/jquery.ui/ui/jquery.ui.sortable.js',\n          'bower_components/jqueryui-touch-punch/jquery.ui.touch-punch.js',\n          'bower_components/qtip2/basic/jquery.qtip.min.js',\n          'bower_components/jquery-slugify/dist/slugify.js',\n          'bower_components/typeahead.js/dist/typeahead.jquery.js',\n          'bower_components/bootstrap/js/dropdown.js',\n          'bower_components/bootstrap/js/modal.js',\n          'bower_components/favico.js/favico.js',\n          'bower_components/select2/select2.min.js',\n          'bower_components/moxie/bin/js/moxie.min.js',\n          'bower_components/plupload/js/plupload.min.js',\n          'bower_components/dompurify/dist/purify.min.js'\n        ],\n        dest: 'shared-data/default-theme/js/libraries.min.js'\n      }\n    },\n    uglify: {\n      options: {\n        mangle: false\n      },\n      js: {\n        files: {\n          'shared-data/default-theme/js/libraries.min.js': ['shared-data/default-theme/js/libraries.min.js']\n        }\n      }\n    },\n    less: {\n      options: {\n        cleancss: true\n      },\n      style: {\n        files: {\n          \"shared-data/default-theme/css/default.css\": \"shared-data/default-theme/less/default.less\"\n        }\n      }\n    },\n    watch: {\n      js: {\n        files: ['shared-data/default-theme/js/*.js'],\n        tasks: ['concat:js', 'uglify:js'],\n        options: {\n          livereload: true,\n        }\n      },\n      css: {\n        files: [\n          'shared-data/default-theme/less/config.less',\n          'shared-data/default-theme/less/default.less',\n          'shared-data/default-theme/less/app/*.less',\n          'shared-data/default-theme/less/libraries/*.less'\n        ],\n        tasks: ['less:style'],\n        options: {\n          livereload: true,\n        }\n      }\n    }\n  });\n\n  grunt.loadNpmTasks('grunt-contrib-concat');\n  grunt.loadNpmTasks('grunt-contrib-uglify');\n  grunt.loadNpmTasks('grunt-contrib-less');\n  grunt.loadNpmTasks('grunt-contrib-watch');\n\n};\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "recursive-exclude locale *\nrecursive-exclude scripts *\nrecursive-exclude testing *\nrecursive-include mailpile/tests/data *\nrecursive-exclude mailpile/tests/data *~\nrecursive-include mailpile/locale *.mo *.po *.pot\nrecursive-include mailpile/plugins *\ninclude Makefile\ninclude README.md\ninclude AGPLv3.txt\ninclude LICENSE-2.0.txt\ninclude requirements.txt\ninclude test-requirements.txt\ninclude requirements-dev.txt\ninclude install_hooks.py\ninclude scripts/compile-messages.sh\ninclude scripts/make-messages.sh\ninclude scripts/mailpile-test.py\ninclude scripts/test-sendmail.sh\ninclude scripts/setup-test.sh\nrecursive-include shared-data *\n"
  },
  {
    "path": "Makefile",
    "content": "# Recipes for stuff\nexport PYTHONPATH := .\n\nhelp:\n\t@echo \"\"\n\t@echo \"BUILD\"\n\t@echo \"    dpkg\"\n\t@echo \"        Create a debian package of this service (in a Docker \"\n\t@echo \"        container).\"\n\t@echo \"\"\n\n\nall:\tsubmodules alltests docs web compilemessages transifex\n\ndev:\n\t@echo export PYTHONPATH=`pwd`\n\narch-dev:\n\tsudo pacman -Syu --needed community/python2-pillow extra/python2-lxml community/python2-jinja \\\n\t                 community/python2-pep8 extra/python2-nose community/phantomjs \\\n\t                 extra/python2-pip community/python2-mock \\\n\t                 extra/ruby community/npm community/spambayes\n\tTMPDIR=`mktemp -d /tmp/aur.XXXXXXXXXX`; \\\n\tcd $$TMPDIR; \\\n\tpacman -Qs '^yuicompressor$$' > /dev/null; \\\n\tif [ $$? -ne 0 ]; then \\\n\t  sudo pacman -S --needed core/base-devel; \\\n\t  curl -s https://aur.archlinux.org/cgit/aur.git/snapshot/yuicompressor.tar.gz | tar xzv; \\\n\t  cd yuicompressor; \\\n\t  makepkg -si; \\\n\t  cd $$TMPDIR; \\\n\tfi; \\\n\tcd /tmp; \\\n\trm -rf $$TMPDIR\n\tsudo pip2 install 'selenium>=2.40.0'\n\twhich lessc >/dev/null || sudo gem install therubyracer less\n\twhich bower >/dev/null || sudo npm install -g bower\n\twhich uglify >/dev/null || sudo npm install -g uglify\n\nfedora-dev:\n\tsudo yum install python-imaging python-lxml python-jinja2 python-pep8 \\\n\t                     ruby ruby-devel python-yui python-nose spambayes \\\n\t                     phantomjs python-pip python-mock npm\n\tsudo yum install rubygems; \\\n\tsudo yum install python-pgpdump || pip install pgpdump\n\tsudo pip install 'selenium>=2.40.0'\n\twhich lessc >/dev/null || sudo gem install therubyracer less\n\twhich bower >/dev/null || sudo npm install -g bower\n\twhich uglify >/dev/null || sudo npm install -g uglify\n\ndebian-dev:\n\tsudo apt-get install python-imaging python-lxml python-jinja2 pep8 \\\n\t                     ruby-dev yui-compressor python-nose spambayes \\\n\t                     python-pip python-mock python-selenium \\\n\t\t\t\t\t\t rubygems-integration\n\tdpkg -l|grep -qP ' nodejs .*nodesource' || sudo apt install npm\n\tsudo apt-get install python-pgpdump || pip install pgpdump\n\twhich phantomjs >/dev/null || sudo apt-get install phantomjs || sudo npm install -g phantomjs\n\twhich lessc >/dev/null || sudo gem install therubyracer less\n\twhich bower >/dev/null || sudo npm install -g bower\n\twhich uglify >/dev/null || sudo npm install -g uglify\n\n\nsubmodules:\n\tgit submodule update --remote\n\ndocs: submodules\n\t@python2.7 mailpile/urlmap.py |grep -v ^FIXME: >doc/URLS.md\n\t@ls -l doc/URLS.md\n\t@python2.7 mailpile/defaults.py |grep -v -e ^FIXME -e ';timestamp' \\\n           >doc/defaults.cfg\n\t@ls -l doc/defaults.cfg\n\nweb: less js\n\t@true\n\nalltests: clean pytests\n\t@chmod go-rwx mailpile/tests/data/gpg-keyring\n\t@DISPLAY= nosetests\n\t@DISPLAY= python2.7 scripts/mailpile-test.py || true\n\t@git checkout mailpile/tests/data/\n\npytests:\n\t@echo -n 'security         ' && python2.7 mailpile/security.py\n\t@echo -n 'urlmap           ' && python2.7 mailpile/urlmap.py -nomap\n\t@echo -n 'search           ' && python2.7 mailpile/search.py\n\t@echo -n 'mailboxes.mbox   ' && python2.7 mailpile/mailboxes/mbox.py\n\t@echo -n 'mailutils.safe   ' && python2.7 mailpile/mailutils/safe.py\n\t@echo -n 'mailutils.addrs  ' && python2.7 mailpile/mailutils/addresses.py\n\t@echo -n 'mailutils.emails ' && python2.7 mailpile/mailutils/emails.py\n\t@echo -n 'config/base      ' && python2.7 mailpile/config/base.py\n\t@echo -n 'config/validators' && python2.7 mailpile/config/validators.py\n\t@echo -n 'config/manager   ' && python2.7 mailpile/config/manager.py\n\t@echo -n 'conn_brokers     ' && python2.7 mailpile/conn_brokers.py\n\t@echo -n 'crypto/autocrypt ' && python2.7 mailpile/crypto/autocrypt.py\n\t@echo -n 'plug...autocrypt ' && python2.7 mailpile/plugins/crypto_autocrypt.py\n\t@echo -n 'crypto/mime      ' && python2.7 mailpile/crypto/mime.py\n\t@echo -n 'index.base       ' && python2.7 mailpile/index/base.py\n\t@echo -n 'index.msginfo    ' && python2.7 mailpile/index/msginfo.py\n\t@echo -n 'index.mailboxes  ' && python2.7 mailpile/index/mailboxes.py\n\t@echo -n 'index.search     ' && python2.7 mailpile/index/search.py\n\t@echo -n 'util             ' && python2.7 mailpile/util.py\n\t@echo -n 'vcard            ' && python2.7 mailpile/vcard.py\n\t@echo -n 'workers          ' && python2.7 mailpile/workers.py\n\t@echo -n 'packing          ' && python2.7 mailpile/packing.py\n\t@echo -n 'mailboxes/pop3   ' && python2.7 mailpile/mailboxes/pop3.py\n\t@echo -n 'mail_source/imap ' && python2.7 mailpile/mail_source/imap.py\n\t@echo -n 'crypto/aes_utils ' && python2.7 mailpile/crypto/aes_utils.py\n\t@echo 'spambayes...        ' && python2.7 mailpile/spambayes/Tester.py\n\t@echo 'crypto/streamer...'   && python2.7 mailpile/crypto/streamer.py\n\t@echo\n\nclean:\n\t@rm -f `find . -name \\\\*.pyc` \\\n\t       `find . -name \\\\*.pyo` \\\n\t       `find . -name \\\\*.mo` \\\n\t        mailpile-tmp.py mailpile.py \\\n\t        ChangeLog AUTHORS \\\n\t        .appver MANIFEST .SELF .*deps \\\n                dist/*.tar.gz dist/*.deb dist/*.rpm \\\n\t        scripts/less-compiler.mk ghostdriver.log\n\t@rm -rf *.egg-info build/ \\\n               mailpile/tests/data/tmp/ testing/tmp/\n\t@rm -f shared-data/multipile/www/admin.cgi\n\nmrproper: clean\n\t@rm -rf shared-data/locale/?? shared-data/locale/??[_@]*\n\t@rm -rf bower_components/ shared-data/locale/mailpile.pot\n\t@rm -rf mp-virtualenv/\n\tgit reset --hard && git clean -dfx\n\nsdist: clean\n\t@python setup.py sdist\n\n#bdist-prep: compilemessages web -- FIXME: Make building web assets work!\nbdist-prep: compilemessages\n\t@true\n\nbdist:\n\t@python setup.py bdist_wheel\n\nvirtualenv: mp-virtualenv/bin/activate\nvirtualenv-dev: mp-virtualenv/bin/.dev\n\nmp-virtualenv/bin/activate:\n\tvirtualenv -p python2.7 --system-site-packages mp-virtualenv\n\tbash -c 'source mp-virtualenv/bin/activate && pip install -r requirements.txt && python setup.py install'\n\t@rm -rf mp-virtualenv/bin/.dev\n\t@echo\n\t@echo NOTE: If you want to test/develop with GnuPG 2.1, you might\n\t@echo       want to activate the virtualenv and then run this script\n\t@echo to build GnuPG 2.1: ./scripts/add-gpgme-and-gnupg-to-venv\n\t@echo\n\nmp-virtualenv/bin/.dev: virtualenv\n\trm -rf mp-virtualenv/lib/python2.7/site-packages/mailpile\n\tcd mp-virtualenv/lib/python2.7/site-packages/ && ln -s ../../../../mailpile\n\trm -rf mp-virtualenv/share/mailpile\n\tcd mp-virtualenv/share/ && ln -s ../../shared-data mailpile\n\t@touch mp-virtualenv/bin/.dev\n\nbower_components:\n\t@bower install\n\njs: bower_components\n\t# Warning: Horrible hack to extract rules from Gruntfile.js\n\t@rm -f shared-data/default-theme/js/libraries.min.js\n\t@rm -f shared-data/default-theme/js/mailpile-min.js.tmp*\n\t@cat Gruntfile.js \\\n                |sed -e '1,/concat:/d ' \\\n                |sed -e '1,/src:/d' -e '/dest:/,$$d' \\\n                |grep / \\\n                |sed -e \"s/[',]/ /g\" \\\n            |xargs sed -e '$$a;' \\\n            >> shared-data/default-theme/js/mailpile-min.js.tmp\n\t@uglify -s shared-data/default-theme/js/mailpile-min.js.tmp \\\n               -o shared-data/default-theme/js/mailpile-min.js.tmp2\n\t@sed -e \"s/@MP_JSBUILD_INFO@/`./scripts/gitwhere.sh`/\" \\\n\t    < shared-data/default-theme/js/libraries.js \\\n\t    > shared-data/default-theme/js/libraries.min.js\n\t@echo '/* Sources...' \\\n\t    >> shared-data/default-theme/js/libraries.min.js\n\t@bower --offline --no-color list \\\n\t    >> shared-data/default-theme/js/libraries.min.js\n\t@echo '*/' \\\n\t    >> shared-data/default-theme/js/libraries.min.js\n\t@cat shared-data/default-theme/js/mailpile-min.js.tmp2 \\\n            >> shared-data/default-theme/js/libraries.min.js\n\t@rm -f shared-data/default-theme/js/mailpile-min.js.tmp*\n\nless: less-compiler bower_components\n\t@cp -fa \\\n                bower_components/select2/select2.png \\\n                bower_components/select2/select2x2.png \\\n                bower_components/select2/select2-spinner.gif \\\n            shared-data/default-theme/css/\n\t@make -s -f scripts/less-compiler.mk\n\nless-loop: less-compiler\n\t@echo 'Running less compiler every 15 seconds. CTRL+C quits.'\n\t@while [ 1 ]; do \\\n                make -s less; \\\n                sleep 15; \\\n        done\n\nless-compiler:\n\tbower install\n\t@cp scripts/less-compiler.in scripts/less-compiler.mk\n\t@find shared-data/default-theme/less/ -name '*.less' \\\n                |perl -npe s'/^/\\t/' \\\n\t\t|perl -npe 's/$$/\\\\/' \\\n                >>scripts/less-compiler.mk\n\t@echo >> scripts/less-compiler.mk\n\t@perl -e 'print \"\\t\\@touch .less-deps\", $/' >> scripts/less-compiler.mk\n\ngenmessages:\n\t@scripts/make-messages.sh\n\ncompilemessages:\n\t@scripts/compile-messages.sh\n\ntransifex:\n\ttx pull -a --minimum-perc=25\n\ttx pull -l is,en_GB\n\n\n# -----------------------------------------------------------------------------\n# BUILD\n# -----------------------------------------------------------------------------\n\ndist/version.txt: mailpile/config/defaults.py scripts/version.py\n\tmkdir -p dist\n\tscripts/version.py > dist/version.txt\n\ndist/mailpile.tar.gz: mrproper genmessages transifex dist/version.txt\n\tgit submodule update --init --recursive\n\tgit submodule foreach 'git reset --hard && git clean -dfx'\n\tmkdir -p dist\n\tscripts/version.py > dist/version.txt\n\ttar --exclude='./packages/debian' --exclude=dist --exclude-vcs -czf dist/mailpile-$$(cat dist/version.txt).tar.gz -C $(shell pwd) .\n\t(cd dist; ln -fs mailpile-$$(cat version.txt).tar.gz mailpile.tar.gz)\n\n.dockerignore: dist/version.txt packages/Dockerfile_debian packages/debian packages/debian/rules\n\tmkdir -p dist\n\tdocker build \\\n\t    --file=packages/Dockerfile_debian \\\n\t    --tag=mailpile-deb-builder \\\n\t    ./\n\ttouch .dockerignore\n\ndpkg: dist/mailpile.tar.gz .dockerignore\n\tdocker run \\\n\t    --rm --volume=$$(pwd)/dist:/mnt/dist \\\n\t    mailpile-deb-builder\n"
  },
  {
    "path": "README.md",
    "content": "# Welcome to Mailpile! #\n\n**IMPORTANT NOTE**\n\nDevelopment on this codebase has halted, until the\n[Python3 rewrite](https://community.mailpile.is/t/a-very-uninformative-progress-update-mailpile-2/785)\nhas completed.\n\nApologies to those who have unanswered, out-standing pull requests and\nissues. 😢 Your efforts are appreciated!\n\nIf you rely on this code and have your own branch which you actively\nmaintain, let us know: we would be happy to link to it.\n\nIf you need to run Mailpile v1 to access legacy data, consider using\nour [legacy Docker images](https://github.com/mailpile/Mailpile-v1-Docker).\n\n\n------------------------------------------------------------------------\n\n## Introduction (Obsolete) ##\n\nMailpile (<https://www.mailpile.is/>) is a modern, fast web-mail client\nwith user-friendly encryption and privacy features. The development of\nMailpile is funded by\n[a large community of backers](https://www.mailpile.is/#community)\nand all code related to the project is and will be released under an OSI\napproved Free Software license.\n\nMailpile places great emphasis on providing a clean, elegant user\ninterface and pleasant user experience. In particular, Mailpile aims to\nmake it easy and convenient to receive and send PGP encrypted or signed\ne-mail.\n\nMailpile's primary user interface is web-based, but it also has a basic\ncommand-line interface and an API for developers. Using web technology\nfor the interface allows Mailpile to function both as a local desktop\napplication (accessed by visiting `localhost` in the browser) or a\nremote web-mail on a personal server or VPS.\n\nThe core of Mailpile is a fast search engine, custom written to deal\nwith large volumes of e-mail on consumer hardware. The search engine\nallows e-mail to be organized using tags (similar to GMail's labels) and\nthe application can be configured to automatically tag incoming mail\neither based on static rules or bayesian classifiers.\n\n\n### Trying Mailpile\n\nIf you need to run Mailpile v1 to access legacy data, consider using\nour [legacy Docker images](https://github.com/mailpile/Mailpile-v1-Docker).\n\n\n## Credits and License ##\n\nBjarni R. Einarsson (<http://bre.klaki.net/>) created this!  If you\nthink it's neat, you should also check out PageKite:\n<https://pagekite.net/>. [Smári](<http://www.smarimccarthy.is/>) and\n[Brennan](https://brennannovak.com) joined the team in 2013 and made\nthis a real project (not just a toy search engine).\n\nThe original GMail team deserve a mention for their inspiring work:\nwishing the Free Software world had something like GMail is what\nmotivated Bjarni to start working on Mailpile. We would also like to\nthank Edward Snowden for inspiring us to try and make PGP usable for\njournalists and everday folks!\n\nContributors:\n\n- Bjarni R. Einarsson (<http://bre.klaki.net/>)\n- Brennan Novak (<https://brennannovak.com/>)\n- Smari McCarthy (<http://www.smarimccarthy.is/>)\n- Lots more, run `git shortlog -s` for a list! (Or check\n  [GitHub](https://github.com/mailpile/Mailpile/graphs/contributors).)\n\nAnd of course, we couldn't do this without [our community of\nbackers](https://www.mailpile.is/#community).\n\nThis program is free software: you can redistribute it and/or modify it\nunder the terms of the GNU Affero General Public License as published by\nthe Free Software Foundation. See the file `COPYING.md` for details.\n\n"
  },
  {
    "path": "babel.cfg",
    "content": "# Extraction from Python source files\n[python: mailpile/**.py]\nencoding = utf-8\n\n[python: shared-data/contrib/**.py]\nencoding = utf-8\n\n[python: shared-data/multiple/**.py]\nencoding = utf-8\n\n[python: shared-data/mailpile-gui/**.py]\nencoding = utf-8\n\n# Extraction from Jinja2 template files\n[jinja2: shared-data/default-theme/html/**]\nencoding = utf-8\nignore_tags = style\nextensions = jinja2.ext.do,jinja2.ext.autoescape,mailpile.www.jinjaextensions.MailpileCommand\nsilent = false\n\n[jinja2: shared-data/contrib/html/**.html]\nencoding = utf-8\nignore_tags = style\nextensions = jinja2.ext.do,jinja2.ext.autoescape,mailpile.www.jinjaextensions.MailpileCommand\nsilent = false\n\n[jinja2: shared-data/contrib/html/**.js]\nencoding = utf-8\nignore_tags = style\nextensions = jinja2.ext.do,jinja2.ext.autoescape,mailpile.www.jinjaextensions.MailpileCommand\nsilent = false\n"
  },
  {
    "path": "bower.json",
    "content": "{\n  \"name\": \"mailpile\",\n  \"version\": \"0.1.0\",\n  \"homepage\": \"https://mailpile.is\",\n  \"authors\": [\n    \"Various\"\n  ],\n  \"description\": \"Front end libraries and dependencies for Mailpile default theme\",\n  \"main\": \"libraries.js\",\n  \"license\": \"Various\",\n  \"private\": true,\n  \"ignore\": [\n    \"**/.*\",\n    \"node_modules\",\n    \"bower_components\",\n    \"test\",\n    \"tests\"\n  ],\n  \"dependencies\": {\n    \"underscore\": \">=1.7.0\",\n    \"bootstrap\": \"~3.3.0\",\n    \"favico.js\": \"~0.3.5\",\n    \"html5shiv\": \"~3.7.0\",\n    \"jquery\": \"~2.1\",\n    \"autosize\": \"~3.0.14\",\n    \"jquery-confirm\": \"~2.1.1\",\n    \"jquery-slugify\": \"~1.0.3\",\n    \"jquery-timer\": \"*\",\n    \"jquery.ui\": \"~1.10\",\n    \"less-elements-old\": \"~1.0\",\n    \"mousetrap\": \"~1.6.0\",\n    \"moxie\": \"~1.3.4\",\n    \"plupload\": \"~2.1.2\",\n    \"qtip2\": \"~2.1.1\",\n    \"rebar\": \"~0.3.2\",\n    \"select2\": \"3.5.4\",\n    \"typeahead.js\": \"~0.10.5\",\n    \"jqueryui-touch-punch\": \"*\",\n    \"dompurify\": \"~1.0.11\"\n  }\n}\n"
  },
  {
    "path": "docker-compose.dev.yml",
    "content": "version: '3.0'\nservices:\n  mailpile_dev:\n    tty: true\n    stdin_open: true\n    container_name: mailpile_dev\n    build:\n      context: .\n      dockerfile: Dockerfile.dev\n    image: mailpile_dev\n    volumes:\n      - .:/Mailpile\n      - .dev-mailpile-data:/mailpile-data:rw\n    ports:\n      - 33412:33411\n\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3.0'\nservices:\n  mailpile:\n    container_name: mailpile\n    build: .\n    image: mailpile\n    volumes:\n      - .:/Mailpile\n      - .dev-mailpile-data:/mailpile-data\n    ports:\n      - 33411:33411\n\n"
  },
  {
    "path": "install_hooks.py",
    "content": "import os\nimport sys\n\ndef symlink_develop(config):\n    if 'develop' in sys.argv:\n        share_path = os.path.join(sys.prefix, 'share')\n\n        if not os.path.exists(share_path):\n            os.makedirs(share_path)\n\n        os.symlink(\n            os.path.join(\n                os.path.dirname(os.path.realpath(__file__)),\n                'shared-data'\n            ),\n            os.path.join(sys.prefix, 'share', 'mailpile')\n        )\n"
  },
  {
    "path": "mailpile/__init__.py",
    "content": "from mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\n\n\n__all__ = ['Mailpile',\n           \"app\", \"commands\", \"plugins\", \"mailutils\", \"search\", \"ui\", \"util\"]\n\n\nclass Mailpile(object):\n    \"\"\"This object provides a simple Python API to Mailpile.\"\"\"\n\n    def __init__(self,\n                 ui=None,\n                 workdir=None,\n                 session=None):\n        import mailpile.app\n        import mailpile.config.defaults\n        import mailpile.ui\n        if not session:\n            ui = ui or mailpile.ui.UserInteraction\n            self._config = mailpile.app.ConfigManager(\n                workdir=workdir, rules=mailpile.config.defaults.CONFIG_RULES)\n            self._session = mailpile.ui.Session(self._config)\n            self._ui = self._session.ui = ui(self._config)\n            self._session.config.load(self._session)\n            self._session.main = True\n        else:\n            self._session = session\n            self._config = session.config\n            self._ui = session.ui\n\n        for cls in mailpile.commands.COMMANDS:\n            names, argspec = cls.SYNOPSIS[1:3], cls.SYNOPSIS[3]\n            if names[0]:\n                setattr(self, *self._mk_action(cls, names[0], argspec))\n            if names[1] and (names[0] != names[1]):\n                setattr(self, *self._mk_action(cls, names[1], argspec))\n\n    def _mk_action(self, cls, cmd, argspec):\n        import mailpile.commands\n        if argspec:\n\n            def fnc(*args, **kwargs):\n                return mailpile.commands.Action(self._session, cmd, args,\n                                                data=kwargs)\n        else:\n\n            def fnc(**kwargs):\n                return mailpile.commands.Action(self._session, cmd, '',\n                                                data=kwargs)\n\n        fnc.__doc__ = '%s(%s)  # %s' % (cmd, argspec or '', cls.__doc__)\n        return cmd.replace('/', '_'), fnc\n\n    def Interact(self):\n        import mailpile.util\n        mailpile.util.QUITTING = False\n        self._session.interactive = self._session.ui.interactive = True\n        try:\n            self._session.config.prepare_workers(self._session, daemons=True)\n            mailpile.app.Interact(self._session)\n        except KeyboardInterrupt:\n            pass\n        finally:\n            mailpile.util.QUITTING = mailpile.util.QUITTING or True\n            self._session.config.stop_workers()\n            self._session.interactive = self._session.ui.interactive = False\n"
  },
  {
    "path": "mailpile/__main__.py",
    "content": "import sys\nfrom mailpile.app import Main\n\n\ndef main():\n    Main(sys.argv[1:])\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "mailpile/app.py",
    "content": "from __future__ import print_function\nimport getopt\nimport gettext\nimport locale\nimport os\nimport re\nimport sys\nimport traceback\n\nimport mailpile.util\nimport mailpile.config.defaults\nimport mailpile.platforms\nfrom mailpile.commands import COMMANDS, Command, Action\nfrom mailpile.config.manager import ConfigManager\nfrom mailpile.conn_brokers import DisableUnbrokeredConnections\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.plugins.core import Help, HelpSplash, HealthCheck\nfrom mailpile.plugins.core import Load, Rescan, Quit\nfrom mailpile.plugins.motd import MessageOfTheDay\nfrom mailpile.plugins.setup_magic import Setup\nfrom mailpile.ui import ANSIColors, Session, UserInteraction, Completer\nfrom mailpile.util import *\n\n_plugins = PluginManager(builtin=__file__)\n\n# This makes sure mailbox \"plugins\" get loaded... has to go somewhere?\nfrom mailpile.mailboxes import *\n\n# This is also a bit silly, should be somewhere else?\nHelp.ABOUT = mailpile.config.defaults.ABOUT\n\n# We may try to load readline later on... maybe?\nreadline = None\n\n\n##[ Main ]####################################################################\n\n\ndef threaded_raw_input(prompt):\n    \"\"\"These shenigans are necessary to let Quit work reliably.\"\"\"\n    def reader(container):\n        try:\n            line = raw_input(prompt).decode('utf-8').strip()\n            container.append(line)\n        except EOFError:\n            pass\n    o = []\n    t = threading.Thread(target=reader, args=(o,))\n    t.daemon = True\n    t.start()\n    while t.isAlive() and not mailpile.util.QUITTING:\n        t.join(timeout=1)\n    if not o:\n        raise EOFError()\n    return o[0]\n\n\ndef write_readline_history(session):\n    try:\n        if session.config.sys.history_length > 0:\n            readline.write_history_file(session.config.history_file())\n        else:\n            safe_remove(session.config.history_file())\n    except (OSError, AttributeError, IOError):\n        pass\n\n\ndef CatchUnixSignals(session):\n    def quit_app(sig, stack):\n        Quit(session, 'quit').run()\n\n    def reload_app(sig, stack):\n        pass\n\n    try:\n        import signal\n        if os.name != 'nt':\n            signal.signal(signal.SIGTERM, quit_app)\n            signal.signal(signal.SIGQUIT, quit_app)\n            signal.signal(signal.SIGUSR1, reload_app)\n        else:\n            signal.signal(signal.SIGTERM, quit_app)\n    except ImportError:\n        pass\n\n\ndef FriendlyPipeTransform(session, opt):\n    \"\"\"Shell-like syntax to invoke the pipe command or change render modes.\"\"\"\n    old_render_mode = None\n    if session.config.prefs.friendly_pipes:\n        if ' |' in opt:\n            cmd, pipe = opt.split(' |', 1)\n            opt = 'pipe %s -- %s' % (pipe.strip(), cmd.strip())\n        elif re.search(' >\\S+$', opt):\n            cmd, pipe = opt.rsplit(' >', 1)\n            opt = 'pipe >%s -- %s' % (pipe, cmd.strip())\n            opt = opt.replace('\\\\|', '|').replace('\\\\>', '>')\n\n        if re.search(' :(json|j?html|text|\\S+.html(?:!\\S+))$', opt):\n            old_render_mode = session.ui.render_mode\n            opt, session.ui.render_mode = opt.rsplit(' :', 1)\n\n    return old_render_mode, opt\n\n\ndef Interact(session):\n    global readline\n    try:\n        import readline as rl  # Unix-only\n        readline = rl\n    except ImportError:\n        pass\n\n    try:\n        if readline:\n            readline.read_history_file(session.config.history_file())\n            readline.set_completer_delims(Completer.DELIMS)\n            readline.set_completer(Completer(session).get_completer())\n            for opt in [\"tab: complete\", \"set show-all-if-ambiguous on\"]:\n                readline.parse_and_bind(opt)\n    except IOError:\n        pass\n\n    # Negative history means no saving state to disk.\n    history_length = session.config.sys.history_length\n    if readline is None:\n        pass  # history currently not supported under Windows / Mac\n    elif history_length >= 0:\n        readline.set_history_length(history_length)\n    else:\n        readline.set_history_length(-history_length)\n\n    try:\n        prompt = session.ui.term.color('mailpile> ',\n                                       color=session.ui.term.BLACK,\n                                       weight=session.ui.term.BOLD,\n                                       readline=(readline is not None))\n        while not mailpile.util.QUITTING:\n            try:\n                with session.ui.term:\n                    if Setup.Next(session.config, 'anything') != 'anything':\n                        session.ui.notify(\n                            _('Mailpile is unconfigured, please run `setup`'\n                              ' or visit the web UI.'))\n                    session.ui.block()\n                    opt = threaded_raw_input(prompt)\n            except KeyboardInterrupt:\n                session.ui.unblock(force=True)\n                session.ui.notify(_('Interrupted. '\n                                    'Press CTRL-D or type `quit` to quit.'))\n                continue\n            session.ui.term.check_max_width()\n            session.ui.unblock(force=True)\n            if opt:\n                old_render_mode, opt = FriendlyPipeTransform(session, opt)\n                if ' ' in opt:\n                    opt, arg = opt.split(' ', 1)\n                else:\n                    arg = ''\n                try:\n                    result = Action(session, opt, arg)\n                    session.ui.block()\n                    session.ui.display_result(result)\n                except UsageError as e:\n                    session.fatal_error(unicode(e))\n                except UrlRedirectException as e:\n                    session.fatal_error('Tried to redirect to: %s' % e.url)\n                if old_render_mode is not None:\n                    session.ui.render_mode = old_render_mode\n    except EOFError:\n        print()\n    finally:\n        session.ui.unblock(force=True)\n\n    write_readline_history(session)\n\n\nclass InteractCommand(Command):\n    SYNOPSIS = (None, 'interact', None, None)\n    ORDER = ('Internals', 2)\n    CONFIG_REQUIRED = False\n    RAISES = (KeyboardInterrupt,)\n\n    def command(self):\n        session, config = self.session, self.session.config\n\n        session.interactive = True\n        if mailpile.platforms.TerminalSupportsAnsiColors():\n            session.ui.term = ANSIColors()\n\n        # Ensure we have a working GnuPG\n        self._gnupg().common_args(will_send_passphrase=True)\n\n        # Create and start the rest of the threads, load the index.\n        if config.loaded_config:\n            Load(session, '').run(quiet=True)\n        else:\n            config.prepare_workers(session, daemons=True)\n\n        # Note: We do *not* update the MOTD on startup, to keep things\n        #       fast, and to avoid leaking our IP on setup, before Tor\n        #       has been configured.\n        splash = HelpSplash(session, 'help', []).run()\n        motd = MessageOfTheDay(session, 'motd', ['--noupdate']).run()\n        session.ui.display_result(splash)\n        print()  # FIXME: This is a hack!\n        session.ui.display_result(motd)\n\n        Interact(session)\n\n        return self._success(_('Ran interactive shell'))\n\n\nclass WaitCommand(Command):\n    SYNOPSIS = (None, 'wait', None, None)\n    ORDER = ('Internals', 2)\n    CONFIG_REQUIRED = False\n    RAISES = (KeyboardInterrupt,)\n\n    def command(self):\n        self.session.ui.display_result(HelpSplash(self.session, 'help', []\n                                                  ).run(interactive=False))\n        while not mailpile.util.QUITTING:\n            time.sleep(1)\n        return self._success(_('Did nothing much for a while'))\n\n\ndef Main(args):\n    try:\n        mailpile.platforms.DetectBinaries(_raise=OSError)\n    except OSError as e:\n        binary = str(e).split()[0]\n        sys.stderr.write(\"\"\"\nRequired binary missing or unusable: %s\n\nIf you know where it is, or would like to skip this test and run Mailpile\nanyway, you can set one of the following environment variables:\n\n    MAILPILE_%s=\"/path/to/binary\"\nor\n    MAILPILE_IGNORE_BINARIES=\"%s\"  # Can be a space-separated list\n\nNote that skipping a binary check may cause the app to become unstable or\nfail in unexpected ways. If it breaks you get to keep both pieces!\n\n\"\"\" % (e, binary.upper(), binary))\n        sys.exit(1)\n\n    # Enable our connection broker, try to prevent badly behaved plugins from\n    # bypassing it.\n    DisableUnbrokeredConnections()\n\n    # Bootstrap translations until we've loaded everything else\n    mailpile.i18n.ActivateTranslation(None, ConfigManager, None)\n    try:\n        # Create our global config manager and the default (CLI) session\n        config = ConfigManager(rules=mailpile.config.defaults.CONFIG_RULES)\n        session = Session(config)\n        cli_ui = session.ui = UserInteraction(config)\n        session.main = True\n        try:\n            CatchUnixSignals(session)\n            config.clean_tempfile_dir()\n            config.load(session)\n        except IOError:\n            if config.sys.debug:\n                session.ui.error(_('Failed to decrypt configuration, '\n                                   'please log in!'))\n        HealthCheck(session, None, []).run()\n        config.prepare_workers(session)\n    except AccessError as e:\n        session.ui.error('Access denied: %s\\n' % e)\n        sys.exit(1)\n\n    try:\n        try:\n            if '--login' in args:\n                a1 = args[:args.index('--login') + 1]\n                a2 = args[len(a1):]\n            else:\n                a1, a2 = args, []\n\n            allopts = []\n            for argset in (a1, a2):\n                shorta, longa = '', []\n                for cls in COMMANDS:\n                    shortn, longn, urlpath, arglist = cls.SYNOPSIS[:4]\n                    if arglist:\n                        if shortn:\n                            shortn += ':'\n                        if longn:\n                            longn += '='\n                    if shortn:\n                        shorta += shortn\n                    if longn:\n                        longa.append(longn.replace(' ', '_'))\n\n                opts, args = getopt.getopt(argset, shorta, longa)\n                allopts.extend(opts)\n                for opt, arg in opts:\n                    session.ui.display_result(Action(\n                        session, opt.replace('-', ''), arg.decode('utf-8')))\n                if args:\n                    session.ui.display_result(Action(\n                        session, args[0], ' '.join(args[1:]).decode('utf-8')))\n\n        except (getopt.GetoptError, UsageError) as e:\n            session.fatal_error(unicode(e))\n\n        if (not allopts) and (not a1) and (not a2):\n            InteractCommand(session).run()\n\n    except KeyboardInterrupt:\n        pass\n\n    except:\n        traceback.print_exc()\n\n    finally:\n        write_readline_history(session)\n\n        # Make everything in the background quit ASAP...\n        mailpile.util.LAST_USER_ACTIVITY = 0\n        mailpile.util.QUITTING = mailpile.util.QUITTING or True\n\n        if config.plugins:\n            config.plugins.process_shutdown_hooks()\n\n        config.stop_workers()\n        if config.index:\n            config.index.save_changes()\n        if config.event_log:\n            config.event_log.close()\n\n        session.ui.display_result(Action(session, 'cleanup', ''))\n\n        if session.interactive and config.sys.debug:\n            session.ui.display_result(Action(session, 'ps', ''))\n\n        # Remove anything that we couldn't remove before\n        safe_remove()\n\n        # Restart the app if that's what was requested\n        if mailpile.util.QUITTING == 'restart':\n            os.execv(sys.argv[0], sys.argv)\n\n\n_plugins.register_commands(InteractCommand, WaitCommand)\n\nif __name__ == \"__main__\":\n    Main(sys.argv[1:])\n"
  },
  {
    "path": "mailpile/auth.py",
    "content": "import time\nfrom urlparse import parse_qs, urlparse\nfrom urllib import quote, urlencode\n\nfrom mailpile.commands import Command\nfrom mailpile.crypto.gpgi import GnuPG\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.security import SecurePassphraseStorage\nfrom mailpile.util import *\n\nGLOBAL_LOGIN_LOCK = CryptoLock()\n\n\nclass UserSession(object):\n    EXPIRE_AFTER = 7 * 24 * 3600\n\n    def __init__(self, ts=None, auth=None, data=None):\n        self.ts = ts or time.time()\n        self.auth = auth\n        self.data = data or {}\n\n    def is_expired(self, now=None):\n        return (self.ts < (now or time.time()) - self.EXPIRE_AFTER)\n\n    def update_ts(self):\n        self.ts = time.time()\n\n\nclass UserSessionCache(dict):\n    def delete_expired(self, now=None):\n        now = now or time.time()\n        for k in self.keys():\n            if self[k].is_expired(now=now):\n                del self[k]\n\n\ndef VerifyAndStorePassphrase(config, passphrase=None, sps=None,\n                                     key=None):\n    if passphrase and not sps:\n        sps = SecurePassphraseStorage(passphrase)\n        passphrase = 'this probably does not really overwrite :-( '\n\n    safe_assert(\n        (sps is not None) and\n        (config.load_master_key(sps)))\n\n    # Fun side effect: changing the passphrase invalidates the message cache\n    import mailpile.mailutils.emails\n    mailpile.mailutils.emails.ClearParseCache(full=True)\n\n    return sps\n\n\ndef SetLoggedIn(cmd, user=None, redirect=False, session_id=None):\n    user = user or 'DEFAULT'\n\n    sid = session_id or cmd.session.ui.html_variables.get('http_session')\n    if sid:\n        if cmd:\n            cmd.session.ui.debug('Logged in %s as %s' % (sid, user))\n        SESSION_CACHE[sid] = UserSession(auth=user, data={\n            't': '%x' % int(time.time()),\n        })\n\n    if cmd:\n        if redirect:\n            return cmd._do_redirect()\n    else:\n        return True\n\n\ndef CheckPassword(config, username, password):\n    # FIXME: Do something with the username\n    username = username or 'DEFAULT'\n    sps = config.passphrases and config.passphrases.get(username)\n    return sps.compare(password) and username\n\n\ndef IndirectPassword(config, pwd):\n    pp = pwd.split(':')\n    if len(pp) > 2 and pp[0] == '_SECRET_':\n        if pp[1] in config.secrets:\n            return config.secrets[pp[1]].password\n        if pp[1] in config.passphrases:\n            return config.passphrases[pp[1]].get_passphrase()\n    return pwd\n\n\nSESSION_CACHE = UserSessionCache()\nLOGIN_FAILURES = []\n\n\ndef LogoutAll():\n    for k in list(SESSION_CACHE.keys()):\n        del SESSION_CACHE[k]\n\n\nclass Authenticate(Command):\n    \"\"\"Authenticate a user (log in)\"\"\"\n    SYNOPSIS = (None, 'login', 'auth/login', None)\n    ORDER = ('Internals', 5)\n    SPLIT_ARG = False\n    IS_INTERACTIVE = True\n\n    CONFIG_REQUIRED = False\n    HTTP_AUTH_REQUIRED = False\n    HTTP_STRICT_VARS = False\n    HTTP_CALLABLE = ('GET', 'POST')\n    HTTP_POST_VARS = {\n        'user': 'User to authenticate as',\n        'pass': 'Password or passphrase'\n    }\n\n    @classmethod\n    def RedirectBack(cls, url, data):\n        qs = [(k, v.encode('utf-8')) for k, vl in data.iteritems() for v in vl\n              if k not in ['_method', '_path'] + cls.HTTP_POST_VARS.keys()]\n        qs = urlencode(qs)\n        url = ''.join([url, '?%s' % qs if qs else ''])\n        raise UrlRedirectException(url)\n\n    def _result(self, result=None):\n        global LOGIN_FAILURES\n        result = result or {}\n        result['login_banner'] = self.session.config.sys.login_banner\n        result['login_failures'] = LOGIN_FAILURES\n        return result\n\n    def _error(self, message, info=None, result=None):\n        global LOGIN_FAILURES\n        LOGIN_FAILURES.append(int(time.time()))\n        time.sleep(min(5, 0.1 + len(LOGIN_FAILURES) / 2))\n        return Command._error(self, message,\n                              info=info, result=self._result(result))\n\n    def _success(self, message, result=None):\n        return Command._success(self, message, result=self._result(result))\n\n    def _do_redirect(self):\n        path = self.data.get('_path', [None])[0]\n\n        # These are here to prevent people from abusing this to redirect to\n        # arbitrary URLs on the Internet.\n        if path:\n            url = urlparse(path)\n            safe_assert(not url.scheme and not url.netloc)\n\n        if (path and\n               not path[1:].startswith(DeAuthenticate.SYNOPSIS[2] or '!') and\n               not path[1:].startswith(self.SYNOPSIS[2] or '!')):\n            self.RedirectBack(self.session.config.sys.http_path + path, self.data)\n        else:\n            raise UrlRedirectException('%s/' % self.session.config.sys.http_path)\n\n    def _do_login(self, user, password, load_index=False, redirect=False):\n        global LOGIN_FAILURES\n        session, config = self.session, self.session.config\n        session_id = self.session.ui.html_variables.get('http_session')\n\n        # This prevents folks from sending us a DEFAULT user (upper case),\n        # which is an internal security bypass below.\n        user = user and user.lower()\n\n        if not user:\n            try:\n                # Verify the passphrase\n                if CheckPassword(config, None, password):\n                    sps = config.passphrases['DEFAULT']\n                else:\n                    sps = VerifyAndStorePassphrase(config, passphrase=password)\n\n                if sps:\n                    # Load the config and index, if necessary\n                    config = self._config()\n                    self._idx(wait=False)\n                    if load_index:\n                        try:\n                            while not config.index:\n                                time.sleep(1)\n                        except KeyboardInterrupt:\n                            pass\n\n                    session.ui.debug('Good passphrase for %s' % session_id)\n                    self.record_user_activity()\n                    LOGIN_FAILURES = []\n                    return self._success(_('Hello world, welcome!'), result={\n                        'authenticated': SetLoggedIn(self, redirect=redirect)\n                    })\n                else:\n                    session.ui.debug('No GnuPG, checking DEFAULT user')\n                    # No GnuPG, see if there is a DEFAULT user in the config\n                    user = 'DEFAULT'\n\n            except (AssertionError, IOError):\n                session.ui.debug('Bad passphrase for %s' % session_id)\n                return self._error(_('Invalid password, please try again'))\n\n        if user in config.logins or user == 'DEFAULT':\n            # FIXME: Salt and hash the password, check if it matches\n            #        the entry in our user/password list (TODO).\n            # NOTE:  This hack effectively disables auth without GnUPG\n            if user == 'DEFAULT':\n                session.ui.debug('FIXME: Unauthorized login allowed')\n                return self._logged_in(redirect=redirect)\n            raise Exception('FIXME')\n\n        return self._error(_('Incorrect username or password'))\n\n    def command(self):\n        session_id = self.session.ui.html_variables.get('http_session')\n\n        if self.data.get('_method', '') == 'POST':\n            if 'pass' in self.data:\n                with GLOBAL_LOGIN_LOCK:\n                    return self._do_login(self.data.get('user', [None])[0],\n                                          self.data['pass'][0],\n                                          redirect=True)\n\n        elif not self.data:\n            password = self.session.ui.get_password(_('Your password: '))\n            return self._do_login(None, password, load_index=True)\n\n        elif (session_id in SESSION_CACHE and\n                SESSION_CACHE[session_id].auth and\n                '_method' in self.data):\n            self._do_redirect()\n\n        return self._success(_('Please log in'))\n\n\nclass DeAuthenticate(Command):\n    \"\"\"De-authenticate a user (log out)\"\"\"\n    SYNOPSIS = (None, 'logout', 'auth/logout', '[<session ID>]')\n    ORDER = ('Internals', 5)\n    SPLIT_ARG = False\n    IS_INTERACTIVE = True\n    CONFIG_REQUIRED = False\n    HTTP_AUTH_REQUIRED = False\n    HTTP_CALLABLE = ('GET', 'POST')\n\n    def command(self):\n        # FIXME: Should this only be a POST request?\n        # FIXME: This needs CSRF protection.\n\n        session_id = self.session.ui.html_variables.get('http_session')\n        if self.args and not session_id:\n            session_id = self.args[0]\n\n        if session_id:\n            try:\n                self.session.ui.debug('Logging out %s' % session_id)\n                del SESSION_CACHE[session_id]\n                return self._success(_('Goodbye!'))\n            except KeyError:\n                pass\n\n        return self._error(_('No session found!'))\n\n\nclass SetPassphrase(Command):\n    \"\"\"Manage storage of passwords (passphrases)\"\"\"\n    SYNOPSIS = (None, 'set/password', 'settings/set/password',\n                      '<keyid> [store|cache-only[:<ttl>]|fail|forget]')\n    ORDER = ('Config', 9)\n    SPLIT_ARG = True\n    IS_INTERACTIVE = True\n    IS_USER_ACTIVITY = True\n    CONFIG_REQUIRED = True\n    HTTP_AUTH_REQUIRED = True\n    HTTP_CALLABLE = ('GET', 'POST')\n    HTTP_QUERY_VARS = {\n        'id': 'KeyID or account name',\n        'mailsource': 'Mail source ID',\n        'mailroute': 'Mail route ID',\n        'is_locked': 'Assume key is locked'}\n    HTTP_POST_VARS = {\n        'password': 'KeyID or account name',\n        'policy-ttl': 'Combined policy and TTL',\n        'policy': 'store|cache-only|fail|forget',\n        'ttl': 'Seconds after which it expires, -1 = never',\n        'update_mailsources': 'If true, update mail source settings',\n        'update_mailroutes': 'If true, update mail route settings',\n        'redirect': 'URL to redirect to on success'}\n\n    def _get_profiles(self):\n        return self.session.config.vcards.find_vcards([], kinds=['profile'])\n\n    def _get_policy(self, fingerprint):\n        if fingerprint in self.session.config.secrets:\n            return self.session.config.secrets[fingerprint].policy\n        elif fingerprint in self.session.config.passphrases:\n            return 'cache-only'\n        return None\n\n    def _massage_key_info(self, fingerprint, key_info, profiles=None, is_locked=False):\n        config = self.session.config\n        fingerprint = fingerprint.lower()\n\n        key_info[\"uids\"].sort(\n            key=lambda k: (k.get(\"name\"), k.get(\"email\"), k.get(\"comment\")))\n\n        if not is_locked:\n            key_info['policy'] = self._get_policy(fingerprint)\n            if key_info['policy'] is None:\n                del key_info['policy']\n\n        key_info[\"accounts\"] = []\n        if profiles is None:\n            profiles = self._get_profiles()\n        for vc in profiles:\n            vc_pgp_key = (vc.pgp_key or '').lower()\n            if vc_pgp_key == fingerprint:\n                key_info[\"accounts\"].append({\n                    'name': vc.fn,\n                    'email': vc.email,\n                    'rid': vc.random_uid})\n\n        return key_info\n\n    def _lookup_key(self, keyid, **kwargs):\n        keylist = self._gnupg().list_secret_keys(selectors=[keyid])\n        if len(keylist) != 1:\n            raise ValueError(\"Too many or too few keys found!\")\n        fingerprint, key_info = keylist.keys()[0], keylist.values()[0]\n        return fingerprint, self._massage_key_info(\n            fingerprint, key_info, **kwargs)\n\n    def _list_keys(self, **kwargs):\n        keylist = self._gnupg().list_secret_keys()\n        profiles = self._get_profiles()\n        for fingerprint, key_info in keylist.iteritems():\n            self._massage_key_info(fingerprint, key_info,\n                                   profiles=profiles, **kwargs)\n        return keylist\n\n    def _get_account(self, cfg):\n        username = cfg.username\n        if cfg.username and '@' not in cfg.username:\n            username = '%s@%s' % (username, cfg.host)\n        return username\n\n    def _user_fingerprint(self, username):\n        return username.replace('@', '_').replace('.', '_').lower()\n\n    def _list_accounts(self, only=None):\n        accounts = {}\n        def _add_account(cfg, which):\n            username = self._get_account(cfg)\n            if (username\n                    and ((username == only) or only is None)\n                    and cfg.auth_type == 'password'):\n                if username in accounts:\n                    accounts[username][which] = cfg.host\n                else:\n                    fingerprint = self._user_fingerprint(username)\n                    accounts[username] = {\n                        which: cfg.host,\n                        'username': username,\n                        'policy': self._get_policy(fingerprint)}\n                    if accounts[username]['policy'] is None:\n                        del accounts[username]['policy']\n\n        for msid, route in self.session.config.routes.iteritems():\n            _add_account(route, 'route')\n        for msid, source in self.session.config.sources.iteritems():\n            _add_account(source, 'source')\n\n        return accounts\n\n    def _account_details(self, account):\n        return self._list_accounts(only=account).get(account)\n\n    def _check_master_password(self, password, account=None, fingerprint=None):\n        return CheckPassword(self.session.config, None, password)\n\n    def _check_password(self, password, account=None, fingerprint=None):\n        if account:\n            # We're going to keep punting on this for a while...\n            return True\n        elif fingerprint:\n            sps = SecurePassphraseStorage(password)\n            gpg = GnuPG(self.session.config)\n            status, sig = gpg.sign('OK', fromkey=fingerprint, passphrase=sps)\n            return (status == 0)\n        else:\n            return True\n\n    def _prepare_result(self, account=None, keyid=None, is_locked=False):\n        if account:\n            fingerprint = self._user_fingerprint(account)\n            result = {'account': self._account_details(account)}\n        elif keyid:\n            fingerprint, info = self._lookup_key(keyid, is_locked=is_locked)\n            result = {'key': info}\n        else:\n            fingerprint = None\n            result = {\n                'keylist': self._list_keys(is_locked=is_locked),\n                'accounts': self._list_accounts()}\n        return fingerprint, result\n\n    def command(self):\n        config = self.session.config\n\n        policyttl = self.args[1] if (len(self.args) > 1) else 'cache-only:-1'\n        is_locked = self.data.get('is_locked', [0])[0]\n        if 'policy-ttl' in self.data:\n            policyttl = self.data['policy-ttl'][0]\n        if ':' in policyttl:\n            policy, ttl = policyttl.split(':')\n        else:\n            policy, ttl = policyttl, -1\n        if 'policy' in self.data:\n            policy = self.data['policy'][0]\n        if 'ttl' in self.data:\n            ttl = self.data['policy'][0]\n        ttl = float(ttl)\n\n        fingerprint = info = account = keyid = None\n        which = self.args[0] if self.args else self.data.get('id', [None])[0]\n        if which and '@' in which:\n            account = which\n        else:\n            keyid = which\n\n        if not account and not keyid:\n            msid = self.data.get('mailsource', [None])[0]\n            if msid:\n                account = self._get_account(config.sources[msid])\n            mrid = self.data.get('mailroute', [None])[0]\n            if mrid:\n                account = self._get_account(config.routes[mrid])\n\n        fingerprint, result = self._prepare_result(\n            account=account, keyid=keyid, is_locked=is_locked)\n\n        if policy in ('display', 'unprotect'):\n            pass_prompt = _('Enter your Mailpile password')\n            pass_check = self._check_master_password\n        else:\n            pass_prompt = _('Enter your password')\n            pass_check = self._check_password\n\n        if self.data.get('_method', None) == 'GET':\n            return self._success(pass_prompt, result)\n\n        safe_assert(fingerprint is not None)\n        fingerprint = fingerprint.lower()\n        if fingerprint in config.secrets:\n            if config.secrets[fingerprint].policy == 'protect':\n                if policy not in ('unprotect', 'display'):\n                    result['error'] = _('That key is managed by Mailpile,'\n                                        ' it cannot be changed directly.')\n                    return self._error(_('Protected secret'), result=result)\n\n        if self.data.get('_method', None) == 'POST':\n            password = self.data.get('password', [None])[0]\n            update_ms = self.data.get('update_mailsources', [False])[0]\n            update_mr = self.data.get('update_mailroutes', [False])[0]\n        else:\n            password = self.session.ui.get_password(pass_prompt + ': ')\n            update_ms = update_mr = (account is not None)\n\n        if update_ms or update_mr:\n            safe_assert(account is not None)\n\n        if not pass_check(password, account=account, fingerprint=fingerprint):\n            result['error'] = _('Password incorrect! Try again?')\n            return self._error(_('Incorrect password'), result=result)\n\n        def _account_matches(cfg):\n            return (account == cfg.username or\n                    account == '%s@%s' % (cfg.username, cfg.host))\n        def happy(msg, refresh=True, changed=True):\n            if changed:\n                # Fun side effect: changing the passphrase invalidates the\n                # message cache\n                import mailpile.mailutils.emails\n                mailpile.mailutils.emails.ClearParseCache(full=True)\n\n                indirect_pwd = '_SECRET_:%s:%s' % (fingerprint, time.time())\n                if update_ms:\n                    for msid, source in config.sources.iteritems():\n                        if _account_matches(source):\n                            source.password = indirect_pwd\n                if update_mr:\n                    for msid, route in config.routes.iteritems():\n                        if _account_matches(route):\n                            route.password = indirect_pwd\n\n                self._background_save(config=True)\n\n            redirect = self.data.get('redirect', [None])[0]\n            if redirect:\n                raise UrlRedirectException(redirect)\n\n            result['op_completed'] = policy\n            if refresh:\n              fp, r = self._prepare_result(account=account, keyid=keyid)\n              result.update(r)\n\n            return self._success(msg, result)\n\n        if policy == 'display':\n            if fingerprint in config.passphrases:\n                pwd = config.passphrases[fingerprint].get_passphrase()\n            elif fingerprint in config.secrets:\n                pwd = config.secrets[fingerprint].password\n            else:\n                return self._error(_('No password found'), result=result)\n            result['stored_password'] = pwd\n            return happy(_('Retrieved stored password'),\n                         refresh=False, changed=False)\n\n        if policy == 'forget':\n            if fingerprint in config.passphrases:\n                del config.passphrases[fingerprint]\n            if fingerprint in config.secrets:\n                config.secrets[fingerprint] = {'policy': 'fail'}\n                del config.secrets[fingerprint]\n            return happy(_('Password forgotten!'))\n\n        if policy == 'fail':\n            if fingerprint in config.passphrases:\n                del config.passphrases[fingerprint]\n            config.secrets[fingerprint] = {'policy': policy}\n            return happy(_('Password will never be stored'))\n\n        if policy == 'store':\n            if fingerprint in config.passphrases:\n                del config.passphrases[fingerprint]\n            config.secrets[fingerprint] = {\n                'password': password,\n                'policy': policy}\n            return happy(_('Password remembered!'))\n\n        elif policy == 'cache-only' and password:\n            sps = SecurePassphraseStorage(password)\n            if ttl > 0:\n                sps.expiration = time.time() + ttl\n            config.passphrases[fingerprint] = sps\n            if fingerprint in config.secrets:\n                config.secrets[fingerprint] = {'policy': 'fail'}\n                del config.secrets[fingerprint]\n            return happy(_('The password has been stored temporarily'))\n\n        else:\n            return self._error(_('Invalid password policy'), result=result)\n\n\nplugin_manager = PluginManager(builtin=True)\nplugin_manager.register_commands(Authenticate,\n                                 DeAuthenticate,\n                                 SetPassphrase)\n"
  },
  {
    "path": "mailpile/command_cache.py",
    "content": "import time\n\nimport mailpile.util\nfrom mailpile.commands import Command\nfrom mailpile.eventlog import Event\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.util import *\nfrom mailpile.ui import Session, BackgroundInteraction\n\n\n_plugins = PluginManager(builtin=__name__)\n\n\nclass CommandCache(object):\n    #\n    # This is an in-memory cache of commands and results we may want to\n    # refresh in the background and/or reuse.\n    #\n    # The way this works:\n    #     - Cache-able commands generate a fingerprint describing themselves.\n    #     - If the fingerprint is found in cache, reuse the result object.\n    #     - Otherwise, run, generate a list of requirements and cache all of:\n    #       the command object itself, the requirements, the result object.\n    #     - Internal state changes (tag ops, new mail, etc.) call mark_dirty()\n    #       describing which assets (requirements) have changed.\n    #     - Periodically, the cache is refreshed, which re-runs any dirtied\n    #       commands and fires events notifying the UI about changes.\n    #\n    # Examples of requirements:\n    #\n    #    - Search terms, eg. 'in:inbox' or 'potato' or 'salad'\n    #    - Messages: 'msg:INDEX' where INDEX is a number (not a MID)\n    #    - Threads: 'thread:MID' were MID is the thread ID.\n    #    - The app configuration: '!config'\n    #\n\n    def __init__(self, debug=None):\n        self.debug = debug or (lambda s: None)\n        self.lock = UiRLock()\n        self._lag = 0.1\n        self.cache = {}       # id -> [exp, req, ss, cmd_obj, res_obj, added]\n        self.dirty = []       # (ts, req): Requirements that changed & when\n        self._dirty_ttl = 10\n\n    def cache_result(self, fprint, expires, req, cmd_obj, result_obj):\n        with self.lock:\n            # Make a snapshot of the session, as it provides context\n            ui = BackgroundInteraction(cmd_obj.session.config,\n                                       log_parent=cmd_obj.session.ui)\n            ss = Session.Snapshot(cmd_obj.session, ui=ui)\n\n            # Note: We cache this even if the requirements are \"dirty\",\n            #       as mere presence in the cache makes this a candidate\n            #       for refreshing.\n            self.cache[str(fprint)] = [expires, req, ss, cmd_obj, result_obj,\n                                       time.time()]\n            self.debug('Cached %s, req=%s' % (fprint, sorted(list(req))))\n\n    def get_result(self, fprint, dirty_check=True, extend=300):\n        with self.lock:\n            exp, req, ss, co, result_obj, a = match = self.cache[fprint]\n        if dirty_check:\n            recent = (a > time.time() - self._lag)\n            dirty = (req & self.dirty_set(after=a))\n            if recent or dirty:\n                # If item is too new, or requirements are dirty, pretend this\n                # item does not exist.\n                self.debug('Suppressing cache result %s, recent=%s dirty=%s'\n                           % (fprint, recent, sorted(list(dirty))))\n                raise KeyError(fprint)\n        match[0] = time.time() + extend\n        co.session = result_obj.session = ss\n        self.debug('Returning cached result for %s' % fprint)\n        return result_obj\n\n    def dirty_set(self, after=0):\n        dirty = set(['!timedout'])\n        with self.lock:\n            for ts, req in self.dirty:\n                if (after == 0) or (ts > after):\n                    dirty |= req\n        return dirty\n\n    def mark_dirty(self, requirements):\n        with self.lock:\n            self.dirty.append((time.time(), set(requirements)))\n        self.debug('Marked dirty: %s' % sorted(list(requirements)))\n\n    def refresh(self, extend=0, runtime=5, event_log=None):\n        if mailpile.util.LIVE_USER_ACTIVITIES > 0:\n            self.debug('Skipping cache refresh, user is waiting.')\n            return\n\n        started = now = time.time()\n        with self.lock:\n            # Expire things from the cache\n            expired = set([f for f in self.cache if self.cache[f][0] < now])\n            for fp in expired:\n                del self.cache[fp]\n\n            # Expire things from the dirty set\n            self.dirty = [(ts, req) for ts, req in self.dirty\n                          if ts >= (now - self._dirty_ttl)]\n\n            # Decide which fingerprints to look at this time around\n            fingerprints = list(self.cache.keys())\n\n        refreshed = []\n        fingerprints.sort(key=lambda k: -self.cache[k][0])\n        for fprint in fingerprints:\n            req = None\n            try:\n                e, req, ss, co, ro, a = self.cache[fprint]\n                now = time.time()\n                dirty = (req & self.dirty_set(after=a))\n                if (a + self._lag < now) and dirty:\n                    if now < started + runtime:\n                        play_nice_with_threads()\n                        co.session = ro.session = ss\n                        ro = co.refresh()\n                        if extend > 0:\n                            e = min(e + extend, now + 5*extend)\n                        if '!timedout' in req:\n                            req.remove('!timedout')\n                        with self.lock:\n                            # Make sure we do not overwrite new results from\n                            # elsewhere at this time.\n                            if self.cache[fprint][-1] == a:\n                                e = max(e, self.cache[fprint][0])  # Clobber?\n                                self.cache[fprint] = [e, req, ss, co, ro, now]\n                            refreshed.append(fprint)\n                    else:\n                        # Out of time, mark as dirty.\n                        req.add('!timedout')\n            except (ValueError, IndexError, TypeError):\n                # Treat broken things as if they had timed out\n                if req:\n                    req.add('!timedout')\n\n        if refreshed and event_log:\n            event_log.log(message=_('New results are available'),\n                          source=self,\n                          data={'cache_ids': refreshed},\n                          flags=Event.COMPLETE)\n            self.debug('Refreshed: %s' % refreshed)\n\n\nclass Cached(Command):\n    \"\"\"Fetch results from the command cache.\"\"\"\n    SYNOPSIS = (None, 'cached', 'cached', '[<cache-id>]')\n    ORDER = ('Internals', 7)\n    HTTP_QUERY_VARS = {'id': 'Cache ID of command to redisplay'}\n    IS_USER_ACTIVITY = False\n    LOG_NOTHING = True\n\n    def max_age(self):\n        # Allow result to be cached by the browser for 2 seconds; we do\n        # this to facilitate cross-tab sharing of cache results.\n        return 2\n\n    # Warning: This depends on internals of Command, how things are run there.\n    def run(self):\n        try:\n            cid = self.args[0] if self.args else self.data.get('id', [None])[0]\n            rv = self.session.config.command_cache.get_result(cid)\n            self.session.copy(rv.session)\n            rv.session.ui.render_mode = self.session.ui.render_mode\n            return rv\n        except:\n            self._starting()\n            self._error(self.FAILURE % {'name': self.name,\n                                        'args': ' '.join(self.args)})\n            return self._finishing(False)\n\n\n_plugins.register_commands(Cached)\n"
  },
  {
    "path": "mailpile/commands.py",
    "content": "# The basic Mailpile command framework.\n#\n# TODO: Merge with plugins/ the division is obsolete and artificial.\n#\nimport json\nimport os\nimport re\nimport shlex\nimport traceback\nimport time\n\nimport mailpile.util\nimport mailpile.security as security\nfrom mailpile.crypto.gpgi import GnuPG\nfrom mailpile.eventlog import Event\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.util import *\nfrom mailpile.vfs import vfs\n\n\n# Commands starting with _ don't get single-letter shortcodes...\nCOMMANDS = []\nCOMMAND_GROUPS = ['Internals', 'Config', 'Searching', 'Tagging', 'Composing']\n\n\nclass Command(object):\n    \"\"\"Generic command object all others inherit from\"\"\"\n    SYNOPSIS = (None,     # CLI shortcode, e.g. A:\n                None,     # CLI shortname, e.g. add\n                None,     # API endpoint, e.g. sys/addmailbox\n                None)     # Positional argument list\n    SYNOPSIS_ARGS = None  # New-style positional argument list\n    API_VERSION = None\n    UI_CONTEXT = None\n    IS_USER_ACTIVITY = False\n    IS_HANGING_ACTIVITY = False\n    IS_INTERACTIVE = False\n    CONFIG_REQUIRED = True\n\n    COMMAND_CACHE_TTL = 0   # < 1 = Not cached\n    CHANGES_SESSION_CONTEXT = False\n\n    FAILURE = 'Failed: %(name)s %(args)s'\n    ORDER = (None, 0)\n    SPLIT_ARG = True  # Uses shlex by default\n    RAISES = (UsageError, UrlRedirectException)\n    WITH_CONTEXT = ()\n    COMMAND_SECURITY = None\n\n    # Event logging settings\n    LOG_NOTHING = False\n    LOG_ARGUMENTS = True\n    LOG_PROGRESS = False\n    LOG_STARTING = '%(name)s: Starting'\n    LOG_FINISHED = '%(name)s: %(message)s'\n\n    # HTTP settings (note: security!)\n    HTTP_CALLABLE = ('GET', )\n    HTTP_POST_VARS = {}\n    HTTP_QUERY_VARS = {}\n    HTTP_BANNED_VARS = {}\n    HTTP_STRICT_VARS = True\n    HTTP_AUTH_REQUIRED = True\n\n    class CommandResult:\n        def __init__(self, command_obj, session,\n                     command_name, doc, result, status, message,\n                     template_id=None, kwargs={}, error_info={}):\n            self.session = session\n            self.command_obj = command_obj\n            self.command_name = command_name\n            self.kwargs = {}\n            self.kwargs.update(kwargs)\n            self.template_id = template_id\n            self.doc = doc\n            self.result = result\n            self.status = status\n            self.error_info = {}\n            self.error_info.update(error_info)\n            self.message = message\n            self.rendered = {}\n            self.renderers = {\n                'json': self.as_json,\n                'html': self.as_html,\n                'text': self.as_text,\n                'css': self.as_css,\n                'csv': self.as_csv,\n                'rss': self.as_rss,\n                'xml': self.as_xml,\n                'txt': self.as_txt,\n                'js': self.as_js\n            }\n\n        def __nonzero__(self):\n            return (self.result and True or False)\n\n        def as_(self, what, *args, **kwargs):\n            if args or kwargs:\n                # Args render things un-cacheable.\n                return self.renderers.get(what)(*args, **kwargs)\n\n            if what not in self.rendered:\n                self.rendered[what] = self.renderers.get(what, self.as_text)()\n            return self.rendered[what]\n\n        def as_text(self):\n            if isinstance(self.result, bool):\n                happy = '%s: %s' % (self.result and _('OK') or _('Failed'),\n                                    self.message or self.doc)\n                if not self.result and self.error_info:\n                    return '%s\\n%s' % (happy,\n                        json.dumps(self.error_info, indent=4,\n                                   default=mailpile.util.json_helper))\n                else:\n                    return happy\n            elif isinstance(self.result, (dict, list, tuple)):\n                return json.dumps(self.result, indent=4, sort_keys=True,\n                    default=mailpile.util.json_helper)\n            else:\n                return unicode(self.result)\n\n        __str__ = lambda self: self.as_text()\n\n        __unicode__ = lambda self: self.as_text()\n\n        def as_dict(self):\n            from mailpile.urlmap import UrlMap\n            um = UrlMap(self.session)\n            rv = {\n                'command': self.command_name,\n                'state': {\n                    'command_url': um.ui_url(self.command_obj),\n                    'context_url': um.context_url(self.command_obj),\n                    'query_args': self.command_obj.state_as_query_args(),\n                    'cache_id': self.command_obj.cache_id(),\n                    'context': self.command_obj.context or ''\n                },\n                'status': self.status,\n                'message': self.message,\n                'result': self.result,\n                'event_id': self.command_obj.event.event_id,\n                'elapsed': '%.3f' % self.session.ui.time_elapsed,\n            }\n            csrf_token = self.session.ui.html_variables.get('csrf_token')\n            if csrf_token:\n                rv['state']['csrf_token'] = csrf_token\n            if self.error_info:\n                rv['error'] = self.error_info\n            for ui_key in [k for k in self.command_obj.data.keys()\n                           if k.startswith('ui_')]:\n                rv[ui_key] = self.command_obj.data[ui_key][0]\n            ev = self.command_obj.event\n            if ev and ev.data.get('password_needed'):\n                rv['password_needed'] = ev.private_data['password_needed']\n            return rv\n\n        def as_csv(self, template=None, result=None):\n            result = self.result if (result is None) else result\n            if (isinstance(result, (list, tuple)) and\n                    (not result or isinstance(result[0], (list, tuple)))):\n                import csv, StringIO\n                output = StringIO.StringIO()\n                writer = csv.writer(output, dialect='excel')\n                for row in result:\n                    writer.writerow([unicode(r).encode('utf-8') for r in row])\n                return output.getvalue().decode('utf-8')\n            else:\n                return ''\n\n        def as_json(self):\n            return self.session.ui.render_json(self.as_dict())\n\n        def as_html(self, template=None):\n            return self.as_template('html', template)\n\n        def as_js(self, template=None):\n            return self.as_template('js', template)\n\n        def as_css(self, template=None):\n            return self.as_template('css', template)\n\n        def as_rss(self, template=None):\n            return self.as_template('rss', template)\n\n        def as_xml(self, template=None):\n            return self.as_template('xml', template)\n\n        def as_txt(self, template=None):\n            return self.as_template('txt', template)\n\n        def as_template(self, ttype,\n                        mode=None, wrap_in_json=False, template=None):\n            cache_id = ''.join(('j' if wrap_in_json else '', ttype,\n                                '/' if template else '', template or '',\n                                ':', mode or 'full'))\n            if cache_id in self.rendered:\n                return self.rendered[cache_id]\n            tpath = self.command_obj.template_path(\n                ttype, template_id=self.template_id, template=template)\n\n            data = self.as_dict()\n            data['title'] = self.message\n            data['render_mode'] = mode or 'full'\n            data['render_template'] = template or 'index'\n\n            rendering = self.session.ui.render_web(self.session.config,\n                                                   [tpath], data)\n            if wrap_in_json:\n                data['result'] = rendering\n                self.rendered[cache_id] = self.session.ui.render_json(data)\n            else:\n                self.rendered[cache_id] = rendering\n\n            return self.rendered[cache_id]\n\n    def __init__(self, session, name=None, arg=None, data=None, async=False):\n        self.session = session\n        self.context = None\n        self.name = self.SYNOPSIS[1] or self.SYNOPSIS[2] or name\n        self.data = data or {}\n        self.status = 'unknown'\n        self.message = name\n        self.error_info = {}\n        self.result = None\n        self.run_async = async\n        if type(arg) in (type(list()), type(tuple())):\n            self.args = tuple(arg)\n        elif arg:\n            if self.SPLIT_ARG is True:\n                try:\n                    self.args = tuple([a.decode('utf-8') for a in\n                                       shlex.split(arg.encode('utf-8'))])\n                except (ValueError, UnicodeEncodeError, UnicodeDecodeError):\n                    raise UsageError(_('Failed to parse arguments'))\n            else:\n                self.args = (arg, )\n        else:\n            self.args = tuple([])\n        if 'arg' in self.data:\n            self.args = tuple(list(self.args) + self.data['arg'])\n        self._create_event()\n\n    def state_as_query_args(self):\n        args = {}\n        if self.args:\n            args['arg'] = self._sloppy_copy(self.args)\n        args.update(self._sloppy_copy(self.data))\n        return args\n\n    def cache_id(self, sqa=None):\n        if self.COMMAND_CACHE_TTL < 1:\n            return ''\n        from mailpile.urlmap import UrlMap\n        args = sorted(list((sqa or self.state_as_query_args()).iteritems()))\n        args += '/%d' % self.session.ui.term.max_width\n        # The replace() stuff makes these usable as CSS class IDs\n        return ('%s-%s' % (UrlMap(self.session).ui_url(self),\n                           md5_hex(str(args))\n                           )).replace('/', '-').replace('.', '-')\n\n    def cache_requirements(self, result):\n        raise NotImplementedError('Cachable commands should override this, '\n                                  'returning a set() of requirements.')\n\n    def cache_result(self, result):\n        if self.COMMAND_CACHE_TTL > 0:\n            try:\n                cache_id = self.cache_id()\n                if cache_id:\n                    self.session.config.command_cache.cache_result(\n                        cache_id,\n                        time.time() + self.COMMAND_CACHE_TTL,\n                        self.cache_requirements(result),\n                        self,\n                        result)\n                    self.session.ui.mark(_('Cached result as %s') % cache_id)\n            except (ValueError, KeyError, TypeError, AttributeError):\n                self._ignore_exception()\n\n    def template_path(self, ttype, template_id=None, template=None):\n        path_parts = (template_id or self.SYNOPSIS[2] or 'command').split('/')\n        if template in (None, ttype, 'as.' + ttype):\n            path_parts.append('index')\n        else:\n            # Security: The template request may come from the URL, so we\n            #           sanitize it very aggressively before heading off\n            #           to the filesystem.\n            clean_tpl = CleanText(template.replace('.%s' % ttype, ''),\n                                  banned=(CleanText.FS +\n                                          CleanText.WHITESPACE))\n            path_parts.append(clean_tpl.clean)\n        path_parts[-1] += '.' + ttype\n        return os.path.join(*path_parts)\n\n    def _gnupg(self, **kwargs):\n        return GnuPG(self.session.config, event=self.event, **kwargs)\n\n    def _config(self):\n        session, config = self.session, self.session.config\n        if not config.loaded_config:\n            config.load(session)\n            parent = session\n            config.prepare_workers(session, daemons=self.IS_INTERACTIVE)\n        if self.IS_INTERACTIVE and not config.daemons_started():\n            config.prepare_workers(session, daemons=True)\n        return config\n\n    def _idx(self, reset=False, wait=True, wait_all=True, quiet=False):\n        session, config = self.session, self._config()\n\n        if not reset and config.index:\n            return config.index\n\n        def __do_load2():\n            config.vcards.load_vcards(session)\n            if not wait_all:\n                session.ui.report_marks(quiet=quiet)\n\n        def __do_load1():\n            with config.interruptable_wait_for_lock():\n                if reset:\n                    config.index = None\n                    session.results = []\n                    session.searched = []\n                    session.displayed = None\n                idx = config.get_index(session)\n                if wait_all:\n                    __do_load2()\n                if not wait:\n                    session.ui.report_marks(quiet=quiet)\n                return idx\n\n        if wait:\n            rv = __do_load1()\n            session.ui.reset_marks(quiet=quiet)\n        else:\n            config.save_worker.add_task(session, 'Load', __do_load1)\n            rv = None\n\n        if not wait_all:\n            config.save_worker.add_task(session, 'Load2', __do_load2)\n\n        return rv\n\n    def _background_save(self,\n                         everything=False, config=False,\n                         index=False, index_full=False,\n                         wait=False, wait_callback=None):\n        session, cfg = self.session, self.session.config\n        aut = cfg.save_worker.add_unique_task\n        if everything or config:\n            aut(session,\n                'Save config',\n                lambda: cfg.save(session, force=(config == '!FORCE')),\n                first=True)\n        if cfg.index:\n            cfg.flush_mbox_cache(session, clear=False, wait=wait)\n            if index_full:\n                aut(session, 'Save index',\n                    lambda: self._idx().save(session),\n                    first=True)\n            elif everything or index:\n                aut(session, 'Save index changes',\n                    lambda: self._idx().save_changes(session),\n                    first=True)\n        if wait:\n            wait_callback = wait_callback or (lambda: True)\n            cfg.save_worker.do(session, 'Waiting', wait_callback)\n\n    def _choose_messages(self, words, allow_ephemeral=False):\n        msg_ids = set()\n        all_words = []\n        for word in words:\n            all_words.extend(word.split(','))\n        for what in all_words:\n            if what.lower() == 'these':\n                if self.session.displayed:\n                    b = self.session.displayed['stats']['start'] - 1\n                    c = self.session.displayed['stats']['count']\n                    msg_ids |= set(self.session.results[b:b + c])\n                else:\n                    self.session.ui.warning(_('No results to choose from!'))\n            elif what.lower() in ('all', '!all', '=!all'):\n                if self.session.results:\n                    msg_ids |= set(self.session.results)\n                else:\n                    self.session.ui.warning(_('No results to choose from!'))\n            elif what.startswith('='):\n                try:\n                    msg_id = int(what.replace('=', ''), 36)\n                    if msg_id >= 0 and msg_id < len(self._idx().INDEX):\n                        msg_ids.add(msg_id)\n                    else:\n                        self.session.ui.warning((_('No such ID: %s')\n                                                 ) % (what[1:], ))\n                except ValueError:\n                    if allow_ephemeral and '-' in what:\n                        msg_ids.add(what[1:])\n                    else:\n                        self.session.ui.warning(_('What message is %s?'\n                                                  ) % (what, ))\n            elif '-' in what:\n                try:\n                    b, e = what.split('-')\n                    msg_ids |= set(self.session.results[int(b) - 1:int(e)])\n                except (ValueError, KeyError, IndexError, TypeError):\n                    self.session.ui.warning(_('What message is %s?'\n                                              ) % (what, ))\n            else:\n                try:\n                    msg_ids.add(self.session.results[int(what) - 1])\n                except (ValueError, KeyError, IndexError, TypeError):\n                    self.session.ui.warning(_('What message is %s?'\n                                              ) % (what, ))\n        return msg_ids\n\n    def _error(self, message, info=None, result=None):\n        self.status = 'error'\n        self.message = message\n\n        ui_message = _('%s error: %s') % (self.name, message)\n        if info:\n            self.error_info.update(info)\n            details = ' '.join(['%s=%s' % (k, info[k]) for k in info])\n            ui_message += ' (%s)' % details\n        self.session.ui.mark(self.name)\n        self.session.ui.error(ui_message)\n\n        if result:\n            return self.view(result)\n        else:\n            return False\n\n    def _success(self, message, result=True):\n        self.status = 'success'\n        self.message = message\n\n        ui_message = '%s: %s' % (self.name, message)\n        self.session.ui.mark(ui_message)\n\n        return self.view(result)\n\n    def _read_file_or_data(self, fn):\n        if fn in self.data:\n            return self.data[fn]\n        else:\n            return vfs.open(fn, 'rb').read()\n\n    def _ignore_exception(self):\n        self.session.ui.debug(traceback.format_exc())\n\n    def _serialize(self, name, function):\n        return function()\n\n    def _background(self, name, function):\n        session, config = self.session, self.session.config\n        return config.scan_worker.add_task(session, name, function)\n\n    def _update_event_state(self, state, log=False):\n        self.event.flags = state\n        self.event.data['elapsed'] = int(1000 * (time.time()-self._start_time))\n\n        if (log or self.LOG_PROGRESS) and not self.LOG_NOTHING:\n            self.event.data['ui'] = str(self.session.ui.__class__.__name__)\n            self.event.data['output'] = self.session.ui.render_mode\n            if self.session.config.event_log:\n                self.session.config.event_log.log_event(self.event)\n\n    def _starting(self):\n        self._start_time = time.time()\n        self._update_event_state(Event.RUNNING)\n        if self.name:\n            self.session.ui.start_command(self.name, self.args, self.data)\n\n    def _fmt_msg(self, message):\n        return message % {'name': self.name,\n                          'status': self.status or '',\n                          'message': self.message or ''}\n\n    def _sloppy_copy(self, data, name=None):\n        if name and (name[:4] in ('pass', 'csrf') or\n                     'password' in name or\n                     'passphrase' in name):\n            data = '(SUPPRESSED)'\n        def copy_value(v):\n            try:\n                unicode(v).encode('utf-8')\n                return unicode(v)[:1024]\n            except (UnicodeEncodeError, UnicodeDecodeError):\n                return '(BINARY DATA)'\n        if isinstance(data, (list, tuple)):\n            return [self._sloppy_copy(i, name=name) for i in data]\n        elif isinstance(data, dict):\n            return dict((k, self._sloppy_copy(v, name=k))\n                        for k, v in data.iteritems())\n        else:\n            return copy_value(data)\n\n    def _create_event(self):\n        private_data = {}\n        if self.LOG_ARGUMENTS:\n            if self.data:\n                private_data['data'] = self._sloppy_copy(self.data)\n            if self.args:\n                private_data['args'] = self._sloppy_copy(self.args)\n\n        self.event = self._make_command_event(private_data)\n\n    def _make_command_event(self, private_data):\n        return Event(source=self,\n                     message=self._fmt_msg(self.LOG_STARTING),\n                     flags=Event.INCOMPLETE,\n                     data={},\n                     private_data=private_data)\n\n    def _finishing(self, rv, just_cleanup=False):\n        if just_cleanup:\n            self._update_finished_event()\n            return rv\n\n        if not self.context:\n            self.context = self.session.get_context(\n                update=self.CHANGES_SESSION_CONTEXT)\n\n        self.session.ui.mark(_('Generating result'))\n        result = self.CommandResult(self, self.session, self.name,\n                                    self.__doc__,\n                                    rv, self.status, self.message,\n                                    error_info=self.error_info)\n        self.cache_result(result)\n\n        if not self.run_async:\n            self._update_finished_event()\n        self.session.last_event_id = self.event.event_id\n        return result\n\n    def _update_finished_event(self):\n        # Update the event!\n        if self.message:\n            self.event.message = self.message\n        if self.error_info:\n            self.event.private_data['error_info'] = self.error_info\n        self.event.message = self._fmt_msg(self.LOG_FINISHED)\n        self._update_event_state(Event.COMPLETE, log=True)\n\n        self.session.ui.mark(self.event.message)\n        self.session.ui.report_marks(\n            details=('timing' in self.session.config.sys.debug))\n        if self.name:\n            self.session.ui.finish_command(self.name)\n\n    def _run_sync(self, enable_cache, *args, **kwargs):\n        try:\n            thread_context_push(command=self,\n                                event=self.event,\n                                session=self.session)\n            self._starting()\n            self._run_args = args\n            self._run_kwargs = kwargs\n\n            if (self.COMMAND_CACHE_TTL > 0 and\n                   'http' not in self.session.config.sys.debug and\n                   enable_cache):\n                cid = self.cache_id()\n                try:\n                    rv = self.session.config.command_cache.get_result(cid)\n                    rv.session.ui = self.session.ui\n                    if self.CHANGES_SESSION_CONTEXT:\n                        self.session.copy(rv.session)\n                    self.session.ui.mark(_('Using pre-cached result object %s') % cid)\n                    self._finishing(True, just_cleanup=True)\n                    return rv\n                except:\n                    pass\n\n            def command(self, *args, **kwargs):\n                if self.CONFIG_REQUIRED:\n                    if not self.session.config.loaded_config:\n                        return self._error(_('Please log in'))\n                    if mailpile.util.QUITTING:\n                        return self._error(_('Shutting down'))\n                return self.command(*args, **kwargs)\n\n            return self._finishing(command(self, *args, **kwargs))\n        except self.RAISES:\n            self.status = 'success'\n            self._finishing(True, just_cleanup=True)\n            raise\n        except:\n            self._ignore_exception()\n            self._error(self.FAILURE % {'name': self.name,\n                                        'args': ' '.join(self.args)})\n            return self._finishing(False)\n        finally:\n            thread_context_pop()\n\n    def _run(self, *args, **kwargs):\n        if self.run_async:\n            def streetcar():\n                try:\n                    with MultiContext(self.WITH_CONTEXT):\n                        rv = self._run_sync(True, *args, **kwargs).as_dict()\n                        self.event.private_data.update(rv)\n                        self._update_finished_event()\n                except:\n                    traceback.print_exc()\n\n            self._starting()\n            self._update_event_state(self.event.RUNNING, log=True)\n            result = Command.CommandResult(self, self.session, self.name,\n                                           self.__doc__,\n                                           {\"resultid\": self.event.event_id},\n                                           \"success\",\n                                           \"Running in background\")\n\n            self.session.config.scan_worker.add_task(self.session, self.name,\n                                                     streetcar, first=True)\n            return result\n\n        else:\n            return self._run_sync(True, *args, **kwargs)\n\n    def _maybe_trigger_cache_refresh(self):\n        if self.data.get('_method') == 'POST':\n            def refresher():\n                self.session.config.command_cache.refresh(\n                    event_log=self.session.config.event_log)\n            self.session.config.scan_worker.add_unique_task(\n                self.session, 'post-refresh', refresher, first=True)\n\n    def record_user_activity(self):\n        mailpile.util.LAST_USER_ACTIVITY = time.time()\n\n    def run(self, *args, **kwargs):\n        if self.COMMAND_SECURITY is not None:\n            forbidden = security.forbid_command(self)\n            if forbidden:\n                return self._error(forbidden)\n\n        with MultiContext(self.WITH_CONTEXT):\n            if self.IS_USER_ACTIVITY:\n                try:\n                    self.record_user_activity()\n                    mailpile.util.LIVE_USER_ACTIVITIES += 1\n                    rv = self._run(*args, **kwargs)\n                    self._maybe_trigger_cache_refresh()\n                    return rv\n                finally:\n                    mailpile.util.LIVE_USER_ACTIVITIES -= 1\n            else:\n                rv = self._run(*args, **kwargs)\n                self._maybe_trigger_cache_refresh()\n                return rv\n\n    def refresh(self):\n        self._create_event()\n        return self._run_sync(False, *self._run_args, **self._run_kwargs)\n\n    def command(self):\n        return None\n\n    def etag_data(self):\n        return []\n\n    def max_age(self):\n        return 0\n\n    @classmethod\n    def view(cls, result):\n        return result\n\n\ndef GetCommand(name):\n    match = [c for c in COMMANDS if name in c.SYNOPSIS[:3]]\n    if len(match) == 1:\n        return match[0]\n    return None\n\n\ndef Action(session, opt, arg, data=None):\n    session.ui.reset_marks(quiet=True)\n    config = session.config\n\n    if not opt:\n        return Help(session, 'help').run()\n\n    # Use the COMMANDS dict by default.\n    command = GetCommand(opt)\n    if command:\n        return command(session, opt, arg, data=data).run()\n\n    # Tags are commands\n    if config.loaded_config:\n        lopt = opt.lower()\n\n        found = None\n        for tag in config.tags.values():\n            if lopt == tag.slug.lower():\n                found = tag\n                break\n        if not found:\n            for tag in config.tags.values():\n                if lopt == tag.name.lower():\n                    found = tag\n                    break\n        if not found:\n            for tag in config.tags.values():\n                if lopt == _(tag.name).lower():\n                    found = tag\n                    break\n\n        if found:\n            a = 'in:%s%s%s' % (found.slug, ' ' if arg else '', arg)\n            return GetCommand('search')(session, opt,\n                                        arg=a, data=data).run()\n\n    # OK, give up!\n    raise UsageError(_('Unknown command: %s') % opt)\n"
  },
  {
    "path": "mailpile/config/__init__.py",
    "content": ""
  },
  {
    "path": "mailpile/config/base.py",
    "content": "from __future__ import print_function\nimport io\nimport json\nimport os\nimport ConfigParser\nfrom urllib import quote, unquote\n\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.util import *\n\nimport mailpile.config.validators as validators\n\n\nclass ConfigValueError(ValueError):\n    pass\n\n\ndef ConfigRule(*args):\n    class _ConfigRule(list):\n        def __init__(self):\n            list.__init__(self, args)\n            self._types = []\n    return _ConfigRule()\n\n\ndef PublicConfigRule(*args):\n    c = ConfigRule(*args)\n    c._types.append('public')\n    return c\n\n\ndef KeyConfigRule(*args):\n    c = ConfigRule(*args)\n    c._types.append('key')\n    return c\n\n\n# FIXME: This should be enforced somehow when variables are altered.\n#        Run in a context?\ndef CriticalConfigRule(*args):\n    c = ConfigRule(*args)\n    c._types += ['critical']\n    return c\n\n\ndef ConfigPrinter(cfg, indent=''):\n    rv = []\n    if isinstance(cfg, dict):\n        pairer = cfg.iteritems()\n    else:\n        pairer = enumerate(cfg)\n    for key, val in pairer:\n        if hasattr(val, 'rules'):\n            preamble = '[%s: %s] ' % (val._NAME, val._COMMENT)\n        else:\n            preamble = ''\n        if isinstance(val, (dict, list, tuple)):\n            if isinstance(val, dict):\n                b, e = '{', '}'\n            else:\n                b, e = '[', ']'\n            rv.append(('%s: %s%s\\n%s\\n%s'\n                       '' % (key, preamble, b, ConfigPrinter(val, '  '), e)\n                       ).replace('\\n  \\n', ''))\n        elif isinstance(val, (str, unicode)):\n            rv.append('%s: \"%s\"' % (key, val))\n        else:\n            rv.append('%s: %s' % (key, val))\n    return indent + ',\\n'.join(rv).replace('\\n', '\\n'+indent)\n\n\nclass InvalidKeyError(ValueError):\n    pass\n\n\nclass CommentedEscapedConfigParser(ConfigParser.RawConfigParser):\n    \"\"\"\n    This is a ConfigParser that allows embedded comments and safely escapes\n    and encodes/decodes values that include funky characters.\n\n    >>> cfg = u'[config/sys: Stuff]\\\\ndebug = True ; Ignored comment'\n    >>> cecp = CommentedEscapedConfigParser()\n    >>> cecp.readfp(io.BytesIO(cfg.encode('utf-8')))\n    >>> cecp.get('config/sys: Stuff', 'debug') == 'True'\n    True\n\n    >>> cecp.items('config/sys: Stuff')\n    [(u'debug', u'True')]\n    \"\"\"\n    NOT_UTF8 = '%C0'  # This byte is never valid at the start of an utf-8\n                      # string, so we use it to mark binary data.\n    SAFE = '!?: /#@<>[]()=-'\n\n    def set(self, section, key, value, comment):\n        key = unicode(key).encode('utf-8')\n        section = unicode(section).encode('utf-8')\n\n        if isinstance(value, unicode):\n            value = quote(value.encode('utf-8'), safe=self.SAFE)\n        elif isinstance(value, str):\n            quoted = quote(value, safe=self.SAFE)\n            if quoted != value:\n                value = self.NOT_UTF8 + quoted\n        else:\n            value = quote(unicode(value).encode('utf-8'), safe=self.SAFE)\n\n        if value.endswith(' '):\n            value = value[:-1] + '%20'\n        if comment:\n            pad = ' ' * (25 - len(key) - len(value)) + ' ; '\n            value = '%s%s%s' % (value, pad, comment)\n        return ConfigParser.RawConfigParser.set(self, section, key, value)\n\n    def _decode_value(self, value):\n        if value.startswith(self.NOT_UTF8):\n            return unquote(value[len(self.NOT_UTF8):])\n        else:\n            return unquote(value).decode('utf-8')\n\n    def get(self, section, key):\n        key = unicode(key).encode('utf-8')\n        section = unicode(section).encode('utf-8')\n        value = ConfigParser.RawConfigParser.get(self, section, key)\n        return self._decode_value(value)\n\n    def items(self, section):\n        return [(k.decode('utf-8'), self._decode_value(i)) for k, i\n                in ConfigParser.RawConfigParser.items(self, section)]\n\n\ndef _MakeCheck(pcls, name, comment, rules, write_watcher):\n    class Checker(pcls):\n        _NAME = name\n        _RULES = rules\n        _COMMENT = comment\n        _WWATCHER = write_watcher\n    return Checker\n\n\ndef RuledContainer(pcls):\n    \"\"\"\n    Factory for abstract 'container with rules' class. See ConfigDict for\n    details, examples and tests.\n    \"\"\"\n\n    class _RuledContainer(pcls):\n        RULE_COMMENT = 0\n        RULE_CHECKER = 1\n        # Reserved ...\n        RULE_DEFAULT = -1\n        RULE_CHECK_MAP = {\n            bool: validators.BoolCheck,\n            'bin': validators.NotUnicode,\n            'bool': validators.BoolCheck,\n            'b36': validators.B36Check,\n            'dir': validators.DirCheck,\n            'directory': validators.DirCheck,\n            'ignore': validators.IgnoreCheck,\n            'email': validators.EmailCheck,\n            'False': False, 'false': False,\n            'file': validators.FileCheck,\n            'float': float,\n            'gpgkeyid': validators.GPGKeyCheck,\n            'hostname': validators.HostNameCheck,\n            'int': int,\n            'long': long,\n            'multiline': unicode,\n            'new file': validators.NewPathCheck,\n            'new dir': validators.NewPathCheck,\n            'new directory': validators.NewPathCheck,\n            'path': validators.PathCheck,\n            str: unicode,\n            'slashslug': validators.SlashSlugCheck,\n            'slug': validators.SlugCheck,\n            'str': unicode,\n            'True': True, 'true': True,\n            'timestamp': long,\n            'unicode': unicode,\n            'url': validators.UrlCheck, # FIXME: check more than the scheme?\n            'webroot': validators.WebRootCheck\n        }\n        def _default_write_watcher(self, *args):\n            self._changed = True\n\n        _NAME = 'container'\n        _RULES = None\n        _COMMENT = None\n        _MAGIC = True\n        _WWATCHER = _default_write_watcher\n\n        def __init__(self, *args, **kwargs):\n            rules = kwargs.get('_rules', self._RULES or {})\n            self._name = kwargs.get('_name', self._NAME)\n            self._comment = kwargs.get('_comment', self._COMMENT)\n            self._write_watcher = kwargs.get('_write_watcher', self._WWATCHER)\n            enable_magic = kwargs.get('_magic', self._MAGIC)\n            for kw in ('_rules', '_comment', '_name', '_magic', '_write_watcher'):\n                if kw in kwargs:\n                    del kwargs[kw]\n\n            pcls.__init__(self)\n            self._key = self._name\n            self._rules_source = rules\n            self._changed = False\n            self.rules = {}\n            self.set_rules(rules)\n            self.update(*args, **kwargs)\n\n            self._magic = enable_magic  # Enable the getitem/getattr magic\n\n        def __str__(self):\n            return json.dumps(self, sort_keys=True, indent=2)\n\n        def __unicode__(self):\n            return json.dumps(self, sort_keys=True, indent=2)\n\n        def as_config_bytes(self, _type=None, _xtype=None):\n            of = io.BytesIO()\n            self.as_config(_type=_type, _xtype=_xtype).write(of)\n            return of.getvalue()\n\n        def key_types(self, key):\n            if key not in self.rules:\n                key = '_any'\n            if key in self.rules and hasattr(self.rules[key], '_types'):\n                return self.rules[key]._types\n            else:\n                return []\n\n        def as_config(self, config=None, _type=None, _xtype=None):\n            config = config or CommentedEscapedConfigParser()\n            section = self._name\n            if self._comment:\n                section += ': %s' % self._comment\n            added_section = False\n\n            keys = self.rules.keys()\n            if _type:\n                keys = [k for k in keys if _type in self.key_types(k)]\n\n            ignore = self.ignored_keys() | set(['_any'])\n            if not _type:\n                if not keys or '_any' in keys:\n                    keys.extend(self.keys())\n\n            keys = [k for k in sorted(set(keys)) if k not in ignore]\n            set_keys = set(self.keys())\n\n            for key in keys:\n                if not hasattr(self[key], 'as_config'):\n                    if key in self.rules:\n                        comment = self.rules[key][self.RULE_COMMENT]\n                    else:\n                        comment = ''\n                    value = self[key]\n                    if value is not None and value != '':\n                        if key not in set_keys:\n                            key = ';' + key\n                            comment = '(default) ' + comment\n                        if not added_section:\n                            config.add_section(str(section))\n                            added_section = True\n                        if _xtype not in self.key_types(key) or not _xtype:\n                            config.set(section, key, value, comment)\n            for key in keys:\n                if hasattr(self[key], 'as_config'):\n                    if isinstance(self[key], list):\n                        # If a list is marked public, we export all items\n                        self[key].as_config(config=config)\n                    else:\n                        self[key].as_config(\n                            config=config, _type=_type, _xtype=_xtype)\n\n            return config\n\n        def reset(self, rules=True, data=True):\n            raise Exception(_('Please override this method'))\n\n        def set_rules(self, rules):\n            safe_assert(isinstance(rules, dict))\n            self.reset()\n            for key, rule in rules.iteritems():\n                self.add_rule(key, rule)\n\n        def add_rule(self, key, rule):\n            if not ((isinstance(rule, (list, tuple))) and\n                    (key == CleanText(key, banned=CleanText.NONVARS).clean) and\n                    (not self.real_hasattr(key))):\n                raise TypeError('add_rule(%s, %s): Bad key or rule.'\n                                % (key, rule))\n\n            orule, rule = rule, ConfigRule(*rule[:])\n            if hasattr(orule, '_types'):\n                rule._types = orule._types\n\n            self.rules[key] = rule\n            check = rule[self.RULE_CHECKER]\n            try:\n                check = self.RULE_CHECK_MAP.get(check, check)\n                rule[self.RULE_CHECKER] = check\n            except TypeError:\n                pass\n\n            name = '%s/%s' % (self._name, key)\n            comment = rule[self.RULE_COMMENT]\n            value = rule[self.RULE_DEFAULT]\n            ww = self.real_getattr('_write_watcher')\n\n            if (isinstance(check, dict) and value is not None\n                    and not isinstance(value, (dict, list))):\n                raise TypeError(_('Only lists or dictionaries can contain '\n                                  'dictionary values (key %s).') % name)\n\n            if isinstance(value, dict) and check is False:\n                pcls.__setitem__(self, key, ConfigDict(_name=name,\n                                                       _comment=comment,\n                                                       _write_watcher=ww,\n                                                       _rules=value))\n\n            elif isinstance(value, dict):\n                if value:\n                    raise ConfigValueError(_('Subsections must be immutable '\n                                             '(key %s).') % name)\n                sub_rule = {'_any': [rule[self.RULE_COMMENT], check, None]}\n                checker = _MakeCheck(ConfigDict, name, check, sub_rule, ww)\n                pcls.__setitem__(self, key, checker())\n                rule[self.RULE_CHECKER] = checker\n\n            elif isinstance(value, list):\n                if value:\n                    raise ConfigValueError(_('Lists cannot have default '\n                                             'values (key %s).') % name)\n                sub_rule = {'_any': [rule[self.RULE_COMMENT], check, None]}\n                checker = _MakeCheck(ConfigList, name, comment, sub_rule, ww)\n                pcls.__setitem__(self, key, checker())\n                rule[self.RULE_CHECKER] = checker\n\n            elif not isinstance(value, (type(None), int, long, bool,\n                                        float, str, unicode)):\n                raise TypeError(_('Invalid type \"%s\" for key \"%s\" (value: %s)'\n                                  ) % (type(value), name, repr(value)))\n\n        def __fixkey__(self, key):\n            return key.lower()\n\n        def fmt_key(self, key):\n            return key.lower()\n\n        def get_rule(self, key):\n            key = self.__fixkey__(key)\n            rule = self.rules.get(key, None)\n            if rule is None:\n                if '_any' in self.rules:\n                    rule = self.rules['_any']\n                else:\n                    raise InvalidKeyError(_('Invalid key for %s: %s'\n                                            ) % (self._name, key))\n            if isinstance(rule[self.RULE_CHECKER], dict):\n                rule = rule[:]\n                rule[self.RULE_CHECKER] = _MakeCheck(\n                    ConfigDict,\n                    '%s/%s' % (self._name, key),\n                    rule[self.RULE_COMMENT],\n                    rule[self.RULE_CHECKER],\n                    self._write_watcher)\n            return rule\n\n        def ignored_keys(self):\n            return set([k for k in self.rules\n                if self.rules[k][self.RULE_CHECKER] == validators.IgnoreCheck])\n\n        def walk(self, path, parent=0, key_types=None):\n            if '.' in path:\n                sep = '.'\n            else:\n                sep = '/'\n            path_parts = path.split(sep)\n            cfg = self\n            if parent:\n                vlist = path_parts[-parent:]\n                path_parts[-parent:] = []\n            else:\n                vlist = []\n            for part in path_parts:\n                if key_types is not None:\n                    if [t for t in cfg.key_types(part) if t not in key_types]:\n                        raise AccessError(_('Access denied to %s') % part)\n                cfg = cfg[part]\n            if parent:\n                return tuple([cfg] + vlist)\n            else:\n                return cfg\n\n        def get(self, key, default=None):\n            key = self.__fixkey__(key)\n            if key in self:\n                return pcls.__getitem__(self, key)\n            if default is None and key in self.rules:\n                return self.rules[key][self.RULE_DEFAULT]\n            return default\n\n        def __getitem__(self, key):\n            key = self.__fixkey__(key)\n            if key in self.rules or '_any' in self.rules:\n                return self.get(key)\n            return pcls.__getitem__(self, key)\n\n        def real_getattr(self, attr):\n            try:\n                return pcls.__getattribute__(self, attr)\n            except AttributeError:\n                return False\n\n        def real_hasattr(self, attr):\n            try:\n                pcls.__getattribute__(self, attr)\n                return True\n            except AttributeError:\n                return False\n\n        def real_setattr(self, attr, value):\n            return pcls.__setattr__(self, attr, value)\n\n        def __getattr__(self, attr, default=None):\n            if self.real_hasattr(attr) or not self.real_getattr('_magic'):\n                return pcls.__getattribute__(self, attr)\n            return self[attr]\n\n        def __setattr__(self, attr, value):\n            if self.real_hasattr(attr) or not self.real_getattr('_magic'):\n                return self.real_setattr(attr, value)\n            self.__setitem__(attr, value)\n\n        def __passkey__(self, key, value):\n            if hasattr(value, '__passkey__'):\n                value._key = key\n                value._name = '%s/%s' % (self._name, key)\n\n        def __passkey_recurse__(self, key, value):\n            if hasattr(value, '__passkey__'):\n                if isinstance(value, (list, tuple)):\n                    for k in range(0, len(value)):\n                        value.__passkey__(value.__fixkey__(k), value[k])\n                elif isinstance(value, dict):\n                    for k in value:\n                        value.__passkey__(value.__fixkey__(k), value[k])\n\n        def __createkey_and_setitem__(self, key, value):\n            pcls.__setitem__(self, key, value)\n\n        def __setitem__(self, key, value):\n            key = self.__fixkey__(key)\n            checker = self.get_rule(key)[self.RULE_CHECKER]\n            if not checker is True:\n                if checker is False:\n                    if isinstance(value, dict) and isinstance(self[key], dict):\n                        for k, v in value.iteritems():\n                            self[key][k] = v\n                        return\n                    raise ConfigValueError(_('Modifying %s/%s is not '\n                                             'allowed') % (self._name, key))\n                elif isinstance(checker, (list, set, tuple)):\n                    if value not in checker:\n                        raise ConfigValueError(_('Invalid value for %s/%s: %s'\n                                                 ) % (self._name, key, value))\n                elif isinstance(checker, (type, type(RuledContainer))):\n                    try:\n                        if value is None:\n                            value = checker()\n                        else:\n                            value = checker(value)\n                    except (ConfigValueError):\n                        raise\n                    except (validators.IgnoreValue):\n                        return\n                    except (ValueError, TypeError):\n                        raise ValueError(_('Invalid value for %s/%s: %s'\n                                           ) % (self._name, key, value))\n                else:\n                    raise Exception(_('Unknown constraint for %s/%s: %s'\n                                      ) % (self._name, key, checker))\n\n            write_watcher = self.real_getattr('_write_watcher')\n            if write_watcher is not None:\n                write_watcher(self, key, value)\n\n            self.__passkey__(key, value)\n            self.__createkey_and_setitem__(key, value)\n            self.__passkey_recurse__(key, value)\n\n        def extend(self, src):\n            for val in src:\n                self.append(val)\n\n        def __iadd__(self, src):\n            self.extend(src)\n            return self\n\n    return _RuledContainer\n\n\nclass ConfigList(RuledContainer(list)):\n    \"\"\"\n    A sanity-checking, self-documenting list of program settings.\n\n    Instances of this class are usually contained within a ConfigDict.\n\n    >>> lst = ConfigList(_rules={'_any': ['We only like ints', int, 0]})\n    >>> lst.append('1')\n    '0'\n    >>> lst.extend([2, '3'])\n    >>> lst\n    [1, 2, 3]\n\n    >>> lst += ['1', '2']\n    >>> lst\n    [1, 2, 3, 1, 2]\n\n    >>> lst.extend(range(0, 100))\n    >>> lst['c'] == lst[int('c', 36)]\n    True\n    \"\"\"\n    def reset(self, rules=True, data=True):\n        if rules:\n            self.rules = {}\n        if data:\n            self[:] = []\n\n    def __createkey_and_setitem__(self, key, value):\n        while key > len(self):\n            self.append(self.rules['_any'][self.RULE_DEFAULT])\n        if key == len(self):\n            self.append(value)\n        else:\n            list.__setitem__(self, key, value)\n\n    def append(self, value):\n        list.append(self, None)\n        try:\n            self[len(self) - 1] = value\n            return b36(len(self) - 1).lower()\n        except:\n            self[len(self) - 1:] = []\n            raise\n\n    def __passkey__(self, key, value):\n        if hasattr(value, '__passkey__'):\n            key = b36(key).lower()\n            value._key = key\n            value._name = '%s/%s' % (self._name, key)\n\n    def __fixkey__(self, key):\n        if isinstance(key, (str, unicode)):\n            try:\n                key = int(key, 36)\n            except ValueError:\n                pass\n        return key\n\n    def get(self, key, default=None):\n        try:\n            return list.__getitem__(self, self.__fixkey__(key))\n        except IndexError:\n            return default\n\n    def __getitem__(self, key):\n        return list.__getitem__(self, self.__fixkey__(key))\n\n    def fmt_key(self, key):\n        f = b36(self.__fixkey__(key)).lower()\n        return ('0000' + f)[-4:] if (len(f) < 4) else f\n\n    def iterkeys(self):\n        return (self.fmt_key(i) for i in range(0, len(self)))\n\n    def iteritems(self):\n        for k in self.iterkeys():\n            yield (k, self[k])\n\n    def keys(self):\n        return list(self.iterkeys())\n\n    def all_keys(self):\n        return list(self.iterkeys())\n\n    def values(self):\n        return self[:]\n\n    def update(self, *args):\n        for l in args:\n            l = list(l)\n            for i in range(0, len(self)):\n                self[i] = l[i]\n            for i in range(len(self), len(l)):\n                self.append(l[i])\n\n\nclass ConfigDict(RuledContainer(dict)):\n    \"\"\"\n    A sanity-checking, self-documenting dictionary of program settings.\n\n    The object must be initialized with a dictionary which describes in\n    a structured way what variables exist, what their legal values are,\n    and what their defaults are and what they are for.\n\n    Each variable definition expects three values:\n       1. A human readable description of what the variable is\n       2. A data type / sanity check\n       3. A default value\n\n    If the sanity check is itself a dictionary of rules, values are expected\n    to be dictionaries or lists of items that match the rules defined. This\n    should be used with an empty list or dictionary as a default value.\n\n    Configuration data can be nested by including a dictionary of further\n    rules in place of the default value.\n\n    If the default value is an empty list, it is assumed to be a list of\n    values of the type specified.\n\n    Examples:\n\n    >>> pot = ConfigDict(_rules={'potatoes': ['How many potatoes?', 'int', 0],\n    ...                          'carrots': ['How many carrots?', int, 99],\n    ...                          'liquids': ['Fluids we like', False, {\n    ...                                         'water': ['Liters', int, 0],\n    ...                                         'vodka': ['Liters', int, 12]\n    ...                                      }],\n    ...                          'tags': ['Tags', {'c': ['C', int, 0],\n    ...                                            'x': ['X', str, '']}, []],\n    ...                          'colors': ['Colors', ('red', 'blue'), []]})\n    >>> sorted(pot.keys()), sorted(pot.values())\n    (['colors', 'liquids', 'tags'], [[], [], {}])\n\n    >>> pot['potatoes'] = pot['liquids']['vodka'] = \"123\"\n    >>> pot['potatoes']\n    123\n    >>> pot['liquids']['vodka']\n    123\n    >>> pot['carrots']\n    99\n\n    >>> pot.walk('liquids.vodka')\n    123\n    >>> pot.walk('liquids/vodka', parent=True)\n    ({...}, 'vodka')\n\n    >>> pot['colors'].append('red')\n    '0'\n    >>> pot['colors'].extend(['blue', 'red', 'red'])\n    >>> pot['colors']\n    ['red', 'blue', 'red', 'red']\n\n    >>> pot['tags'].append({'c': '123', 'x': 'woots'})\n    '0'\n    >>> pot['tags'][0]['c']\n    123\n    >>> pot['tags'].append({'z': 'invalid'})\n    Traceback (most recent call last):\n        ...\n    ValueError: Invalid value for config/tags/1: ...\n\n    >>> pot['evil'] = 123\n    Traceback (most recent call last):\n        ...\n    InvalidKeyError: Invalid key for config: evil\n    >>> pot['liquids']['evil'] = 123\n    Traceback (most recent call last):\n        ...\n    InvalidKeyError: Invalid key for config/liquids: evil\n    >>> pot['potatoes'] = \"moo\"\n    Traceback (most recent call last):\n        ...\n    ValueError: Invalid value for config/potatoes: moo\n    >>> pot['colors'].append('green')\n    Traceback (most recent call last):\n        ...\n    ConfigValueError: Invalid value for config/colors/4: green\n\n    >>> pot.rules['potatoes']\n    ['How many potatoes?', <type 'int'>, 0]\n\n    >>> isinstance(pot['liquids'], ConfigDict)\n    True\n    \"\"\"\n    _NAME = 'config'\n\n    def reset(self, rules=True, data=True):\n        if rules:\n            self.rules = {}\n        if data:\n            for key in self.keys():\n                if hasattr(self[key], 'reset'):\n                    self[key].reset(rules=rules, data=data)\n                else:\n                    dict.__delitem__(self, key)\n\n    def all_keys(self):\n        return list(set(self.keys()) | set(self.rules.keys())\n                    - self.ignored_keys() - set(['_any']))\n\n    def append(self, value):\n        \"\"\"Add to the dict using an autoselected key\"\"\"\n        if '_any' in self.rules:\n            k = b36(max([int(k, 36) for k in self.keys()] + [-1]) + 1).lower()\n            self[k] = value\n            return k\n        else:\n            raise UsageError(_('Cannot append to fixed dict'))\n\n    def update(self, *args, **kwargs):\n        \"\"\"Reimplement update, so it goes through our sanity checks.\"\"\"\n        for src in args:\n            if hasattr(src, 'keys'):\n                for key in src:\n                    self[key] = src[key]\n            else:\n                for key, val in src:\n                    self[key] = val\n        for key in kwargs:\n            self[key] = kwargs[key]\n\n    def parse_config(self, session, data, source='internal'):\n        \"\"\"\n        Parse a config file fragment. Invalid data will be ignored, but will\n        generate warnings in the session UI. Returns True on a clean parse,\n        False if any of the settings were bogus.\n\n        >>> cfg.parse_config(session, '[config/sys]\\\\nfd_cache_size = 123\\\\n')\n        True\n        >>> cfg.sys.fd_cache_size\n        123\n\n        >>> cfg.parse_config(session, '[config/bogus]\\\\nblabla = bla\\\\n')\n        False\n        >>> [l[1] for l in session.ui.log_buffer if 'bogus' in l[1]][0]\n        'Invalid (internal): section config/bogus does not exist'\n\n        >>> cfg.parse_config(session, '[config/sys]\\\\nhistory_length = 321\\\\n'\n        ...                                          'bogus_variable = 456\\\\n')\n        False\n        >>> cfg.sys.history_length\n        321\n        >>> [l[1] for l in session.ui.log_buffer if 'bogus_var' in l[1]][0]\n        u'Invalid (internal): section config/sys, ...\n        \"\"\"\n        parser = CommentedEscapedConfigParser()\n        parser.readfp(io.BytesIO(str(data)))\n\n        def item_sorter(i):\n            try:\n                return (int(i[0], 36), i[1])\n            except (ValueError, IndexError, KeyError, TypeError):\n                return i\n\n        all_okay = True\n        for section in parser.sections():\n            okay = True\n            cfgpath = section.split(':')[0].split('/')[1:]\n            cfg = self\n            added_parts = []\n            for part in cfgpath:\n                if cfg.fmt_key(part) in cfg.keys():\n                    cfg = cfg[part]\n                elif '_any' in cfg.rules:\n                    cfg[part] = {}\n                    cfg = cfg[part]\n                else:\n                    if session:\n                        msg = _('Invalid (%s): section %s does not '\n                                'exist') % (source, section)\n                        session.ui.warning(msg)\n                    all_okay = okay = False\n            items = parser.items(section) if okay else []\n            items.sort(key=item_sorter)\n            for var, val in items:\n                try:\n                    cfg[var] = val\n                except (ValueError, KeyError, IndexError):\n                    if session:\n                        msg = _(u'Invalid (%s): section %s, variable %s=%s'\n                                ) % (source, section, var, val)\n                        session.ui.warning(msg)\n                    all_okay = okay = False\n        return all_okay\n\n\nclass PathDict(ConfigDict):\n    _RULES = {\n        '_any': ['Data directory', 'directory', '']\n    }\n\n\nif __name__ == \"__main__\":\n    import doctest\n    import sys\n    import copy\n\n    import mailpile.config.defaults\n    import mailpile.ui\n\n    rules = copy.deepcopy(mailpile.config.defaults.CONFIG_RULES)\n    rules.update({\n        'nest1': ['Nest1', {\n            'nest2': ['Nest2', str, []],\n            'nest3': ['Nest3', {\n                'nest4': ['Nest4', str, []]\n            }, []],\n        }, {}]\n    })\n    cfg = ConfigDict(_rules=rules)\n    session = mailpile.ui.Session(cfg)\n    session.ui = mailpile.ui.SilentInteraction(cfg)\n    session.ui.block()\n\n    result = doctest.testmod(optionflags=doctest.ELLIPSIS)\n    print('%s' % (result, ))\n    if result.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/config/defaults.py",
    "content": "from __future__ import print_function\nAPPVER = \"1.0.0rc6\"\nABOUT = \"\"\"\\\nMailpile.py              a tool             Copyright 2013-2018, Mailpile ehf\n v%8.0008s         for searching and               <https://www.mailpile.is/>\n               organizing piles of e-mail\n\nThis program is free software: you can redistribute it and/or modify it under\nthe terms of either the GNU Affero General Public License as published by the\nFree Software Foundation. See the file COPYING.md for details.\n\"\"\" % APPVER\n#############################################################################\nimport os\nimport sys\nimport time\n\nfrom mailpile.config.base import PathDict\nfrom mailpile.config.base import ConfigRule as c\nfrom mailpile.config.base import CriticalConfigRule as X\nfrom mailpile.config.base import PublicConfigRule as p\nfrom mailpile.config.base import KeyConfigRule as k\n\n\n_ = lambda string: string\n\n\nDEFAULT_SENDMAIL = '|/usr/sbin/sendmail -i %(rcpt)s'\nCONFIG_PLUGINS = []\nCONFIG_RULES = {\n    'version': p(_('Mailpile program version'), str, APPVER),\n    'homedir': p(_('Location of Mailpile data'), False, '(unset)'),\n    'timestamp': [_('Configuration timestamp'), int, int(time.time())],\n    'master_key': k(_('Master symmetric encryption key'), str, ''),\n    'sys': p(_('Technical system settings'), False, {\n        'fd_cache_size': p(_('Max files kept open at once'), int,         500),\n        'minfree_mb':    p(_('Required free disk space (MB)'), int,      1024),\n        'history_length': (_('History length (lines, <0=no save)'), int,  100),\n        'http_host':     p(_('Listening host for web UI'),\n                           'hostname', 'localhost'),\n        'http_port':     p(_('Listening port for web UI'), int,         33411),\n        'http_path':     p(_('HTTP path of web UI'), 'webroot',            ''),\n        'http_no_auth':  X(_('Disable HTTP authentication'),      bool, False),\n        'ajax_timeout':   (_('AJAX Request timeout'), int,              10000),\n        'postinglist_kb': (_('Posting list target size in KB'), int,       64),\n        'sort_max':       (_('Max results we sort \"well\"'), int,         2500),\n        'snippet_max':    (_('Max length of metadata snippets'), int,     275),\n        'debug':         p(_('Debugging flags'), str,                      ''),\n        'experiments':    (_('Enabled experiments'), str,                  ''),\n        'gpg_keyserver':  (_('Host:port of PGP keyserver'),\n                           str, 'pool.sks-keyservers.net'),\n        'gpg_home':      p(_('Override the home directory of GnuPG'),\n                           'dir', None),\n        'gpg_binary':    p(_('Override the default GPG binary path'),\n                           'file', None),\n        'local_mailbox_id': (_('Local read/write Maildir'), 'b36',         ''),\n        'mailindex_file':   (_('Metadata index file'), 'file',             ''),\n        'postinglist_dir': (_('Search index directory'), 'dir',            ''),\n        'mailbox':        [_('Mailboxes we index'), 'bin',                 []],\n        'plugins_early': p(_('Plugins to load before login'),\n                           CONFIG_PLUGINS, []),\n        'plugins':        [_('Plugins to load after login'),\n                           CONFIG_PLUGINS, []],\n        'path':           [_('Locations of assorted data'), False, {\n            'html_theme': [_('User interface theme'), 'dir', 'default-theme'],\n            'vcards':     [_('Location of vCards'), 'dir', 'vcards'],\n            'event_log':  [_('Location of event log'), 'dir', 'logs'],\n        }],\n        'lockdown':      p(_('Demo mode, disallow changes'), str,          ''),\n        'login_banner':  p(_('A custom banner for the login page'), str,   ''),\n        'proxy':         p(_('Proxy settings'), False, {\n            'protocol':  p(_('Proxy protocol'),\n                           [\"tor\", \"tor-risky\", \"socks5\", \"socks4\", \"http\",\n                            \"none\", \"system\", \"unknown\"], \"system\"),\n            'fallback':  p(_('Allow fallback to direct conns'), bool, False),\n            'username':   (_('User name'), str, ''),\n            'password':   (_('Password'), str, ''),\n            'host':      p(_('Host'), str, ''),\n            'port':      p(_('Port'), int, 8080),\n            'no_proxy':  p(_('List of hosts to avoid proxying'), str,\n                           'localhost, 127.0.0.1, ::1')\n        }),\n        'tor': p(_('Tor settings'), False, {\n            'binary':    p(_('Override the default Tor binary path'),\n                           'file', None),\n            'systemwide':p(_('Use shared system-wide Tor (not our own)'),\n                                                                   bool, True),\n            'socks_host':p(_('Socks host'), str, ''),\n            'socks_port':p(_('Socks Port'), int, 0),\n            'ctrl_port': p(_('Control Port'), int, 0),\n            'ctrl_auth': p(_('Control Password'), str, '')\n        })\n    }),\n    'prefs': p(_(\"User preferences\"), False, {\n        'num_results':     (_('Search results per page'), int,             20),\n        'rescan_interval': (_('Misc. data refresh frequency'), int,       900),\n        'open_in_browser': p(_('Open in browser on startup'), bool,      True),\n        'auto_mark_as_read':(_('Automatically mark e-mail as read'),\n                                                                   bool, True),\n        'web_content':     (_('Download content from the web'),\n                            [\"off\", \"anon\", \"on\"],                  \"unknown\"),\n        'html5_sandbox':   (_('Use HTML5 sandboxes'), bool,              True),\n        'attachment_urls': (_('URLs to treat as attachments (regex)'), str, []),\n        'weak_crypto_max_age': (\n               _('Accept weak crypto in messages older than this (unix time)'),\n                                                                  int,      0),\n        'encrypted_block_html': (_('Never display HTML from encrypted mail'),\n                                                                   bool, True),\n        'encrypted_block_web': (_('Never fetch web content from encrypted mail'),\n                                                                   bool, True),\n        'gpg_use_agent':   (_('Use the local GnuPG agent'), bool,       False),\n        'gpg_clearsign':  X(_('Inline PGP signatures or attached'),\n                            bool, False),\n        'gpg_recipient':   (_('Encrypt local data to ...'), 'gpgkeyid',    ''),\n        'gpg_email_key':   (_('Enable e-mail based public key distribution'),\n                            bool, True),\n        'gpg_html_wrap':   (_('Wrap keys and signatures in helpful HTML'),\n                            bool, True),\n        'antiphishing':    (_(\"Enable experimental anti-phishing heuristics \"),\n                                                                  bool, False),\n        'key_tofu':        (_(\"Key Import Behaviour\"), False, {\n            'autocrypt':   (_('Auto-import keys using Autocrypt state machine'),\n                            bool, True),\n            'historic':    (_('Auto-import keys using communication history'),\n                            bool, True),\n            'hist_min':    (_('Require this many signed- or encrypted e-mails'),\n                            int, 3),\n            'hist_recent': (_('Consider the most recent N e-mails (per sender)'),\n                            int, 6),\n            'hist_origins':(_('Origins to auto-import keys from (historic)'),\n                            str, 'e-mail, wkd, koo'),\n            'min_interval': (_('Interval between TOFU checks (per sender)'),\n                            int, 1800),\n        }),\n        'key_trust':       (_(\"Key Trust Model\"), False, {\n            'threshold':    (_('Minimum number of signatures required'),\n                             int, 5),\n            'window_days':  (_('Window of time (days) to evaluate trust'),\n                             int, 180),\n            'sig_warn_pct': (_('Signed ratio (%) above which we expect sigs'),\n                             int, 80),\n            'key_trust_pct':(_('Ratio of key use (%) above which we trust key'),\n                             int, 90),\n            'key_new_pct':  (_('Consider key new below this ratio (%) of sigs'),\n                             int, 10)\n        }),\n        'openpgp_header': X(_('Advertise PGP preferences in a header?'),\n                            ['', 'sign', 'encrypt', 'signencrypt'],\n                            'signencrypt'),\n        'crypto_policy':  X(_('Default encryption policy for outgoing mail'),\n                            str, 'none'),\n        'inline_pgp':      (_('Use inline PGP when possible'), bool,     True),\n        'encrypt_subject': (_('Encrypt subjects by default'), bool,      True),\n        'default_order':   (_('Default sort order'), str,          'rev-date'),\n        'obfuscate_index':X(_('Key to use to scramble the index'), str,    ''),\n        'index_encrypted':X(_('Make encrypted content searchable'),\n                            bool, False),\n        'encrypt_mail':   X(_('Encrypt locally stored mail'), bool,     False),\n        'encrypt_index':  X(_('Encrypt the local search index'), bool,  False),\n        'encrypt_vcards': X(_('Encrypt the contact database'), bool,     True),\n        'encrypt_events': X(_('Encrypt the event log'), bool,            True),\n        'encrypt_misc':   X(_('Encrypt misc. local data'), bool,         True),\n        'allow_deletion': X(_('Allow permanent deletion of e-mails'),\n                                                                  bool, False),\n        'deletion_ratio': X(_('Max fraction of source mail to delete per pass'),\n                                                                 float,  0.75),\n# FIXME:\n#       'backup_to_web':  X(_('Backup settings and keys to mobile web app'),\n#                                                                  bool, True),\n#       'backup_to_email':X(_('Backup settings and keys to e-mail'),  str, ''),\n        'rescan_command':  (_('Command run before rescanning'), str,       ''),\n        'default_email':   (_('Default outgoing e-mail address'), 'email', ''),\n        'default_route':   (_('Default outgoing mail route'), str, ''),\n        'line_length':     (_('Target line length, <40 disables reflow'),\n                            int, 65),\n        'always_bcc_self': (_('Always BCC self on outgoing mail'), bool, True),\n        'default_messageroute': (_('Default outgoing mail route'), str,    ''),\n        'language':       p(_('User interface language'), str,             ''),\n        'vcard':           [_(\"vCard import/export settings\"), False, {\n            'importers':   [_(\"vCard import settings\"), False,             {}],\n            'exporters':   [_(\"vCard export settings\"), False,             {}],\n            'context':     [_(\"vCard context helper settings\"), False,     {}],\n        }],\n        'friendly_pipes':  (_(\"Enable sh-like pipes in the CLI\"), bool,  True),\n    }),\n    'web': (_(\"Web Interface Preferences\"), False, {\n        'keybindings':     (_('Enable keyboard short-cuts'), bool, False),\n        'developer_mode':  (_('Enable developer-only features'), bool, False),\n        'friendly_dates':  (_('UI uses \"friendly\" date/times'), bool,    True),\n        'setup_complete':  (_('User completed setup experience'), bool, False),\n        'display_density': (_('Display density of interface'), str, 'comfy'),\n        'quoted_reply':    (_('Quote replies to messages'), str, 'unset'),\n        'nag_backup_key':   (_('Nag user to backup their key'), int, 0),\n        'subtags_collapsed': (_('Collapsed subtags in sidebar'), str, []),\n        'donate_visibility': (_('Display donate link in topbar?'), bool, True),\n        'email_html_hint':   (_('Display HTML hints?'), bool, True),\n        'email_crypto_hint': (_('Display crypto hints?'), bool, True),\n        'email_reply_hint':  (_('Display reply hints?'), bool, True),\n        'email_tag_hint':    (_('Display tagging hints?'), bool, True),\n        'release_notes':     (_('Display release notes?'), bool, True)\n    }),\n    'logins': [_('Credentials allowed to access Mailpile'), {\n        'password':        (_('Salted and hashed password'), str, '')\n    }, {}],\n    'secrets': [_('Secrets the user wants saved'), {\n        'password':        (_('A secret'), str, ''),\n        'policy':          (_('Security policy'),\n                            [\"store\", \"cache-only\", \"fail\", \"protect\"],\n                            'store')\n    }, {}],\n    'tls': [_('Settings for TLS certificate validation'), {\n        'server':          (_('Server hostname:port'), str, ''),\n        'accept_certs':    (_('SHA256 of acceptable certs'), str, []),\n        'use_web_ca':      (_('Use web certificate authorities'), bool, True)\n    }, {}],\n    'routes': [_('Outgoing message routes'), {\n        'name':            (_('Route name'), str, ''),\n        'protocol':        (_('Messaging protocol'),\n                            [\"smtp\", \"smtptls\", \"smtpssl\", \"local\"],\n                            'smtp'),\n        'username':        (_('User name'), str, ''),\n        'password':        (_('Password'), str, ''),\n        'auth_type':       (_('Authentication scheme'), str, 'password-cleartext'),\n        'command':         (_('Shell command'), str, ''),\n        'host':            (_('Host'), str, ''),\n        'port':            (_('Port'), int, 587)\n    }, {}],\n    'sources': [_('Incoming message sources'), {\n        'name':            (_('Source name'), str, ''),\n        'profile':         (_('Profile this source belongs to'), str, ''),\n        'enabled':         (_('Is this mail source enabled?'), bool, True),\n        'protocol':        (_('Mail source protocol'),\n                            [\"local\",\n                             \"imap\", \"imap_ssl\", \"imap_tls\",\n                             \"pop3\", \"pop3_ssl\",\n                             # These are all obsolete, handled as local:\n                             \"mbox\", \"maildir\", \"macmaildir\", \"gmvault\"],\n                            ''),\n        'pre_command':     (_('Shell command run before syncing'), str, ''),\n        'post_command':    (_('Shell command run after syncing'), str, ''),\n        'interval':        (_('How frequently to check for mail'), int, 300),\n        'username':        (_('User name'), str, ''),\n        'password':        (_('Password'), str, ''),\n        'auth_type':       (_('Authentication scheme'), str, 'password'),\n        'host':            (_('Host'), str, ''),\n        'port':            (_('Port'), int, 993),\n        'keepalive':       (_('Keep server connections alive'), bool, False),\n        'discovery':       (_('Mailbox discovery policy'), False, {\n            'paths':       (_('Paths to watch for new mailboxes'), 'bin', []),\n            'policy':      (_('Default mailbox policy'),\n                            ['unknown', 'ignore', 'watch',\n                             'read', 'move', 'sync'], 'unknown'),\n            'local_copy':  (_('Copy mail to a local mailbox?'), bool, False),\n            'parent_tag':  (_('Parent tag for mailbox tags'), str, '!CREATE'),\n            'guess_tags':  (_('Guess which local tags match'), bool, True),\n            'create_tag':  (_('Create a tag for each mailbox?'), bool, True),\n            'visible_tags':(_('Make tags visible by default?'), bool, True),\n            'process_new': (_('Is a potential source of new mail'), bool, True),\n            'apply_tags':  (_('Tags applied to messages'), str, []),\n            'max_mailboxes':(_('Max mailboxes to add'), int, 100),\n        }),\n        'mailbox': (_('Mailboxes'), {\n            'name':        (_('The name of this mailbox'), str, ''),\n            'path':        (_('Mailbox source path'), str, ''),\n            'policy':      (_('Mailbox policy'),\n                            ['unknown', 'ignore', 'read', 'move', 'sync',\n                             'inherit'], 'inherit'),\n            'local':       (_('Local mailbox path'), 'bin', ''),\n            'process_new': (_('Is a source of new mail'), bool, True),\n            'primary_tag': (_('A tag representing this mailbox'), str, ''),\n            'apply_tags':  (_('Tags applied to messages'), str, []),\n        }, {})\n    }, {}]\n}\n\n\nif __name__ == \"__main__\":\n    import mailpile.config.defaults\n    from mailpile.config.base import ConfigDict\n\n    print('%s' % (ConfigDict(_name='mailpile',\n                             _comment='Base configuration',\n                             _rules=mailpile.config.defaults.CONFIG_RULES\n                             ).as_config_bytes(), ))\n"
  },
  {
    "path": "mailpile/config/detect.py",
    "content": "try:\n    import ssl\nexcept ImportError:\n    ssl = None\n\ntry:\n    import sockschain as socks\nexcept ImportError:\n    try:\n        import socks\n    except ImportError:\n        socks = None\n"
  },
  {
    "path": "mailpile/config/manager.py",
    "content": "from __future__ import print_function\nimport copy\nimport cPickle\nimport io\nimport jinja2\nimport json\nimport os\nimport socket\nimport sys\nimport random\nimport re\nimport threading\nimport fasteners\nimport traceback\nimport ConfigParser\nimport errno\n\nfrom urllib import quote, unquote, getproxies\nfrom urlparse import urlparse\n\ntry:\n    from appdirs import AppDirs\nexcept ImportError:\n    AppDirs = None\n\nimport mailpile.platforms\nfrom mailpile.command_cache import CommandCache\nfrom mailpile.crypto.streamer import DecryptingStreamer\nfrom mailpile.crypto.gpgi import GnuPG\nfrom mailpile.eventlog import EventLog, Event, GetThreadEvent\nfrom mailpile.httpd import HttpWorker\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.mailboxes import OpenMailbox, NoSuchMailboxError, wervd\nfrom mailpile.mailutils import FormatMbxId, MBX_ID_LEN\nfrom mailpile.search import MailIndex\nfrom mailpile.search_history import SearchHistory\nfrom mailpile.security import SecurePassphraseStorage\nfrom mailpile.ui import Session, BackgroundInteraction\nfrom mailpile.util import *\nfrom mailpile.vcard import VCardStore\nfrom mailpile.vfs import vfs, FilePath, MailpileVfsRoot\nfrom mailpile.workers import Worker, ImportantWorker, DumbWorker, Cron\nimport mailpile.i18n\nimport mailpile.security\nimport mailpile.util\nimport mailpile.vfs\n\nfrom mailpile.config.base import *\nfrom mailpile.config.paths import DEFAULT_WORKDIR, DEFAULT_SHARED_DATADIR\nfrom mailpile.config.paths import LOCK_PATHS\nfrom mailpile.config.defaults import APPVER\nfrom mailpile.config.detect import socks\nfrom mailpile.www.jinjaloader import MailpileJinjaLoader\n\n\nMAX_CACHED_MBOXES = 5\n\nGLOBAL_INDEX_CHECK = ConfigLock()\nGLOBAL_INDEX_CHECK.acquire()\n\n\nclass ConfigManager(ConfigDict):\n    \"\"\"\n    This class manages the live global mailpile configuration. This includes\n    the settings themselves, as well as global objects like the index and\n    references to any background worker threads.\n    \"\"\"\n    def __init__(self, workdir=None, shareddatadir=None, rules={}):\n        ConfigDict.__init__(self, _rules=rules, _magic=False)\n\n        self.workdir = os.path.abspath(workdir or DEFAULT_WORKDIR())\n        self.gnupghome = None\n        mailpile.vfs.register_alias('/Mailpile', self.workdir)\n\n        self.shareddatadir = os.path.abspath(shareddatadir or\n                                             DEFAULT_SHARED_DATADIR())\n        mailpile.vfs.register_alias('/Share', self.shareddatadir)\n\n        self.vfs_root = MailpileVfsRoot(self)\n        mailpile.vfs.register_handler(0000, self.vfs_root)\n\n        self.conffile = os.path.join(self.workdir, 'mailpile.cfg')\n        self.conf_key = os.path.join(self.workdir, 'mailpile.key')\n        self.conf_pub = os.path.join(self.workdir, 'mailpile.rc')\n\n        # Process lock files are not actually created until the first acquire()\n        self.lock_pubconf, self.lock_workdir = LOCK_PATHS(self.workdir)\n        self.lock_pubconf = fasteners.InterProcessLock(self.lock_pubconf)\n\n        # If the master key changes, we update the file on save, otherwise\n        # the file is untouched. So we keep track of things here.\n        self._master_key = ''\n        self._master_key_ondisk = None\n        self._master_key_passgen = -1\n        self.detected_memory_corruption = False\n\n        # Make sure we have a silent background session\n        self.background = Session(self)\n        self.background.ui = BackgroundInteraction(self)\n\n        self.plugins = None\n        self.tor_worker = None\n        self.http_worker = None\n        self.dumb_worker = DumbWorker('Dumb worker', self.background)\n        self.slow_worker = self.dumb_worker\n        self.scan_worker = self.dumb_worker\n        self.save_worker = self.dumb_worker\n        self.other_workers = []\n        self.mail_sources = {}\n\n        self.event_log = None\n        self.index = None\n        self.index_loading = None\n        self.index_check = GLOBAL_INDEX_CHECK\n        self.vcards = {}\n        self.search_history = SearchHistory()\n        self._mbox_cache = []\n        self._running = {}\n        self._lock = ConfigRLock()\n        self.loaded_config = False\n\n        def cache_debug(msg):\n            if self.background and 'cache' in self.sys.debug:\n                self.background.ui.debug(msg)\n        self.command_cache = CommandCache(debug=cache_debug)\n\n        self.passphrases = {\n            'DEFAULT': SecurePassphraseStorage(),\n        }\n\n        self.jinja_env = jinja2.Environment(\n            loader=MailpileJinjaLoader(self),\n            cache_size=400,\n            autoescape=True,\n            trim_blocks=True,\n            extensions=['jinja2.ext.i18n', 'jinja2.ext.with_',\n                        'jinja2.ext.do', 'jinja2.ext.autoescape',\n                        'mailpile.www.jinjaextensions.MailpileCommand']\n        )\n\n        self.cron_schedule = {}\n        self.cron_worker = Cron(self.cron_schedule, 'Cron worker', self.background)\n        self.cron_worker.daemon = True\n        self.cron_worker.start()\n\n        self._magic = True  # Enable the getattr/getitem magic\n\n    def create_and_lock_workdir(self, session):\n        # Make sure workdir exists and that other processes are not using it.\n        if not os.path.exists(self.workdir):\n            if session:\n                session.ui.notify(_('Creating: %s') % self.workdir)\n            os.makedirs(self.workdir, mode=0o700)\n            mailpile.platforms.RestrictReadAccess(self.workdir)\n\n        # Once acquired, lock_workdir is only released by process termination.\n        if not isinstance(self.lock_workdir, fasteners.InterProcessLock):\n            ipl = fasteners.InterProcessLock(self.lock_workdir)\n            if ipl.acquire(blocking=False):\n                 self.lock_workdir = ipl\n            else:\n                if session:\n                    session.ui.error(_('Another Mailpile or program is'\n                                       ' using the profile directory'))\n                sys.exit(1)\n\n    def load(self, session, *args, **kwargs):\n        from mailpile.plugins.core import Rescan\n\n        # This should happen somewhere, may as well happen here. We don't\n        # rely on Python's random for anything important, but it's still\n        # nice to seed it well.\n        random.seed(os.urandom(8))\n\n        keep_lockdown = self.sys.lockdown\n        with self._lock:\n            rv = self._unlocked_load(session, *args, **kwargs)\n\n        if not kwargs.get('public_only'):\n            # If the app version does not match the config, run setup.\n            if self.version != APPVER:\n                from mailpile.plugins.setup_magic import Setup\n                Setup(session, 'setup').run()\n\n            # Trigger background-loads of everything\n            Rescan(session, 'rescan')._idx(wait=False)\n\n            # Record where our GnuPG keys live\n            self.gnupghome = GnuPG(self).gnupghome()\n\n        if keep_lockdown:\n            self.sys.lockdown = keep_lockdown\n        return rv\n\n    def load_master_key(self, passphrase, _raise=None):\n        keydata = []\n\n        if passphrase.is_set():\n            with open(self.conf_key, 'rb') as fd:\n                hdrs = dict(l.split(': ', 1) for l in fd if ': ' in l)\n                salt = hdrs.get('Salt', '').strip()\n                kdfp = hdrs.get('KDF', '').strip() or None\n\n            if kdfp:\n                try:\n                    kdf, params = kdfp.split(' ', 1)\n                    kdfp = {}\n                    kdfp[kdf] = json.loads(params)\n                except ValueError:\n                    kdfp = {}\n\n            parser = lambda d: keydata.extend(d)\n            for (method, sps) in passphrase.stretches(salt, params=kdfp):\n                try:\n                    with open(self.conf_key, 'rb') as fd:\n                        decrypt_and_parse_lines(fd, parser, self,\n                                                newlines=True,\n                                                gpgi=GnuPG(self),\n                                                passphrase=sps)\n                    break\n                except IOError:\n                    keydata = []\n\n        if keydata:\n            self.passphrases['DEFAULT'].copy(passphrase)\n            self.set_master_key(''.join(keydata))\n            self._master_key_ondisk = self.get_master_key()\n            self._master_key_passgen = self.passphrases['DEFAULT'].generation\n            return True\n        else:\n            if _raise is not None:\n                raise _raise('Failed to decrypt master key')\n            return False\n\n    def _load_config_lines(self, filename, lines):\n        collector = lambda ll: lines.extend(ll)\n        if os.path.exists(filename):\n            with open(filename, 'rb') as fd:\n                decrypt_and_parse_lines(fd, collector, self)\n\n    def _discover_plugins(self):\n        # Discover plugins and update the config rule to match\n        from mailpile.plugins import PluginManager\n        self.plugins = PluginManager(config=self, builtin=True).discover([\n            os.path.join(self.shareddatadir, 'contrib'),\n            os.path.join(self.workdir, 'plugins')\n        ])\n        self.sys.plugins.rules['_any'][\n            self.RULE_CHECKER] = [None] + self.plugins.loadable()\n        self.sys.plugins_early.rules['_any'][\n            self.RULE_CHECKER] = [None] + self.plugins.loadable_early()\n\n    def _configure_default_plugins(self):\n        if (len(self.sys.plugins) == 0) and self.loaded_config:\n            self.sys.plugins.extend(self.plugins.DEFAULT)\n            for plugin in self.plugins.WANTED:\n                if plugin in self.plugins.available():\n                    self.sys.plugins.append(plugin)\n        else:\n            for pos in self.sys.plugins.keys():\n                name = self.sys.plugins[pos]\n                if name in self.plugins.RENAMED:\n                    self.sys.plugins[pos] = self.plugins.RENAMED[name]\n\n    def _unlocked_load(self, session, public_only=False):\n        # This method will attempt to load the full configuration.\n        #\n        # The Mailpile configuration is in two parts:\n        #    - public data in \"mailpile.rc\"\n        #    - private data in \"mailpile.cfg\" (encrypted)\n        #\n        # This method may successfully load and process from the public part,\n        # but fail to load the encrypted part due to a lack of authentication.\n        # In this case IOError will be raised.\n        #\n        if not public_only:\n            self.create_and_lock_workdir(session)\n        if session is None:\n            session = self.background\n        if self.index:\n            self.index_check.acquire()\n            self.index = None\n\n        # Set the homedir default\n        self.rules['homedir'][2] = self.workdir\n        self._rules_source['homedir'][2] = self.workdir\n        self.reset(rules=False, data=True)\n        self.loaded_config = False\n        pub_lines, prv_lines = [], []\n        try:\n            self._load_config_lines(self.conf_pub, pub_lines)\n            if public_only:\n                return\n\n            if os.path.exists(self.conf_key):\n                mailpile.platforms.RestrictReadAccess(self.conf_key)\n                self.load_master_key(self.passphrases['DEFAULT'],\n                                     _raise=IOError)\n            self._load_config_lines(self.conffile, prv_lines)\n        except IOError:\n            self.loaded_config = False\n            raise\n        except (ValueError, OSError):\n            # Bad data in config or config doesn't exist: just forge onwards\n            pass\n        finally:\n            ## The following things happen, no matter how loading went...\n\n            # Discover plugins first, as this affects what is or is not valid\n            # in the configuration file.\n            self._discover_plugins()\n\n            # Parse once (silently), to figure out which plugins to load...\n            self.parse_config(None, '\\n'.join(pub_lines), source=self.conf_pub)\n            self.parse_config(None, '\\n'.join(prv_lines), source=self.conffile)\n\n            # Enable translations!\n            mailpile.i18n.ActivateTranslation(\n                session, self, self.prefs.language)\n\n            # Configure and load plugins as per config requests\n            with mailpile.i18n.i18n_disabled:\n                self._configure_default_plugins()\n                self.load_plugins(session)\n\n            # Now all the plugins are loaded, reset and parse again!\n            self.reset_rules_from_source()\n            self.parse_config(session, '\\n'.join(pub_lines), source=self.conf_pub)\n            self.parse_config(session, '\\n'.join(prv_lines), source=self.conffile)\n            self._changed = False\n\n            # Do this again, so renames and cleanups persist\n            self._configure_default_plugins()\n\n        ## The following events only happen when we've successfully loaded\n        ## both config files!\n\n        # Open event log\n        dec_key_func = lambda: self.get_master_key()\n        enc_key_func = lambda: (self.prefs.encrypt_events and\n                                self.get_master_key())\n        self.event_log = EventLog(self.data_directory('event_log',\n                                                      mode='rw', mkdir=True),\n                                  dec_key_func, enc_key_func\n                                  ).load()\n        if 'log' in self.sys.debug:\n            self.event_log.ui_watch(session.ui)\n        else:\n            self.event_log.ui_unwatch(session.ui)\n\n        # Configure security module\n        mailpile.security.KNOWN_TLS_HOSTS = self.tls\n\n        # Load VCards\n        self.vcards = VCardStore(self, self.data_directory('vcards',\n                                                           mode='rw',\n                                                           mkdir=True))\n\n        # Recreate VFS root in case new things have been found\n        self.vfs_root.rescan()\n\n        # Load Search History\n        self.search_history = SearchHistory.Load(self,\n                                                 merge=self.search_history)\n\n        # OK, we're happy\n        self.loaded_config = True\n\n    def reset_rules_from_source(self):\n        with self._lock:\n            self.set_rules(self._rules_source)\n            self.sys.plugins.rules['_any'][\n                self.RULE_CHECKER] = [None] + self.plugins.loadable()\n            self.sys.plugins_early.rules['_any'][\n                self.RULE_CHECKER] = [None] + self.plugins.loadable_early()\n\n    def load_plugins(self, session):\n        with self._lock:\n            from mailpile.plugins import PluginManager\n            plugin_list = set(PluginManager.REQUIRED +\n                              self.sys.plugins +\n                              self.sys.plugins_early)\n            for plugin in plugin_list:\n                if plugin is not None:\n                    session.ui.mark(_('Loading plugin: %s') % plugin)\n                    self.plugins.load(plugin)\n            session.ui.mark(_('Processing manifests'))\n            self.plugins.process_manifests()\n            self.prepare_workers(session)\n\n    def save(self, *args, **kwargs):\n        with self._lock:\n            self._unlocked_save(*args, **kwargs)\n\n    def get_master_key(self):\n        if not self._master_key:\n            return ''\n\n        k1, k2, k3 = (k[1:] for k in self._master_key)\n        if k1 == k2 == k3:\n            # This is the only result we like!\n            return k1\n        else:\n            # Hard fail into read-only lockdown. The periodic health\n            # check will notify the user we are broken.\n            self.detected_memory_corruption = True\n\n        # Try and recover; best 2 out of 3.\n        if k1 in (k2, k3):\n            return self.set_master_key(k1)\n        if k2 in (k1, k3):\n            return self.set_master_key(k2)\n\n        mailpile.util.QUITTING = True\n        raise IOError(\"Failed to access master_key\")\n\n    def set_master_key(self, key):\n        # Prefix each key with a unique character to prevent optimization\n        self._master_key = [i + key for i in ('1', '2', '3')]\n        return key\n\n    def _delete_old_master_keys(self, keyfile):\n        \"\"\"\n        We keep old master key files around for up to 5 days, so users can\n        revert if they make some sort of horrible mistake. After that we\n        delete the backups because they're technically a security risk.\n        \"\"\"\n        maxage = time.time() - (5 * 24 * 3600)\n        prefix = os.path.basename(keyfile) + '.'\n        dirname = os.path.dirname(keyfile)\n        for f in os.listdir(dirname):\n            fn = os.path.join(dirname, f)\n            if f.startswith(prefix) and (os.stat(fn).st_mtime < maxage):\n                safe_remove(fn)\n\n    def _save_master_key(self, keyfile):\n        if not self.get_master_key():\n            return False\n\n        # We keep the master key in a file of its own...\n        want_renamed_keyfile = None\n        master_passphrase = self.passphrases['DEFAULT']\n        if (self._master_key_passgen != master_passphrase.generation\n                or self._master_key_ondisk != self.get_master_key()):\n            if os.path.exists(keyfile):\n                want_renamed_keyfile = keyfile + ('.%x' % time.time())\n\n        if not want_renamed_keyfile and os.path.exists(keyfile):\n            # Key file exists, nothing needs to be changed. Happy!\n            # Delete any old key backups we have laying around\n            self._delete_old_master_keys(keyfile)\n            return True\n\n        # Figure out whether we are encrypting to a GPG key, or using\n        # symmetric encryption (with the 'DEFAULT' passphrase).\n        gpgr = self.prefs.get('gpg_recipient', '').replace(',', ' ')\n        tokeys = (gpgr.split()\n                  if (gpgr and gpgr not in ('!CREATE', '!PASSWORD'))\n                  else None)\n\n        if not tokeys and not master_passphrase.is_set():\n            # Without recipients or a passphrase, we cannot save!\n            return False\n\n        if not tokeys:\n            salt = b64w(os.urandom(32).encode('base64'))\n        else:\n            salt = ''\n\n        # FIXME: Create event and capture GnuPG state?\n        mps = master_passphrase.stretched(salt)\n        gpg = GnuPG(self, passphrase=mps)\n        status, encrypted_key = gpg.encrypt(self.get_master_key(), tokeys=tokeys)\n        if status == 0:\n            if salt:\n                h, b = encrypted_key.replace('\\r', '').split('\\n\\n', 1)\n                encrypted_key = ('%s\\nSalt: %s\\nKDF: %s\\n\\n%s'\n                    % (h, salt, mps.is_stretched or 'None', b))\n            try:\n                with open(keyfile + '.new', 'wb') as fd:\n                    fd.write(encrypted_key)\n                mailpile.platforms.RestrictReadAccess(keyfile + '.new')\n                if want_renamed_keyfile:\n                    os.rename(keyfile, want_renamed_keyfile)\n                os.rename(keyfile + '.new', keyfile)\n                self._master_key_ondisk = self.get_master_key()\n                self._master_key_passgen = master_passphrase.generation\n\n                # Delete any old key backups we have laying around\n                self._delete_old_master_keys(keyfile)\n\n                return True\n            except:\n                if (want_renamed_keyfile and\n                        os.path.exists(want_renamed_keyfile)):\n                    os.rename(want_renamed_keyfile, keyfile)\n                raise\n\n        return False\n\n    def _unlocked_save(self, session=None, force=False):\n        newfile = '%s.new' % self.conffile\n        pubfile = self.conf_pub\n        keyfile = self.conf_key\n\n        self.create_and_lock_workdir(None)\n        self.timestamp = int(time.time())\n\n        if session and self.event_log:\n            if 'log' in self.sys.debug:\n                self.event_log.ui_watch(session.ui)\n            else:\n                self.event_log.ui_unwatch(session.ui)\n\n        # Save the public config data first\n        # Warn other processes against reading public data during write\n        # But wait for 2 s max so other processes can't block Mailpile.\n        try:\n            locked = self.lock_pubconf.acquire(blocking=True, timeout=2)\n            with open(pubfile, 'wb') as fd:\n                fd.write(self.as_config_bytes(_type='public'))\n        finally:\n            if locked:\n                self.lock_pubconf.release()\n        if not self.loaded_config:\n            return\n\n        # Save the master key if necessary (and possible)\n        master_key_saved = self._save_master_key(keyfile)\n\n        # We abort the save here if nothing has changed.\n        if not force and not self._changed:\n            return\n\n        # Reset our \"changed\" tracking flag. Any changes that happen\n        # during the subsequent saves will mark us dirty again, since\n        # we can't be sure the changes got written out.\n        self._changed = False\n\n        # This slight over-complication, is a reaction to segfaults in\n        # Python 2.7.5's fd.write() method.  Let's just feed it chunks\n        # of data and hope for the best. :-/\n        config_bytes = self.as_config_bytes(_xtype='public')\n        config_chunks = (config_bytes[i:i + 4096]\n                         for i in range(0, len(config_bytes), 4096))\n\n        from mailpile.crypto.streamer import EncryptingStreamer\n        if self.get_master_key() and master_key_saved:\n            subj = self.mailpile_path(self.conffile)\n            with EncryptingStreamer(self.get_master_key(),\n                                    dir=self.tempfile_dir(),\n                                    header_data={'subject': subj},\n                                    name='Config') as fd:\n                for chunk in config_chunks:\n                    fd.write(chunk)\n                fd.save(newfile)\n        else:\n            # This may result in us writing the master key out in the\n            # clear, but that is better than losing data. :-(\n            with open(newfile, 'wb') as fd:\n                for chunk in config_chunks:\n                    fd.write(chunk)\n\n        # Keep the last 5 config files around... just in case.\n        backup_file(self.conffile, backups=5, min_age_delta=900)\n        if mailpile.platforms.RenameCannotOverwrite():\n            try:\n                # We only do this if we have to; we would rather just\n                # use rename() as it's (more) atomic.\n                os.remove(self.conffile)\n            except (OSError, IOError):\n                pass\n        os.rename(newfile, self.conffile)\n\n        # If we are shutting down, just stop here.\n        if mailpile.util.QUITTING:\n            return\n\n        # Enable translations\n        mailpile.i18n.ActivateTranslation(None, self, self.prefs.language)\n\n        # Recreate VFS root in case new things have been configured\n        self.vfs_root.rescan()\n\n        # Reconfigure the connection broker\n        from mailpile.conn_brokers import Master as ConnBroker\n        ConnBroker.configure()\n\n        # Notify workers that things have changed. We do this before\n        # the prepare_workers() below, because we only want to notify\n        # workers that were already running.\n        self._unlocked_notify_workers_config_changed()\n\n        # Prepare any new workers\n        self.prepare_workers(daemons=self.daemons_started(), changed=True)\n\n        # Invalidate command cache contents that depend on the config\n        self.command_cache.mark_dirty([u'!config'])\n\n    def _find_mail_source(self, mbx_id, path=None):\n        if path:\n            path = FilePath(path).raw_fp\n            if path[:5] == '/src:':\n                return self.sources[path[5:].split('/')[0]]\n            if path[:4] == 'src:':\n                return self.sources[path[4:].split('/')[0]]\n        for src in self.sources.values():\n            # Note: we cannot test 'mbx_id in ...' because of case sensitivity.\n            if src.mailbox[FormatMbxId(mbx_id)] is not None:\n                return src\n        return None\n\n    def get_mailboxes(self, with_mail_source=None,\n                            mail_source_locals=False):\n        try:\n            mailboxes = [(FormatMbxId(k),\n                          self.sys.mailbox[k],\n                          self._find_mail_source(k))\n                          for k in self.sys.mailbox.keys()\n                          if self.sys.mailbox[k] != '/dev/null']\n        except (AttributeError):\n            # Config not loaded, nothing to see here\n            return []\n\n        if with_mail_source is True:\n            mailboxes = [(i, p, s) for i, p, s in mailboxes if s]\n        elif with_mail_source is False:\n            if mail_source_locals:\n                mailboxes = [(i, p, s) for i, p, s in mailboxes\n                             if (not s) or (not s.enabled)]\n            else:\n                mailboxes = [(i, p, s) for i, p, s in mailboxes if not s]\n        else:\n            pass  # All mailboxes, with or without mail sources\n\n        if mail_source_locals:\n            for i in range(0, len(mailboxes)):\n                mid, path, src = mailboxes[i]\n                mailboxes[i] = (mid,\n                                src and src.mailbox[mid].local or path,\n                                src)\n\n        mailboxes.sort()\n        return mailboxes\n\n    def is_editable_message(self, msg_info):\n        for ptr in msg_info[MailIndex.MSG_PTRS].split(','):\n            if not self.is_editable_mailbox(ptr[:MBX_ID_LEN]):\n                return False\n        editable = False\n        for tid in msg_info[MailIndex.MSG_TAGS].split(','):\n            try:\n                if self.tags and self.tags[tid].flag_editable:\n                    editable = True\n            except (KeyError, AttributeError):\n                pass\n        return editable\n\n    def is_editable_mailbox(self, mailbox_id):\n        try:\n            mailbox_id = ((mailbox_id is None and -1) or\n                          (mailbox_id == '' and -1) or\n                          int(mailbox_id, 36))\n            local_mailbox_id = int(self.sys.get('local_mailbox_id', 'ZZZZZ'),\n                                   36)\n            return (mailbox_id == local_mailbox_id)\n        except ValueError:\n            return False\n\n    def load_pickle(self, pfn, delete_if_corrupt=False):\n        pickle_path = os.path.join(self.workdir, pfn)\n        try:\n            with open(pickle_path, 'rb') as fd:\n                master_key = self.get_master_key()\n                if master_key:\n                    from mailpile.crypto.streamer import DecryptingStreamer\n                    with DecryptingStreamer(fd,\n                                            mep_key=master_key,\n                                            name='load_pickle(%s)' % pfn\n                                            ) as streamer:\n                        data = streamer.read()\n                        streamer.verify(_raise=IOError)\n                else:\n                    data = fd.read()\n            return cPickle.loads(data)\n        except (cPickle.UnpicklingError, IOError, EOFError, OSError):\n            if delete_if_corrupt:\n                safe_remove(pickle_path)\n            raise IOError('Load/unpickle failed: %s' % pickle_path)\n\n    def save_pickle(self, obj, pfn, encrypt=True):\n        ppath = os.path.join(self.workdir, pfn)\n        if encrypt and self.get_master_key() and self.prefs.encrypt_misc:\n            from mailpile.crypto.streamer import EncryptingStreamer\n            with EncryptingStreamer(self.get_master_key(),\n                                    dir=self.tempfile_dir(),\n                                    header_data={'subject': pfn},\n                                    name='save_pickle') as fd:\n                cPickle.dump(obj, fd, protocol=0)\n                fd.save(ppath)\n        else:\n            with open(ppath, 'wb') as fd:\n                cPickle.dump(obj, fd, protocol=0)\n\n    def _mailbox_info(self, mailbox_id, prefer_local=True):\n        try:\n            with self._lock:\n                mbx_id = FormatMbxId(mailbox_id)\n                mfn = self.sys.mailbox[mbx_id]\n                src = self._find_mail_source(mailbox_id, path=mfn)\n                pfn = 'pickled-mailbox.%s' % mbx_id.lower()\n                if prefer_local and src and src.mailbox[mbx_id] is not None:\n                    mfn = src and src.mailbox[mbx_id].local or mfn\n                else:\n                    pfn += '-R'\n        except (KeyError, TypeError):\n            traceback.print_exc()\n            raise NoSuchMailboxError(_('No such mailbox: %s') % mailbox_id)\n        return mbx_id, src, FilePath(mfn), pfn\n\n    def save_mailbox(self, session, pfn, mbox):\n        if pfn is not None:\n            mbox.save(session,\n                      to=pfn, pickler=lambda o, f: self.save_pickle(o, f))\n\n    def uncache_mailbox(self, session, entry, drop=True, force_save=False):\n        \"\"\"\n        Safely remove a mailbox from the cache, saving any state changes to\n        the encrypted pickles.\n\n        If the mailbox is still in use somewhere in the app (as measured by\n        the Python reference counter), we DON'T remove from cache, to ensure\n        each mailbox is represented by exactly one object at a time.\n        \"\"\"\n        pfn, mbx_id = entry[:2]  # Don't grab mbox, to not add more refs\n        if pfn:\n            def dropit(l):\n                return [c for c in l if (c[0] != pfn)]\n        else:\n            def dropit(l):\n                return [c for c in l if (c[1] != mbx_id)]\n\n        with self._lock:\n            mboxes = [c[2] for c in self._mbox_cache\n                      if ((c[0] == pfn) if pfn else (c[1] == mbx_id))]\n            if len(mboxes) < 1:\n                # Not found, nothing to do here\n                return\n\n            # At this point, if the mailbox is not in use, there should be\n            # exactly 2 references to it: in mboxes and self._mbox_cache.\n            # However, sys.getrefcount always returns one extra for itself.\n            if sys.getrefcount(mboxes[0]) > 3:\n                if force_save:\n                    self.save_mailbox(session, pfn, mboxes[0])\n                return\n\n            # This may be slow, but it has to happen inside the lock\n            # otherwise we run the risk of races.\n            self.save_mailbox(session, pfn, mboxes[0])\n\n            if drop:\n                self._mbox_cache = dropit(self._mbox_cache)\n            else:\n                keep2 = self._mbox_cache[-MAX_CACHED_MBOXES:]\n                keep1 = dropit(self._mbox_cache[:-MAX_CACHED_MBOXES])\n                self._mbox_cache = keep1 + keep2\n\n    def cache_mailbox(self, session, pfn, mbx_id, mbox):\n        \"\"\"\n        Add a mailbox to the cache, potentially evicting other entries if the\n        cache has grown too large.\n        \"\"\"\n        with self._lock:\n            if pfn is not None:\n                self._mbox_cache = [\n                    c for c in self._mbox_cache if c[0] != pfn]\n            elif mbx_id:\n                self._mbox_cache = [\n                    c for c in self._mbox_cache if c[1] != mbx_id]\n            self._mbox_cache.append((pfn, mbx_id, mbox))\n            flush = self._mbox_cache[:-MAX_CACHED_MBOXES]\n        for entry in flush:\n            pfn, mbx_id = entry[:2]\n            self.save_worker.add_unique_task(\n                session, 'Save mailbox %s/%s (drop=%s)' % (mbx_id, pfn, False),\n                lambda: self.uncache_mailbox(session, entry, drop=False))\n\n    def flush_mbox_cache(self, session, clear=True, wait=False):\n        if wait:\n            saver = self.save_worker.do\n        else:\n            saver = self.save_worker.add_task\n        with self._lock:\n            flush = self._mbox_cache[:]\n        for entry in flush:\n            pfn, mbx_id = entry[:2]\n            saver(session,\n                  'Save mailbox %s/%s (drop=%s)' % (mbx_id, pfn, clear),\n                  lambda: self.uncache_mailbox(session, entry,\n                                               drop=clear, force_save=True),\n                  unique=True)\n\n    def find_mboxids_and_sources_by_path(self, *paths):\n        def _au(p):\n            return unicode(p[1:] if (p[:5] == '/src:') else\n                           p if (p[:4] == 'src:') else\n                           vfs.abspath(p))\n        abs_paths = dict((_au(p), [p]) for p in paths)\n        with self._lock:\n            for sid, src in self.sources.iteritems():\n                for mid, info in src.mailbox.iteritems():\n                    umfn = _au(self.sys.mailbox[mid])\n                    if umfn in abs_paths:\n                        abs_paths[umfn].append((mid, src))\n                    if info.local:\n                        lmfn = _au(info.local)\n                        if lmfn in abs_paths:\n                            abs_paths[lmfn].append((mid, src))\n\n            for mid, mfn in self.sys.mailbox.iteritems():\n                umfn = _au(mfn)\n                if umfn in abs_paths:\n                    if umfn[:4] == u'src:':\n                        src = self.sources.get(umfn[4:].split('/')[0])\n                    else:\n                        src = None\n                    abs_paths[umfn].append((mid, src))\n\n        return dict((p[0], p[1]) for p in abs_paths.values() if p[1:])\n\n    def open_mailbox_path(self, session, path, register=False, raw_open=False):\n        path = vfs.abspath(path)\n        mbox = mbx_mid = mbx_src = None\n        with self._lock:\n            msmap = self.find_mboxids_and_sources_by_path(unicode(path))\n            if msmap:\n                mbx_mid, mbx_src = list(msmap.values())[0]\n\n            if (register or raw_open) and mbx_mid is None:\n                mbox = dict(((i, m) for p, i, m in self._mbox_cache)\n                            ).get(path, None)\n\n                if path.raw_fp.startswith('/src:'):\n                    path = FilePath(path.raw_fp[1:])\n\n                if mbox:\n                    pass\n                elif path.raw_fp.startswith('src:'):\n                    msrc_id = path.raw_fp[4:].split('/')[0]\n                    msrc = self.mail_sources.get(msrc_id)\n                    if msrc:\n                        mbox = msrc.open_mailbox(None, path.raw_fp)\n                else:\n                    mbox = OpenMailbox(path.raw_fp, self, create=False)\n\n                if register:\n                    mbx_mid = self.sys.mailbox.append(unicode(path))\n                    mbox = None  # Force a re-open below\n\n                elif mbox:\n                    # (re)-add to the cache; we need to do this here\n                    # because we did the opening ourselves instead of\n                    # invoking open_mailbox as below.\n                    self.cache_mailbox(session, None, path.raw_fp, mbox)\n\n        if mbx_mid is not None:\n            mbx_mid = FormatMbxId(mbx_mid)\n            if mbox is None:\n                mbox = self.open_mailbox(session, mbx_mid, prefer_local=True)\n            return (mbx_mid, mbox)\n\n        elif raw_open and mbox:\n            return (mbx_mid, mbox)\n\n        raise ValueError('Not found')\n\n    def open_mailbox(self, session, mailbox_id,\n                     prefer_local=True, from_cache=False):\n        mbx_id, src, mfn, pfn = self._mailbox_info(mailbox_id,\n                                                   prefer_local=prefer_local)\n        with self._lock:\n            mbox = dict(((p, m) for p, i, m in self._mbox_cache)\n                        ).get(pfn, None)\n        try:\n            if mbox is None:\n                if from_cache:\n                    return None\n                if session:\n                    session.ui.mark(_('%s: Updating: %s') % (mbx_id, mfn))\n                mbox = self.load_pickle(pfn, delete_if_corrupt=True)\n            if prefer_local and not mbox.is_local:\n                mbox = None\n            else:\n                mbox.update_toc()\n        except AttributeError:\n            mbox = None\n        except KeyboardInterrupt:\n            raise\n        except IOError:\n            mbox = None\n        except:\n            if self.sys.debug:\n                traceback.print_exc()\n            mbox = None\n\n        if mbox is None:\n            if session:\n                session.ui.mark(_('%s: Opening: %s (may take a while)'\n                                  ) % (mbx_id, mfn))\n            editable = self.is_editable_mailbox(mbx_id)\n            if src is not None:\n                msrc = self.mail_sources.get(src._key)\n                mbox = msrc.open_mailbox(mbx_id, mfn.raw_fp) if msrc else None\n            if mbox is None:\n                mbox = OpenMailbox(mfn.raw_fp, self, create=editable)\n            mbox.editable = editable\n            mbox.is_local = prefer_local\n\n        # Always set these, they can't be pickled\n        mbox._decryption_key_func = lambda: self.get_master_key()\n        mbox._encryption_key_func = lambda: (self.get_master_key() if\n                                             self.prefs.encrypt_mail else None)\n\n        # Finally, re-add to the cache\n        self.cache_mailbox(session, pfn, mbx_id, mbox)\n\n        return mbox\n\n    def create_local_mailstore(self, session, name=None):\n        path = os.path.join(self.workdir, 'mail')\n        with self._lock:\n            if name is None:\n                name = '%5.5x' % random.randint(0, 16**5)\n                while os.path.exists(os.path.join(path, name)):\n                    name = '%5.5x' % random.randint(0, 16**5)\n            if name != '':\n                if not os.path.exists(path):\n                    root_mbx = wervd.MailpileMailbox(path)\n                if name.startswith(path) and '..' not in name:\n                    path = name\n                else:\n                    path = os.path.join(path, os.path.basename(name))\n\n            mbx = wervd.MailpileMailbox(path)\n            mbx._decryption_key_func = lambda: self.get_master_key()\n            mbx._encryption_key_func = lambda: (self.get_master_key() if\n                                                self.prefs.encrypt_mail else None)\n            return FilePath(path), mbx\n\n    def open_local_mailbox(self, session):\n        with self._lock:\n            local_id = self.sys.get('local_mailbox_id', None)\n            if local_id is None or local_id == '':\n                mailbox, mbx = self.create_local_mailstore(session, name='')\n                local_id = FormatMbxId(self.sys.mailbox.append(mailbox))\n                self.sys.local_mailbox_id = local_id\n            else:\n                local_id = FormatMbxId(local_id)\n        return local_id, self.open_mailbox(session, local_id)\n\n    def get_passphrase(self, keyid,\n                       description=None, prompt=None, error=None,\n                       no_ask=False, no_cache=False):\n        if not no_cache:\n            keyidL = keyid.lower()\n            for sid in self.secrets.keys():\n                if sid.endswith(keyidL):\n                    secret = self.secrets[sid]\n                    if secret.policy == 'always-ask':\n                        no_cache = True\n                    elif secret.policy == 'fail':\n                        return False, None\n                    elif secret.policy != 'cache-only':\n                        sps = SecurePassphraseStorage(secret.password)\n                        return (keyidL, sps)\n\n        if not no_cache:\n            if keyid in self.passphrases:\n                return (keyid, self.passphrases[keyid])\n            if keyidL in self.passphrases:\n                return (keyidL, self.passphrases[keyidL])\n            for fprint in self.passphrases:\n                if fprint.endswith(keyid):\n                    return (fprint, self.passphrases[fprint])\n                if fprint.lower().endswith(keyidL):\n                    return (fprint, self.passphrases[fprint])\n\n        if not no_ask:\n            # This will either record details to the event of the currently\n            # running command/operation, or register a new event. This does\n            # not work as one might hope if ops cross a thread boundary...\n            ev = GetThreadEvent(\n                create=True,\n                message=prompt or _('Please enter your password'),\n                source=self)\n\n            details = {'id': keyid}\n            if prompt: details['msg'] = prompt\n            if error: details['err'] = error\n            if description: details['dsc'] = description\n            if 'password_needed' in ev.private_data:\n                ev.private_data['password_needed'].append(details)\n            else:\n                ev.private_data['password_needed'] = [details]\n\n            ev.data['password_needed'] = True\n\n            # Post a password request to the event log...\n            self.event_log.log_event(ev)\n\n        return None, None\n\n    def get_profile(self, email=None):\n        find = email or self.prefs.get('default_email', None)\n        default_sig = _('Sent using Mailpile, Free Software '\n                        'from www.mailpile.is')\n        default_profile = {\n            'name': None,\n            'email': find,\n            'messageroute': self.prefs.default_messageroute,\n            'signature': default_sig,\n            'crypto_policy': 'none',\n            'crypto_format': 'none',\n            'vcard': None\n        }\n\n        profiles = []\n        if find:\n            profiles = [self.vcards.get_vcard(find)]\n        if not profiles or not profiles[0]:\n            profiles = self.vcards.find_vcards([], kinds=['profile'])\n        if profiles:\n            profiles.sort(key=lambda k: ((0 if k.route else 1),\n                                         (-len(k.recent_history())),\n                                         (-len(k.sources()))))\n\n        if profiles and profiles[0]:\n            profile = profiles[0]\n            psig = profile.signature\n            proute = profile.route\n            default_profile.update({\n                'name': profile.fn,\n                'email': find or profile.email,\n                'signature': psig if (psig is not None) else default_sig,\n                'messageroute': (proute if (proute is not None)\n                                 else self.prefs.default_messageroute),\n                'crypto_policy': profile.crypto_policy or 'none',\n                'crypto_format': profile.crypto_format or 'none',\n                'vcard': profile\n            })\n\n        return default_profile\n\n    def get_route(self, frm, rcpts=['-t']):\n        if len(rcpts) == 1:\n            if rcpts[0].lower().endswith('.onion'):\n                return {\"protocol\": \"smtorp\",\n                        \"host\": rcpts[0].split('@')[-1],\n                        \"port\": 25,\n                        \"auth_type\": \"\",\n                        \"username\": \"\",\n                        \"password\": \"\"}\n        routeid = self.get_profile(frm)['messageroute']\n        if self.routes[routeid] is not None:\n            return self.routes[routeid]\n        else:\n            raise ValueError(_(\"Route %s for %s does not exist.\"\n                               ) % (routeid, frm))\n\n    def data_directory(self, ftype, mode='rb', mkdir=False):\n        \"\"\"\n        Return the path to a data directory for a particular type of file\n        data, optionally creating the directory if it is missing.\n\n        >>> p = cfg.data_directory('html_theme', mode='r', mkdir=False)\n        >>> p == os.path.abspath('shared-data/default-theme')\n        True\n        \"\"\"\n        # This should raise a KeyError if the ftype is unrecognized\n        bpath = self.sys.path.get(ftype)\n        if not bpath.startswith('/'):\n            cpath = os.path.join(self.workdir, bpath)\n            if os.path.exists(cpath) or 'w' in mode:\n                bpath = cpath\n                if mkdir and not os.path.exists(cpath):\n                    os.mkdir(cpath)\n            else:\n                bpath = os.path.join(self.shareddatadir, bpath)\n        return os.path.abspath(bpath)\n\n    def data_file_and_mimetype(self, ftype, fpath, *args, **kwargs):\n        # The theme gets precedence\n        core_path = self.data_directory(ftype, *args, **kwargs)\n        path, mimetype = os.path.join(core_path, fpath), None\n\n        # If there's still nothing there, check our plugins\n        if not os.path.exists(path):\n            from mailpile.plugins import PluginManager\n            path, mimetype = PluginManager().get_web_asset(fpath, path)\n\n        if os.path.exists(path):\n            return path, mimetype\n        else:\n            return None, None\n\n    def history_file(self):\n        return os.path.join(self.workdir, 'history')\n\n    def mailindex_file(self):\n        return os.path.join(self.workdir, 'mailpile.idx')\n\n    def mailpile_path(self, path):\n        base = (self.workdir + os.sep).replace(os.sep+os.sep, os.sep)\n        if path.startswith(base):\n            return path[len(base):]\n\n        rbase = os.path.realpath(base) + os.sep\n        rpath = os.path.realpath(path)\n        if rpath.startswith(rbase):\n            return rpath[len(rbase):]\n\n        return path\n\n    def tempfile_dir(self):\n        d = os.path.join(self.workdir, 'tmp')\n        if not os.path.exists(d):\n            os.mkdir(d)\n        return d\n\n    def clean_tempfile_dir(self):\n        try:\n            td = self.tempfile_dir()\n            files = os.listdir(td)\n            random.shuffle(files)\n            for fn in files:\n                fn = os.path.join(td, fn)\n                if os.path.isfile(fn):\n                    safe_remove(fn)\n        except (OSError, IOError):\n            pass\n\n    def postinglist_dir(self, prefix):\n        d = os.path.join(self.workdir, 'search')\n        if not os.path.exists(d):\n            os.mkdir(d)\n        d = os.path.join(d, prefix and prefix[0] or '_')\n        if not os.path.exists(d):\n            os.mkdir(d)\n        return d\n\n    def need_more_disk_space(self, required=0, nodefault=False, ratio=1.0):\n        \"\"\"Returns a path where we need more disk space, None if all is ok.\"\"\"\n        if self.detected_memory_corruption:\n            return '/'\n        if not (nodefault and required):\n            required = ratio * max(required, self.sys.minfree_mb * 1024 * 1024)\n        for path in (self.workdir, ):\n           if get_free_disk_bytes(path) < required:\n               return path\n        return None\n\n    def interruptable_wait_for_lock(self):\n        # This construct allows the user to CTRL-C out of things.\n        delay = 0.01\n        while self._lock.acquire(False) == False:\n            if mailpile.util.QUITTING:\n                raise KeyboardInterrupt('Quitting')\n            time.sleep(delay)\n            delay = min(1, delay*2)\n        self._lock.release()\n        return self._lock\n\n    def get_index(self, session):\n        # Note: This is a long-running lock, but having two sets of the\n        # index would really suck and this should only ever happen once.\n        with self.interruptable_wait_for_lock():\n            if self.index:\n                return self.index\n            self.index_loading = MailIndex(self)\n            self.index_loading.load(session)\n            self.index = self.index_loading\n            self.index_loading = None\n            try:\n                self.index_check.release()\n            except:\n                pass\n            return self.index\n\n    def get_path_index(self, session, path):\n        \"\"\"\n        Get a search index by path (instead of the default), or None if\n        no matching index is found.\n        \"\"\"\n        idx = None\n\n        mi, mbox = self.open_mailbox_path(session, path, raw_open=True)\n        if mbox:\n            idx = mbox.get_index(self, mbx_mid=mi)\n\n        # Return a sad, boring, empty index.\n        if idx is None:\n            import mailpile.index.base\n            idx = mailpile.index.base.BaseIndex(self)\n\n        return idx\n\n    def get_proxy_settings(self):\n        if self.sys.proxy.protocol == 'system':\n            proxy_list = getproxies()\n            for proto in ('socks5', 'socks4', 'http'):\n                for url in proxy_list.values():\n                    if url.lower().startswith(proto+'://'):\n                        try:\n                            p, host, port = url.replace('/', '').split(':')\n                            return {\n                                'protocol': proto,\n                                'fallback': self.sys.proxy.fallback,\n                                'host': host,\n                                'port': int(port),\n                                'no_proxy': self.sys.proxy.no_proxy}\n                        except (ValueError, IndexError, KeyError):\n                            pass\n        elif self.sys.proxy.protocol in ('tor', 'tor-risky'):\n            if self.tor_worker is not None:\n                return {\n                    'protocol': self.sys.proxy.protocol,\n                    'fallback': self.sys.proxy.fallback,\n                    'host': '127.0.0.1',\n                    'port': self.tor_worker.socks_port,\n                    'no_proxy': self.sys.proxy.no_proxy}\n        return self.sys.proxy\n\n    def open_file(self, ftype, fpath, mode='rb', mkdir=False):\n        if '..' in fpath:\n            raise ValueError(_('Parent paths are not allowed'))\n        fpath, mt = self.data_file_and_mimetype(ftype, fpath,\n                                                mode=mode, mkdir=mkdir)\n        if not fpath:\n            raise IOError(2, 'Not Found')\n        return fpath, open(fpath, mode), mt\n\n    def daemons_started(config, which=None):\n        return ((which or config.save_worker)\n                not in (None, config.dumb_worker))\n\n    def get_mail_source(config, src_id, start=False, changed=False):\n        ms_thread = config.mail_sources.get(src_id)\n        if (ms_thread and not ms_thread.isAlive()):\n            ms_thread = None\n        if not ms_thread:\n            from mailpile.mail_source import MailSource\n            src_config = config.sources[src_id]\n            ms_thread = MailSource(config.background, src_config)\n            if start:\n                config.mail_sources[src_id] = ms_thread\n                ms_thread.start()\n                if changed:\n                    ms_thread.wake_up()\n        return ms_thread\n\n    def start_tor_worker(config):\n        from mailpile.conn_brokers import Master as ConnBroker\n        from mailpile.crypto.tor import Tor\n        config.tor_worker = Tor(\n            config=config, session=config.background,\n            callbacks=[lambda c: ConnBroker.configure()])\n        config.tor_worker.start()\n        return config.tor_worker\n\n    def prepare_workers(self, *args, **kwargs):\n        with self._lock:\n            return self._unlocked_prepare_workers(*args, **kwargs)\n\n    def _unlocked_prepare_workers(config, session=None, changed=False,\n                                  daemons=False, httpd_spec=None):\n\n        # Set our background UI to something that can log.\n        if session:\n            config.background.ui = BackgroundInteraction(\n                config, log_parent=session.ui)\n\n        # Tell conn broker that we exist\n        from mailpile.conn_brokers import Master as ConnBroker\n        ConnBroker.set_config(config)\n        if 'connbroker' in config.sys.debug:\n            ConnBroker.debug_callback = lambda msg: config.background.ui.debug(msg)\n        else:\n            ConnBroker.debug_callback = None\n\n        def start_httpd(sspec=None):\n            sspec = sspec or (config.sys.http_host, config.sys.http_port,\n                              config.sys.http_path or '')\n            if sspec[0].lower() != 'disabled' and sspec[1] >= 0:\n                try:\n                    if mailpile.platforms.NeedExplicitPortCheck():\n                        try:\n                            socket.socket().connect((sspec[0],sspec[1]))\n                            port_in_use = True\n                        except socket.error:\n                            port_in_use = False\n                        if port_in_use:\n                            raise socket.error(errno.EADDRINUSE)\n                    config.http_worker = HttpWorker(config.background, sspec)\n                    config.http_worker.start()\n                except socket.error as e:\n                    if e[0] == errno.EADDRINUSE:\n                        session.ui.error(\n                            _('Port %s:%s in use by another Mailpile or program'\n                              ) % (sspec[0], sspec[1]))\n\n        # We may start the HTTPD without the loaded config...\n        if not config.loaded_config:\n            if daemons and not config.http_worker:\n                 start_httpd(httpd_spec)\n            return\n\n        # Start the other workers\n        if daemons:\n            for src_id in config.sources.keys():\n                try:\n                    config.get_mail_source(src_id, start=True, changed=changed)\n                except (ValueError, KeyError):\n                    pass\n\n            should_launch_tor = ((not config.sys.tor.systemwide)\n                and (config.sys.proxy.protocol.startswith('tor')))\n            if config.tor_worker is None:\n                if should_launch_tor:\n                    config.start_tor_worker()\n            elif not should_launch_tor:\n                config.tor_worker.stop_tor()\n                config.tor_worker = None\n\n            if config.slow_worker == config.dumb_worker:\n                config.slow_worker = Worker('Slow worker', config.background)\n                config.slow_worker.wait_until = lambda: (\n                    (not config.save_worker) or config.save_worker.is_idle())\n                config.slow_worker.start()\n            if config.scan_worker == config.dumb_worker:\n                config.scan_worker = Worker('Scan worker', config.background)\n                config.slow_worker.wait_until = lambda: (\n                    (not config.save_worker) or config.save_worker.is_idle())\n                config.scan_worker.start()\n            if config.save_worker == config.dumb_worker:\n                config.save_worker = ImportantWorker('Save worker',\n                                                     config.background)\n                config.save_worker.start()\n            if not config.cron_worker:\n                config.cron_worker = Cron(\n                    config.cron_schedule, 'Cron worker', config.background)\n                config.cron_worker.start()\n            if not config.http_worker:\n                start_httpd(httpd_spec)\n            if not config.other_workers:\n                from mailpile.plugins import PluginManager\n                for worker in PluginManager.WORKERS:\n                    w = worker(config.background)\n                    w.start()\n                    config.other_workers.append(w)\n\n        # Update the cron jobs, if necessary\n        if config.cron_worker and config.event_log:\n            from mailpile.postinglist import GlobalPostingList\n            from mailpile.plugins.core import HealthCheck\n            def gpl_optimize():\n                if HealthCheck.check(config.background, config):\n                    rs_interval = (config.prefs.rescan_interval or 1800)\n                    runtime = rs_interval / 10\n                    ratio = 2.0 / (7*24*3600.0 / rs_interval) # Optimize 2x/week\n                    config.slow_worker.add_unique_task(\n                        config.background, 'Optimize GPL',\n                        lambda: GlobalPostingList.Optimize(\n                            config.background,\n                            config.index,\n                            lazy=(not user_probably_asleep()),\n                            ratio=ratio,\n                            runtime=runtime))\n\n            # Schedule periodic rescanning, if requested.\n            rescan_interval = config.prefs.rescan_interval\n            if rescan_interval:\n                def rescan():\n                    from mailpile.plugins.core import Rescan\n                    if 'rescan' not in config._running:\n                        rsc = Rescan(config.background, 'rescan')\n                        rsc.serialize = False\n                        config.slow_worker.add_unique_task(\n                            config.background, 'Rescan',\n                            lambda: rsc.run(slowly=True, cron=True))\n                        gpl_optimize()\n                config.cron_worker.add_task('rescan', rescan_interval, rescan)\n            else:\n                config.cron_worker.add_task('gpl_optimize', 1800, gpl_optimize)\n\n            def metadata_index_saver():\n                config.save_worker.add_unique_task(\n                    config.background, 'save_metadata_index',\n                    lambda: config.index.save_changes())\n            config.cron_worker.add_task(\n                'save_metadata_index', 900, metadata_index_saver)\n\n            def search_history_saver():\n                config.save_worker.add_unique_task(\n                    config.background, 'save_search_history',\n                    lambda: config.search_history.save(config))\n            config.cron_worker.add_task(\n                'save_search_history', 900, search_history_saver)\n\n            def refresh_command_cache():\n                config.scan_worker.add_unique_task(\n                    config.background, 'refresh_command_cache',\n                    lambda: config.command_cache.refresh(\n                        event_log=config.event_log),\n                    first=True)\n            config.cron_worker.add_task(\n                'refresh_command_cache', 5, refresh_command_cache)\n\n            # Schedule plugin jobs\n            from mailpile.plugins import PluginManager\n\n            def interval(i):\n                if isinstance(i, (str, unicode)):\n                    i = config.walk(i)\n                return int(i)\n\n            def wrap_fast(func):\n                def wrapped():\n                    return func(config.background)\n                return wrapped\n\n            def wrap_slow(func):\n                def wrapped():\n                    config.slow_worker.add_unique_task(\n                        config.background, job,\n                        lambda: func(config.background))\n                return wrapped\n            for job, (i, f) in PluginManager.FAST_PERIODIC_JOBS.iteritems():\n                config.cron_worker.add_task(job, interval(i), wrap_fast(f))\n            for job, (i, f) in PluginManager.SLOW_PERIODIC_JOBS.iteritems():\n                config.cron_worker.add_task(job, interval(i), wrap_slow(f))\n\n    def _unlocked_get_all_workers(config):\n        return (config.mail_sources.values() +\n                config.other_workers +\n                [config.http_worker,\n                 config.tor_worker,\n                 config.slow_worker,\n                 config.scan_worker,\n                 config.cron_worker])\n\n    def stop_workers(config):\n        try:\n            self.index_check.release()\n        except:\n            pass\n\n        with config._lock:\n            worker_list = config._unlocked_get_all_workers()\n            config.other_workers = []\n            config.tor_worker = None\n            config.http_worker = None\n            config.cron_worker = None\n            config.slow_worker = config.dumb_worker\n            config.scan_worker = config.dumb_worker\n\n        for wait in (False, True):\n            for w in worker_list:\n                if w and w.isAlive():\n                    if config.sys.debug and wait:\n                        print('Waiting for %s' % w)\n                    w.quit(join=wait)\n\n        # Flush the mailbox cache (queues save worker jobs)\n        config.flush_mbox_cache(config.background, clear=True)\n\n        # Handle the save worker last, once all the others are\n        # no longer feeding it new things to do.\n        with config._lock:\n            save_worker = config.save_worker\n            config.save_worker = config.dumb_worker\n        if config.sys.debug:\n            print('Waiting for %s' % save_worker)\n\n        from mailpile.postinglist import PLC_CACHE_FlushAndClean\n        PLC_CACHE_FlushAndClean(config.background, keep=0)\n        config.search_history.save(config)\n        save_worker.quit(join=True)\n\n        if config.sys.debug:\n            # Hooray!\n            print('All stopped!')\n\n    def _unlocked_notify_workers_config_changed(config):\n        worker_list = config._unlocked_get_all_workers()\n        for worker in worker_list:\n            if hasattr(worker, 'notify_config_changed'):\n                worker.notify_config_changed()\n\n\n##############################################################################\n\nif __name__ == \"__main__\":\n    import copy\n    import doctest\n    import sys\n    import mailpile.config.base\n    import mailpile.config.defaults\n    import mailpile.config.manager\n    import mailpile.plugins.tags\n    import mailpile.ui\n\n    rules = copy.deepcopy(mailpile.config.defaults.CONFIG_RULES)\n    rules.update({\n        'nest1': ['Nest1', {\n            'nest2': ['Nest2', str, []],\n            'nest3': ['Nest3', {\n                'nest4': ['Nest4', str, []]\n            }, []],\n        }, {}]\n    })\n    cfg = mailpile.config.manager.ConfigManager(rules=rules)\n    session = mailpile.ui.Session(cfg)\n    session.ui = mailpile.ui.SilentInteraction(cfg)\n    session.ui.block()\n\n    for tries in (1, 2):\n        # This tests that we can set (and reset) dicts of unnested objects\n        cfg.tags = {}\n        assert(cfg.tags.a is None)\n        for tn in range(0, 11):\n            cfg.tags.append({'name': 'Test Tag %s' % tn})\n        assert(cfg.tags.a['name'] == 'Test Tag 10')\n\n        # This tests the same thing for lists\n        #cfg.profiles = []\n        #assert(len(cfg.profiles) == 0)\n        #cfg.profiles.append({'name': 'Test Profile'})\n        #assert(len(cfg.profiles) == 1)\n        #assert(cfg.profiles[0].name == 'Test Profile')\n\n        # This is the complicated one: multiple nesting layers\n        cfg.nest1 = {}\n        assert(cfg.nest1.a is None)\n        cfg.nest1.a = {\n            'nest2': ['hello', 'world'],\n            'nest3': [{'nest4': ['Hooray']}]\n        }\n        cfg.nest1.b = {\n            'nest2': ['hello', 'world'],\n            'nest3': [{'nest4': ['Hooray', 'Bravo']}]\n        }\n        assert(cfg.nest1.a.nest3[0].nest4[0] == 'Hooray')\n        assert(cfg.nest1.b.nest3[0].nest4[1] == 'Bravo')\n\n    assert(cfg.sys.http_port ==\n           mailpile.config.defaults.CONFIG_RULES['sys'][-1]['http_port'][-1])\n    assert(cfg.sys.path.vcards == 'vcards')\n    assert(cfg.walk('sys.path.vcards') == 'vcards')\n\n    # Verify that the tricky nested stuff from above persists and\n    # load/save doesn't change lists.\n    for passes in (1, 2, 3):\n        cfg2 = mailpile.config.manager.ConfigManager(rules=rules)\n        cfg2.parse_config(session, cfg.as_config_bytes())\n        cfg.parse_config(session, cfg2.as_config_bytes())\n        assert(cfg2.nest1.a.nest3[0].nest4[0] == 'Hooray')\n        assert(cfg2.nest1.b.nest3[0].nest4[1] == 'Bravo')\n        assert(len(cfg2.nest1) == 2)\n        assert(len(cfg.nest1) == 2)\n        assert(len(cfg.tags) == 11)\n\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                              extraglobs={'cfg': cfg,\n                                          'session': session})\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/config/paths.py",
    "content": "import os\nimport sys\n\nimport mailpile.platforms\n\ntry:\n    from appdirs import AppDirs\nexcept ImportError:\n    AppDirs = None\n\n\ndef _ensure_exists(path, mode=0o700):\n    if not os.path.exists(path):\n        head, tail = os.path.split(path)\n        _ensure_exists(head)\n        os.mkdir(path, mode)\n    return path\n\n\ndef LEGACY_DEFAULT_WORKDIR(profile):\n    if profile == 'default':\n        # Backwards compatibility: If the old ~/.mailpile exists, use it.\n        workdir = os.path.expanduser('~/.mailpile')\n        if os.path.exists(workdir) and os.path.isdir(workdir):\n            return workdir\n\n    return os.path.join(\n        mailpile.platforms.GetAppDataDirectory(), 'Mailpile', profile)\n\n\ndef DEFAULT_WORKDIR():\n    # The Mailpile environment variable trumps everything\n    workdir = os.getenv('MAILPILE_HOME')\n    if workdir:\n        return _ensure_exists(workdir)\n\n    # Which profile?\n    profile = os.getenv('MAILPILE_PROFILE', 'default')\n\n    # Check if we have a legacy setup we need to preserve\n    workdir = LEGACY_DEFAULT_WORKDIR(profile)\n    if not AppDirs or (os.path.exists(workdir) and os.path.isdir(workdir)):\n        return _ensure_exists(workdir)\n\n    # Use platform-specific defaults\n    # via https://github.com/ActiveState/appdirs\n    dirs = AppDirs(\"Mailpile\", \"Mailpile ehf\")\n    return _ensure_exists(os.path.join(dirs.user_data_dir, profile))\n\n\ndef DEFAULT_SHARED_DATADIR():\n    # IMPORTANT: This code is duplicated in mailpile-admin.py.\n    #            If it needs changing please change both places!\n    env_share = os.getenv('MAILPILE_SHARED')\n    if env_share is not None:\n        return env_share\n\n    # Check if we are running in a virtual env\n    # http://stackoverflow.com/questions/1871549/python-determine-if-running-inside-virtualenv\n    # We must also check that we are installed in the virtual env,\n    # not just that we are running in a virtual env.\n    if ((hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix'))\n            and __file__.startswith(sys.prefix)):\n        return os.path.join(sys.prefix, 'share', 'mailpile')\n\n    # Check if we've been installed to /usr/local (or equivalent)\n    usr_local = os.path.join(sys.prefix, 'local')\n    if __file__.startswith(usr_local):\n        return os.path.join(usr_local, 'share', 'mailpile')\n\n    # Check if we are in /usr/ (sys.prefix)\n    if __file__.startswith(sys.prefix):\n        return os.path.join(sys.prefix, 'share', 'mailpile')\n\n    # Else assume dev mode, source tree layout\n    return os.path.join(\n        os.path.dirname(__file__), '..', '..', 'shared-data')\n\n\ndef DEFAULT_LOCALE_DIRECTORY():\n    \"\"\"Get the gettext translation object, no matter where our CWD is\"\"\"\n    return os.path.join(DEFAULT_SHARED_DATADIR(), \"locale\")\n\n\ndef LOCK_PATHS(workdir=None):\n    if workdir is None:\n        workdir = DEFAULT_WORKDIR()\n    return (\n        os.path.join(workdir, 'public-lock'),\n        os.path.join(workdir, 'workdir-lock'))\n"
  },
  {
    "path": "mailpile/config/validators.py",
    "content": "from __future__ import print_function\nimport os\nimport socket\nimport re\n\ntry:\n    import win_inet_pton\nexcept ImportError:\n    pass\n\nfrom urlparse import urlparse\n\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as n\nfrom mailpile.util import *\n\n\ndef BoolCheck(value):\n    \"\"\"\n    Convert common yes/no strings into boolean values.\n\n    >>> BoolCheck('yes')\n    True\n    >>> BoolCheck('no')\n    False\n\n    >>> BoolCheck('true')\n    True\n    >>> BoolCheck('false')\n    False\n\n    >>> BoolCheck('on')\n    True\n    >>> BoolCheck('off')\n    False\n\n    >>> BoolCheck('wiggle')\n    Traceback (most recent call last):\n        ...\n    ValueError: Invalid boolean: wiggle\n    \"\"\"\n    bool_val = truthy(value, default=None)\n    if bool_val is None:\n        raise ValueError(_('Invalid boolean: %s') % value)\n    return bool_val\n\n\ndef SlugCheck(slug, allow=''):\n    \"\"\"\n    Verify that a string is a valid URL slug.\n\n    >>> SlugCheck('_Foo-bar.5')\n    '_foo-bar.5'\n\n    >>> SlugCheck('Bad Slug')\n    Traceback (most recent call last):\n        ...\n    ValueError: Invalid URL slug: Bad Slug\n\n    >>> SlugCheck('Bad/Slug')\n    Traceback (most recent call last):\n        ...\n    ValueError: Invalid URL slug: Bad/Slug\n    \"\"\"\n    if not slug == CleanText(unicode(slug),\n                             banned=(CleanText.NONDNS.replace(allow, ''))\n                             ).clean:\n        raise ValueError(_('Invalid URL slug: %s') % slug)\n    return slug.lower()\n\n\ndef SlashSlugCheck(slug):\n    \"\"\"\n    Verify that a string is a valid URL slug (slashes allowed).\n\n    >>> SlashSlugCheck('Okay/Slug')\n    'okay/slug'\n    \"\"\"\n    return SlugCheck(slug, allow='/')\n\n\ndef RouteProtocolCheck(proto):\n    \"\"\"\n    Verify that the protocol is actually a protocol.\n    (FIXME: Should reference a list of registered protocols...)\n\n    >>> RouteProtocolCheck('SMTP')\n    'smtp'\n    \"\"\"\n    proto = str(proto).strip().lower()\n    if proto not in (\"smtp\", \"smtptls\", \"smtpssl\", \"local\"):\n        raise ValueError(_('Invalid message delivery protocol: %s') % proto)\n    return proto\n\ndef DnsNameValid(dnsname):\n    \"\"\"\n    Tests whether a string is a valid dns name, returns a boolean value\n    \"\"\"\n    if not dnsname or not DNSNAME_RE.match(dnsname):\n        return False\n    else:\n        return True\n\ndef HostNameValid(host):\n    \"\"\"\n    Tests whether a string is a valid host-name, return a boolean value\n\n    >>> HostNameValid(\"127.0.0.1\")\n    True\n\n    >>> HostNameValid(\"::1\")\n    True\n\n    >>> HostNameValid(\"localhost\")\n    True\n\n    >>> HostNameValid(\"22.45\")\n    False\n    \"\"\"\n    valid = False\n    for attr in [\"AF_INET\",\"AF_INET6\"]:\n        try:\n            socket.inet_pton(socket.__getattribute__(attr), host)\n            valid = True\n            break\n        except (socket.error):\n            pass\n    if not valid:\n        # the host is not an IP so check if its a hostname i.e. 'localhost' or 'site.com'\n        if not host or (not DnsNameValid(host) and not ALPHA_RE.match(host)):\n            return False\n        else:\n            return True\n    else:\n        return True\n\ndef HostNameCheck(host):\n    \"\"\"\n    Verify that a string is a valid host-name, return it lowercased.\n\n    >>> HostNameCheck('foo.BAR.baz')\n    'foo.bar.baz'\n\n    >>> HostNameCheck('127.0.0.1')\n    '127.0.0.1'\n\n    >>> HostNameCheck('not/a/hostname')\n    Traceback (most recent call last):\n        ...\n    ValueError: Invalid hostname: not/a/hostname\n    \"\"\"\n    # Check DNS, IPv4, and finally IPv6\n    if not HostNameValid(host):\n        raise ValueError(_('Invalid hostname: %s') % host)\n    return str(host).lower()\n\n\ndef B36Check(b36val):\n    \"\"\"\n    Verify that a string is a valid path base-36 integer.\n\n    >>> B36Check('Aa')\n    'aa'\n\n    >>> B36Check('.')\n    Traceback (most recent call last):\n        ...\n    ValueError: invalid ...\n    \"\"\"\n    int(b36val, 36)\n    return str(b36val).lower()\n\n\ndef NotUnicode(string):\n    \"\"\"\n    Make sure a string is NOT unicode.\n    \"\"\"\n    if isinstance(string, unicode):\n        string = string.encode('utf-8')\n    if not isinstance(string, str):\n        return str(string)\n    return string\n\n\ndef PathCheck(path):\n    \"\"\"\n    Verify that a string is a valid path, make it absolute.\n\n    >>> PathCheck('/etc/../')\n    '/'\n\n    >>> PathCheck('/no/such/path')\n    Traceback (most recent call last):\n        ...\n    ValueError: File/directory does not exist: /no/such/path\n    \"\"\"\n    if isinstance(path, unicode):\n        path = path.encode('utf-8')\n    path = os.path.expanduser(path)\n    if not os.path.exists(path):\n        raise ValueError(_('File/directory does not exist: %s') % path)\n    return os.path.abspath(path)\n\n\ndef WebRootCheck(path):\n    \"\"\"\n    Verify that a string is a valid web root path, normalize the slashes.\n\n    >>> WebRootCheck('/')\n    ''\n\n    >>> WebRootCheck('/foo//bar////baz//')\n    '/foo/bar/baz'\n\n    >>> WebRootCheck('/foo/$%!')\n    Traceback (most recent call last):\n        ...\n    ValueError: Invalid web root: /foo/$%!\n    \"\"\"\n    p = re.sub('/+', '/', '/%s/' % path)[:-1]\n    if (p != CleanText(p, banned=CleanText.NONPATH).clean):\n        raise ValueError('Invalid web root: %s' % path)\n    return p\n\n\ndef FileCheck(path=None):\n    \"\"\"\n    Verify that a string is a valid path to a file, make it absolute.\n\n    >>> FileCheck('/etc/../etc/passwd')\n    '/etc/passwd'\n\n    >>> FileCheck('/')\n    Traceback (most recent call last):\n        ...\n    ValueError: Not a file: /\n    \"\"\"\n    if path in (None, 'None', 'none', ''):\n        return None\n    path = PathCheck(path)\n    if not os.path.isfile(path):\n        raise ValueError(_('Not a file: %s') % path)\n    return path\n\n\ndef DirCheck(path=None):\n    \"\"\"\n    Verify that a string is a valid path to a directory, make it absolute.\n\n    >>> DirCheck('/etc/../')\n    '/'\n\n    >>> DirCheck('/etc/passwd')\n    Traceback (most recent call last):\n        ...\n    ValueError: Not a directory: /etc/passwd\n    \"\"\"\n    if path in (None, 'None', 'none', ''):\n        return None\n    path = PathCheck(path)\n    if not os.path.isdir(path):\n        raise ValueError(_('Not a directory: %s') % path)\n    return path\n\n\ndef NewPathCheck(path):\n    \"\"\"\n    Verify that a string is a valid path to a directory, make it absolute.\n\n    >>> NewPathCheck('/magic')\n    '/magic'\n\n    >>> NewPathCheck('/no/such/path/magic')\n    Traceback (most recent call last):\n        ...\n    ValueError: File/directory does not exist: /no/such/path\n    \"\"\"\n    PathCheck(os.path.dirname(path))\n    return os.path.abspath(path)\n\ndef UrlCheck(url):\n    \"\"\"\n    Verify that a url parsed string has a valid uri scheme\n\n    >>> UrlCheck(\"http://mysite.com\")\n    'http://mysite.com'\n\n    >>> UrlCheck(\"/not-valid.net\")\n    Traceback (most recent call last):\n        ...\n    ValueError: Not a valid url: ...\n\n    >>> UrlCheck(\"tallnet://some-host.com\")\n    Traceback (most recent call last):\n        ...\n    ValueError: Not a valid url: tallnet://some-host.com\n    \"\"\"\n    uri = urlparse(url)\n    if not uri.scheme in URI_SCHEMES:\n        raise ValueError(_(\"Not a valid url: %s\") % url)\n    else:\n        return url\n\ndef EmailCheck(email):\n    \"\"\"\n    Verify that a string is a valid email\n\n    >>> EmailCheck(\"test@test.com\")\n    'test@test.com'\n    \"\"\"\n    if not EMAIL_RE.match(email):\n        raise ValueError(_(\"Not a valid e-mail: %s\") % email)\n    return email\n\n\ndef GPGKeyCheck(value):\n    \"\"\"\n    Strip a GPG fingerprint of all spaces, make sure it seems valid.\n    Will also accept e-mail addresses, for legacy reasons.\n\n    >>> GPGKeyCheck('User@Foo.com')\n    'User@Foo.com'\n\n    >>> GPGKeyCheck('1234 5678 abcd EF00')\n    '12345678ABCDEF00'\n\n    >>> GPGKeyCheck('12345678')\n    '12345678'\n\n    >>> GPGKeyCheck('B906 EA4B 8A28 15C4 F859  6F9F 47C1 3F3F ED73 5179')\n    'B906EA4B8A2815C4F8596F9F47C13F3FED735179'\n\n    >>> GPGKeyCheck('B906 8A28 15C4 F859  6F9F 47C1 3F3F ED73 5179')\n    Traceback (most recent call last):\n        ...\n    ValueError: Not a GPG key ID or fingerprint\n\n    >>> GPGKeyCheck('B906 8X28 1111 15C4 F859  6F9F 47C1 3F3F ED73 5179')\n    Traceback (most recent call last):\n        ...\n    ValueError: Not a GPG key ID or fingerprint\n    \"\"\"\n    value = value.replace(' ', '').replace('\\t', '').strip()\n    if value in ('!CREATE', '!PASSWORD'):\n        return value\n    try:\n        if len(value) not in (8, 16, 40):\n            raise ValueError(_('Not a GPG key ID or fingerprint'))\n        if re.match(r'^[0-9A-F]+$', value.upper()) is None:\n            raise ValueError(_('Not a GPG key ID or fingerprint'))\n    except ValueError:\n        try:\n            return EmailCheck(value)\n        except ValueError:\n            raise ValueError(_('Not a GPG key ID or fingerprint'))\n    return value.upper()\n\n\nclass IgnoreValue(Exception):\n    pass\n\n\ndef IgnoreCheck(data):\n    raise IgnoreValue()\n\n\nif __name__ == \"__main__\":\n    import doctest\n    import sys\n    result = doctest.testmod(optionflags=doctest.ELLIPSIS)\n    print('%s' % (result, ))\n    if result.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/conn_brokers.py",
    "content": "from __future__ import print_function\n# Connection brokers facilitate & manage incoming and outgoing connections.\n#\n# The idea is that code actually tells us what it wants to do, so we can\n# choose an appropriate mechanism for connecting or receiving incoming\n# connections.\n#\n# Libraries which use socket.create_connection can be monkey-patched\n# to use a broker on a connection-by-connection bases like so:\n#\n#     with broker.context(need=[broker.OUTGOING_CLEARTEXT,\n#                               broker.OUTGOING_SMTP]) as ctx:\n#         conn = somelib.connect(something)\n#         print 'Connected with encryption: %s' % ctx.encryption\n#\n# The context variable will then contain metadata about what sort of\n# connection was made.\n#\n# See the Capability class below for a list of attributes that can be\n# used to describe an outgoing (or incoming) connection.\n#\n# In particular, using the master broker will implement a prioritised\n# connection strategy where the most secure options are tried first and\n# things gracefully degrade. Protocols like IMAP, SMTP or POP3 will be\n# transparently upgraded to use STARTTLS.\n#\n# TODO:\n#    - Implement a TorBroker\n#    - Implement a PageKiteBroker\n#    - Implement HTTP/SMTP/IMAP/POP3 TLS upgrade-brokers\n#    - Prevent unbrokered socket.socket connections\n#\nimport datetime\nimport socket\nimport ssl\nimport subprocess\nimport sys\nimport threading\nimport time\nimport traceback\n\ntry:\n    import cryptography\n    import cryptography.hazmat.backends\n    import cryptography.hazmat.primitives.hashes\n    try:\n        import cryptography.x509 as cryptography_x509\n    except ImportError:\n        cryptography_x509 = None\nexcept ImportError:\n    cryptography = None\n\n# Import SOCKS proxy support...\ntry:\n    import sockschain as socks\nexcept ImportError:\n    try:\n        import socks\n    except ImportError:\n        socks = None\n\nimport mailpile.security as security\nfrom mailpile.i18n import gettext\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.commands import Command\nfrom mailpile.util import md5_hex, dict_merge, monkey_patch\nfrom mailpile.security import tls_sock_cert_sha256\n\n\n_ = lambda s: s\n\nKNOWN_ONION_MAP = {\n    'www.mailpile.is': 'clgs64523yi2bkhz.onion'\n}\n\n\noriginal_scc = socket.create_connection\nmonkey_clean_scc = socket.create_connection\nmonkey_thread_local = threading.local()\n\n\ndef MonkeySockCreateConn(*args, **kwargs):\n    if hasattr(monkey_thread_local, 'scc'):\n        cconn = (monkey_thread_local.scc or [monkey_clean_scc])[-1]\n    else:\n        cconn = monkey_clean_scc\n    return cconn(*args, **kwargs)\n\n\ndef _explain_encryption(sock):\n    try:\n        algo, proto, bits = sock.cipher()\n        return (\n            _('%(tls_version)s (%(bits)s bit %(algorithm)s)')\n        ) % {\n            'bits': bits,\n            'tls_version': proto,\n            'algorithm': algo}\n    except (ValueError, AttributeError):\n        return _('no encryption')\n\n\nclass Capability(object):\n    \"\"\"\n    These are constants defining different types of outgoing or incoming\n    connections. Brokers use these to describe what sort of connections they\n    are capable of handling, and calling code uses these to describe the\n    intent of network connection.\n    \"\"\"\n    OUTGOING_RAW = 'o:raw'      # Request this to avoid meddling brokers\n    OUTGOING_ENCRYPTED = 'o:e'  # Request this if sending encrypted data\n    OUTGOING_CLEARTEXT = 'o:c'  # Request this if sending clear-text data\n    OUTGOING_TRACKABLE = 'o:t'  # Reject this to require anonymity\n    OUTGOING_SMTP = 'o:smtp'    # These inform brokers what protocol is being\n    OUTGOING_IMAP = 'o:imap'    # .. used, to allow protocol-specific features\n    OUTGOING_POP3 = 'o:pop3'    # .. such as enabling STARTTLS or upgrading\n    OUTGOING_HTTP = 'o:http'    # .. HTTP to HTTPS.\n    OUTGOING_HTTPS = 'o:https'  # ..\n    OUTGOING_SMTPS = 'o:smtps'  # ..\n    OUTGOING_POP3S = 'o:pop3s'  # ..\n    OUTGOING_IMAPS = 'o:imaps'  # ..\n\n    INCOMING_RAW = 20\n    INCOMING_LOCALNET = 21\n    INCOMING_INTERNET = 22\n    INCOMING_DARKNET = 23\n    INCOMING_SMTP = 24\n    INCOMING_IMAP = 25\n    INCOMING_POP3 = 26\n    INCOMING_HTTP = 27\n    INCOMING_HTTPS = 28\n\n    ALL_OUTGOING = set([OUTGOING_RAW, OUTGOING_ENCRYPTED, OUTGOING_CLEARTEXT,\n                        OUTGOING_TRACKABLE,\n                        OUTGOING_SMTP, OUTGOING_IMAP, OUTGOING_POP3,\n                        OUTGOING_SMTPS, OUTGOING_IMAPS, OUTGOING_POP3S,\n                        OUTGOING_HTTP, OUTGOING_HTTPS])\n\n    ALL_OUTGOING_ENCRYPTED = set([OUTGOING_RAW, OUTGOING_TRACKABLE,\n                                  OUTGOING_ENCRYPTED,\n                                  OUTGOING_HTTPS, OUTGOING_SMTPS,\n                                  OUTGOING_POP3S, OUTGOING_IMAPS])\n\n    ALL_INCOMING = set([INCOMING_RAW, INCOMING_LOCALNET, INCOMING_INTERNET,\n                        INCOMING_DARKNET, INCOMING_SMTP, INCOMING_IMAP,\n                        INCOMING_POP3, INCOMING_HTTP, INCOMING_HTTPS])\n\n\nclass CapabilityFailure(IOError):\n    \"\"\"\n    This exception is raised when capability requirements can't be satisfied.\n    It extends the IOError, so unaware code just thinks the network is lame.\n\n    >>> try:\n    ...     raise CapabilityFailure('boo')\n    ... except IOError:\n    ...     print('ok')\n    ok\n    \"\"\"\n    pass\n\n\nclass Url(str):\n    def __init__(self, *args, **kwargs):\n        str.__init__(self, *args, **kwargs)\n        self.encryption = None\n        self.anonymity = None\n        self.on_internet = False\n        self.on_localnet = False\n        self.on_darknet = None\n\n\nclass BrokeredContext(object):\n    \"\"\"\n    This is the context returned by the BaseConnectionBroker.context()\n    method. It takes care of monkey-patching the socket.create_connection\n    method and then cleaning the mess up afterwards, and collecting metadata\n    from the brokers describing what sort of connection was established.\n\n    WARNING: In spite of our best efforts (locking, etc.), mixing brokered\n             and unbrokered code will not work well at all. The patching\n             approach also limits us to initiating one outgoing connection\n             at a time.\n    \"\"\"\n    def __init__(self, broker, need=None, reject=None, oneshot=False):\n        self._broker = broker\n        self._need = need\n        self._reject = reject\n        self._oneshot = oneshot\n        self._reset()\n\n    def __str__(self):\n        hostport = '%s:%s' % (self.address or ('unknown', 'none'))\n        if self.error:\n            return _('Failed to connect to %s: %s') % (hostport, self.error)\n\n        if self.anonymity:\n            network = self.anonymity\n        elif self.on_darknet:\n            network = self.on_darknet\n        elif self.on_localnet:\n            network = _('the local network')\n        elif self.on_internet:\n            network = _('the Internet')\n        else:\n            return _('Attempting to connect to %(host)s') % {'host': hostport}\n\n        return _('Connected to %(host)s over %(network)s with %(encryption)s.'\n                 ) % {\n            'network': network,\n            'host': hostport,\n            'encryption': self.encryption or _('no encryption')\n        }\n\n    def _reset(self):\n        self.error = None\n        self.address = None\n        self.encryption = None\n        self.anonymity = None\n        self.on_internet = False\n        self.on_localnet = False\n        self.on_darknet = None\n\n    def __enter__(self, *args, **kwargs):\n        def create_brokered_conn(address, *a, **kw):\n            self._reset()\n            return self._broker.create_conn_with_caps(\n                address, self, self._need, self._reject, *a, **kw)\n\n        if hasattr(monkey_thread_local, 'scc'):\n            monkey_thread_local.scc.append(create_brokered_conn)\n        else:\n            monkey_thread_local.scc = [create_brokered_conn]\n            if socket.create_connection != MonkeySockCreateConn:\n                socket.create_connection = MonkeySockCreateConn\n\n        return self\n\n    def __exit__(self, *args, **kwargs):\n        monkey_thread_local.scc.pop(-1)\n\n\nclass BaseConnectionBroker(Capability):\n    \"\"\"\n    This is common code used by most of the connection brokers.\n    \"\"\"\n    SUPPORTS = []\n\n    def __init__(self, master=None):\n        self.supports = list(self.SUPPORTS)[:]\n        self.master = master\n        self._config = None\n        self._debug = master._debug if (master is not None) else None\n\n    def configure(self):\n        self.supports = list(self.SUPPORTS)[:]\n\n    def set_config(self, config):\n        self._config = config\n        self.configure()\n\n    def config(self):\n        if self._config is not None:\n            return self._config\n        if self.master is not None:\n            return self.master.config()\n        return None\n\n    def _raise_or_none(self, exc, why):\n        if exc is not None:\n            raise exc(why)\n        return None\n\n    def _check(self, need, reject, _raise=CapabilityFailure):\n        for n in need or []:\n            if n not in self.supports:\n                if self._debug is not None:\n                    self._debug('%s: lacking capabilty %s' % (self, n))\n                return self._raise_or_none(_raise, 'Lacking %s' % n)\n        for n in reject or []:\n            if n in self.supports:\n                if self._debug is not None:\n                    self._debug('%s: unwanted capabilty %s' % (self, n))\n                return self._raise_or_none(_raise, 'Unwanted %s' % n)\n        if self._debug is not None:\n            self._debug('%s: checks passed!' % (self, ))\n        return self\n\n    def _describe(self, context, conn):\n        return conn\n\n    def debug(self, val):\n        self._debug = val\n        return self\n\n    def context(self, need=None, reject=None, oneshot=False):\n        return BrokeredContext(self, need=need, reject=reject, oneshot=oneshot)\n\n    def create_conn_with_caps(self, address, context, need, reject,\n                              *args, **kwargs):\n        if context.address is None:\n            context.address = address\n        conn = self._check(need, reject)._create_connection(context, address,\n                                                            *args, **kwargs)\n        return self._describe(context, conn)\n\n    def create_connection(self, address, *args, **kwargs):\n        n = kwargs.get('need', None)\n        r = kwargs.get('reject', None)\n        c = kwargs.get('context', None)\n        for kw in ('need', 'reject', 'context'):\n            if kw in kwargs:\n                del kwargs[kw]\n        return self.create_conn_with_caps(address, c, n, r, *args, **kwargs)\n\n    # Should implement socket.create_connection or an equivalent.\n    # Context, if not None, should be informed with metadata about the\n    # connection.\n    def _create_connection(self, context, address, *args, **kwargs):\n        raise NotImplementedError('Subclasses override this')\n\n    def get_urls(self, listening_fd,\n                 need=None, reject=None, **kwargs):\n        try:\n            return self._check(need, reject)._get_urls(listening_fd, **kwargs)\n        except CapabilityFailure:\n            return []\n\n    # Returns a list of Url objects for this listener\n    def _get_urls(self, listening_fd,\n                  proto=None, username=None, password=None):\n        raise NotImplementedError('Subclasses override this')\n\n\nclass TcpConnectionBroker(BaseConnectionBroker):\n    \"\"\"\n    The basic raw TCP/IP connection broker.\n\n    The only clever thing this class does, is to avoid trying to connect\n    to .onion addresses, preventing that from leaking over DNS.\n    \"\"\"\n    SUPPORTS = (\n        # Normal TCP/IP is not anonymous, and we do not have incoming\n        # capability unless we have a public IP.\n        (Capability.ALL_OUTGOING) |\n        (Capability.ALL_INCOMING - set([Capability.INCOMING_INTERNET]))\n    )\n    LOCAL_NETWORKS = ['localhost', '127.0.0.1', '::1']\n    FIXED_NO_PROXY_LIST = ['localhost', '127.0.0.1', '::1']\n    DEBUG_FMT = '%s: Raw TCP conn to: %s'\n\n    def configure(self):\n        BaseConnectionBroker.configure(self)\n        # FIXME: If our config indicates we have a public IP, add the\n        #        INCOMING_INTERNET capability.\n\n    def _describe(self, context, conn):\n        try:\n            (host, port) = conn.getpeername()[:2]\n            if host.lower() in self.LOCAL_NETWORKS:\n                context.on_localnet = True\n            else:\n                context.on_internet = True\n        except TypeError:\n            # conn.getpeername() may return None\n            pass\n        context.encryption = None\n        return conn\n\n    def _in_no_proxy_list(self, address):\n        cfg_no_proxy = self.config().get_proxy_settings().get('no_proxy', '')\n        no_proxy = (self.FIXED_NO_PROXY_LIST +\n                    [a.lower().strip() for a in cfg_no_proxy.split(',')])\n        return (address[0].lower() in no_proxy)\n\n    def _avoid(self, address):\n        proxy_settings = self.config().get_proxy_settings()\n        if (proxy_settings['protocol'] not in  ('none', 'unknown', 'system')\n                and not proxy_settings.get('fallback', False)\n                and not self._in_no_proxy_list(address)):\n            raise CapabilityFailure('Proxy fallback is disabled')\n\n    def _broker_avoid(self, address):\n        if address[0].endswith('.onion'):\n            raise CapabilityFailure('Cannot connect to .onion addresses')\n\n    def _conn(self, address, *args, **kwargs):\n        clean_kwargs = dict((k, v) for k, v in kwargs.iteritems()\n                            if not k.startswith('_'))\n        return original_scc(address, *args, **clean_kwargs)\n\n    def _create_connection(self, context, address, *args, **kwargs):\n        self._avoid(address)\n        self._broker_avoid(address)\n        if self._debug is not None:\n            self._debug(self.DEBUG_FMT % (self, address))\n        return self._conn(address, *args, **kwargs)\n\n\nclass SocksConnBroker(TcpConnectionBroker):\n    \"\"\"\n    This broker offers the same services as the TcpConnBroker, but over a\n    SOCKS connection.\n    \"\"\"\n    SUPPORTS = []\n    CONFIGURED = Capability.ALL_OUTGOING\n    PROXY_TYPES = ('socks5', 'http', 'socks4')\n    DEFAULT_PROTO = 'socks5'\n\n    DEBUG_FMT = '%s: Raw SOCKS5 conn to: %s'\n    IOERROR_FMT = _('SOCKS error, %s')\n    IOERROR_MSG = {\n        'timed out': _('timed out'),\n        'Host unreachable': _('host unreachable'),\n        'Connection refused': _('connection refused')\n    }\n\n    def _describe(self, context, conn):\n        context.on_darknet = ('proxy (%s:%d)'\n            % (self.proxy_config['host'], self.proxy_config['port']))\n        return conn\n\n    def __init__(self, *args, **kwargs):\n        TcpConnectionBroker.__init__(self, *args, **kwargs)\n        self.proxy_config = None\n        self.typemap = {}\n\n    def configure(self):\n        BaseConnectionBroker.configure(self)\n        proxy_settings = self.config().get_proxy_settings()\n        if proxy_settings.get('protocol') in self.PROXY_TYPES:\n            self.proxy_config = proxy_settings\n            self.supports = list(self.CONFIGURED)[:]\n            self.typemap = {\n                'socks5': socks.PROXY_TYPE_SOCKS5,\n                'socks4': socks.PROXY_TYPE_SOCKS4,\n                'tor': socks.PROXY_TYPE_SOCKS5,       # For TorConnBroker\n                'tor-risky': socks.PROXY_TYPE_SOCKS5, # For TorConnBroker\n                'http': socks.PROXY_TYPE_HTTP}\n        else:\n            self.proxy_config = None\n            self.supports = []\n\n    def _auth_args(self):\n        return {\n            'username': self.proxy_config.get('username') or None,\n            'password': self.proxy_config.get('password') or None}\n\n    def _avoid(self, address):\n        if self._in_no_proxy_list(address):\n            raise CapabilityFailure('Proxy to %s:%s disabled by policy'\n                                    ) % address\n\n    def _fix_address_tuple(self, address):\n        return (str(address[0]), int(address[1]))\n\n    def _conn(self, address, timeout=None, source_address=None, **kwargs):\n        sock = socks.socksocket()\n        proxytype = self.typemap.get(self.proxy_config.get('protocol'),\n                                     self.typemap[self.DEFAULT_PROTO])\n        sock.setproxy(proxytype=proxytype,\n                      addr=self.proxy_config.get('host'),\n                      port=int(self.proxy_config.get('port', 0)),\n                      rdns=True,\n                      **self._auth_args())\n        if timeout and timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:\n            sock.settimeout(float(timeout))\n        if source_address:\n            raise CapabilityFailure('Cannot bind source address')\n        try:\n            address = self._fix_address_tuple(address)\n            sock.connect(address)\n        except socks.ProxyError as e:\n            if self._debug is not None:\n                self._debug(traceback.format_exc())\n            code, msg = e.message\n            raise IOError(_(self.IOERROR_FMT\n                            ) % (_(self.IOERROR_MSG.get(msg, msg)), ))\n        return sock\n\n\nclass TorConnBroker(SocksConnBroker):\n    \"\"\"\n    This broker offers the same services as the TcpConnBroker, but over Tor.\n\n    This removes the \"trackable\" capability, so requests that reject it can\n    find their way here safely...\n\n    This broker only volunteers to carry encrypted traffic, because Tor\n    exit nodes may be hostile.\n    \"\"\"\n    SUPPORTS = []\n    CONFIGURED = (Capability.ALL_OUTGOING_ENCRYPTED\n                  - set([Capability.OUTGOING_TRACKABLE]))\n    REJECTS = None\n    PROXY_TYPES = ('tor', )\n    DEFAULT_PROTO = 'tor'\n\n    DEBUG_FMT = '%s: Raw Tor conn to: %s'\n    IOERROR_FMT = _('Tor error, %s')\n    IOERROR_MSG = dict_merge(SocksConnBroker.IOERROR_MSG, {\n        'bad input': _('connection refused')  # FIXME: Is this right?\n    })\n\n    def _describe(self, context, conn):\n        context.on_darknet = 'Tor'\n        context.anonymity = 'Tor'\n        return conn\n\n    def _auth_args(self):\n        # FIXME: Tor uses the auth information as a signal to change\n        #        circuits. We may have use for this at some point.\n        return {}\n\n    def _fix_address_tuple(self, address):\n        host = str(address[0])\n        return (KNOWN_ONION_MAP.get(host.lower(), host), int(address[1]))\n\n    def _broker_avoid(self, address):\n        # Disable the avoiding of .onion addresses added above\n        pass\n\n\nclass TorRiskyBroker(TorConnBroker):\n    \"\"\"\n    This differs from the TorConnBroker in that it will allow \"cleartext\"\n    traffic to anywhere - this is dangerous, because exit nodes could mess\n    with our traffic.\n    \"\"\"\n    CONFIGURED = (Capability.ALL_OUTGOING\n                  - set([Capability.OUTGOING_TRACKABLE]))\n    DEBUG_FMT = '%s: Risky Tor conn to: %s'\n    PROXY_TYPES = ('tor-risky', )\n    DEFAULT_PROTO = 'tor-risky'\n\n\nclass TorOnionBroker(TorConnBroker):\n    \"\"\"\n    This broker offers the same services as the TcpConnBroker, but over Tor.\n\n    This removes the \"trackable\" capability, so requests that reject it can\n    find their way here safely...\n\n    This differs from the TorConnBroker in that it will allow \"cleartext\"\n    traffic, since we trust the traffic never leaves the Tor network and\n    we don't have hostile exits to worry about.\n    \"\"\"\n    SUPPORTS = []\n    CONFIGURED = (Capability.ALL_OUTGOING\n                  - set([Capability.OUTGOING_TRACKABLE]))\n    REJECTS = None\n    DEBUG_FMT = '%s: Tor onion conn to: %s'\n    PROXY_TYPES = ('tor', 'tor-risky')\n\n    def _broker_avoid(self, address):\n        host = KNOWN_ONION_MAP.get(address[0], address[0])\n        if not host.endswith('.onion'):\n            raise CapabilityFailure('Can only connect to .onion addresses')\n\n\nclass BaseConnectionBrokerProxy(TcpConnectionBroker):\n    \"\"\"\n    Brokers based on this establish a RAW connection and then manipulate it\n    in some way, generally to implement proxying or TLS wrapping.\n    \"\"\"\n    SUPPORTS = []\n    WANTS = [Capability.OUTGOING_RAW]\n    REJECTS = None\n\n    def _proxy_address(self, address):\n        return address\n\n    def _proxy(self, conn):\n        raise NotImplementedError('Subclasses override this')\n\n    def _wrap_ssl(self, conn):\n        if self._debug is not None:\n            self._debug('%s: Wrapping socket with SSL' % (self, ))\n        return ssl.wrap_socket(conn)\n\n    def _create_connection(self, context, address, *args, **kwargs):\n        address = self._proxy_address(address)\n        if self.master:\n            conn = self.master.create_conn_with_caps(\n                address, context, self.WANTS, self.REJECTS, *args, **kwargs)\n        else:\n            conn = TcpConnectionBroker._create_connection(self, context,\n                                                          address,\n                                                          *args, **kwargs)\n        return self._proxy(conn)\n\n\nclass AutoTlsConnBroker(BaseConnectionBrokerProxy):\n    \"\"\"\n    This broker tries to auto-upgrade connections to use TLS, or at\n    least do the SSL handshake here so we can record info about it.\n    \"\"\"\n    SUPPORTS = [Capability.OUTGOING_HTTP, Capability.OUTGOING_HTTPS,\n                Capability.OUTGOING_IMAPS, Capability.OUTGOING_SMTPS,\n                Capability.OUTGOING_POP3S]\n    WANTS = [Capability.OUTGOING_RAW, Capability.OUTGOING_ENCRYPTED]\n\n    def _describe(self, context, conn):\n        context.encryption = _explain_encryption(conn)\n        return conn\n\n    def _proxy_address(self, address):\n        if address[0].endswith('.onion'):\n            raise CapabilityFailure('I do not like .onion addresses')\n        if int(address[1]) != 443:\n            # FIXME: Import HTTPS Everywhere database to make this work?\n            raise CapabilityFailure('Not breaking clear-text HTTP yet')\n        return address\n\n    def _proxy(self, conn):\n        return self._wrap_ssl(conn)\n\n\nclass AutoSmtpStartTLSConnBroker(BaseConnectionBrokerProxy):\n    pass\n\n\nclass AutoImapStartTLSConnBroker(BaseConnectionBrokerProxy):\n    pass\n\n\nclass AutoPop3StartTLSConnBroker(BaseConnectionBrokerProxy):\n    pass\n\n\nclass MasterBroker(BaseConnectionBroker):\n    \"\"\"\n    This is the master broker. It implements a prioritised list of\n    connection brokers, each of which is tried in turn until a match\n    is found. As such, more secure brokers should register themselves\n    with a higher priority - if they fail, we fall back to less\n    secure connection strategies.\n    \"\"\"\n    def __init__(self, *args, **kwargs):\n        BaseConnectionBroker.__init__(self, *args, **kwargs)\n        self.brokers = []\n        self.history = []\n        self._debug = self._debugger\n        self.debug_callback = None\n\n    def configure(self):\n        for prio, cb in self.brokers:\n            cb.configure()\n\n    def _debugger(self, *args, **kwargs):\n        if self.debug_callback is not None:\n            self.debug_callback(*args, **kwargs)\n\n    def register_broker(self, priority, cb):\n        \"\"\"\n        Brokers should register themselves with priorities as follows:\n\n           - 1000-1999: Content-agnostic raw connections\n           - 3000-3999: Secure network layers: VPNs, Tor, I2P, ...\n           - 5000-5999: Proxies required to reach the wider Internet\n           - 7000-7999: Protocol enhancments (non-security related)\n           - 9000-9999: Security-related protocol enhancements\n\n        \"\"\"\n        self.brokers.append((priority, cb(master=self)))\n        self.brokers.sort()\n        self.brokers.reverse()\n\n    def get_fd_context(self, fileno):\n        for t, fd, context in reversed(self.history):\n            if fd == fileno:\n                return context\n        return BrokeredContext(self)\n\n    def create_conn_with_caps(self, address, context, need, reject,\n                              *args, **kwargs):\n        history_event = kwargs.get('_history_event')\n        if history_event is None:\n            history_event = [int(time.time()), None, context]\n            self.history = self.history[-50:]\n            self.history.append(history_event)\n            kwargs['_history_event'] = history_event\n        else:\n            history_event[-1] = context\n\n        if context.address is None:\n            context.address = address\n\n        et = v = t = None\n        for prio, cb in self.brokers:\n            try:\n                conn = cb.debug(self._debug).create_conn_with_caps(\n                    address, context, need, reject, *args, **kwargs)\n                if conn:\n                    history_event[1] = conn.fileno()\n                    return conn\n            except (CapabilityFailure, NotImplementedError):\n                # These are internal; we assume they're already logged\n                # for debugging but don't bother the user with them.\n                pass\n            except:\n                et, v, t = sys.exc_info()\n        if et is not None:\n            context.error = '%s' % v\n            raise et, v, t\n\n        context.error = _('No connection method found')\n        raise CapabilityFailure(context.error)\n\n    def get_urls(self, listening_fd, need=None, reject=None):\n        urls = []\n        for prio, cb in self.brokers:\n            urls.extend(cb.debug(self._debug).get_urls(listening_fd))\n        return urls\n\n\ndef DisableUnbrokeredConnections():\n    \"\"\"Enforce the use of brokers EVERYWHERE!\"\"\"\n    def CreateConnWarning(*args, **kwargs):\n        print('*** socket.create_connection used without a broker ***')\n        traceback.print_stack()\n        raise IOError('FIXME: Please use within a broker context')\n    monkey_clean_scc = CreateConnWarning\n    socket.create_connection = CreateConnWarning\n\n\nclass NetworkHistory(Command):\n    \"\"\"Show recent network history\"\"\"\n    SYNOPSIS = (None, 'logs/network', 'logs/network', None)\n    ORDER = ('Internals', 6)\n    CONFIG_REQUIRED = False\n    IS_USER_ACTIVITY = False\n\n    class CommandResult(Command.CommandResult):\n        def as_text(self):\n            if self.result:\n                def fmt(result):\n                    dt = datetime.datetime.fromtimestamp(result[0])\n                    return '%2.2d:%2.2d %s' % (dt.hour, dt.minute, result[-1])\n                return '\\n'.join(fmt(r) for r in self.result)\n            return _('No network events recorded')\n\n    def command(self):\n        return self._success(_('Listed recent network events'),\n                             result=Master.history)\n\n\nclass GetTlsCertificate(Command):\n    \"\"\"Fetch and parse a server's TLS certificate\"\"\"\n    SYNOPSIS = (None, 'crypto/tls/getcert', 'crypto/tls/getcert', '[--tofu-save|--tofu-clear]')\n    ORDER = ('Internals', 6)\n    CONFIG_REQUIRED = False\n    IS_USER_ACTIVITY = False\n    HTTP_CALLABLE = ('GET', 'POST')\n    HTTP_QUERY_VARS = {\n        'tofu-clear': 'Remove from TOFU certificate store',\n        'tofu-save': 'Save to our TOFU certificate store',\n        'host': 'Name of remote server'\n    }\n\n    class CommandResult(Command.CommandResult):\n        def as_text(self):\n            if self.result:\n                def fmt(h, r):\n                    return '%s:\\t%s' % (h, r[-1] or r[1])\n                return '\\n'.join(fmt(h, r) for h, r in self.result.iteritems())\n            return _('No certificates found')\n\n    def command(self):\n        if self.data.get('_method', 'POST') != 'POST':\n            # Allow HTTP GET as a no-op, so the user can see a friendly form.\n            return self._success(_('Examine TLS certificates'))\n\n        config = self.session.config\n        tofu_save = self.data.get('tofu-save', '--tofu-save' in self.args)\n        tofu_clear = self.data.get('tofu-clear', '--tofu-clear' in self.args)\n        hosts = (list(s for s in self.args if not s.startswith('--')) +\n                 self.data.get('host', []))\n\n        def ts(t):\n            return int(time.mktime(t.timetuple()))\n\n        def oidName(oid):\n            return {\n                '2.5.4.3': 'commonName',\n                '2.5.4.4': 'surname',\n                '2.5.4.5': 'serialNumber',\n                '2.5.4.6': 'countryName',\n                '2.5.4.7': 'localityName',\n                '2.5.4.8': 'stateOrProvinceName',\n                '2.5.4.9': 'streetAddress',\n                '2.5.4.10': 'organizationName',\n                '2.5.4.11': 'organizationalUnitName'\n                }.get(oid.dotted_string,\n                      getattr(oid, '_name', oid.dotted_string))\n\n        def oidmap(entries):\n            return dict((oidName(e.oid), e.value) for e in entries)\n\n        def subjmap(stext):\n            def subjpair(kv):\n                k, v = kv.split('=', 1)\n                return ({'CN': 'commonName',\n                         'C': 'countryName',\n                         'ST': 'stateOrProvinceName',\n                         'L': 'localityName',\n                         'O': 'organizationName',\n                         'OU': 'organizationalUnitName'}.get(k, k), v)\n            parts = []\n            for part in stext.strip().split('/'):\n                if '=' in part:\n                    parts.append(part)\n                elif parts:\n                    parts[-1] += '/' + part\n            return dict(subjpair(kv) for kv in parts)\n\n        def fingerprint(cert_sha_256):\n            fp = ['%2.2x' % ord(b) for b in cert_sha_256]\n            fp2 = [fp[i*2] + fp[i*2 + 1] for i in range(0, len(fp)/2)]\n            return fp2\n\n        def pts(t):\n            dt, tz = t.rsplit(' ', 1)  # Strip off the timezone\n            return datetime.datetime.strptime(dt, '%b %d %H:%M:%S %Y')\n\n        def parse_pem_cert(cert_pem, s256):\n            cert_sha_256 = s256.decode('base64')\n            now = datetime.datetime.today()\n            if cryptography_x509 is None:\n                # Shell out to openssl, boo.\n                (stdout, stderr) = subprocess.Popen(\n                    ['openssl', 'x509',\n                        '-subject', '-issuer', '-dates', '-noout'],\n                    stdout=subprocess.PIPE, stderr=subprocess.PIPE,\n                    stdin=subprocess.PIPE).communicate(input=str(cert_pem))\n                if not stdout:\n                    raise ValueError(stderr)\n                details = dict(l.split('=', 1)\n                               for l in stdout.strip().splitlines()\n                               if l and '=' in l)\n                details['notAfter'] = pts(details['notAfter'])\n                details['notBefore'] = pts(details['notBefore'])\n                return {\n                    'fingerprint': fingerprint(cert_sha_256),\n                    'date_matches': False,\n                    'date_matches': ((details['notBefore'] < now) and\n                                     (details['notAfter'] > now)),\n                    'not_valid_before': ts(details['notBefore']),\n                    'not_valid_after': ts(details['notAfter']),\n                    'subject': subjmap(details['subject']),\n                    'issuer': subjmap(details['issuer'])}\n            else:\n                parsed = cryptography_x509.load_pem_x509_certificate(\n                    str(cert_pem),\n                    cryptography.hazmat.backends.default_backend())\n                return {\n                    'fingerprint': fingerprint(cert_sha_256),\n                    'date_matches': ((parsed.not_valid_before < now) and\n                                     (parsed.not_valid_after > now)),\n                    'not_valid_before': ts(parsed.not_valid_before),\n                    'not_valid_after': ts(parsed.not_valid_after),\n                    'subject': oidmap(parsed.subject),\n                    'issuer': oidmap(parsed.issuer)}\n\n        def attempt_starttls(addr, sock):\n            # Attempt a minimal SMTP interaction, for STARTTLS support\n\n            # We attempt a non-blocking peek unless we're sure this is\n            # a port normally used for clear-text SMTP.\n            peeking = int(addr[1]) not in (25, 587, 143)\n\n            # If this isn't a known TLS port, then we sleep a bit to give a\n            # greeting time to arrive.\n            if peeking and int(addr[1]) not in (443, 465, 993, 995):\n                time.sleep(0.4)\n\n            try:\n                # Look for an SMTP (or IMAP) greeting\n                if peeking:\n                    sock.setblocking(0)\n                    # Note: This will throw a TypeError if we are connected\n                    #       over Tor (or other SOCKS).\n                    first = sock.recv(1024, socket.MSG_PEEK) or ''\n                else:\n                    sock.settimeout(10)\n                    first = sock.recv(1024) or ''\n\n                if first[:4] == '220 ':\n                    # This is an SMTP greeting\n                    if peeking:\n                        sock.setblocking(1)\n                        sock.recv(1024)\n                    sock.sendall('EHLO example.com\\r\\n')\n                    if (sock.recv(1024) or '')[:1] == '2':\n                        sock.sendall('STARTTLS\\r\\n')\n                        sock.recv(1024)\n\n                elif first[:4] == '* OK':\n                    # This is an IMAP4 greeting\n                    if peeking:\n                        sock.setblocking(1)\n                        sock.recv(1024)\n                    sock.sendall('* STARTTLS\\r\\n')\n                    sock.recv(1024)\n\n            except (TypeError, IOError, OSError):\n                pass\n            finally:\n                sock.setblocking(1)\n\n        certs = {}\n        ok = changes = 0\n        for host in hosts:\n            try:\n                addr = host.replace(' ', '').split(':') + ['443']\n                addr = (addr[0], int(addr[1]))\n\n                try:\n                    with Master.context(need=[Master.OUTGOING_ENCRYPTED,\n                                              Master.OUTGOING_RAW]) as ctx:\n                        sock = socket.create_connection(addr, timeout=30)\n                    attempt_starttls(addr, sock)\n                    ssls = ssl.wrap_socket(sock, use_web_ca=True, tofu=False)\n                    hostname_matches = True\n                    cert_validated = True\n\n                except (ssl.SSLError, ssl.CertificateError) as e:\n                    if isinstance(e, ssl.CertificateError):\n                        cert_validated = True\n                        hostname_matches = False\n                    else:\n                        cert_validated = False\n                        hostname_matches = 'unknown'\n\n                    with Master.context(need=[Master.OUTGOING_ENCRYPTED,\n                                              Master.OUTGOING_RAW]) as ctx:\n                        sock = socket.create_connection(addr, timeout=30)\n                    attempt_starttls(addr, sock)\n                    ssls = ssl.wrap_socket(sock, use_web_ca=False, tofu=False)\n\n                cert = ssls.getpeercert(True)\n                s256 = tls_sock_cert_sha256(cert=cert)\n                ssls.close()\n\n                cfg_key = md5_hex('%s:%d' % addr)\n                if tofu_clear:\n                    if cfg_key in config.tls.keys():\n                        del config.tls[cfg_key]\n                        changes += 1\n                if tofu_save:\n                    if cfg_key not in config.tls.keys():\n                        config.tls[cfg_key] = {'server': '%s:%d' % addr}\n                    cert_tofu = config.tls[cfg_key]\n                    cert_tofu.use_web_ca = False\n                    cert_tofu.accept_certs.append(s256)\n                    changes += 1\n                else:\n                    cert_tofu = config.tls.get(cfg_key, {})\n\n                tofu_seen = s256 in cert_tofu.get('accept_certs', [])\n                using_tofu = not cert_tofu.get('use_web_ca', True)\n                cert = {\n                    'current_time': int(time.time()),\n                    'cert_validated': cert_validated,\n                    'hostname_matches': hostname_matches,\n                    'tofu_seen': tofu_seen,\n                    'using_tofu': using_tofu,\n                    'tofu_invalid': (using_tofu and not tofu_seen),\n                    'pem': ssl.DER_cert_to_PEM_cert(cert)}\n\n                cert.update(parse_pem_cert(cert['pem'], s256))\n\n                certs[host] = (True, s256, cert, None)\n                ok += 1\n            except Exception as e:\n                certs[host] = (\n                    False, _('Failed to fetch certificate'), unicode(e),\n                    traceback.format_exc())\n\n        if changes:\n            self._background_save(config=True)\n\n        if ok:\n            return self._success(_('Downloaded TLS certificates'),\n                                 result=certs)\n        else:\n            return self._error(_('Failed to download TLS certificates'),\n                               result=certs)\n\n\ndef SslWrapOnlyOnce(org_sslwrap, sock, *args, **kwargs):\n    \"\"\"\n    Since we like to wrap things our own way, this make ssl.wrap_socket\n    into a no-op in the cases where we've alredy wrapped a socket.\n    \"\"\"\n    if not isinstance(sock, ssl.SSLSocket):\n        ctx = Master.get_fd_context(sock.fileno())\n        try:\n            if 'server_hostname' not in kwargs:\n                kwargs['server_hostname'] = ctx.address[0]\n            sock = org_sslwrap(sock, *args, **kwargs)\n            ctx.encryption = _explain_encryption(sock)\n        except (socket.error, IOError, ssl.SSLError, ssl.CertificateError) as e:\n            ctx.error = '%s' % e\n            raise\n    return sock\n\n\ndef SslContextWrapOnlyOnce(org_ctxwrap, self, sock, *args, **kwargs):\n    return SslWrapOnlyOnce(\n        lambda s, *a, **kwa: org_ctxwrap(self, s, *a, **kwa),\n        sock, *args, **kwargs)\n\n\n_ = gettext\n\n\nif __name__ != \"__main__\":\n    Master = MasterBroker()\n    register = Master.register_broker\n    register(1000, TcpConnectionBroker)\n    register(9500, AutoTlsConnBroker)\n    register(9500, AutoSmtpStartTLSConnBroker)\n    register(9500, AutoImapStartTLSConnBroker)\n    register(9500, AutoPop3StartTLSConnBroker)\n\n    if socks is not None:\n        register(1500, SocksConnBroker)\n        register(3500, TorConnBroker)\n        register(3500, TorRiskyBroker)\n        register(3500, TorOnionBroker)\n\n    # Note: At this point we have already imported security, which\n    #       also monkey-patches these same functions. This is a good\n    #       thing and is deliberate. :-)\n    ssl.wrap_socket = monkey_patch(ssl.wrap_socket, SslWrapOnlyOnce)\n    if hasattr(ssl, 'SSLContext'):\n        ssl.SSLContext.wrap_socket = monkey_patch(\n           ssl.SSLContext.wrap_socket, SslContextWrapOnlyOnce)\n\n    from mailpile.plugins import PluginManager\n    _plugins = PluginManager(builtin=__file__)\n    _plugins.register_commands(NetworkHistory, GetTlsCertificate)\n\nelse:\n    import doctest\n    import sys\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                              extraglobs={})\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/crypto/__init__.py",
    "content": ""
  },
  {
    "path": "mailpile/crypto/aes_utils.py",
    "content": "from __future__ import print_function\n# This is a compatibility wrapper for using whatever AES library is handy.\n# By default we support Cryptography and pyCrypto, with a preference for\n# Cryptography.\n\n# IMPORTANT:\n#\n# We currently only implement AES CTR mode, since this code is primarily\n# being used to write data to disk for long-term storage; the malleability\n# of CTR is considered a feature; if a bit gets flipped that doesn't destroy\n# all of the following blocks.\n#\n# This does mean we need to take special care with our IVs/nonces!\n#\nimport os\nimport struct\nfrom hashlib import md5\n\n\ndef make_cryptography_utils():\n    import os\n    import cryptography.hazmat.backends\n    from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes\n\n    def _aes_ctr(key, nonce):\n        # Notes:\n        #\n        # The funky business with the prefixed nonce is because the first\n        # iteration of this code used pycrypto's Crypto.Util.Counter with\n        # the prefix argument. Cryptography doesn't have such a counter API,\n        # but if we carefully set the nonce we can achieve compatibility.\n        #\n        # The MD5 digests save the caller from having to know our internal\n        # size requirements; AES wants 128, we just mix all the bits we're\n        # given. We expect the input to already be strongly random (so MD5's\n        # weaknesses shouldn't matter), but it may of the wrong size.\n        #\n        hashed_key = md5(key).digest()\n        prefixed_nonce = md5(nonce).digest()[:8] + '\\0\\0\\0\\0\\0\\0\\0\\1'\n        return Cipher(\n            algorithms.AES(hashed_key),\n            modes.CTR(prefixed_nonce),\n            backend=cryptography.hazmat.backends.default_backend())\n\n    def aes_ctr_encryptor(key, nonce):\n        return _aes_ctr(key, nonce).encryptor().update\n\n    def aes_ctr_decryptor(key, nonce):\n        return _aes_ctr(key, nonce).decryptor().update\n\n    return aes_ctr_encryptor, aes_ctr_decryptor\n\n\ndef make_pycrypto_utils():\n    from Crypto.Cipher import AES\n    from Crypto.Util import Counter\n\n    def _nonce_as_int(nonce):\n        i1, i2, i3, i4 = struct.unpack(\">IIII\", nonce)\n        return (i1 << 96 | i2 << 64 | i3 << 32 | i4)\n\n    def _aes_ctr(key, nonce):\n        # Notes:\n        #\n        # A previous iteration of this code used the Counter with a prefix,\n        # which limited us to 2**64 iterations. This has been change to just\n        # set an initial value and allow wraparound.\n        #\n        # The MD5 digests save the caller from having to know our internal\n        # size requirements; AES wants 128, we just mix all the bits we're\n        # given. We expect the input to already be strongly random (so MD5's\n        # weaknesses shouldn't matter), but it may of the wrong size.\n        #\n        hashed_key = md5(key).digest()\n        prefixed_nonce = md5(nonce).digest()[:8] + '\\0\\0\\0\\0\\0\\0\\0\\1'\n        counter = Counter.new(128, initial_value=_nonce_as_int(prefixed_nonce))\n        return AES.new(hashed_key, mode=AES.MODE_CTR, counter=counter)\n\n    def aes_ctr_encryptor(key, nonce):\n        return _aes_ctr(key, nonce).encrypt\n\n    def aes_ctr_decryptor(key, nonce):\n        return _aes_ctr(key, nonce).decrypt\n\n    return aes_ctr_encryptor, aes_ctr_decryptor\n\n\ndef make_dummy_utils():\n    def aes_ctr_encryptor(key):\n        return lambda d: d\n\n    def aes_ctr_decryptor(key):\n        return lambda d: d\n\n    return aes_ctr_encryptor, aes_ctr_decryptor\n\n\n##############################################################################\n\ntry:\n    aes_ctr_encryptor, aes_ctr_decryptor = make_cryptography_utils()\nexcept ImportError:\n    try:\n        aes_ctr_encryptor, aes_ctr_decryptor = make_pycrypto_utils()\n    except ImportError:\n        raise ImportError(\"Please pip install cryptography (or pycrypto)\")\n\n\ndef getrandbits(count):\n    bits = os.urandom(count // 8)\n    rint = 0\n    while bits:\n        rint = (rint << 8) | struct.unpack(\"B\", bits[0])[0]\n        bits = bits[1:]\n    return rint\n\ndef aes_ctr_encrypt(key, iv, data):\n    return aes_ctr_encryptor(key, iv)(data)\n\ndef aes_ctr_decrypt(key, iv, data):\n    return aes_ctr_decryptor(key, iv)(data)\n\n\nif __name__ == \"__main__\":\n    import base64\n\n    bogus_key = \"01234567890abcdef\"\n    bogus_nonce = \"this is a bogus nonce that is bogus\"\n    hello = \"hello world\"\n\n    results = []\n    for name, backend in (('Cryptography', make_cryptography_utils),\n                          ('pyCrypto', make_pycrypto_utils)):\n        aes_ctr_encryptor, aes_ctr_decryptor = backend()\n\n        ct1 = aes_ctr_encryptor(bogus_key, bogus_nonce)(hello)\n        results.append((name, base64.b64encode(ct1)))\n\n        ct2 = aes_ctr_encrypt(bogus_key, bogus_nonce, hello)\n        results.append((name, base64.b64encode(ct2)))\n\n        assert(aes_ctr_decrypt(bogus_key, bogus_nonce, ct1) ==\n               aes_ctr_decryptor(bogus_key, bogus_nonce)(ct1) ==\n               hello)\n\n\n    # Make sure all the results are the same\n    okay = True\n    r1 = results[0]\n    for result in results[1:]:\n        if r1[1] != result[1]:\n            print('%s != %s' % (r1, result))\n            okay = False\n    assert(okay)\n\n    # This verifies we can decrypt some snippets of data that were\n    # generated with a previous iteration of mailpile.crypto.streamer\n    from mailpile.util import sha512b64 as genkey\n    legacy_data = \"part two, yeaaaah\\n\"\n    legacy_nonce = \"2c1c43936034cae20eef86d961cb6570\"\n    legacy_key = genkey(\"test key\", legacy_nonce)[:32].strip()\n    legacy_ct = base64.b64decode(\"D+lBOPrtV+amUCAtoFPCzxsZ\")\n    decrypted = aes_ctr_decrypt(legacy_key, legacy_nonce, legacy_ct)\n    assert(legacy_data == decrypted)\n\n    print(\"ok\")\n"
  },
  {
    "path": "mailpile/crypto/autocrypt.py",
    "content": "from __future__ import print_function\n# Copyright (C) 2018 Jack Dodds & Mailpile ehf.\n# This code is part of Mailpile and is hereby released under the\n# Gnu Affero Public Licence v.3 - see ../../COPYING and ../../AGPLv3.txt.\n#\n\"\"\"\nThis file contains low-level Autocrypt code: constants, parsing, etc.\n\nThe higher level application logic (state database, persistance etc.) is\nmostly in mailpile.plugins.crypto_autocrypt, but inevitably some has\nleaked into mailpile.crypto.gpgi and mailpile.crypto.mime.\n\nExamples (and doctests):\n\n\n# Canonicalize e-mail addresses according to Autocrypt conventions\n>>> canonicalize_email('BrE@maILPile.is')\n'bre@mailpile.is'\n\n\n# Parse the Autocrypt header\n>>> ach = 'addr=bre@mailpile.is; _a=b; _c=d; keydata=aGVsbG8='\n>>> hv = parse_autocrypt_headervalue(ach, optional_attrs=['_a'])\n>>> hv['addr']\n'bre@mailpile.is'\n>>> hv['keydata']\n'hello'\n>>> hv['_a']\n'b'\n>>> hv.get('_c') is None\nTrue\n>>> hv.get('prefer-encrypt') is None\nTrue\n\n# Invalid autocrypt headers return {}\n>>> parse_autocrypt_headervalue('addr=bre@mailpile.is')\n{}\n>>> parse_autocrypt_headervalue('keydata=aGVsbG8=')\n{}\n>>> parse_autocrypt_headervalue('unknown=attribute; ' + ach)\n{}\n\n# Invalid prefer-encrypt values just get ignored\n>>> hv = parse_autocrypt_headervalue('prefer-encrypt=bogus; ' + ach)\n>>> hv.get('prefer-encrypt') is None\nTrue\n>>> hv = parse_autocrypt_headervalue('prefer-encrypt=mutual; ' + ach)\n>>> hv.get('prefer-encrypt') == 'mutual'\nTrue\n\n\n# Generate a valid Autocrypt header\n>>> make_autocrypt_header('bre@mailpile.is', 'hello', prefer_encrypt_mutual=True)\n'addr=bre@mailpile.is; prefer-encrypt=mutual; keydata=aGVsbG8='\n\n\n# Autocrypt setup-codes are used to secure our PGP keys\n>>> generate_autocrypt_setup_code(random_data='fake random garbage data')\n'1189-1868-6510-5211-5608-1629-1262-5635-4164'\n>>> len(generate_autocrypt_setup_code())\n44\n>>> generate_autocrypt_setup_code() != generate_autocrypt_setup_code()\nTrue\n\n\n# AutocryptRecommendations combine a key and a policy of what to do\n>>> ar = AutocryptRecommendation('disable')\n>>> ar.policy\n'disable'\n>>> ar.key_sig is None\nTrue\n\n# Combining recommendations for multiple parties has specific rules\n>>> ar2 = AutocryptRecommendation('encrypt', key_sig='12345')\n>>> AutocryptRecommendation.Synchronize(ar, ar2)\n'disable'\n>>> str(ar2)\n'disable'\n\n# Not just anything is a valid recommendation\n>>> ar2.policy = 'bogus'\nTraceback (most recent call last):\n   ...\nValueError: Invalid Autocrypt policy: bogus\n\n\n\"\"\"\nimport base64\nimport datetime\nimport os\nimport pgpdump\nimport struct\nimport time\n\n\nAUTOCRYPT_IGNORE_MIMETYPES = ('multipart/report', )\n\n\ndef canonicalize_email(address):\n    try:\n        localpart, domain = address.split('@')\n    except (ValueError, AttributeError):\n        # Just return invalid e-mails unchanged, there is no sensible way\n        # to canonicalize such a thing.\n        return address\n\n    # FIXME: Ensure domain is ASCII, if not, punycode it\n    domain = domain.lower()\n\n    # FIXME: Ensure we're using the \"empty locale\"\n    localpart = localpart.lower()\n\n    # NOTE: We deliberately do not strip plussed parts or perform any other\n    #       normalization of the localpart beyond lowercasing. This is both\n    # to comply with the Autocrypt Level 1 spec, but also because being able\n    # to use plussed parts to allow differing cryptographic identities to\n    # share the same e-mail account is something power users like to do.\n\n    return '%s@%s' % (localpart, domain)\n\n\ndef parse_autocrypt_headervalue(value, optional_attrs=None):\n    # Based on:\n    #\n    # https://github.com/mailencrypt/inbome/blob/master/src/inbome/parse.py\n    \"\"\"\n    Parse an AutoCrypt header. Will return an empty dict if parsing fails.\n\n    Optional attributes may be added to the result dictionary, but only the ones\n    listed in optional_attrs (a list or dict); others are ignored.\n    \"\"\"\n    result_dict = {}\n    try:\n        for x in value.split(\";\"):\n            kv = x.split(\"=\", 1)\n            name = kv[0].strip()\n            value = kv[1].strip()\n            if name in (\"addr\", \"prefer-encrypt\"):\n                result_dict[name] = value\n            elif name == \"keydata\":\n                keydata_base64 = \"\".join(value.split())\n                keydata = base64.b64decode(keydata_base64)\n                result_dict[name] = keydata\n            elif name[:1] == '_':\n                if optional_attrs and name in optional_attrs:\n                    result_dict[name] = value\n            else:\n                # Unknown value detected, refuse to parse any further\n                return {}\n    except (ValueError, TypeError, IndexError):\n        return {}\n\n    if \"keydata\" not in result_dict:\n        # found no keydata, ignoring header\n        return {}\n\n    if \"addr\" not in result_dict:\n        # found no e-mail address, ignoring header\n        return {}\n    else:\n        result_dict[\"addr\"] = canonicalize_email(result_dict[\"addr\"])\n\n    if result_dict.get(\"prefer-encrypt\") not in (\"mutual\", None):\n        # Invalid prefer-encrypt value; treat as nopreference\n        del result_dict['prefer-encrypt']\n\n    return result_dict\n\n\ndef extract_autocrypt_header(msg, to=None, optional_attrs=None):\n    # Autocrypt requires there only be one From header\n    froms = msg.get_all(\"From\") or []\n    if len(froms) != 1:\n        return {}\n\n    # Extract the from address for comparisons below. We compare the\n    # canonicalized versions, which is not the strictest interpretation\n    # of the spec, but feels like a reasonable balance here.\n    from mailpile.mailutils.addresses import AddressHeaderParser\n    from_addrs = AddressHeaderParser(froms[0])\n    if len(from_addrs) != 1:\n        return {}\n    from_addr = canonicalize_email(from_addrs[0].address)\n\n    to = canonicalize_email(to) if to else None\n    all_results = []\n    for inb in (msg.get_all(\"Autocrypt\") or []):\n        res = parse_autocrypt_headervalue(inb, optional_attrs=optional_attrs)\n        if res:\n            if ((not to or canonicalize_email(res['addr']) == to) and\n                    (canonicalize_email(res['addr']) == from_addr)):\n                all_results.append(res)\n\n    # Return parsed header iff we found exactly one.\n    if len(all_results) == 1:\n        return all_results[0]\n    else:\n        return {}\n\n\ndef extract_autocrypt_gossip_headers(msg, to=None, optional_attrs=None):\n    to = canonicalize_email(to) if to else None\n    all_results = []\n    for inb in (msg.get_all(\"Autocrypt-Gossip\") or []):\n        res = parse_autocrypt_headervalue(inb, optional_attrs=optional_attrs)\n        if res and (not to or res['addr'] == to):\n            all_results.append(res)\n\n    return all_results\n\n\ndef make_autocrypt_header(addr, binary_key,\n                          prefer_encrypt_mutual=False, prefix='Autocrypt'):\n    prefix = '%s: ' % prefix\n    pem = ' prefer-encrypt=mutual;' if prefer_encrypt_mutual else ''\n    hdr = '%saddr=%s;%s keydata=' % (prefix, addr, pem)\n    for c in base64.b64encode(binary_key).strip():\n        if (len(hdr) % 78) == 0: hdr += ' '\n        hdr += c\n    return hdr[len(prefix):]\n\n\ndef generate_autocrypt_setup_code(random_data=None):\n    \"\"\"\n    Generate a passphrase/setup-code compliant with Autocrypt Level 1.\n\n    From the spec: An Autocrypt Level 1 MUA MUST generate a Setup Code as\n    UTF-8 string of 36 numeric characters, divided into nine blocks of four,\n    separated by dashes. The dashes are part of the secret code and there\n    are no spaces. This format holds about 119 bits of entropy. It is\n    designed to be unambiguous, pronounceable, script-independent (Chinese,\n    Cyrillic etc.), easily input on a mobile device and split into blocks\n    that are easily kept in short term memory.\n    \"\"\"\n    random_data = random_data or os.urandom(16)  # 16 bytes = 128 bits entropy\n    ints = struct.unpack('>4I', random_data[:16])\n    ival = ints[0] + (ints[1] << 32) + (ints[2] << 64) + (ints[3] << 96)\n    blocks = []\n    while len(blocks) < 9:\n        blocks.append('%4.4d' % (ival % 10000))\n        ival //= 10000\n    return '-'.join(blocks)\n\n\n# FIXME: Add a with_signing_subkeys=True, implement. This deviates\n#        from the Autocrypt spec, because Autocrypt says nothing about\n#        signatures. But we're almost always signing our mail, and w/o\n#        the subkeys the signatures cannot be checked.\ndef UNUSED_get_minimal_PGP_key(keydata,\n                               user_id=None, subkey_id=None, binary_out=False):\n    \"\"\"\n    Accepts a PGP key (armored or binary) and returns a minimal PGP key\n    containing exactly five packets (base64 or binary) defining a\n    primary key, a single user id with one self-signature, and a\n    single encryption subkey with one self-signature. Such a five packet\n    key MUST be used in Autocrypt headers (Level 1 Spec section 2.1.1).\n    The unrevoked user id with newest unexpired self-signature and the\n    unrevoked encryption-capable subkey with newest unexpired\n    self-signature are selected from the input key.\n    If user_id is provided, a user id containing that string will be\n    selected if there is one, otherwise any user id will be accepted.\n    If subkey_id is specified, only a subkey with that id will be selected.\n\n    Along with the new key, the selected user id and subkey id are returned.\n    Returns None if there is a failure.\n    \"\"\"\n    def _get_int4(data, offset):\n        '''Pull four bytes from data at offset and return as an integer.'''\n        return ((data[offset] << 24) + (data[offset + 1] << 16) +\n                (data[offset + 2] << 8) + data[offset + 3])\n\n    def _exp_time(creation_time, exp_time_subpacket_data):\n\n        life_s = _get_int4(exp_time_subpacket_data, 0)\n        if not life_s:\n            return 0\n        return packet.creation_time + datetime.timedelta( seconds = life_s)\n\n    def _pgp_header(type, body_length):\n\n        if body_length < 192:\n            return bytearray([type+0xC0, body_length])\n        elif body_length < 8384:\n            return bytearray([type+0xC0, (body_length-192)//256+192,\n                                                 (body_length-192)%256])\n        else:\n            return bytearray([type+0xC0, 255,\n                    body_length//(1<<24), body_length//(1<<16) % 256,\n                    body_length//1<<8 % 256, body_length % 256])\n\n    pri_key = None\n    u_id = None\n    u_id_sig = None\n    u_id_match = False\n    s_key = None\n    s_key_sig = None\n    user_id = canonicalize_email(user_id) if user_id else None\n    now = datetime.datetime.utcfromtimestamp(time.time())\n\n    if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in keydata:\n        packet_iter = pgpdump.AsciiData(keydata).packets()\n    else:\n        packet_iter = pgpdump.BinaryData(keydata).packets()\n\n    try:\n        packet = next(packet_iter)\n    except:\n        packet = None\n\n    while packet:\n\n        if packet.raw == 6 and pri_key:     # Primary key must be the first\n            break                           # and only the first packet.\n        elif packet.raw != 6 and not pri_key:\n            break\n\n        elif packet.raw == 6:               # Primary Public-Key Packet\n            pri_key = packet\n\n        elif packet.raw == 13:              # User ID Packet\n            u_id_try = packet\n            u_id_sig_try = None\n            u_id_try_match = (\n                not user_id or (user_id == canonicalize_email(u_id_try.user)))\n\n            # Accept a nonmatching u_id IFF no other u_id matches.\n            if u_id_match and not u_id_try_match:\n                u_id_try = None\n\n            for packet in packet_iter:\n                if packet.raw != 2:         # Signature Packet\n                    break\n                elif not u_id_try:\n                    continue\n                                            # User ID certification\n                elif packet.raw_sig_type in (0x10, 0x11, 0x12, 0x13, 0x1F):\n                    if (pri_key.fingerprint.endswith(packet.key_id) and\n                            (not packet.expiration_time or\n                                packet.expiration_time > now) and\n                            (not u_id_sig_try or\n                                u_id_sig_try.creation_time\n                                    < packet.creation_time)):\n                        u_id_sig_try = packet\n                                            # Certification revocation\n                elif packet.raw_sig_type == 0x30:\n                    if pri_key.fingerprint.endswith(packet.key_id):\n                        u_id_try = None\n                        u_id_sig_try = None\n\n            # Select unrevoked user id with newest unexpired self-signature\n            if u_id_try and u_id_sig_try and (\n                    not u_id or not u_id_sig or\n                    u_id_try_match and not u_id_match or\n                    u_id_sig_try.creation_time >= u_id_sig.creation_time):\n                u_id = u_id_try\n                u_id_sig = u_id_sig_try\n                u_id_match = u_id_try_match\n            continue    # Skip next(packet_iter) - for has done it.\n\n        elif packet.raw == 14:              # Public-Subkey Packet\n            s_key_try = packet\n            s_key_sig_try = None\n\n            # Honour a request for specific subkey and check for expiry.\n            if ((subkey_id and not s_key_try.fingerprint.endswith(subkey_id))\n                    or (s_key_try.expiration_time and\n                        s_key_try.expiration_time < now)):\n                s_key_try = None\n\n            for packet in packet_iter:\n                if packet.raw != 2:         # Signature Packet\n                    break\n                elif not s_key_try:\n                    continue\n                                            # Subkey Binding Signature\n                elif packet.raw_sig_type == 0x18:\n                    packet.key_expire_time = None\n                    if (pri_key.fingerprint.endswith(packet.key_id) and\n                            not packet.expiration_time or\n                            packet.expiration_time >= now):\n                        can_encrypt = True  # Assume encrypt -- FIXME\n                        for subpacket in packet.subpackets:\n                            if subpacket.subtype == 9:  # Key expiration\n                                packet.key_expire_time = _exp_time(\n                                    packet.creation_time, subpacket.data)\n                            elif subpacket.subtype == 27:   # Key flags\n                                can_encrypt |= subpacket.data[0] & 0x0C\n                        if can_encrypt and (not packet.key_expire_time or\n                                            packet.key_expire_time >= now):\n                            s_key_sig_try = packet\n                                            # Subkey revocation signature\n                elif packet.raw_sig_type == 0x28:\n                    if pri_key.fingerprint.endswith(packet.key_id):\n                        s_key_try = None\n                        s_key_sig_try = None\n\n            # Select unrevoked encryption-capable subkey with newest\n            # unexpired self-signature (ignores newness of key itself).\n            if s_key_try and s_key_sig_try and (not s_key_sig or\n                    s_key_sig_try.creation_time >= s_key_sig.creation_time):\n                s_key = s_key_try\n                s_key_sig = s_key_sig_try\n            continue    # Skip next(packet_iter) - for has done it.\n\n        try:\n            packet = next(packet_iter)\n        except:\n            packet = None\n\n    if not(pri_key and u_id and u_id_sig and s_key and s_key_sig):\n        return '', None, None\n\n    newkey = (\n        _pgp_header(pri_key.raw, len(pri_key.data)) + pri_key.data +\n        _pgp_header(u_id.raw, len(u_id.data)) + u_id.data +\n        _pgp_header(u_id_sig.raw, len(u_id_sig.data)) + u_id_sig.data +\n        _pgp_header(s_key.raw, len(s_key.data)) + s_key.data +\n        _pgp_header(s_key_sig.raw, len(s_key_sig.data)) + s_key_sig.data )\n\n    if not binary_out:\n        newkey = base64.b64encode(newkey)\n\n    return newkey, u_id.user, s_key.key_id\n\n\nclass AutocryptRecommendation(object):\n    DISABLE    = \"disable\"\n    DISCOURAGE = \"discourage\"\n    ENABLE     = \"enable\"\n    ENCRYPT    = \"encrypt\"\n\n    ORDERED_POLICIES = (DISABLE, DISCOURAGE, ENABLE, ENCRYPT)\n\n    def __init__(self, policy, key_sig=None):\n        self.key_sig = self._policy = None\n        self.set_recommendation(policy, key_sig)\n\n    def __str__(self):\n        if self.policy in (self.DISABLE,):\n            return self.policy\n        return \"%s (key=%s)\" % (self.policy, self.key_sig)\n\n    @classmethod\n    def Synchronize(cls, *recommendations):\n        \"\"\"\n        This will synchronize a set of Autocrypt recommendations to whatever\n        the lowest common denomitor is, and then return that policy.\n        \"\"\"\n        if not recommendations:\n            return cls.DISABLE\n        lowest_common_policy = cls.ORDERED_POLICIES[min(\n            cls.ORDERED_POLICIES.index(r.policy) for r in recommendations)]\n        for r in recommendations:\n            r.policy = lowest_common_policy\n        return lowest_common_policy\n\n    def set_recommendation(self, policy, key_sig=None):\n        if policy not in self.ORDERED_POLICIES:\n            raise ValueError('Invalid Autocrypt policy: %s' % policy)\n        if policy != self.DISABLE and key_sig is None and self.key_sig is None:\n            raise ValueError('Policy %s requires a key' % policy)\n        self._policy = policy\n        if key_sig is not None:\n            self.key_sig = key_sig\n\n    policy = property(lambda self: self._policy, set_recommendation)\n\n\nif __name__ == \"__main__\":\n    import sys\n    import doctest\n\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS)\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/crypto/gpgi.py",
    "content": "#coding:utf-8\nfrom __future__ import print_function\nimport os\nimport string\nimport sys\nimport time\nimport re\nimport StringIO\nimport tempfile\nimport threading\nimport traceback\nimport urllib\nimport select\nimport pgpdump\nimport pgpdump.utils\nimport base64\nimport quopri\nfrom datetime import datetime\nfrom email.parser import Parser\nfrom email.message import Message\nfrom threading import Thread\n\nimport mailpile.platforms\nfrom mailpile.i18n import gettext\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.crypto.state import *\nfrom mailpile.crypto.mime import MimeSigningWrapper, MimeEncryptingWrapper\nfrom mailpile.safe_popen import Popen, PIPE, Safe_Pipe\n\n\n_ = lambda s: s\n\nDEFAULT_KEYSERVERS = [\"hkps://hkps.pool.sks-keyservers.net\",\n                      \"hkp://subset.pool.sks-keyservers.net\"]\nDEFAULT_KEYSERVER_OPTIONS = [\n  'ca-cert-file=%s' % __file__.replace('.pyc', '.py')]\nDEFAULT_IMPORT_OPTIONS = [\n  'import-minimal']\n\nGPG_KEYID_LENGTH = 8\nGNUPG_HOMEDIR = None  # None=use what gpg uses\nGPG_BINARY = mailpile.platforms.GetDefaultGnuPGCommand\nGPG_VERSIONS = {}\nBLOCKSIZE = 65536\n\nopenpgp_algorithms = {1: _(\"RSA\"),\n                      2: _(\"RSA (encrypt only)\"),\n                      3: _(\"RSA (sign only)\"),\n                      16: _(\"ElGamal (encrypt only)\"),\n                      17: _(\"DSA\"),\n                      20: _(\"ElGamal (encrypt/sign) [COMPROMISED]\"),\n                      22: _(\"EdDSA\"),\n                      999: _(\"Unknown\")}\n# For details on type 20 compromisation, see\n# http://lists.gnupg.org/pipermail/gnupg-announce/2003q4/000160.html\n\nENTROPY_LOCK = threading.Lock()\n\nclass GnuPGEventUpdater:\n    \"\"\"\n    Parse the GPG response into something useful for the Event Log.\n    \"\"\"\n    def __init__(self, event):\n        from mailpile.eventlog import Event\n        self.event = event or Event()\n\n    def _log(self, section, message):\n        data = section.get('gnupg', [])\n        if data:\n            data[-1].append(message)\n\n    def _log_private(self, message):\n        self._log(self.event.private_data, message)\n\n    def _log_public(self, message):\n        self._log(self.event.private_data, message)\n        self._log(self.event.data, message)\n\n    def running_gpg(self, why):\n        for section in (self.event.data, self.event.private_data):\n            data = section.get('gnupg', [])\n            data.append([why, int(time.time())])\n            section['gnupg'] = data\n\n    def update_args(self, args):\n        self._log_public(' '.join(args))\n\n    def update_sent_passphrase(self):\n        self._log_public('Sent passphrase')\n\n    def _parse_gpg_line(self, line):\n        if line.startswith('[GNUPG:] '):\n            pass  # FIXME: Parse for machine-readable data\n        elif line.startswith('gpg: '):\n            self._log_private(line[5:].strip())\n\n    def update_stdout(self, line):\n        self._parse_gpg_line(line)\n\n    def update_stderr(self, line):\n        self._parse_gpg_line(line)\n\n    def update_return_code(self, code):\n        self._log_public('GnuPG returned %s' % code)\n\n\nclass GnuPGResultParser:\n    \"\"\"\n    Parse the GPG response into EncryptionInfo and SignatureInfo.\n    \"\"\"\n    def __init__(rp, decrypt_requires_MDC=True, debug=None):\n        rp.decrypt_requires_MDC = decrypt_requires_MDC\n        rp.debug = debug or (lambda t: True)\n\n        rp.signature_info = SignatureInfo()\n        rp.signature_info[\"protocol\"] = \"openpgp\"\n\n        rp.encryption_info = EncryptionInfo()\n        rp.encryption_info[\"protocol\"] = \"openpgp\"\n\n        rp.plaintext = \"\"\n\n    def parse(rp, retvals):\n        signature_info = rp.signature_info\n        encryption_info = rp.encryption_info\n        from mailpile.mailutils.emails import ExtractEmailAndName\n\n        # Belt & suspenders: work around some buggy GnuPG status codes\n        gpg_stderr = ''.join(retvals[1][\"stderr\"])\n\n        # First pass, set some initial state.\n        locked, missing = [], []\n        for data in retvals[1][\"status\"]:\n            keyword = data[0].strip()  # The last keyword often ends in \\n\n\n            if keyword == 'NEED_PASSPHRASE':\n                locked += [data[2]]\n                encryption_info.part_status = \"lockedkey\"\n                encryption_info[\"locked_keys\"] = list(set(locked))\n\n            elif keyword == 'GOOD_PASSPHRASE':\n                encryption_info[\"locked_keys\"] = []\n\n            elif keyword == \"DECRYPTION_FAILED\":\n                missing += [x[1].strip() for x in retvals[1][\"status\"]\n                            if x[0] == \"NO_SECKEY\"]\n                if missing:\n                    encryption_info[\"missing_keys\"] = list(set(missing))\n                if encryption_info.part_status != \"lockedkey\":\n                    if missing:\n                        encryption_info.part_status = \"missingkey\"\n                    else:\n                        encryption_info.part_status = \"error\"\n\n            elif keyword == \"DECRYPTION_OKAY\":\n                if (rp.decrypt_requires_MDC and\n                       'message was not integrity protected' in gpg_stderr):\n                    rp.debug('Message not integrity protected, failing.')\n                    encryption_info.part_status = \"error\"\n                else:\n                    encryption_info.part_status = \"decrypted\"\n                    rp.plaintext = \"\".join(retvals[1][\"stdout\"])\n\n            elif keyword == \"ENC_TO\":\n                keylist = encryption_info.get(\"have_keys\", [])\n                if data[1] not in keylist:\n                    keylist.append(data[1].strip())\n                encryption_info[\"have_keys\"] = list(set(keylist))\n\n            elif keyword == \"PLAINTEXT\":\n                encryption_info.filename = data[3].strip()\n\n            elif signature_info.part_status == \"none\":\n                # Only one of these will ever be emitted per key, use\n                # this to set initial state. We may end up revising\n                # the status depending on more info later.\n                if keyword in (\"GOODSIG\", \"BADSIG\"):\n                    email, fn = ExtractEmailAndName(\n                        \" \".join(data[2:]).decode('utf-8'))\n                    signature_info[\"name\"] = fn\n                    signature_info[\"email\"] = email\n                    signature_info.part_status = ((keyword == \"GOODSIG\")\n                                                  and \"unverified\"\n                                                  or \"invalid\")\n                    rp.plaintext = \"\".join(retvals[1][\"stdout\"])\n\n                elif keyword == \"ERRSIG\":\n                    signature_info.part_status = \"error\"\n                    signature_info[\"keyinfo\"] = data[1]\n                    signature_info[\"timestamp\"] = int(data[5])\n\n        # Second pass, this may update/mutate the state set above\n        for data in retvals[1][\"status\"]:\n            keyword = data[0].strip()  # The last keyword often ends in \\n\n\n            if keyword == \"NO_SECKEY\":\n                keyid = data[1].strip()\n                if \"missing_keys\" not in encryption_info:\n                    encryption_info[\"missing_keys\"] = [keyid]\n                elif keyid not in encryption_info[\"missing_keys\"]:\n                    encryption_info[\"missing_keys\"].append(keyid)\n                while keyid in encryption_info[\"have_keys\"]:\n                    encryption_info[\"have_keys\"].remove(keyid)\n\n            elif keyword == \"VALIDSIG\":\n                # FIXME: Determine trust level, between new, unverified,\n                #        verified, untrusted.\n                signature_info[\"keyinfo\"] = data[1]\n                signature_info[\"timestamp\"] = int(data[3])\n\n            elif keyword in (\"EXPKEYSIG\", \"REVKEYSIG\"):\n                email, fn = ExtractEmailAndName(\n                    \" \".join(data[2:]).decode('utf-8'))\n                signature_info[\"name\"] = fn\n                signature_info[\"email\"] = email\n                signature_info.part_status = ((keyword == \"EXPKEYSIG\")\n                                              and \"expired\"\n                                              or \"revoked\")\n\n          # FIXME: This appears to be spammy. Is my key borked, or\n          #        is GnuPG being stupid?\n          #\n          # elif keyword == \"KEYEXPIRED\":  # Ignoring: SIGEXPIRED\n          #     signature_info.part_status = \"expired\"\n            elif keyword == \"KEYREVOKED\":\n                signature_info.part_status = \"revoked\"\n            elif keyword == \"NO_PUBKEY\":\n                signature_info.part_status = \"unknown\"\n\n            elif keyword in (\"TRUST_ULTIMATE\", \"TRUST_FULLY\"):\n                if signature_info.part_status == \"unverified\":\n                    signature_info.part_status = \"verified\"\n            elif (keyword == \"DECRYPTION_INFO\" and\n                     encryption_info.part_status == \"decrypted\"\n                     and rp.decrypt_requires_MDC):\n                mdc_method = data[1].strip()\n                aead_algo = data[3].strip() if len(data) > 3 else 0\n                if not mdc_method and not aeadalgo:\n                    encryption_info.part_status = \"error\"\n\n        if encryption_info.part_status == \"error\":\n            rp.plaintext = \"\"\n\n        return rp\n\n\nclass GnuPGRecordParser:\n    def __init__(self):\n        self.keys = {}\n        self.curkeyid = None\n        self.curdata = None\n\n        self.record_fields = [\"record\", \"validity\", \"keysize\", \"keytype\",\n                              \"keyid\", \"creation_date\", \"expiration_date\",\n                              \"uidhash\", \"ownertrust\", \"uid\", \"sigclass\",\n                              \"capabilities\", \"flag\", \"sn\", \"hashtype\",\n                              \"curve\"]\n        self.record_types = [\"pub\", \"sub\", \"ssb\", \"fpr\", \"uat\", \"sec\", \"tru\",\n                             \"sig\", \"rev\", \"uid\", \"gpg\", \"rvk\", \"grp\"]\n        self.record_parsers = [self.parse_pubkey, self.parse_subkey,\n                               self.parse_subkey, self.parse_fingerprint,\n                               self.parse_userattribute, self.parse_privkey,\n                               self.parse_trust, self.parse_signature,\n                               self.parse_revoke, self.parse_uidline,\n                               self.parse_none, self.parse_revocation_key,\n                               self.parse_keygrip]\n\n        self.dispatch = dict(zip(self.record_types, self.record_parsers))\n\n    def parse(self, lines):\n        for line in lines:\n            self.parse_line(line)\n        return self.keys\n\n    def parse_line(self, line):\n        line = dict(zip(self.record_fields,\n                        map(lambda s: s.replace(\"\\\\x3a\", \":\"),\n                        stubborn_decode(line).strip().split(\":\"))))\n        r = self.dispatch.get(line[\"record\"], self.parse_unknown)\n        r(line)\n\n    def _parse_dates(self, line):\n        for ts in ('expiration_date', 'creation_date'):\n            if line.get(ts) and '-' not in line[ts]:\n                line[ts+'_ts'] = line[ts]\n                try:\n                    unixtime = int(line[ts])\n                    if unixtime > 946684800:  # 2000-01-01\n                        dt = datetime.fromtimestamp(unixtime)\n                        line[ts] = dt.strftime('%Y-%m-%d')\n                except ValueError:\n                    line[ts] = '1970-01-01'\n\n    def _parse_keydata(self, line):\n        line[\"keytype_name\"] = _(openpgp_algorithms.get(int(line[\"keytype\"]),\n                                                        'Unknown'))\n        line[\"capabilities_map\"] = {\n            \"encrypt\": \"E\" in line[\"capabilities\"],\n            \"sign\": \"S\" in line[\"capabilities\"],\n            \"certify\": \"C\" in line[\"capabilities\"],\n            \"authenticate\": \"A\" in line[\"capabilities\"],\n        }\n        line[\"disabled\"] = \"D\" in line[\"capabilities\"]\n        line[\"revoked\"] = \"r\" in line[\"validity\"]\n        line[\"expired\"] = \"e\" in line[\"validity\"]\n\n        self._parse_dates(line)\n\n        return line\n\n    def _clean_curdata(self):\n        for v in self.curdata.keys():\n            if self.curdata[v] == \"\":\n                del self.curdata[v]\n        del self.curdata[\"record\"]\n\n    def parse_pubkey(self, line):\n        self.curkeyid = line[\"keyid\"]\n        self.curdata = self.keys[self.curkeyid] = self._parse_keydata(line)\n        self.curdata[\"subkeys\"] = []\n        self.curdata[\"uids\"] = []\n        self.curdata[\"secret\"] = (self.curdata[\"record\"] == \"sec\")\n        self.parse_uidline(self.curdata)\n        self._clean_curdata()\n\n    def parse_subkey(self, line):\n        self.curdata = self._parse_keydata(line)\n        self.keys[self.curkeyid][\"subkeys\"].append(self.curdata)\n        self._clean_curdata()\n\n    def parse_fingerprint(self, line):\n        fpr = line[\"uid\"]\n        self.curdata[\"fingerprint\"] = fpr\n        if len(self.curkeyid) < len(fpr):\n            self.keys[fpr] = self.keys[self.curkeyid]\n            del(self.keys[self.curkeyid])\n            self.curkeyid = fpr\n\n    def parse_userattribute(self, line):\n        # TODO: We are currently ignoring user attributes as not useful.\n        #       We may at some point want to use --attribute-fd and read\n        #       in user photos and such?\n        pass\n\n    def parse_privkey(self, line):\n        self.parse_pubkey(line)\n\n    def parse_uidline(self, line):\n        email, name, comment = parse_uid(line[\"uid\"])\n        self._parse_dates(line)\n        if email or name or comment:\n            self.keys[self.curkeyid][\"uids\"].append({\n                \"email\": email,\n                \"name\": name,\n                \"comment\": comment,\n                \"creation_date\": line[\"creation_date\"]\n            })\n        else:\n            pass  # This is the case where a uid or sec line have no\n                  # information aside from the creation date, which we\n                  # parse elsewhere. As these lines are effectively blank,\n                  # we omit them to simplify presentation to the user.\n\n    def parse_trust(self, line):\n        # FIXME: We are currently ignoring commentary from the Trust DB.\n        pass\n\n    def parse_signature(self, line):\n        # FIXME: This is probably wrong; signatures are on UIDs and not\n        #        the key itself. No? Yes? Figure this out.\n        if \"signatures\" not in self.keys[self.curkeyid]:\n            self.keys[self.curkeyid][\"signatures\"] = []\n        sig = {\n            \"signer\": line[9],\n            \"signature_date\": line[5],\n            \"keyid\": line[4],\n            \"trust\": line[10],\n            \"keytype\": line[4]\n        }\n        self.keys[self.curkeyid][\"signatures\"].append(sig)\n\n    def parse_keygrip(self, line):\n        self.curdata[\"keygrip\"] = line[\"uid\"]\n\n    def parse_revoke(self, line):\n        pass  # FIXME\n\n    def parse_revocation_key(self, line):\n        pass  # FIXME\n\n    def parse_unknown(self, line):\n        print(\"Unknown line with code '%s'\" % (line,))\n\n    def parse_none(line):\n        pass\n\n\nUID_PARSE_RE = \"^([^\\(\\<]+?){0,1}( \\((.+?)\\)){0,1}( \\<(.+?)\\>){0,1}\\s*$\"\n\n\ndef stubborn_decode(text):\n    if isinstance(text, unicode):\n        return text\n    try:\n        return text.decode(\"utf-8\")\n    except UnicodeDecodeError:\n        try:\n            return text.decode(\"iso-8859-1\")\n        except UnicodeDecodeError:\n            return uidstr.decode(\"utf-8\", \"replace\")\n\n\ndef parse_uid(uidstr):\n    matches = re.match(UID_PARSE_RE, uidstr)\n    if matches:\n        email = matches.groups(0)[4] or \"\"\n        comment = matches.groups(0)[2] or \"\"\n        name = matches.groups(0)[0] or \"\"\n    else:\n        if '@' in uidstr and ' ' not in uidstr:\n            email, name = uidstr, \"\"\n        else:\n            email, name = \"\", uidstr\n        comment = \"\"\n\n    return email, name, comment\n\n\nclass StreamReader(Thread):\n    def __init__(self, name, fd, callback, lines=True):\n        Thread.__init__(self, target=self.readin, args=(fd, callback))\n        self.name = name\n        self.state = 'startup'\n        self.lines = lines\n        self.start()\n\n    def __str__(self):\n        return '%s(%s/%s, lines=%s)' % (Thread.__str__(self),\n                                        self.name, self.state, self.lines)\n\n    def readin(self, fd, callback):\n        try:\n            if self.lines:\n                self.state = 'read'\n                for line in iter(fd.readline, b''):\n                    self.state = 'callback'\n                    callback(line)\n                    self.state = 'read'\n            else:\n                while True:\n                    self.state = 'read'\n                    buf = fd.read(BLOCKSIZE)\n                    self.state = 'callback'\n                    callback(buf)\n                    if buf == \"\":\n                        break\n        except:\n            traceback.print_exc()\n        finally:\n            self.state = 'done'\n            fd.close()\n\n\nclass StreamWriter(Thread):\n    def __init__(self, name, fd, output, partial_write_ok=False):\n        Thread.__init__(self, target=self.writeout, args=(fd, output))\n        self.name = name\n        self.state = 'startup'\n        self.partial_write_ok = partial_write_ok\n        self.start()\n\n    def __str__(self):\n        return '%s(%s/%s)' % (Thread.__str__(self), self.name, self.state)\n\n    def writeout(self, fd, output):\n        if isinstance(output, (str, unicode, bytearray)):\n            total = len(output)\n            output = StringIO.StringIO(output)\n        else:\n            total = 0\n        try:\n            while True:\n                self.state = 'read'\n                line = output.read(BLOCKSIZE)\n                if line == \"\":\n                    break\n                self.state = 'write'\n                fd.write(line)\n                total -= len(line)\n            output.close()\n        except:\n            if not self.partial_write_ok:\n                print('%s: %s bytes left' % (self, total))\n                traceback.print_exc()\n        finally:\n            self.state = 'done'\n            fd.close()\n\n\nDEBUG_GNUPG = False\n\nclass GnuPG:\n    \"\"\"\n    Wrap GnuPG and make all functionality feel Pythonic.\n    \"\"\"\n    ARMOR_BEGIN_SIGNED    = '-----BEGIN PGP SIGNED MESSAGE-----'\n    ARMOR_BEGIN_SIGNATURE = '-----BEGIN PGP SIGNATURE-----'\n    ARMOR_END_SIGNED      = '-----END PGP SIGNATURE-----'\n    ARMOR_END_SIGNATURE   = '-----END PGP SIGNATURE-----'\n\n    ARMOR_BEGIN_ENCRYPTED = '-----BEGIN PGP MESSAGE-----'\n    ARMOR_END_ENCRYPTED   = '-----END PGP MESSAGE-----'\n\n    ARMOR_BEGIN_PUB_KEY   = '-----BEGIN PGP PUBLIC KEY BLOCK-----'\n    ARMOR_END_PUB_KEY     = '-----END PGP PUBLIC KEY BLOCK-----'\n\n    LAST_KEY_USED = 'DEFAULT'  # This is a 1-value global cache\n\n    def __init__(self, config,\n                 session=None, use_agent=None, debug=False, dry_run=False,\n                 event=None, passphrase=None):\n        global DEBUG_GNUPG\n        self.available = None\n        self.outputfds = [\"stdout\", \"stderr\", \"status\"]\n        self.errors = []\n        self.event = GnuPGEventUpdater(event)\n        self.session = session\n        self.config = config or (session and session.config) or None\n        self.status_filenames = []\n        if self.config:\n            DEBUG_GNUPG = ('gnupg' in self.config.sys.debug)\n            self.homedir = self.config.sys.gpg_home or GNUPG_HOMEDIR\n            self.gpgbinary = self.config.sys.gpg_binary or GPG_BINARY()\n            self.passphrases = self.config.passphrases\n            self.passphrase = (passphrase if (passphrase is not None) else\n                               self.passphrases['DEFAULT']).get_reader()\n            self.use_agent = (use_agent if (use_agent is not None)\n                              else self.config.prefs.gpg_use_agent)\n        else:\n            self.homedir = GNUPG_HOMEDIR\n            self.gpgbinary = GPG_BINARY()\n            self.passphrases = None\n            if passphrase:\n                self.passphrase = passphrase.get_reader()\n            else:\n                self.passphrase = None\n            self.use_agent = use_agent\n        self.dry_run = dry_run\n        self.debug = (self._debug_all if (debug or DEBUG_GNUPG)\n                      else self._debug_none)\n\n    def prepare_passphrase(self, keyid, signing=False, decrypting=False):\n        \"\"\"Query the Mailpile secrets for a usable passphrase.\"\"\"\n        def _use(kid, sps_reader):\n            self.passphrase = sps_reader\n            GnuPG.LAST_KEY_USED = kid\n            return True\n\n        if self.config:\n            message = []\n            if decrypting:\n                message.append(_(\"Your PGP key is needed for decrypting.\"))\n            if signing:\n                message.append(_(\"Your PGP key is needed for signing.\"))\n            match, sps = self.config.get_passphrase(keyid,\n                prompt=_('Unlock your encryption key'),\n                description=' '.join(message))\n            if match:\n                return _use(match, sps.get_reader())\n\n        self.passphrase = None  # This *may* allow use of the GnuPG agent\n        return False\n\n    def _debug_all(self, msg):\n        if self.session:\n            self.session.debug(msg.rstrip())\n        else:\n            print('%s' % str(msg).rstrip())\n\n    def _debug_none(self, msg):\n        pass\n\n    def set_home(self, path):\n        self.homedir = path\n\n    def version(self):\n        \"\"\"Returns a string representing the GnuPG version number.\"\"\"\n        self.event.running_gpg(_('Checking GnuPG version'))\n        retvals = self.run([\"--version\"], novercheck=True)\n        return retvals[1][\"stdout\"][0].split('\\n')[0]\n\n    def version_tuple(self, update=False):\n        \"\"\"Returns a tuple representing the GnuPG version number.\"\"\"\n        global GPG_VERSIONS\n        if update or not GPG_VERSIONS.get(self.gpgbinary):\n            match = re.search( \"(\\d+).(\\d+).(\\d+)\", self.version() )\n            version = tuple(int(v) for v in match.groups())\n            GPG_VERSIONS[self.gpgbinary] = version\n        return GPG_VERSIONS[self.gpgbinary]\n\n    def gnupghome(self):\n        \"\"\"Returns the location of the GnuPG keyring\"\"\"\n        self.event.running_gpg(_('Checking GnuPG home directory'))\n        rv = self.run([\"--version\"], novercheck=True)[1][\"stdout\"][0]\n        for l in rv.splitlines():\n            if l.startswith('Home: '):\n                return os.path.expanduser(l[6:].strip())\n        return os.path.expanduser(os.getenv('GNUPGHOME', '~/.gnupg'))\n\n    def is_available(self):\n        try:\n            self.event.running_gpg(_('Checking GnuPG availability'))\n            self.version_tuple(update=True)\n            self.available = True\n        except OSError:\n            self.available = False\n\n        return self.available\n\n    def common_args(self,\n                    args=None, version=None,\n                    will_send_passphrase=False,\n                    interactive=False):\n        if args is None:\n            args = []\n        if version is None:\n            version = self.version_tuple()\n\n        args.insert(0, self.gpgbinary)\n        args.insert(1, \"--utf8-strings\")\n\n        # Disable SHA1 and compression in all things GnuPG\n        args[1:1] = [\"--personal-digest-preferences=SHA512\",\n                     \"--personal-compress-preferences=Uncompressed\",\n                     \"--digest-algo=SHA512\",\n                     \"--cert-digest-algo=SHA512\"]\n\n        if self.homedir:\n            args.insert(1, \"--homedir=%s\" % self.homedir)\n\n        if version > (2, 1, 11):\n            binaries = mailpile.platforms.DetectBinaries()\n            for which, setting in (('GnuPG_dirmngr', 'dirmngr-program'),\n                                   ('GnuPG_agent',   'agent-program')):\n                if which in binaries:\n                    args.insert(1, \"--%s=%s\" % (setting, binaries[which]))\n                else:\n                    print('wtf: %s not in %s' % (which, binaries))\n\n        if (not self.use_agent) or will_send_passphrase:\n            if version < (1, 5):\n                args.insert(1, \"--no-use-agent\")\n            elif version > (2, 1, 11):\n                args.insert(1, \"--pinentry-mode=loopback\")\n            else:\n                raise ImportError('Mailpile requires GnuPG 1.4.x or 2.1.12+ !')\n\n        if not interactive:\n            args.insert(1, \"--with-colons\")\n            args.insert(1, \"--verbose\")\n            args.insert(1, \"--batch\")\n            args.insert(1, \"--enable-progress-filter\")\n            if self.status_filenames:\n                args.insert(1, \"--status-file=%s\" % self.status_filenames[-1])\n            if will_send_passphrase:\n                args.insert(2, \"--passphrase-fd=0\")\n\n        if self.dry_run:\n            args.insert(1, \"--dry-run\")\n\n        return args\n\n    def run(self, *args, **kwargs):\n        # This wrapper handles temporary status files. Since we may recursively\n        # invoke ourselves, we keep a stack of tempfiles and push/pop from the\n        # list.\n        fd = tempfile.NamedTemporaryFile(delete=False)\n        fd.close()  # Avoid potential conflicts on Windows\n        try:\n            self.status_filenames.append(fd.name)\n            return self.run_without_status(*args, **kwargs)\n        finally:\n            os.remove(self.status_filenames.pop(-1))\n\n    def run_without_status(self,\n            args=None, gpg_input=None, outputfd=None, partial_read_ok=False,\n            send_passphrase=False, _raise=None, novercheck=False):\n        if novercheck:\n            version = (1, 4)\n        else:\n            version = self.version_tuple()\n\n        args = self.common_args(\n            args=list(args if args else []),\n            version=version,\n            will_send_passphrase=(self.passphrase and send_passphrase))\n\n        self.outputbuffers = dict([(x, []) for x in self.outputfds])\n        self.threads = {}\n        gpg_retcode = -1\n        proc = None\n        try:\n            if send_passphrase and (self.passphrase is None):\n                self.debug('Running WITHOUT PASSPHRASE %s' % ' '.join(args))\n                self.debug(''.join(traceback.format_stack()))\n            else:\n                self.debug('Running %s' % ' '.join(args))\n\n            # Here we go!\n            self.event.update_args(args)\n            proc = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, bufsize=0)\n\n            # GnuPG is a bit crazy, and requires that the passphrase\n            # be sent and the filehandle closed before anything else\n            # interesting happens.\n            if send_passphrase and self.passphrase is not None:\n                self.passphrase.seek(0, 0)\n                c = self.passphrase.read(BLOCKSIZE)\n                while c != '':\n                    proc.stdin.write(c)\n                    c = self.passphrase.read(BLOCKSIZE)\n                proc.stdin.write('\\n')\n                self.event.update_sent_passphrase()\n\n            wtf = ' '.join(args)\n            self.threads = {\n                \"stderr\": StreamReader('gpgi-stderr(%s)' % wtf,\n                                       proc.stderr, self.parse_stderr)\n            }\n\n            if outputfd:\n                self.threads[\"stdout\"] = StreamReader(\n                    'gpgi-stdout-to-fd(%s)' % wtf,\n                    proc.stdout, outputfd.write, lines=False)\n            else:\n                self.threads[\"stdout\"] = StreamReader(\n                    'gpgi-stdout-parsed(%s)' % wtf,\n                    proc.stdout, self.parse_stdout)\n\n            if gpg_input:\n                # If we have output, we just stream it. Technically, this\n                # doesn't really need to be a thread at the moment.\n                self.debug('<<STDOUT<< %d bytes' % (len(gpg_input), ))\n                StreamWriter('gpgi-output(%s)' % wtf,\n                             proc.stdin, gpg_input,\n                             partial_write_ok=partial_read_ok).join()\n            else:\n                proc.stdin.close()\n\n            # Reap GnuPG\n            gpg_retcode = proc.wait()\n\n            # Consume the contents of the status file\n            if self.status_filenames:\n                with open(self.status_filenames[-1], 'r') as status_fd:\n                    for line in iter(status_fd.readline, b''):\n                        self.parse_status(line)\n        finally:\n            # Close this so GPG will terminate. This should already have\n            # been done, but we're handling errors here...\n            if proc and proc.stdin:\n                proc.stdin.close()\n\n        # Update event with return code\n        self.event.update_return_code(gpg_retcode)\n\n        # Reap the threads\n        self._reap_threads()\n\n        if outputfd:\n            outputfd.close()\n\n        if gpg_retcode != 0 and _raise:\n            raise _raise('GnuPG failed, exit code: %s' % gpg_retcode)\n\n        return gpg_retcode, self.outputbuffers\n\n    def _reap_threads(self):\n        for tries in (1, 2, 3):\n            for name, thr in self.threads.iteritems():\n                if thr.isAlive():\n                    thr.join(timeout=15)\n                    if thr.isAlive() and tries > 1:\n                        print('WARNING: Failed to reap thread %s' % thr)\n\n    def parse_status(self, line, *args):\n        self.debug('<<STATUS<< %s' % line)\n        line = line.replace(\"[GNUPG:] \", \"\")\n        if line == \"\":\n            return\n        elems = line.split(\" \")\n        self.outputbuffers[\"status\"].append(elems)\n\n    def parse_stdout(self, line):\n        self.event.update_stdout(line)\n        self.debug('<<STDOUT<< %s' % line)\n        self.outputbuffers[\"stdout\"].append(line)\n\n    def parse_stderr(self, line):\n        self.event.update_stderr(line)\n        self.debug('<<STDERR<< %s' % line)\n        self.outputbuffers[\"stderr\"].append(line)\n\n    def parse_keylist(self, keylist):\n        rlp = GnuPGRecordParser()\n        return rlp.parse(keylist)\n\n    def list_keys(self, selectors=None):\n        \"\"\"\n        >>> g = GnuPG(None)\n        >>> g.list_keys()[0]\n        0\n        \"\"\"\n        list_keys = [\"--fingerprint\"]\n        for sel in set(selectors or []):\n            list_keys += [\"--list-keys\", sel]\n        if not selectors:\n            list_keys += [\"--list-keys\"]\n        self.event.running_gpg(_('Fetching GnuPG public key list (selectors=%s)'\n                                 ) % ', '.join(selectors or []))\n        retvals = self.run(list_keys)\n        return self.parse_keylist(retvals[1][\"stdout\"])\n\n    def list_secret_keys(self, selectors=None):\n        #\n        # Note: The selectors that are passed by default work around a bug\n        #       in GnuPG < 2.1, where --list-secret-keys does not list\n        #       details about key capabilities or expiry for\n        #       --list-secret-keys unless a selector is provided. A dot\n        #       is reasonably likely to appear in all PGP keys, as it is\n        #       a common component of e-mail addresses (and @ does not\n        #       work as a selector for some reason...)\n        #\n        #       The downside of this workaround is that keys with no e-mail\n        #       address or an address like alice@localhost won't be found.\n        #       So we disable this hack on GnuPG >= 2.1.\n        #\n        if not selectors and self.version_tuple() < (2, 1):\n            selectors = [\".\", \"a\", \"e\", \"i\", \"p\", \"t\", \"k\"]\n\n        list_keys = [\"--fingerprint\"]\n        if selectors:\n            for sel in selectors:\n                # FIXME - In 2.1.18 and 1.4.21 only one --list-keys is needed.\n                list_keys += [\"--list-secret-keys\", sel]\n        else:\n            list_keys += [\"--list-secret-keys\"]\n\n        self.event.running_gpg(_('Fetching GnuPG secret key list (selectors=%s)'\n                                 ) % ', '.join(selectors or ['None']))\n        retvals = self.run(list_keys)\n        secret_keys = self.parse_keylist(retvals[1][\"stdout\"])\n\n        # Another unfortunate thing GnuPG < 2.1 does, is it hides the disabled\n        # state when listing secret keys; it seems internally only the\n        # public key is disabled. This makes it hard for us to reason about\n        # which keys can actually be used, so we compensate...\n        list_keys = [\"--fingerprint\"]\n        for fprint in set(secret_keys):\n            # FIXME - In both 2.1.18 and 1.4.21 only one --list-keys is needed.\n            list_keys += [\"--list-keys\", fprint]\n        retvals = self.run(list_keys)\n        public_keys = self.parse_keylist(retvals[1][\"stdout\"])\n        for fprint, info in public_keys.iteritems():\n            if fprint in set(secret_keys):\n                for k in (\"disabled\", \"revoked\", \"expired\"):\n                    secret_keys[fprint][k] = info[k]\n\n        return secret_keys\n\n    def import_keys(self,\n            key_data=None,\n            import_options=DEFAULT_IMPORT_OPTIONS,\n            filter_uid_emails=None):\n        \"\"\"\n        Imports gpg keys from a file object or string.\n        >>> key_data = open(\"testing/pub.key\").read()\n        >>> g = GnuPG(None)\n        >>> g.import_keys(key_data)\n        {'failed': [], 'updated': [{'details_text': 'unchanged', 'details': 0, 'fingerprint': '08A650B8E2CBC1B02297915DC65626EED13C70DA'}], 'imported': [], 'results': {'sec_dups': 0, 'unchanged': 1, 'num_uids': 0, 'skipped_new_keys': 0, 'no_userids': 0, 'num_signatures': 0, 'num_revoked': 0, 'sec_imported': 0, 'sec_read': 0, 'not_imported': 0, 'count': 1, 'imported_rsa': 0, 'imported': 0, 'num_subkeys': 0}}\n        \"\"\"\n        self.event.running_gpg(_('Importing key to GnuPG key chain'))\n        cmd = [\"--import\"]\n        if filter_uid_emails:\n            expr = ' || '.join('uid =~ %s' % e for e in filter_uid_emails)\n            cmd[1:1] = ['--import-filter', 'keep-uid=%s' % expr]\n        for opt in import_options:\n            cmd[1:1] = ['--import-options', opt]\n        retvals = self.run(cmd, gpg_input=key_data)\n        return self._parse_import(retvals[1][\"status\"])\n\n    def _parse_import(self, output):\n        res = {\"imported\": [], \"updated\": [], \"failed\": []}\n        for x in output:\n            if x[0] == \"IMPORTED\":\n                res[\"imported\"].append({\n                    \"fingerprint\": x[1],\n                    \"username\": x[2].rstrip()\n                })\n            elif x[0] == \"IMPORT_OK\":\n                reasons = {\n                    \"0\": \"unchanged\",\n                    \"1\": \"new key\",\n                    \"2\": \"new user IDs\",\n                    \"4\": \"new signatures\",\n                    \"8\": \"new subkeys\",\n                    \"16\": \"contains private key\",\n                    \"17\": \"contains new private key\",\n                }\n                res[\"updated\"].append({\n                    \"details\": int(x[1]),\n                    # FIXME: Reasons may be ORed! This does NOT handle that.\n                    \"details_text\": reasons.get(x[1], str(x[1])),\n                    \"fingerprint\": x[2].rstrip(),\n                })\n            elif x[0] == \"IMPORT_PROBLEM\":\n                reasons = {\n                    \"0\": \"no reason given\",\n                    \"1\": \"invalid certificate\",\n                    \"2\": \"issuer certificate missing\",\n                    \"3\": \"certificate chain too long\",\n                    \"4\": \"error storing certificate\",\n                }\n                res[\"failed\"].append({\n                    \"details\": int(x[1]),\n                    \"details_text\": reasons.get(x[1], str(x[1])),\n                    \"fingerprint\": x[2].rstrip()\n                })\n            elif x[0] == \"IMPORT_RES\":\n                res[\"results\"] = {\n                    \"count\": int(x[1]),\n                    \"no_userids\": int(x[2]),\n                    \"imported\": int(x[3]),\n                    \"imported_rsa\": int(x[4]),\n                    \"unchanged\": int(x[5]),\n                    \"num_uids\": int(x[6]),\n                    \"num_subkeys\": int(x[7]),\n                    \"num_signatures\": int(x[8]),\n                    \"num_revoked\": int(x[9]),\n                    \"sec_read\": int(x[10]),\n                    \"sec_imported\": int(x[11]),\n                    \"sec_dups\": int(x[12]),\n                    \"skipped_new_keys\": int(x[13]),\n                    \"not_imported\": int(x[14].rstrip()),\n                }\n        return res\n\n    def decrypt(self, data,\n            outputfd=None, passphrase=None, as_lines=False, require_MDC=True):\n        \"\"\"\n        Note that this test will fail if you don't replace the recipient with\n        one whose key you control.\n        >>> g = GnuPG(None)\n        >>> ct = g.encrypt(\"Hello, World\", to=[\"smari@mailpile.is\"])[1]\n        >>> g.decrypt(ct)[\"text\"]\n        'Hello, World'\n        \"\"\"\n        if passphrase is not None:\n            self.passphrase = passphrase.get_reader()\n        elif GnuPG.LAST_KEY_USED:\n            # This is an opportunistic approach to passphrase usage... we\n            # just hope the passphrase we used last time will work again.\n            # If we are right, we are done. If we are wrong, the output\n            # will tell us which key IDs to look for in our secret stash.\n            self.prepare_passphrase(GnuPG.LAST_KEY_USED, decrypting=True)\n\n        self.event.running_gpg(_('Decrypting %d bytes of data') % len(data))\n        for tries in (1, 2):\n            retvals = self.run([\"--decrypt\"], gpg_input=data,\n                                              outputfd=outputfd,\n                                              send_passphrase=True)\n            if tries == 1:\n                keyid = None\n                for msg in reversed(retvals[1]['status']):\n                    # Reverse order so DECRYPTION_OKAY overrides KEY_CONSIDERED.\n                    # If decryption is not ok, look for good passphrase, retry.\n                    if  msg[0] == 'DECRYPTION_OKAY':\n                        break\n                    elif (msg[0] == 'NEED_PASSPHRASE') and (passphrase is None):\n                        # This message is output by gpg 1.4 but not 2.1.\n                        if self.prepare_passphrase(msg[2], decrypting=True):\n                            keyid = msg[2]\n                            break\n                    elif (msg[0] == 'KEY_CONSIDERED') and (passphrase is None):\n                        # This message is output by gpg 2.1 but not 1.4.\n                        if self.prepare_passphrase(msg[1], decrypting=True):\n                            keyid = msg[1]\n                            break\n                if not keyid:\n                    break\n\n        if as_lines:\n            as_lines = retvals[1][\"stdout\"]\n            retvals[1][\"stdout\"] = []\n\n        rp = GnuPGResultParser(decrypt_requires_MDC=require_MDC,\n                               debug=self.debug).parse(retvals)\n        return (rp.signature_info, rp.encryption_info,\n                as_lines or rp.plaintext)\n\n    def base64_segment(self, dec_start, dec_end, skip, line_len, line_end = 2):\n        \"\"\"\n        Given the start and end index of a desired segment of decoded data,\n        this function finds smallest segment of an encoded base64 array that\n        when decoded will include the desired decoded segment.\n        It's assumed that the base64 data has a uniform line structure of\n        line_len encoded characters including line_end eol characters,\n        and that there are skip header characters preceding the base64 data.\n        \"\"\"\n        enc_start =  4*(dec_start/3)\n        dec_skip  =  dec_start - 3*enc_start/4\n        enc_start += line_end*(enc_start/(line_len-line_end))\n        enc_end =    4*(dec_end/3)\n        enc_end +=   line_end*(enc_end/(line_len-line_end))\n\n        return enc_start, enc_end, dec_skip\n\n    def pgp_packet_hdr_parse(self, header, prev_partial = False):\n        \"\"\"\n        Parse the header of a PGP packet to get the packet type, header length,\n        and data length.  Extra trailing characters in header are ignored.\n        prev_partial indicates that the previous packet was a partial packet.\n        An illegal header returns type -1, lengths 0.\n        Header format is defined in RFC4880 section 4.\n        \"\"\"\n        hdr = bytearray(header.ljust( 6, chr(0)))\n        if not prev_partial:\n            hdr_len = 1\n        else:\n            hdr[1:] = hdr           # Partial block headers don't have a tag\n            hdr[0] = 0              # Insert a dummy tag.\n            hdr_len = 0\n        is_partial = False\n\n        if prev_partial or (hdr[0] & 0xC0) == 0xC0:\n            # New format packet\n            ptag = hdr[0] & 0x3F\n            body_len = hdr[1]\n            lengthtype = 0\n            hdr_len += 1\n            if body_len < 192:\n                pass\n            elif body_len <= 223:\n                hdr_len += 1\n                body_len = ((body_len - 192) << 8) + hdr[2] + 192\n            elif body_len == 255:\n                hdr_len += 4\n                body_len =  ( (hdr[2] << 24) + (hdr[3] << 16) +\n                                (hdr[4] << 8)  + hdr[5] )\n            else:\n                # Partial packet headers are only legal for data packets.\n                if not prev_partial and not ptag in {8,9,11,18}:\n                    return (-1, 0, 0, False)\n                # Could do extra testing here.\n                is_partial = True\n                body_len = 1 << (hdr[1] & 0x1F)\n\n        elif (hdr[0] & 0xC0) == 0x80:\n            # Old format packet\n            ptag = (hdr[0] & 0x3C) >> 2\n            lengthtype = hdr[0] & 0x03\n            if lengthtype < 3:\n                hdr_len = 2\n                body_len = hdr[1]\n                if lengthtype > 0:\n                    hdr_len = 3\n                    body_len = (body_len << 8) + hdr[2]\n                if lengthtype > 1:\n                    hdr_len = 5\n                    body_len = (\n                        (body_len << 16) + (hdr[3] << 8) + hdr[4] )\n            else:\n                # Kludgy extra test for compressed packets w/ \"unknown\" length\n                # gpg generates these in signed-only files. Check for valid\n                # compression algorithm id to minimize false positives.\n                if ptag != 8 or (hdr[1] < 1 or hdr[1] > 3):\n                    return (-1, 0, 0, False)\n                hdr_len = 1\n                body_len = -1\n        else:\n            return (-1, 0, 0, False)\n\n        if hdr_len > len(header):\n            return (-1, 0, 0, False)\n\n        return ptag, hdr_len, body_len, is_partial\n\n\n    def sniff(self, data, encoding = None):\n        \"\"\"\n        Checks arbitrary data to see if it is a PGP object and returns a set\n        that indicates the kind(s) of object found. The names of the set\n        elements are based on RFC3156 content types with 'pgp-' stripped so\n        they can be used in sniffers for other protocols, e.g. S/MIME.\n        There are additional set elements 'armored' and 'unencrypted'.\n\n        This code should give no false negatives, but may give false positives.\n        For efficient handling of encoded data, only small segments are decoded.\n        Armored files are detected by their armor header alone.\n        Non-armored data is detected by looking for a sequence of valid PGP\n        packet headers.\n        \"\"\"\n\n        found = set()\n        is_base64 = False\n        is_quopri = False\n        line_len = 0\n        line_end = 1\n        enc_start = 0\n        enc_end = 0\n        dec_start = 0\n        skip = 0\n        ptag = 0\n        hdr_len = 0\n        body_len = 0\n        partial = False\n        offset_enc = 0\n        offset_dec = 0\n        offset_packet = 0\n\n        # Identify encoding and base64 line length.\n        if encoding and encoding.lower() == 'base64':\n            line_len = data.find('\\n') + 1          # Assume uniform length\n            if line_len < 0:\n                line_len = len(data)\n            elif line_len > 1 and data[line_len-2] == '\\r':\n                line_end = 2\n            if line_len - line_end > 76:            # Maximum per RFC2045 6.8\n                return found\n            enc_end = line_len\n            try:\n                segment = base64.b64decode(data[enc_start:enc_end])\n            except TypeError:\n                return found\n            is_base64 = True\n\n        elif encoding and encoding.lower() == 'quoted-printable':\n            # Can't selectively decode quopri because encoded length is data\n            # dependent due to escapes!  Just decode one medium length segment.\n            # This is enough to contain the first few packets of a long file.\n            try:\n                segment = quopri.decodestring(data[0:1500])\n            except TypeError:\n                return found                # *** ? Docs don't list exceptions\n            is_quopri = True\n        else:\n            line_len = len(data)\n            segment = data                          # *** Shallow copy?\n\n        if not segment:\n            found = set()\n        elif not (ord(segment[0]) & 0x80):\n            # Not a PGP packet header if MSbit is 0.  Check for armoured data.\n            found.add('armored')\n            if segment.startswith(self.ARMOR_BEGIN_SIGNED):\n                # Clearsigned\n                found.add('unencrypted')\n                found.add('signature')\n            elif segment.startswith(self.ARMOR_BEGIN_SIGNATURE):\n                # Detached signature\n                found.add('signature')\n            elif segment.startswith(self.ARMOR_BEGIN_ENCRYPTED):\n                # PGP uses the same armor header for encrypted and signed only\n                # Fortunately gpg --decrypt handles both!\n                found.add('encrypted')\n            elif segment.startswith(self.ARMOR_BEGIN_PUB_KEY):\n                found.add('key')\n            else:\n                found = set()\n        else:\n            # Could be PGP packet header. Check for sequence of legal headers.\n            while skip < len(segment) and body_len != -1:\n                # Check this packet header.\n                prev_partial = partial\n                ptag, hdr_len, body_len, partial = (\n                    self.pgp_packet_hdr_parse(segment[skip:], prev_partial))\n\n                if prev_partial or partial:\n                    pass\n                elif ptag == 11:\n                    found.add('unencrypted')    # Literal Data\n                elif ptag ==  1:\n                    found.add('encrypted')      # Encrypted Session Key\n                elif ptag ==  9:\n                    found.add('encrypted')      # Symmetrically Encrypted Data\n                elif ptag ==  18:\n                    found.add('encrypted')      # Symmetrically Encrypted & MDC\n                elif ptag ==  2:\n                    found.add('signature')      # Signature\n                elif ptag ==  4:\n                    found.add('signature')      # One-Pass Signature\n                elif ptag ==  6:\n                    found.add('key')            # Public Key\n                elif ptag ==  14:\n                    found.add('key')            # Public Subkey\n                elif ptag == 8:                 # Compressed Data Packet\n                    # This is a kludge.  Signed, non-encrypted files made by gpg\n                    # (but no other gpg files) consist of one compressed data\n                    # packet of unknown length which contains the signature\n                    # and data packets.\n                    # This appears to be an interpretation of RFC4880 2.3.\n                    # The compression prevents selective parsing of headers.\n                    # So such packets are assumed to be signed messages.\n                    if dec_start == 0 and body_len == -1:\n                        found.add('signature')\n                        found.add('unencrypted')\n                elif ptag < 0  or ptag > 19:\n                    found = set()\n                    return found\n\n                dec_start += hdr_len + body_len\n                skip = dec_start\n                if is_base64 and body_len != -1:\n                    enc_start, enc_end, skip = self.base64_segment(dec_start,\n                                        dec_start + 6, 0, line_len, line_end )\n                    segment = base64.b64decode(data[enc_start:enc_end])\n\n            if is_base64 and body_len != -1 and skip != len(segment):\n                # End of last packet does not match end of data.\n                found = set()\n        return found\n\n    def remove_armor(self, text):\n        lines = text.strip().splitlines(True)\n        if lines[0].startswith(self.ARMOR_BEGIN_SIGNED):\n            for idx in reversed(range(0, len(lines))):\n                if lines[idx].startswith(self.ARMOR_BEGIN_SIGNATURE):\n                    lines = lines[:idx]\n                    while lines and lines[0].strip():\n                        lines.pop(0)\n                    break\n        return ''.join(lines).strip()\n\n    def verify(self, data, signature=None):\n        \"\"\"\n        >>> g = GnuPG(None)\n        >>> s = g.sign(\"Hello, World\", _from=\"smari@mailpile.is\",\n            clearsign=True)[1]\n        >>> g.verify(s)\n        \"\"\"\n        params = [\"--verify\"]\n        if signature:\n            sig = tempfile.NamedTemporaryFile()\n            sig.write(signature)\n            sig.flush()\n            params.append(sig.name)\n            params.append(\"-\")\n\n        self.event.running_gpg(_('Checking signature in %d bytes of data'\n                                 ) % len(data))\n        ret, retvals = self.run(params, gpg_input=data, partial_read_ok=True)\n\n        rp = GnuPGResultParser(debug=self.debug)\n        return rp.parse([None, retvals]).signature_info\n\n    def encrypt(self, data, tokeys=[], armor=True,\n                            sign=False, fromkey=None, throw_keyids=False):\n        \"\"\"\n        >>> g = GnuPG(None)\n        >>> g.encrypt(\"Hello, World\", to=[\"smari@mailpile.is\"])[0]\n        0\n        \"\"\"\n        if tokeys:\n            action = [\"--encrypt\", \"--yes\", \"--expert\",\n                      \"--trust-model\", \"always\"]\n            for r in tokeys:\n                action.append(\"--recipient\")\n                action.append(r)\n            action.extend([])\n            self.event.running_gpg(_('Encrypting %d bytes of data to %s'\n                                     ) % (len(data), ', '.join(tokeys)))\n        else:\n            action = [\"--symmetric\", \"--yes\", \"--expert\"]\n            self.event.running_gpg(_('Encrypting %d bytes of data with password'\n                                     ) % len(data))\n\n        if armor:\n            action.append(\"--armor\")\n        if sign:\n            action.append(\"--sign\")\n        if sign and fromkey:\n            action.append(\"--local-user\")\n            action.append(fromkey)\n        if throw_keyids:\n            action.append(\"--throw-keyids\")\n        if fromkey:\n            self.prepare_passphrase(fromkey, signing=True)\n\n        retvals = self.run(action, gpg_input=data,\n                           send_passphrase=(sign or not tokeys))\n\n        return retvals[0], \"\".join(retvals[1][\"stdout\"])\n\n    def sign(self, data,\n             fromkey=None, armor=True, detach=True, clearsign=False,\n             passphrase=None):\n        \"\"\"\n        >>> g = GnuPG(None)\n        >>> g.sign(\"Hello, World\", fromkey=\"smari@mailpile.is\")[0]\n        0\n        \"\"\"\n        if passphrase is not None:\n            self.passphrase = passphrase.get_reader()\n        if fromkey and passphrase is None:\n            self.prepare_passphrase(fromkey, signing=True)\n\n        if detach and not clearsign:\n            action = [\"--detach-sign\"]\n        elif clearsign:\n            action = [\"--clearsign\"]\n        else:\n            action = [\"--sign\"]\n        if armor:\n            action.append(\"--armor\")\n        if fromkey:\n            action.append(\"--local-user\")\n            action.append(fromkey)\n\n        self.event.running_gpg(_('Signing %d bytes of data with %s'\n                                 ) % (len(data), fromkey or _('default')))\n        retvals = self.run(action, gpg_input=data, send_passphrase=True)\n\n        self.passphrase = None\n        return retvals[0], \"\".join(retvals[1][\"stdout\"])\n\n    def sign_key(self, keyid, signingkey=None):\n        action = [\"--yes\", \"--sign-key\", keyid]\n        if signingkey:\n            action.insert(1, \"-u\")\n            action.insert(2, signingkey)\n\n        self.event.running_gpg(_('Signing key %s with %s'\n                                 ) % (keyid, signingkey or _('default')))\n        retvals = self.run(action, send_passphrase=True)\n\n        return retvals\n\n    def delete_key(self, key_fingerprint):\n        cmd = ['--yes', '--delete-secret-and-public-key', key_fingerprint]\n        return self.run(cmd)\n\n    def recv_key(self, keyid,\n                 keyservers=DEFAULT_KEYSERVERS,\n                 keyserver_options=(DEFAULT_KEYSERVER_OPTIONS+DEFAULT_IMPORT_OPTIONS)):\n        if not keyid[:2] == '0x':\n            keyid = '0x%s' % keyid\n        self.event.running_gpg(_('Downloading key %s from key servers'\n                                 ) % (keyid))\n        for keyserver in keyservers:\n            cmd = ['--keyserver', keyserver,\n                   '--recv-key', self._escape_hex_keyid_term(keyid)]\n            for opt in keyserver_options:\n                cmd[2:2] = ['--keyserver-options', opt]\n            retvals = self.run(cmd)\n            if 'unsupported' not in ''.join(retvals[1][\"stdout\"]):\n                break\n        return self._parse_import(retvals[1][\"status\"])\n\n    def parse_hpk_response(self, lines):\n        results = {}\n        lines = [x.strip().split(\":\") for x in lines]\n        curpub = None\n        for line in lines:\n            if line[0] == \"info\":\n                pass\n            elif line[0] == \"pub\":\n                curpub = line[1]\n                validity = line[6]\n                if line[5]:\n                    if int(line[5]) < time.time():\n                        validity += 'e'\n                results[curpub] = {\n                    \"created\": datetime.fromtimestamp(int(line[4])),\n                    \"created_ts\": int(line[4]),\n                    \"keytype_name\": _(openpgp_algorithms.get(int(line[2]),\n                                                             'Unknown')),\n                    \"keysize\": line[3],\n                    \"validity\": validity,\n                    \"uids\": [],\n                    \"fingerprint\": curpub\n                }\n            elif line[0] == \"uid\":\n                email, name, comment = parse_uid(urllib.unquote(line[1]))\n                results[curpub][\"uids\"].append({\"name\": name,\n                                                \"email\": email,\n                                                \"comment\": comment})\n        return results\n\n    def search_key(self, term,\n                   keyservers=DEFAULT_KEYSERVERS,\n                   keyserver_options=(DEFAULT_KEYSERVER_OPTIONS+DEFAULT_IMPORT_OPTIONS)):\n        self.event.running_gpg(_('Searching for key for %s in key servers'\n                                 ) % (term))\n        for keyserver in keyservers:\n            cmd = ['--keyserver', keyserver,\n                   '--fingerprint',\n                   '--search-key', self._escape_hex_keyid_term(term)]\n            for opt in keyserver_options:\n                cmd[2:2] = ['--keyserver-options', opt]\n            retvals = self.run(cmd)\n            if 'unsupported' not in ''.join(retvals[1][\"stdout\"]):\n                break\n        return self.parse_hpk_response(retvals[1][\"stdout\"])\n\n    def get_pubkey(self, keyid):\n        return self.export_pubkeys(selectors=[keyid])\n\n    def get_minimal_key(self, key_id=None, user_id=None, armor=True):\n        # Note: We are not stripping revoked subkeys, revocations are\n        #       rare but important. A more nuanced approach might only\n        #       include *recent* revocations, but we don't have the\n        #       tooling for that.\n        args = [\n            '--export-options', 'export-minimal',\n            '--export-filter', 'drop-subkey=expired-t||disabled-t']\n        selector = key_id or user_id\n        if not selector:\n            raise ValueError('Export what key?')\n        if user_id:\n            args.extend(['--export-filter', 'keep-uid=uid =~ %s' % user_id])\n        return self.export_pubkeys(\n            extra_args=args, armor=armor, selectors=[selector])\n\n    def export_pubkeys(self, selectors=None, armor=True, extra_args=[]):\n        self.event.running_gpg(_('Exporting keys %s from keychain'\n                                 ) % (selectors,))\n        retvals = self.run((extra_args or []) +\n                           (['--armor'] if armor else []) +\n                           (['--export']) +\n                           (selectors or []))[1][\"stdout\"]\n        return \"\".join(retvals)\n\n    def export_privkeys(self, selectors=None):\n        retvals = self.run(['--armor',\n                            '--export-secret-keys'] + (selectors or [])\n                            )[1][\"stdout\"]\n        return \"\".join(retvals)\n\n    def address_to_keys(self, address):\n        res = {}\n        keys = self.list_keys(selectors=[address])\n        for key, props in keys.iteritems():\n            if any([x[\"email\"] == address for x in props[\"uids\"]]):\n                res[key] = props\n\n        return res\n\n    def _escape_hex_keyid_term(self, term):\n        \"\"\"Prepends a 0x to hexadecimal key ids.\n\n        For example, D13C70DA is converted to 0xD13C70DA. This is required\n        by version 2.x of GnuPG (and is accepted by 1.x).\n        \"\"\"\n        is_hex_keyid = False\n        if len(term) == GPG_KEYID_LENGTH or len(term) == 2*GPG_KEYID_LENGTH:\n            hex_digits = set(string.hexdigits)\n            is_hex_keyid = all(c in hex_digits for c in term)\n\n        if is_hex_keyid:\n            return '0x%s' % term\n        else:\n            return term\n\n    def chat(self, gpg_args, callback, *args, **kwargs):\n        \"\"\"This lets a callback have a chat with the GPG process...\"\"\"\n        gpg_args = (\n            self.common_args(interactive=True, will_send_passphrase=True) + [\n                # We may be interactive, but we're not a human!\n                \"--no-tty\",\n                \"--command-fd=0\",\n                \"--status-fd=1\"\n            ] + (gpg_args or []))\n\n        proc = None\n        try:\n            # Here we go!\n            self.debug('Running %s' % ' '.join(gpg_args))\n            self.event.update_args(gpg_args)\n            proc = Popen(gpg_args, stdin=PIPE, stdout=PIPE, stderr=PIPE,\n                         bufsize=0, long_running=True)\n\n            return callback(proc, *args, **kwargs)\n        finally:\n            # Close this so GPG will terminate. This should already have\n            # been done, but we're handling errors here...\n            if proc and proc.stdin:\n                proc.stdin.close()\n            if proc:\n                self.event.update_return_code(proc.wait())\n            else:\n                self.event.update_return_code(-1)\n\n\ndef GetKeys(gnupg, config, people):\n    keys = []\n    missing = []\n    ambig = []\n\n    # First, we go to the contact database and get a list of keys.\n    for person in set(people):\n        if '#' in person:\n            keys.append(person.rsplit('#', 1)[1])\n        else:\n            vcard = config.vcards.get_vcard(person)\n            if vcard:\n                # It is the VCard's job to give us the best key first.\n                lines = [vcl for vcl in vcard.get_all('KEY')\n                         if vcl.value.startswith('data:application'\n                                                 '/x-pgp-fingerprint,')]\n                if len(lines) > 0:\n                    keys.append(lines[0].value.split(',', 1)[1])\n                else:\n                    missing.append(person)\n            else:\n                missing.append(person)\n\n    # Load key data from gnupg for use below\n    if keys:\n        all_keys = gnupg.list_keys(selectors=keys)\n    else:\n        all_keys = {}\n\n    if missing:\n        # Keys are missing, so we try to just search the keychain\n        all_keys.update(gnupg.list_keys(selectors=missing))\n        found = []\n        for key_id, key in all_keys.iteritems():\n            for uid in key.get(\"uids\", []):\n                if uid.get(\"email\", None) in missing:\n                    missing.remove(uid[\"email\"])\n                    found.append(uid[\"email\"])\n                    keys.append(key_id)\n                elif uid.get(\"email\", None) in found:\n                    ambig.append(uid[\"email\"])\n\n    # Next, we go make sure all those keys are really in our keychain.\n    fprints = all_keys.keys()\n    for key in keys:\n        key = key.upper()\n        if key.startswith('0x'):\n            key = key[2:]\n        if key not in fprints:\n            match = [k for k in fprints if k.endswith(key)]\n            if len(match) == 0:\n                missing.append(key)\n            elif len(match) > 1:\n                ambig.append(key)\n\n    if missing:\n        raise KeyLookupError(_('Keys missing for %s'\n                               ) % ', '.join(missing), missing)\n    elif ambig:\n        ambig = list(set(ambig))\n        raise KeyLookupError(_('Keys ambiguous for %s'\n                               ) % ', '.join(ambig), ambig)\n    return keys\n\n\nclass OpenPGPMimeSigningWrapper(MimeSigningWrapper):\n    CONTAINER_PARAMS = (('micalg', 'pgp-sha512'),\n                        ('protocol', 'application/pgp-signature'))\n    SIGNATURE_TYPE = 'application/pgp-signature'\n    SIGNATURE_DESC = 'OpenPGP Digital Signature'\n\n    def crypto(self):\n        return GnuPG(self.config, event=self.event)\n\n    def get_keys(self, who):\n        return GetKeys(self.crypto(), self.config, who)\n\n\nclass OpenPGPMimeEncryptingWrapper(MimeEncryptingWrapper):\n    CONTAINER_PARAMS = (('protocol', 'application/pgp-encrypted'), )\n    ENCRYPTION_TYPE = 'application/pgp-encrypted'\n    ENCRYPTION_VERSION = 1\n\n    # FIXME: Define _encrypt, allow throw_keyids\n\n    def crypto(self):\n        return GnuPG(self.config, event=self.event)\n\n    def get_keys(self, who):\n        return GetKeys(self.crypto(), self.config, who)\n\n\nclass OpenPGPMimeSignEncryptWrapper(OpenPGPMimeEncryptingWrapper):\n    CONTAINER_PARAMS = (('protocol', 'application/pgp-encrypted'), )\n    ENCRYPTION_TYPE = 'application/pgp-encrypted'\n    ENCRYPTION_VERSION = 1\n\n    def crypto(self):\n        return GnuPG(self.config)\n\n    def _encrypt(self, message_text, tokeys=None, armor=False):\n        from_key = self.get_keys([self.sender])[0]\n        # FIXME: Allow throw_keyids here.\n        return self.crypto().encrypt(message_text,\n                                     tokeys=tokeys, armor=True,\n                                     sign=True, fromkey=from_key)\n\n    def _update_crypto_status(self, part):\n        part.signature_info.part_status = 'verified'\n        part.encryption_info.part_status = 'decrypted'\n\n\nclass GnuPGExpectScript(threading.Thread):\n    STARTUP = 'Startup'\n    START_GPG = 'Start GPG'\n    FINISHED = 'Finished'\n    SEND_PASSPHRASE = '!!<SPS'\n    SEND_EOF = '!!<SEND_EOF'\n    SCRIPT = []\n    VARIABLES = {}\n    DESCRIPTION = 'GnuPG Expect Script'\n    RUNNING_STATES = [STARTUP, START_GPG]\n\n    DEFAULT_TIMEOUT = 60 # Infinite wait isn't desirable\n\n    def __init__(self, gnupg,\n                 sps=None, event=None, variables={}, on_complete=None):\n        threading.Thread.__init__(self)\n        self.daemon = True\n        self._lock = threading.RLock()\n        self.before = ''\n        with self._lock:\n            self.state = self.STARTUP\n            self.gnupg = gnupg\n            self.event = event\n            self.variables = variables or self.VARIABLES\n            self._on_complete = [on_complete] if on_complete else []\n            self.main_script = self.SCRIPT[:]\n            self.sps = sps\n            if sps:\n                self.variables['passphrase'] = '!!<SPS'\n\n    def __str__(self):\n        return '%s: %s' % (threading.Thread.__str__(self), self.state)\n\n    running = property(lambda self: (self.state in self.RUNNING_STATES))\n    failed = property(lambda self: False)\n\n    def in_state(self, state):\n        pass\n\n    def set_state(self, state):\n        self.state = state\n        self.in_state(state)\n\n    def sendline(self, proc, line):\n        if line == self.SEND_PASSPHRASE:\n            self.gnupg.debug('>>STDIN>> [Passphrase from secure passphrase store]')\n            reader = self.sps.get_reader()\n            while True:\n                c = reader.read()\n                if c != '':\n                    proc.stdin.write(c)\n                else:\n                    proc.stdin.write('\\n')\n                    break\n        elif line == self.SEND_EOF:\n            self.gnupg.debug('>>STDIN>> [EOF]')\n            proc.stdin.close()\n        elif line is not None:\n            self.gnupg.debug('>>STDIN>> \"%s\"' % line)\n            proc.stdin.write(line.encode('utf-8'))\n            proc.stdin.write('\\n')\n\n    def _expecter(self, proc, exp, timebox):\n        while timebox[0] > 0:\n            read_char = proc.stdout.read(1)\n            if read_char:\n                self.before += read_char\n                if exp in self.before:\n                    if exp:\n                        self.gnupg.debug('==Found: %s' % exp)\n                        self.before = self.before.split(exp)[0]\n                    return True\n                elif read_char == '\\n':\n                    self.gnupg.debug('<<STDIN<< %s' % self.before)\n            elif proc.poll() is not None:\n                return False\n            else:\n                time.sleep(1)\n        return False\n\n    def expect_exact(self, proc, exp, timeout=None):\n        from mailpile.util import RunTimed, TimedOut\n        timeout = timeout if (timeout and timeout > 0) else self.DEFAULT_TIMEOUT\n        timebox = [timeout]\n        self.before = ''\n        try:\n            if not exp:\n                return True\n            self.gnupg.debug('==Expect(%ss): %s' % (timeout, exp))\n            if RunTimed(timeout, self._expecter, proc, exp, timebox):\n                return True\n            else:\n                raise TimedOut()\n        except TimedOut:\n            timebox[0] = 0\n            self.gnupg.debug('Timed out')\n            print('Boo! %s not found in %s' % (exp, self.before))\n            raise\n\n    def run_script(self, proc, script):\n        for exp, rpl, tmo, state in script:\n            self.expect_exact(proc, exp, timeout=tmo)\n            if rpl:\n                self.sendline(proc, (rpl % self.variables).strip())\n            if state:\n                self.set_state(state)\n        stderr = proc.stderr.read()\n        if stderr:\n            self.gnupg.debug('<<STDERR<< %s' % stderr)\n\n    def gpg_args(self):\n        return ['--no-use-agent', '--list-keys']\n\n    def run(self):\n        try:\n            self.set_state(self.START_GPG)\n            gpg = self.gnupg\n            gpg.event.running_gpg(_(self.DESCRIPTION) % self.variables)\n            gpg.chat(self.gpg_args(), self.run_script, self.main_script)\n            self.set_state(self.FINISHED)\n        except:\n            import traceback\n            traceback.print_exc()\n        finally:\n            with self._lock:\n                if self.state != self.FINISHED:\n                    self.state = 'Failed: ' + self.state\n                for name, callback in self._on_complete:\n                    callback()\n                self._on_complete = None\n\n    def on_complete(self, name, callback):\n        with self._lock:\n            if self._on_complete is not None:\n                if name not in [o[0] for o in self._on_complete]:\n                    self._on_complete.append((name, callback))\n            else:\n                callback()\n\n\nclass GnuPGBaseKeyGenerator(GnuPGExpectScript):\n    \"\"\"This is a background thread which generates a new PGP key.\"\"\"\n    AWAITING_LOCK = 'Pending keygen'\n    KEY_SETUP = 'Key Setup'\n    GATHER_ENTROPY = 'Creating key'\n    CREATED_KEY = 'Created key'\n    HAVE_KEY = 'Have Key'\n    KEYTYPE_RSA = 1\n    KEYTYPE_CURVE25519 = 25519\n    VARIABLES = {\n        'keytype': '1',\n        'bits': '2048',\n        'name': 'Mailpile Generated Key',\n        'email': '',\n        'comment': 'www.mailpile.is',\n        'passphrase': 'mailpile'}\n    DESCRIPTION = _('Creating a %(bits)s bit GnuPG key')\n    RUNNING_STATES = (GnuPGExpectScript.RUNNING_STATES +\n                      [AWAITING_LOCK, KEY_SETUP, GATHER_ENTROPY, HAVE_KEY])\n\n    failed = property(lambda self: (not self.running and\n                                    not self.generated_key))\n\n    def __init__(self, *args, **kwargs):\n        super(GnuPGBaseKeyGenerator, self).__init__(*args, **kwargs)\n        self.generated_key = None\n\n    def in_state(self, state):\n        if state == self.HAVE_KEY:\n             self.generated_key = self.before.strip().split()[-1]\n\n    def run(self):\n        # In order to minimize risk of timeout during key generation (due to\n        # lack of entropy), we serialize them here using a global lock\n        self.set_state(self.AWAITING_LOCK)\n        if self.event:\n            self.event.message = _('Waiting to generate a PGP key.')\n        with ENTROPY_LOCK:\n            if self.event:\n                self.event.data['keygen_gotlock'] = 1\n                self.event.message = _('Generating new PGP key.')\n            super(GnuPGBaseKeyGenerator, self).run()\n\n\nclass GnuPG14KeyGenerator(GnuPGBaseKeyGenerator):\n    \"\"\"This is the GnuPG 1.4x specific PGP key generation script.\"\"\"\n    B = GnuPGBaseKeyGenerator\n\n    # Note: If GnuPG starts asking for things in a different order,\n    #       we'll needlessly fail. To address this, we'd need to make\n    # the expect logic smarter. However, since GnuPG 1.4.x isn't being\n    # developed anymore, this is unlikely to change.\n\n    SCRIPT = [\n        ('GET_LINE keygen.algo',        '%(keytype)s',   -1, B.KEY_SETUP),\n        ('GET_LINE keygen.size',           '%(bits)s',   -1, None),\n        ('GET_LINE keygen.valid',                 '0',   -1, None),\n        ('GET_LINE keygen.name',           '%(name)s',   -1, None),\n        ('GET_LINE keygen.email',         '%(email)s',   -1, None),\n        ('GET_LINE keygen.comment',     '%(comment)s',   -1, None),\n        ('GET_HIDDEN passphrase',    '%(passphrase)s',   -1, None),\n        ('GOT_IT',                               None,   -1, B.GATHER_ENTROPY),\n        ('KEY_CREATED',                          None, 7200, B.CREATED_KEY),\n        ('\\n',                                   None,   -1, B.HAVE_KEY)]\n\n    def gpg_args(self):\n        return ['--no-use-agent', '--allow-freeform-uid', '--gen-key']\n\n\nclass GnuPG21RSAKeyGenerator(GnuPG14KeyGenerator):\n    \"\"\"This is the GnuPG 2.1.x specific RSA PGP key generation script.\"\"\"\n    B = GnuPGBaseKeyGenerator\n    SCRIPT = [\n        ('',               'Key-Type: %(keytype)s',      -1, B.KEY_SETUP),\n        ('',               'Key-Length: %(bits)s',       -1, None),\n        ('',               'Key-Usage: sign',            -1, None),\n        ('',               'Subkey-Type: %(keytype)s',   -1, None),\n        ('',               'Subkey-Length: %(bits)s',    -1, None),\n        ('',               'Subkey-Usage: encrypt',      -1, None),\n        ('',               'Passphrase: %(passphrase)s', -1, None),\n        ('',               'Name-Real: %(name)s',        -1, None),\n        ('',               'Name-Email: %(email)s',      -1, None),\n        ('',               'Name-Comment: %(comment)s',  -1, None),\n        ('',               'Expire-Date: 0',             -1, None),\n        ('',               B.SEND_EOF,                   -1, B.GATHER_ENTROPY),\n        ('KEY_CREATED',    None,                       1800, B.CREATED_KEY),\n        ('\\n',             None,                         -1, B.HAVE_KEY)]\n\n    def sendline(self, proc, line):\n        # Filter out any unset attributes to keep GnuPG happy.\n        if line and not line.strip().endswith(':'):\n            super(GnuPG14KeyGenerator, self).sendline(proc, line)\n\n    def gpg_args(self):\n        # --yes should keep GnuPG from complaining if there already exists\n        #       a key with this UID.\n        return ['--yes', '--allow-freeform-uid', '--batch', '--gen-key']\n\n\nclass GnuPG21Curve25519KeyGenerator(GnuPG21RSAKeyGenerator):\n    \"\"\"This is the GnuPG 2.1.x specific ECC PGP key generation script.\"\"\"\n    B = GnuPGBaseKeyGenerator\n    SCRIPT = [\n        ('',               'Key-Type: eddsa',            -1, B.KEY_SETUP),\n        ('',               'Key-Curve: Ed25519',         -1, None),\n        ('',               'Key-Usage: sign',            -1, None),\n        ('',               'Subkey-Type: ecdh',          -1, None),\n        ('',               'Subkey-Curve: Curve25519',   -1, None),\n        ('',               'Subkey-Usage: encrypt',      -1, None),\n        ('',               'Passphrase: %(passphrase)s', -1, None),\n        ('',               'Name-Real: %(name)s',        -1, None),\n        ('',               'Name-Email: %(email)s',      -1, None),\n        ('',               'Name-Comment: %(comment)s',  -1, None),\n        ('',               'Expire-Date: 0',             -1, None),\n        ('',               B.SEND_EOF,                   -1, B.GATHER_ENTROPY),\n        ('KEY_CREATED',    None,                        300, B.CREATED_KEY),\n        ('\\n',             None,                         -1, B.HAVE_KEY)]\n\n\nclass GnuPGDummyKeyGenerator(GnuPGBaseKeyGenerator):\n    \"\"\"A dummy key generator class, for incompatible versions of GnuPG.\"\"\"\n\n    DESCRIPTION = _('Unable to create a %(bits)s bit key, wrong GnuPG version')\n\n    def __init__(self, *args, **kwargs):\n        GnuPGBaseKeyGenerator.__init__(self, *args, **kwargs)\n        self.generated_key = False\n\n    def run(self):\n        with self._lock:\n            self.gnupg.event.running_gpg(_(self.DESCRIPTION) % self.variables)\n            self.set_state(self.FINISHED)\n            for name, callback in self._on_complete:\n                callback()\n            self._on_complete = None\n\n\ndef GnuPGKeyGenerator(gnupg, **kwargs):\n    \"\"\"Return an instanciated generator, depending on GnuPG version.\"\"\"\n    consts = GnuPGBaseKeyGenerator\n    version = gnupg.version_tuple()\n    variables = kwargs.get('variables', consts.VARIABLES)\n\n    if version < (1, 5):\n        return GnuPG14KeyGenerator(gnupg, **kwargs)\n\n    elif version >= (2, 1, 12):\n        # Version 2.1.12 was the first version with the key generation\n        # and --pinentry=loopback semantics we require.  We just don't\n        # even try with older versions.\n        if variables.get('keytype') == consts.KEYTYPE_CURVE25519:\n            return GnuPG21Curve25519KeyGenerator(gnupg, **kwargs)\n        else:\n            return GnuPG21RSAKeyGenerator(gnupg, **kwargs)\n\n    else:\n        return GnuPGDummyKeyGenerator(gnupg, **kwargs)\n\n\n# Reset our translation variable\n_ = gettext\n\n## Include the SKS keyserver certificates here ##\nKEYSERVER_CERTIFICATE=\"\"\"\n-----BEGIN CERTIFICATE-----\nMIIFizCCA3OgAwIBAgIJAK9zyLTPn4CPMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNV\nBAYTAk5PMQ0wCwYDVQQIDARPc2xvMR4wHAYDVQQKDBVza3Mta2V5c2VydmVycy5u\nZXQgQ0ExHjAcBgNVBAMMFXNrcy1rZXlzZXJ2ZXJzLm5ldCBDQTAeFw0xMjEwMDkw\nMDMzMzdaFw0yMjEwMDcwMDMzMzdaMFwxCzAJBgNVBAYTAk5PMQ0wCwYDVQQIDARP\nc2xvMR4wHAYDVQQKDBVza3Mta2V5c2VydmVycy5uZXQgQ0ExHjAcBgNVBAMMFXNr\ncy1rZXlzZXJ2ZXJzLm5ldCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC\nggIBANdsWy4PXWNUCkS3L//nrd0GqN3dVwoBGZ6w94Tw2jPDPifegwxQozFXkG6I\n6A4TK1CJLXPvfz0UP0aBYyPmTNadDinaB9T4jIwd4rnxl+59GiEmqkN3IfPsv5Jj\nMkKUmJnvOT0DEVlEaO1UZIwx5WpfprB3mR81/qm4XkAgmYrmgnLXd/pJDAMk7y1F\n45b5zWofiD5l677lplcIPRbFhpJ6kDTODXh/XEdtF71EAeaOdEGOvyGDmCO0GWqS\nFDkMMPTlieLA/0rgFTcz4xwUYj/cD5e0ZBuSkYsYFAU3hd1cGfBue0cPZaQH2HYx\nQk4zXD8S3F4690fRhr+tki5gyG6JDR67aKp3BIGLqm7f45WkX1hYp+YXywmEziM4\naSbGYhx8hoFGfq9UcfPEvp2aoc8u5sdqjDslhyUzM1v3m3ZGbhwEOnVjljY6JJLx\nMxagxnZZSAY424ZZ3t71E/Mn27dm2w+xFRuoy8JEjv1d+BT3eChM5KaNwrj0IO/y\nu8kFIgWYA1vZ/15qMT+tyJTfyrNVV/7Df7TNeWyNqjJ5rBmt0M6NpHG7CrUSkBy9\np8JhimgjP5r0FlEkgg+lyD+V79H98gQfVgP3pbJICz0SpBQf2F/2tyS4rLm+49rP\nfcOajiXEuyhpcmzgusAj/1FjrtlynH1r9mnNaX4e+rLWzvU5AgMBAAGjUDBOMB0G\nA1UdDgQWBBTkwyoJFGfYTVISTpM8E+igjdq28zAfBgNVHSMEGDAWgBTkwyoJFGfY\nTVISTpM8E+igjdq28zAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQAR\nOXnYwu3g1ZjHyley3fZI5aLPsaE17cOImVTehC8DcIphm2HOMR/hYTTL+V0G4P+u\ngH+6xeRLKSHMHZTtSBIa6GDL03434y9CBuwGvAFCMU2GV8w92/Z7apkAhdLToZA/\nX/iWP2jeaVJhxgEcH8uPrnSlqoPBcKC9PrgUzQYfSZJkLmB+3jEa3HKruy1abJP5\ngAdQvwvcPpvYRnIzUc9fZODsVmlHVFBCl2dlu/iHh2h4GmL4Da2rRkUMlbVTdioB\nUYIvMycdOkpH5wJftzw7cpjsudGas0PARDXCFfGyKhwBRFY7Xp7lbjtU5Rz0Gc04\nlPrhDf0pFE98Aw4jJRpFeWMjpXUEaG1cq7D641RpgcMfPFvOHY47rvDTS7XJOaUT\nBwRjmDt896s6vMDcaG/uXJbQjuzmmx3W2Idyh3s5SI0GTHb0IwMKYb4eBUIpQOnB\ncE77VnCYqKvN1NVYAqhWjXbY7XasZvszCRcOG+W3FqNaHOK/n/0ueb0uijdLan+U\nf4p1bjbAox8eAOQS/8a3bzkJzdyBNUKGx1BIK2IBL9bn/HravSDOiNRSnZ/R3l9G\nZauX0tu7IIDlRCILXSyeazu0aj/vdT3YFQXPcvt5Fkf5wiNTo53f72/jYEJd6qph\nWrpoKqrwGwTpRUCMhYIUt65hsTxCiJJ5nKe39h46sg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB\nhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV\nBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5\nMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT\nEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR\nQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh\ndGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR\n6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X\npz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC\n9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV\n/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf\nZd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z\n+pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w\nqP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah\nSL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC\nu9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf\nFobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq\ncrxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E\nFgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB\n/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl\nwFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM\n4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV\n2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna\nFxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ\nCuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK\nboHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke\njkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL\nS0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb\nQOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl\n0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB\nNVOFBkpdn627G190\n-----END CERTIFICATE-----\n\"\"\"\n"
  },
  {
    "path": "mailpile/crypto/keyinfo.py",
    "content": "from __future__ import print_function\nimport time\nimport traceback\n\nimport pgpdump\nimport pgpdump.packet\nfrom pgpdump.utils import PgpdumpException, get_int4\nfrom mailpile.util import dict_merge\n\n\n# Patch pgpdump so it stops crashing on weird public keys #####################\n\ndef monkey_patch_pgpdump():\n    # Add Algorithm 22 to the lookup table\n    pgpdump.packet.AlgoLookup.pub_algorithms[22] = 'EdDSA'\n\n    # Patch the key parser to just silently ignore strange keys\n    orig_pkm = pgpdump.packet.PublicKeyPacket.parse_key_material\n\n    def _patched_pkm(self, offset):\n        try:\n            return orig_pkm(self, offset)\n        except PgpdumpException:\n            return offset\n    pgpdump.packet.PublicKeyPacket.parse_key_material = _patched_pkm\n\n\n# FIXME: Perhaps we should be checking pgpdump versions? But most of\n#        these are actually API changes, not just bugfixes. It's likely\n# that versions 1.6+ will continue to throw exceptsions on \"unknown\" key\n# types... if/when 1.6 or 2.x get released, we'll just have to revisit\n# this logic.\nmonkey_patch_pgpdump()\n\n\n# Classes for storing PGP key info ############################################\n\nclass ustr(str):\n    def __new__(cls, content):\n        return super(ustr, cls).__new__(cls, content.upper())\n\n\nclass RestrictedDict(dict):\n    KEYS = {}\n\n    @classmethod\n    def prep_properties(cls):\n        def mk_prop(k):\n            return property(lambda s: s[k], lambda s, v: s.__setitem__(k, v))\n        for k in cls.KEYS:\n            setattr(cls, k, mk_prop(k))\n\n    def __init__(self, *args, **kwargs):\n        dict.__init__(self, *args, **kwargs)\n        for k, (t, d) in self.KEYS.items():\n            if k not in self:\n                if t in (list, dict):\n                    self[k] = t()\n                else:\n                    self[k] = d\n\n    def keys(self):\n        kl = list(dict.keys(self))\n        for dk in (k for k in self.KEYS if k not in kl):\n            kl.append(dk)\n        return sorted(kl)\n\n    def __setitem__(self, item, value):\n        if item[:1] != '_':\n            if item not in self.KEYS:\n                raise KeyError('Invalid key: %s' % item)\n            if not isinstance(value, self.KEYS[item][0]):\n                try:\n                    if isinstance(value, unicode):\n                        # Value is unicode, we want other: encode, convert\n                        value = self.KEYS[item][0](value.encode('utf-8'))\n                    elif isinstance(value, str):\n                        # Value is not unicode, we want unicode: decode, convert\n                        value = self.KEYS[item][0](value.decode('utf-8'))\n                    else:\n                        # Neither unicode nor string, just try to convert\n                        value = self.KEYS[item][0](value)\n                except (TypeError, ValueError):\n                    raise TypeError(\n                        'Bad type for %s: %s (want %s)'\n                        % (item, value, self.KEYS[item][0].__name__))\n        dict.__setitem__(self, item, value)\n\n    def __getitem__(self, item):\n        if item[:1] == '_':\n            return dict.__getitem__(self, item)\n        else:\n            return dict.get(self, item, self.KEYS[item][1])\n\n\nclass KeyUID(RestrictedDict):\n    KEYS = {\n        'name':    (unicode, ''),\n        'email':   (str, ''),\n        'comment': (unicode, '')}\n\n    def __repr__(self):\n        parts = []\n        if self['name']:\n            parts.append(self['name'])\n        if self['email']:\n            parts.append('<%s>' % self['email'])\n        if self['comment']:\n            parts.append('(%s)' % self['comment'])\n        return ' '.join(parts)\n\n\nclass KeyInfo(RestrictedDict):\n    KEY_TRUSTED_CODES = ('u', 'f')  # Note: Ignoring marginal keys\n    KEY_INVALID_CODES = ('i', 'd', 'e', 'r', 'n')\n    KEYS = {\n        'fingerprint':  (ustr, 'MISSING'),\n        'capabilities': (str, ''),\n        'keytype_name': (str, 'unknown'),\n        'keytype_code': (int, 0),\n        'keysize':      (int, 0),\n        'created':      (int, 0),\n        'expires':      (int, 0),\n        'validity':     (str, '?'),\n        'key_source':   (str, None),\n        'uids':         (list, None),\n        'subkeys':      (list, None),\n        'is_subkey':    (bool, False),\n        'have_secret':  (bool, False),\n        'on_keychain':  (bool, False),\n        'in_vcards':    (bool, False)}\n\n    expired = property(lambda k: time.time() > k.expires > 0)\n\n    is_usable = property(lambda k: (k.validity not in k.KEY_INVALID_CODES\n                                    and not k.expired))\n\n    can_encrypt = property(lambda k: ('e' in k.capabilities.lower()\n                                      and k.is_usable))\n\n    can_sign = property(lambda k: ('s' in k.capabilities.lower()\n                                   and k.is_usable))\n\n    def summary(self, full_fingerprint=False):\n        \"\"\"\n        Generate a short string summarizing the key's main properties: key ID,\n        UIDs, expiration date, algorithm, size, capabilities, and validity.\n\n        Note: If summary ends with !, the key is invalid/unusable.\n        \"\"\"\n        now = time.time()\n        emails = ','.join(sorted([u.email for u in self.uids if u.email]))\n        return '%s%s%s/%s%s/%s%s' % (\n            self.fingerprint[-(9999 if full_fingerprint else 16):],\n            ('=%s' % emails) if emails else '',\n            ('<%x' % self.expires) if self.expires else '',\n            self.keytype_name[:3],\n            self.keysize,\n            self.capabilities,\n            ('' if self.is_usable else '!'))\n\n    def __repr__(self):\n        if self.is_subkey:\n            return self.summary()\n        return '{ %s }' % '\\n  '.join(\n            '%-12s = %s' % (k, self[k])\n            for k in self.keys() if self[k] is not None)\n\n    def ensure_autocrypt_uid(keyinfo, ac_uid):\n        \"\"\"Ensure we include the email from the Autocrypt header in a UID.\"\"\"\n        if keyinfo.is_subkey:\n            return\n        found = 0\n        for uid in keyinfo.uids:\n            if uid.email == ac_uid.email:\n                uid.comment = uid.comment + '(Autocrypt)'\n                found += 1\n        if not found:\n            keyinfo.uids += [ac_uid]\n\n    def add_subkey_capabilities(keyinfo, now=None):\n        \"\"\"Make key \"inherit\" the capabilities of any un-expired subkeys.\"\"\"\n        now = now or time.time()\n        key_caps = set(c for c in keyinfo.capabilities\n                       if c in ('c', 'e', 's'))\n        combined_caps = set(c.upper() for c in key_caps)\n        for subkey in keyinfo.subkeys:\n            if not (0 < subkey.expires < now):\n                combined_caps |= set(c.upper() for c in subkey.capabilities)\n        keyinfo.capabilities = '%s%s' % (\n            ''.join(sorted(list(combined_caps))),\n            ''.join(sorted(list(key_caps))))\n\n    def synthesize_validity(keyinfo, now=None):\n        \"\"\"Synthesize key validity property.\"\"\"\n        # FIXME: Revocations?\n        now = now or time.time()\n        if (0 < keyinfo.expires < now\n                and keyinfo.validity not in keyinfo.KEY_INVALID_CODES):\n            keyinfo.validity = 'e'\n\n    def recalculate_expiration(keyinfo, now=None):\n        \"\"\"Adjust the main expiration date to take subkeys into account.\"\"\"\n        now = now or time.time()\n\n        # For each capability, figure out what is the latest expiration date\n        # provided by a subkey for that capability.\n        expirations = {}\n        for cap in set(c for c in keyinfo.capabilities if c in ('C', 'E', 'S')):\n            for subkey in keyinfo.subkeys:\n                if subkey.expires and not (0 < subkey.expires < now):\n                    expirations[cap] = max(subkey.expires, expirations.get(cap, 0))\n\n        for cap in expirations:\n            # If the subkey is not expired, and provides a capability our\n            # main key doesn't have, then its expiration date matters.\n            if cap.lower() not in keyinfo.capabilities:\n                keyinfo.expires = min(subkey.expires, keyinfo.expires)\n\n    @classmethod\n    def FromGPGI(cls, gpgi_keyinfo):\n        mki = cls(\n            created=int(gpgi_keyinfo.get(\"creation_date_ts\",\n                                         gpgi_keyinfo.get('created_ts', 0))),\n            expires=int(gpgi_keyinfo.get(\"expiration_date_ts\", 0)),\n            capabilities=gpgi_keyinfo.get(\"capabilities\", \"\"),\n            have_secret=gpgi_keyinfo.get(\"secret\", False))\n        for k in ('fingerprint', 'validity', 'keytype_name'):\n            mki[k] = str(gpgi_keyinfo[k])\n        for k in ('keysize', ):\n            mki[k] = int(gpgi_keyinfo[k])\n        for uid in gpgi_keyinfo.get('uids', []):\n            mki.uids.append(KeyUID(\n                name=uid.get(\"name\", \"\"),\n                email=uid.get(\"email\", \"\"),\n                comment=uid.get(\"comment\", \"\")))\n        mki.capabilities = ''.join(sorted([c for c in mki.capabilities]))\n        return mki\n\n\nclass MailpileKeyInfo(KeyInfo):\n    KEYS = dict_merge(KeyInfo.KEYS, {\n        'vcards':       (dict, None),\n        'origins':      (list, None),\n        'is_autocrypt': (bool, False),\n        'is_gossip':    (bool, False),\n        'is_preferred': (bool, False),\n        'is_pinned':    (bool, False),\n        'scores':       (dict, None),\n        'score_stars':  (int, 0),\n        'score_reason': (unicode, None),\n        'score':        (int, 0)})\n\n\nKeyUID.prep_properties()\nKeyInfo.prep_properties()\nMailpileKeyInfo.prep_properties()\n\n\ndef get_keyinfo(data, autocrypt_header=None,\n                key_info_class=KeyInfo, key_uid_class=KeyUID,\n                key_source=None):\n    \"\"\"\n    This method will parse a stream of OpenPGP packets into a list of KeyInfo\n    objects.\n\n    Note: Signatures are not validated, this code only parses the data.\n    \"\"\"\n    try:\n        if \"-----BEGIN\" in data:\n            ak = pgpdump.AsciiData(data)\n        else:\n            ak = pgpdump.BinaryData(data)\n        packets = list(ak.packets())\n    except (TypeError, IndexError, PgpdumpException):\n        traceback.print_exc()\n        return []\n\n    def _unixtime(packet, seconds=0, days=0):\n        return (packet.raw_creation_time\n                + (days or 0) * 24 * 3600\n                + (seconds or 0))\n\n    results = []\n    last_uid = key_uid_class()  # Dummy\n    last_key = key_info_class()  # Dummy\n    last_pubkeypacket = None\n    main_key_id = None\n    for m in packets:\n        try:\n            if isinstance(m, pgpdump.packet.PublicKeyPacket):\n                size = str(int(1.024 *\n                               round(len('%x' % (m.modulus or 0)) / 0.256)))\n                last_pubkeypacket = m\n                last_key = key_info_class(\n                    key_source=key_source,\n                    fingerprint=m.fingerprint,\n                    keytype_name=m.pub_algorithm or '',\n                    keytype_code=m.raw_pub_algorithm,\n                    keysize=size)\n                if isinstance(m, pgpdump.packet.PublicSubkeyPacket):\n                    last_key.is_subkey = True\n                    results[-1].subkeys.append(last_key)\n                else:\n                    main_key_id = m.key_id\n                    results.append(last_key)\n\n                # Older pgpdumps may fail here and cause traceback noise, but\n                # the loop will limp onwards.\n                last_key.created = _unixtime(m)\n                if m.raw_days_valid > 0:\n                    last_key.expires = _unixtime(m, days=m.raw_days_valid)\n                    if last_key.expires == last_key.created:\n                        last_key.expires = 0\n\n            elif isinstance(m, pgpdump.packet.UserIDPacket) and results:\n                last_uid = key_uid_class(name=m.user_name, email=m.user_email)\n                last_key.uids.append(last_uid)\n\n            elif isinstance(m, pgpdump.packet.SignaturePacket) and results:\n                # Note: We don't actually check the signature; we trust\n                #       GnuPG will if we decide to use this key.\n                if m.key_id == main_key_id:\n                    for s in m.subpackets:\n                        if s.subtype == 9:\n                            exp = _unixtime(last_pubkeypacket, seconds=get_int4(s.data, 0))\n                            last_key.expires = max(last_key.expires, exp)\n                        elif s.subtype == 27:\n                            caps = set(c for c in last_key.capabilities)\n                            for flag, c in ((0x01, 'c'), (0x02, 's'),\n                                            (0x0C, 'e'), (0x20, 'a')):\n                                if s.data[0] & flag:\n                                    caps.add(c)\n                            last_key.capabilities = ''.join(caps)\n\n        except (TypeError, AttributeError, KeyError, IndexError, NameError):\n            traceback.print_exc()\n\n    autocrypt_uid = None\n    if autocrypt_header:\n        # The autocrypt spec tells us that the visible addr= attribute\n        # overrides whatever is on the key itself, so we synthesize a\n        # fake UID here so the info is correct in an Autocrypt context.\n        autocrypt_uid = key_uid_class(\n            email=autocrypt_header['addr'],\n            comment='Autocrypt')\n\n    now = time.time()\n    for keyinfo in results:\n        keyinfo.synthesize_validity(now=now)\n        keyinfo.add_subkey_capabilities(now=now)\n        keyinfo.recalculate_expiration(now=now)\n        if autocrypt_uid is not None:\n            keyinfo.ensure_autocrypt_uid(autocrypt_uid)\n\n    return results\n\n\nif __name__ == \"__main__\":\n    import sys\n\n    for f in sys.argv[1:]:\n        with open(f, 'r') as fd:\n            keyinfo = get_keyinfo(fd.read())[0]\n            print('%s' % keyinfo)\n            print('%s' % keyinfo.summary(full_fingerprint=True))\n            print('Is usable = %s, Can encrypt = %s, Can sign = %s' % (\n                keyinfo.is_usable, keyinfo.can_encrypt, keyinfo.can_sign))\n            print('')\n\n# EOF\n"
  },
  {
    "path": "mailpile/crypto/mime.py",
    "content": "from __future__ import print_function\n# These are methods to do with MIME and crypto, implementing PGP/MIME.\n\nimport math\nimport random\nimport re\nimport StringIO\nimport email.parser\n\nfrom email import encoders\nfrom email.mime.multipart import MIMEMultipart\nfrom email.mime.base import MIMEBase\n\nfrom mailpile.crypto.state import EncryptionInfo, SignatureInfo\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.mailutils.generator import Generator\n\n\n##[ Common utilities ]#########################################################\n\ndef Normalize(payload):\n    # http://tools.ietf.org/html/rfc3156 says we must:\n    #\n    #   - use CRLF everywhere\n    #   - strip trailing whitespace\n    #   - end with a CRLF\n    #\n    # In particlar, the stripping of trailing whitespace seems (based on\n    # experiments with mutt), to in practice mean \"strip trailing whitespace\n    # off the last line\"...\n    #\n    text = re.sub(r'\\r?\\n', '\\r\\n', payload).rstrip(' \\t')\n    if not text.endswith('\\r\\n'):\n        text += '\\r\\n'\n    return text\n\n\ndef MessageAsString(part, unixfrom=False):\n    buf = StringIO.StringIO()\n    Generator(buf).flatten(part, unixfrom=unixfrom)\n    return Normalize(buf.getvalue()).replace('--\\r\\n--', '--\\r\\n\\r\\n--')\n\n\nclass EncryptionFailureError(ValueError):\n    def __init__(self, message, to_keys):\n        ValueError.__init__(self, message)\n        self.to_keys = to_keys\n\n\nclass SignatureFailureError(ValueError):\n    def __init__(self, message, from_key):\n        ValueError.__init__(self, message)\n        self.from_key = from_key\n\n\nDEFAULT_CHARSETS = ['utf-8', 'iso-8859-1']\n\n\ndef _decode_text_part(part, payload, charsets=None):\n    for cs in (c for c in ([part.get_content_charset() or None] +\n                           (charsets or DEFAULT_CHARSETS)) if c):\n        try:\n            return cs, payload.decode(cs)\n        except (UnicodeDecodeError, TypeError, LookupError):\n            pass\n    return '8bit', payload\n\n\ndef _update_text_payload(part, payload, charsets=None):\n    if 'content-transfer-encoding' in part:\n        # We want this recalculated by the charset setting below\n        del part['content-transfer-encoding']\n    charset, payload = _decode_text_part(part, payload, charsets=charsets)\n    part.set_payload(payload, charset)\n\n\n##[ Methods for unwrapping encrypted parts ]###################################\n\n\ndef MimeAttachmentDisposition(part, kind, newpart):\n    \"\"\"\n    Create a Content-Disposition header for a processed attachment using the\n    original file name if available from the PGP packet, otherwise try\n    stripping the extension from the unprocessed attachment file name.\n    \"\"\"\n    # Delete embedded \\n and \\r (shouldn't get_filename() do this itself??).\n    filename = part.get_filename().replace('\\n','').replace('\\r','')\n    if filename:\n        filename = filename.decode('utf-8', 'replace')\n        part.encryption_info[\"description\"] = _(\"Decrypted: %s\") % filename\n\n    if part.encryption_info.filename:\n         newfilename = part.encryption_info.filename\n    else:\n        # get_filename() can parse quoted, folded and RFC2231 names.\n        # If there's no filename in Content-Disposition it tries Content-Type.\n        newfilename = filename\n        if 'armored' in kind and newfilename.endswith('.asc'):\n            newfilename = newfilename[:len(newfilename)-len('.asc')]\n        elif newfilename.endswith('.gpg'):\n            newfilename = newfilename[:len(newfilename)-len('.gpg')]\n\n    # add_header() does quoting, folding, maybe someday RFC2231?.\n    newpart.add_header('Content-Disposition', 'attachment',\n                       filename=newfilename.encode('utf-8'))\n\n\ndef MimeReplacePart(part, newpart, keep_old_headers=False):\n    \"\"\"\n    Replace a MIME part with new version (decrypted, signature verified, ... ).\n    retaining headers from the old part that are not in the new part. The\n    headers that would be overwritten will be renamed and kept if the\n    keep_old_headers variable is set to a prefix string.\n\n    MIME headers (Content-*) get special treatment.\n\n    Returns a set of the headers that got copied from the new part.\n    \"\"\"\n    part.set_payload(newpart.get_payload())\n\n    # Original MIME headers must go, whether we're replacing them or not.\n    for hdr in [k for k in part.keys() if k.lower().startswith('content-')]:\n        while hdr in part:\n            del part[hdr]\n\n    # If we're keeping the non-MIME old headers, make copies now before\n    # they get deleted below.\n    if keep_old_headers:\n        if not isinstance(keep_old_headers, str):\n            keep_old_headers = \"Old\"\n        for h in newpart.keys():\n            headers = (part.get_all(h) or [])\n            if (len(headers) == 1) and (part[h] == newpart[h]):\n                continue\n            for v in headers:\n                part.add_header('X-%s-%s' % (keep_old_headers, h), v)\n\n    for h in newpart.keys():\n        while h in part:\n            del part[h]\n\n    copied = set([])\n    for h, v in newpart.items():\n        part.add_header(h, v)\n        if not h.lower().startswith('content-'):\n            copied.add(h)\n\n    return copied\n\n\ndef UnwrapMimeCrypto(part, protocols=None, psi=None, pei=None, charsets=None,\n                     unwrap_attachments=True, require_MDC=True,\n                     depth=0, sibling=0, efail_unsafe=False, allow_decrypt=True):\n    \"\"\"\n    This method will replace encrypted and signed parts with their\n    contents and set part attributes describing the security properties\n    instead.\n    \"\"\"\n\n    # Guard against maliciously constructed emails\n    if depth > 6:\n        return\n\n    part.signature_info = SignatureInfo(parent=psi)\n    part.encryption_info = EncryptionInfo(parent=pei)\n\n    part.signed_headers = set([])\n    part.encrypted_headers = set([])\n\n    mimetype = part.get_content_type() or 'text/plain'\n    disposition = part['content-disposition'] or \"\"\n    encoding = part['content-transfer-encoding'] or \"\"\n\n    # FIXME: Check the protocol. PGP? Something else?\n    # FIXME: This is where we add hooks for other MIME encryption\n    #        schemes, so route to callbacks by protocol.\n    crypto_cls = protocols['openpgp']\n\n    if part.is_multipart():\n        # Containers are by default not bubbly\n        part.signature_info.bubbly = False\n        part.encryption_info.bubbly = False\n\n    if part.is_multipart() and mimetype == 'multipart/signed':\n        try:\n            boundary = part.get_boundary()\n            payload, signature = part.get_payload()\n\n            # The Python get_payload() method likes to rewrite headers,\n            # which breaks signature verification. So we manually parse\n            # out the raw payload here.\n            head, raw_payload, junk = part.as_string(\n                ).replace('\\r\\n', '\\n').split('\\n--%s\\n' % boundary, 2)\n\n            part.signature_info = crypto_cls().verify(\n                Normalize(raw_payload), signature.get_payload())\n            part.signature_info.bubble_up(psi)\n\n            # Reparent the contents up, removing the signature wrapper\n            hdrs = MimeReplacePart(part, payload,\n                                   keep_old_headers='PH-Renamed')\n            part.signed_headers = hdrs\n\n            # Try again, in case we just unwrapped another layer\n            # of multipart/something.\n            UnwrapMimeCrypto(part,\n                             protocols=protocols,\n                             psi=part.signature_info,\n                             pei=part.encryption_info,\n                             charsets=charsets,\n                             unwrap_attachments=unwrap_attachments,\n                             require_MDC=require_MDC,\n                             depth=depth+1, sibling=sibling,\n                             efail_unsafe=efail_unsafe,\n                             allow_decrypt=allow_decrypt)\n\n        except (IOError, OSError, ValueError, IndexError, KeyError):\n            part.signature_info = SignatureInfo()\n            part.signature_info[\"status\"] = \"error\"\n            part.signature_info.bubble_up(psi)\n\n    elif part.is_multipart() and mimetype == 'multipart/encrypted':\n        try:\n            if not allow_decrypt:\n                raise ValueError('Decryption forbidden, MIME structure is weird')\n            preamble, payload = part.get_payload()\n            (part.signature_info, part.encryption_info, decrypted) = (\n                crypto_cls().decrypt(\n                    payload.as_string(), require_MDC=require_MDC))\n        except (IOError, OSError, ValueError, IndexError, KeyError):\n            part.encryption_info = EncryptionInfo()\n            part.encryption_info[\"status\"] = \"error\"\n\n        part.signature_info.bubble_up(psi)\n        part.encryption_info.bubble_up(pei)\n\n        if part.encryption_info['status'] == 'decrypted':\n            newpart = email.parser.Parser().parsestr(decrypted)\n\n            # Reparent the contents up, removing the encryption wrapper\n            hdrs = MimeReplacePart(part, newpart,\n                                   keep_old_headers='MH-Renamed')\n\n            # Is there a Memory-Hole/Protected-Headers force-display part?\n            pl = part.get_payload()\n            if hdrs and isinstance(pl, list):\n                if (pl[0]['content-type'].startswith('text/rfc822-headers;')\n                        and 'protected-headers' in pl[0]['content-type']):\n                    # Parse these headers as well and override the top level,\n                    # again. This is to be sure we see the same thing as\n                    # everyone else (same algo as enigmail).\n                    data = email.parser.Parser().parsestr(\n                        pl[0].get_payload(), headersonly=True)\n                    for h in data.keys():\n                        while h in part:\n                            del part[h]\n                        part[h] = data[h]\n                        hdrs.add(h)\n\n                    # Finally just delete the part, we're done with it!\n                    del pl[0]\n\n            part.encrypted_headers = hdrs\n            if part.signature_info[\"status\"] != 'none':\n                part.signed_headers = hdrs\n\n            # Try again, in case we just unwrapped another layer\n            # of multipart/something.\n            UnwrapMimeCrypto(part,\n                             protocols=protocols,\n                             psi=part.signature_info,\n                             pei=part.encryption_info,\n                             charsets=charsets,\n                             unwrap_attachments=unwrap_attachments,\n                             require_MDC=require_MDC,\n                             depth=depth+1, sibling=sibling,\n                             efail_unsafe=efail_unsafe,\n                             allow_decrypt=allow_decrypt)\n\n    # If we are still multipart after the above shenanigans (perhaps due\n    # to an error state), recurse into our subparts and unwrap them too.\n    elif part.is_multipart():\n        for count, sp in enumerate(part.get_payload()):\n            # EFail mitigation: We decrypt attachments and the first part\n            # of a nested multipart structure, but not any subsequent parts.\n            # This allows rewriting of messages to *append* cleartext, but\n            # disallows rewriting that pushes \"inline\" encrypted content\n            # further down to where the recipient might not notice it.\n            sp_disp = (unwrap_attachments and sp['content-disposition']) or \"\"\n            allow_decrypt = (efail_unsafe\n                    or (count == sibling == 0)\n                    or sp_disp.startswith('attachment')) and allow_decrypt\n            UnwrapMimeCrypto(sp,\n                             protocols=protocols,\n                             psi=part.signature_info,\n                             pei=part.encryption_info,\n                             charsets=charsets,\n                             unwrap_attachments=unwrap_attachments,\n                             require_MDC=require_MDC,\n                             depth=depth+1, sibling=count,\n                             efail_unsafe=efail_unsafe,\n                             allow_decrypt=allow_decrypt)\n\n    elif disposition.startswith('attachment'):\n        # The sender can attach signed/encrypted/key files without following\n        # rules for naming or mime type.\n        # So - sniff to detect parts that need processing and identify protocol.\n        kind = ''\n        for protocol in protocols:\n            crypto_cls = protocols[protocol]\n            kind = crypto_cls().sniff(part.get_payload(), encoding)\n            if kind:\n                break\n\n        if unwrap_attachments and ('encrypted' in kind or 'signature' in kind):\n            # Messy! The PGP decrypt operation is also needed for files which\n            # are encrypted and signed, and files that are signed only.\n            payload = part.get_payload( None, True )\n            try:\n                if not allow_decrypt:\n                    raise ValueError('Decryption forbidden, MIME structure is weird')\n                (part.signature_info, part.encryption_info, decrypted) = (\n                    crypto_cls().decrypt(\n                        payload, require_MDC=require_MDC))\n            except (IOError, OSError, ValueError, IndexError, KeyError):\n                part.encryption_info = EncryptionInfo()\n                part.encryption_info[\"status\"] = \"error\"\n\n            part.signature_info.bubble_up(psi)\n            part.encryption_info.bubble_up(pei)\n\n            if (part.encryption_info['status'] == 'decrypted' or\n                    part.signature_info['status'] == 'verified'):\n\n                # Force base64 encoding and application/octet-stream type\n                newpart = MIMEBase('application', 'octet-stream')\n                newpart.set_payload(decrypted)\n                encoders.encode_base64(newpart)\n\n                # Add Content-Disposition with appropriate filename.\n                MimeAttachmentDisposition(part, kind, newpart)\n\n                MimeReplacePart(part, newpart)\n\n                # Is there another layer to unwrap?\n                UnwrapMimeCrypto(part,\n                                 protocols=protocols,\n                                 psi=part.signature_info,\n                                 pei=part.encryption_info,\n                                 charsets=charsets,\n                                 unwrap_attachments=unwrap_attachments,\n                                 require_MDC=require_MDC,\n                                 depth=depth+1, sibling=sibling,\n                                 efail_unsafe=efail_unsafe,\n                                 allow_decrypt=allow_decrypt)\n            else:\n                # FIXME: Best action for unsuccessful attachment processing?\n                pass\n\n    elif mimetype == 'text/plain':\n        return UnwrapPlainTextCrypto(part,\n                                     protocols=protocols,\n                                     psi=psi,\n                                     pei=pei,\n                                     charsets=charsets,\n                                     require_MDC=require_MDC,\n                                     depth=depth+1, sibling=sibling,\n                                     efail_unsafe=efail_unsafe,\n                                     allow_decrypt=allow_decrypt)\n\n    else:\n        # FIXME: This is where we would handle cryptoschemes that don't\n        #        appear as multipart/...\n        pass\n\n\n    # Mix in our bubbles\n    part.signature_info.mix_bubbles()\n    part.encryption_info.mix_bubbles()\n\n    # Bubble up!\n    part.signature_info.bubble_up(psi)\n    part.encryption_info.bubble_up(pei)\n\n\ndef UnwrapPlainTextCrypto(part, protocols=None, psi=None, pei=None,\n                                charsets=None, require_MDC=True,\n                                depth=0, sibling=0,\n                                efail_unsafe=False, allow_decrypt=True):\n    \"\"\"\n    This method will replace encrypted and signed parts with their\n    contents and set part attributes describing the security properties\n    instead.\n    \"\"\"\n    payload = part.get_payload(None, True).strip()\n    si = SignatureInfo(parent=psi)\n    ei = EncryptionInfo(parent=pei)\n    for crypto_cls in protocols.values():\n        crypto = crypto_cls()\n\n        if (payload.startswith(crypto.ARMOR_BEGIN_ENCRYPTED) and\n                payload.endswith(crypto.ARMOR_END_ENCRYPTED)):\n            try:\n                if not allow_decrypt:\n                    raise ValueError('Decryption forbidden, MIME structure is weird')\n                si, ei, text = crypto.decrypt(payload, require_MDC=require_MDC)\n                _update_text_payload(part, text, charsets=charsets)\n            except (IOError, OSError, ValueError, IndexError, KeyError):\n                ei = EncryptionInfo()\n                ei[\"status\"] = \"error\"\n            break\n\n        elif (payload.startswith(crypto.ARMOR_BEGIN_SIGNED) and\n                payload.endswith(crypto.ARMOR_END_SIGNED)):\n            try:\n                si = crypto.verify(payload)\n            except (IOError, OSError, ValueError, IndexError, KeyError):\n                si = SignatureInfo()\n                si[\"status\"] = \"error\"\n            _update_text_payload(part, crypto.remove_armor(payload),\n                                 charsets=charsets)\n            break\n\n    part.signature_info = si\n    part.signature_info.bubble_up(psi)\n    part.encryption_info = ei\n    part.encryption_info.bubble_up(pei)\n\n\n##[ Methods for stripping down message headers ]###############################\n\n\ndef ObscureSubject(subject):\n    \"\"\"\n    Replace the Subject line with something nondescript.\n    \"\"\"\n    return '...'\n\n\ndef ObscureNames(hdr):\n    \"\"\"\n    Remove names (leaving e-mail addresses) from the To: and Cc: headers.\n\n    >>> ObscureNames(\"Bjarni R. E. <bre@klaki.net>, e@b.c (Elmer Boop)\")\n    u'<bre@klaki.net>, <e@b.c>'\n\n    \"\"\"\n    from mailpile.mailutils.addresses import AddressHeaderParser\n    return ', '.join('<%s>' % ai.address for ai in AddressHeaderParser(hdr))\n\n\ndef ObscureSender(sender):\n    \"\"\"\n    Remove as much metadata from the From: line as possible.\n    \"\"\"\n    return ObscureNames(sender)\n\n\ndef ObscureAllRecipients(sender):\n    \"\"\"\n    Remove all content from the To: and Cc: lines entirely.\n    \"\"\"\n    return \"recipients-suppressed;\"\n\n\n# A dictionary for use with MimeWrapper's obscured_headers parameter,\n# that will obscure only what is required from the public header.\nOBSCURE_HEADERS_REQUIRED = {\n    'autocrypt-gossip': lambda t: None}\n\n\n# A dictionary for use with MimeWrapper's obscured_headers parameter,\n# that will obscure as much of the metadata from the public header as\n# possible without breaking compatibility.\nOBSCURE_HEADERS_MILD = {\n    'subject': ObscureSubject,\n    'from': ObscureSender,\n    'sender': ObscureSender,\n    'reply-to': ObscureSender,\n    'to': ObscureNames,\n    'cc': ObscureNames,\n    'user-agent': lambda t: None,\n    'autocrypt-gossip': lambda t: None}\n\n\n# A dictionary for use with MimeWrapper's obscured_headers parameter,\n# that will obscure as much of the metadata from the public header as\n# possible. This is only useful with encrypted messages and will badly\n# break things unless the recipient is running an MUA that fully implements\n# Memory Hole / Protected Headers.\nOBSCURE_HEADERS_EXTREME = {\n    'subject': ObscureSubject,\n    'from': ObscureSender,\n    'sender': ObscureSender,\n    'reply-to': ObscureSender,\n    'to': ObscureAllRecipients,\n    'cc': lambda t: None,\n    'date': lambda t: None,\n    'in-reply-to': lambda t: None,\n    'references': lambda t: None,\n    'openpgp': lambda t: None,\n    'user-agent': lambda t: None,\n    'autocrypt-gossip': lambda t: None}\n\n\n##[ Methods for encrypting and signing ]#######################################\n\nclass MimeWrapper:\n    CONTAINER_TYPE = 'multipart/mixed'\n    CONTAINER_PARAMS = ()\n\n    # These are the default \"memory hole\" settings; wrap/protect the\n    # important user-visible headers.\n    WRAPPED_HEADERS = ('subject', 'from', 'to', 'cc', 'date', 'user-agent',\n                       'sender', 'reply-to', 'in-reply-to', 'references',\n                       'openpgp')\n\n    # Force-displayed headers; if these headers get obscured, add a\n    # visible part that shows them to the user in legacy clients.\n    FORCE_DISPLAY_HEADERS = ('subject', 'from', 'to', 'cc')\n\n    # By default, no headers are obscured. That's a user preference,\n    # since there's a trade-off between privacy and compatibility.\n    OBSCURED_HEADERS = OBSCURE_HEADERS_REQUIRED\n\n    def __init__(self, config,\n                 event=None, cleaner=None,\n                 sender=None, recipients=None,\n                 use_html_wrapper=False,\n                 wrapped_headers=None,\n                 obscured_headers=None):\n        from mailpile.mailutils.emails import MakeBoundary\n        self.config = config\n        self.event = event\n        self.sender = sender\n        self.cleaner = cleaner\n        self.recipients = recipients or []\n        self.use_html_wrapper = use_html_wrapper\n        self.container = c = MIMEMultipart(boundary=MakeBoundary())\n\n        self.wrapped_headers = self.WRAPPED_HEADERS\n        if wrapped_headers is not None:\n            self.wrapped_headers = wrapped_headers or ()\n\n        self.obscured_headers = self.OBSCURED_HEADERS\n        if obscured_headers is not None:\n            self.obscured_headers = obscured_headers or {}\n\n        c.set_type(self.CONTAINER_TYPE)\n        c.signature_info = SignatureInfo(bubbly=False)\n        c.encryption_info = EncryptionInfo(bubbly=False)\n        if self.cleaner:\n            self.cleaner(self.container)\n        for pn, pv in self.CONTAINER_PARAMS:\n            self.container.set_param(pn, pv)\n\n    def crypto(self):\n        return NotImplementedError(\"Please override me\")\n\n    def attach(self, part):\n        c = self.container\n        c.attach(part)\n\n        if not hasattr(part, 'signature_info'):\n            part.signature_info = SignatureInfo(parent=c.signature_info)\n            part.encryption_info = EncryptionInfo(parent=c.encryption_info)\n        else:\n            part.signature_info.parent = c.signature_info\n            part.signature_info.bubbly = True\n            part.encryption_info.parent = c.encryption_info\n            part.encryption_info.bubbly = True\n\n        if self.cleaner:\n            self.cleaner(part)\n        del part['MIME-Version']\n        return self\n\n    def get_keys(self, people):\n        return people\n\n    def flatten(self, msg, unixfrom=False):\n        return MessageAsString(msg, unixfrom=unixfrom)\n\n    def get_only_text_part(self, msg):\n        count = 0\n        only_text_part = None\n        for part in msg.walk():\n            if part.is_multipart():\n                continue\n            count += 1\n            mimetype = part.get_content_type() or 'text/plain'\n            if mimetype != 'text/plain' or count != 1:\n                return False\n            else:\n                only_text_part = part\n        return only_text_part\n\n    def wrap(self, msg, **kwargs):\n        # Subclasses override\n        return msg\n\n    def prepare_wrap(self, msg):\n        obscured = self.obscured_headers\n        wrapped = self.wrapped_headers\n\n        obscured_set = set([])\n        to_delete = {}\n        for (h, header_value) in msg.items():\n            if not header_value:\n                continue\n\n            hl = h.lower()\n            if hl == 'mime-version':\n                to_delete[h] = True\n            elif not hl.startswith('content-'):\n                if hl in obscured:\n                    obscured_set.add(h)\n                    oh = obscured[hl](header_value)\n                    if oh:\n                        self.container.add_header(h, oh)\n                else:\n                    self.container.add_header(h, header_value)\n                if hl not in wrapped and hl not in obscured:\n                    to_delete[h] = True\n\n        for h in to_delete:\n            while h in msg:\n                del msg[h]\n\n        if hasattr(msg, 'signature_info'):\n            self.container.signature_info = msg.signature_info\n            self.container.encryption_info = msg.encryption_info\n\n        return self.force_display_headers(msg, obscured_set)\n\n    def force_display_headers(self, msg, obscured_set):\n        # If we aren't changing the structure of the message (adding a\n        # force-display part), we can just wrap the original and be done.\n        if not [k for k in obscured_set\n                if k.lower() in self.FORCE_DISPLAY_HEADERS]:\n            return msg\n\n        header_display = MIMEBase('text', 'rfc822-headers',\n                                  protected_headers=\"v1\")\n        header_display['Content-Disposition'] = 'inline'\n\n        container = MIMEBase('multipart', 'mixed')\n        container.attach(header_display)\n        container.attach(msg)\n\n        # Cleanup...\n        for p in (msg, header_display, container):\n            if 'MIME-Version' in p:\n                del p['MIME-Version']\n        if self.cleaner:\n            self.cleaner(header_display)\n            self.cleaner(msg)\n\n        # NOTE: The copying happens at the end here, because we need the\n        #       cleaner (on msg) to have run first.\n        display_headers = []\n        to_delete = {}\n        for h, v in msg.items():\n            hl = h.lower()\n            if not hl.startswith('content-') and not hl.startswith('mime-'):\n                container.add_header(h, v)\n                if hl in self.FORCE_DISPLAY_HEADERS and h in obscured_set:\n                    display_headers.append('%s: %s' % (h, v))\n                to_delete[h] = True\n        for h in to_delete:\n            while h in msg:\n                del msg[h]\n\n        header_display.set_payload('\\r\\n'.join(reversed(display_headers)))\n\n        return container\n\n\nclass MimeSigningWrapper(MimeWrapper):\n    CONTAINER_TYPE = 'multipart/signed'\n    CONTAINER_PARAMS = ()\n    SIGNATURE_TYPE = 'application/x-signature'\n    SIGNATURE_DESC = 'Abstract Digital Signature'\n\n    def __init__(self, *args, **kwargs):\n        MimeWrapper.__init__(self, *args, **kwargs)\n\n        name = ('OpenPGP-digital-signature.html'\n                if self.use_html_wrapper else\n                'OpenPGP-digital-signature.asc')\n        self.sigblock = MIMEBase(*self.SIGNATURE_TYPE.split('/'))\n        self.sigblock.set_param(\"name\", name)\n        for h, v in ((\"Content-Description\", self.SIGNATURE_DESC),\n                     (\"Content-Disposition\",\n                      \"attachment; filename=\\\"%s\\\"\" % name)):\n            self.sigblock.add_header(h, v)\n\n    def _wrap_sig_in_html(self, sig):\n        return (\n            \"<html><body><h1>%(title)s</h1><p>\\n\\n%(description)s\\n\\n</p>\"\n            \"<pre>\\n%(sig)s\\n</pre><hr>\"\n            \"<i><a href='%(ad_url)s'>%(ad)s</a>.</i></body></html>\"\n            ) % self._wrap_sig_in_html_vars(sig)\n\n    def _wrap_sig_in_html_vars(self, sig):\n        return {\n            # FIXME: We deliberately do not flag these messages for i18n\n            #        translation, since we rely on 7-bit content here so as\n            #        not to complicate the MIME structure of the message.\n            \"title\": \"Digital Signature\",\n            \"description\": (\n                \"This is a digital signature, which can be used to verify\\n\"\n                \"the authenticity of this message. You can safely discard\\n\"\n                \"or ignore this file if your e-mail software does not\\n\"\n                \"support digital signatures.\"),\n            \"ad\": \"Generated by Mailpile\",\n            \"ad_url\": \"https://www.mailpile.is/\",  # FIXME: Link to help?\n            \"sig\": sig}\n\n    def _update_crypto_status(self, part):\n        part.signature_info.part_status = 'verified'\n\n    def wrap(self, msg, prefer_inline=False):\n        from_key = self.get_keys([self.sender])[0]\n\n        if prefer_inline:\n            prefer_inline = self.get_only_text_part(msg)\n        else:\n            prefer_inline = False\n\n        if prefer_inline is not False:\n            message_text = Normalize(prefer_inline.get_payload(None, True)\n                                     .strip() + '\\r\\n\\r\\n')\n            status, sig = self.crypto().sign(message_text,\n                                             fromkey=from_key,\n                                             clearsign=True,\n                                             armor=True)\n            if status == 0:\n                _update_text_payload(prefer_inline, sig)\n                self._update_crypto_status(prefer_inline)\n                return msg\n\n        else:\n            msg = self.prepare_wrap(msg)\n            self.attach(msg)\n            self.attach(self.sigblock)\n            message_text = self.flatten(msg)\n            status, sig = self.crypto().sign(message_text,\n                                             fromkey=from_key, armor=True)\n            if status == 0:\n                if self.use_html_wrapper:\n                    sig = self._wrap_sig_in_html(sig)\n                self.sigblock.set_payload(sig)\n                self._update_crypto_status(self.container)\n                return self.container\n\n        raise SignatureFailureError(_('Failed to sign message!'), from_key)\n\n\nclass MimeEncryptingWrapper(MimeWrapper):\n    CONTAINER_TYPE = 'multipart/encrypted'\n    CONTAINER_PARAMS = ()\n    ENCRYPTION_TYPE = 'application/x-encrypted'\n    ENCRYPTION_VERSION = 0\n\n    def __init__(self, *args, **kwargs):\n        MimeWrapper.__init__(self, *args, **kwargs)\n\n        self.version = MIMEBase(*self.ENCRYPTION_TYPE.split('/'))\n        self.version.set_payload('Version: %s\\n' % self.ENCRYPTION_VERSION)\n        for h, v in ((\"Content-Disposition\", \"attachment\"), ):\n            self.version.add_header(h, v)\n\n        self.enc_data = MIMEBase('application', 'octet-stream')\n        for h, v in ((\"Content-Disposition\",\n                      \"attachment; filename=\\\"OpenPGP-encrypted-message.asc\\\"\"), ):\n            self.enc_data.add_header(h, v)\n\n        self.attach(self.version)\n        self.attach(self.enc_data)\n\n    def _encrypt(self, message_text, tokeys=None, armor=False):\n        return self.crypto().encrypt(message_text,\n                                     tokeys=tokeys, armor=True)\n\n    def _update_crypto_status(self, part):\n        part.encryption_info.part_status = 'decrypted'\n\n    def _add_padding(self, text, chunksize=None):\n        \"\"\"Add up to 16kB of whitespace to the end of a message as padding.\"\"\"\n        if chunksize is None:\n            chunksize = min(max(160, 2 ** int(math.log(len(text), 2))), 8192)\n        pad = ('\\r\\n' + (' ' * 78)) * int(chunksize / 80)\n        return (text\n            + (pad if random.randint(0, 1) else '')\n            + (pad[:chunksize - (len(text) % chunksize)]))\n\n    def wrap(self, msg, prefer_inline=False):\n        to_keys = set(self.get_keys(self.recipients + [self.sender]))\n\n        if prefer_inline:\n            prefer_inline = self.get_only_text_part(msg)\n        else:\n            prefer_inline = False\n\n        if prefer_inline is not False:\n            message_text = self._add_padding(\n                Normalize(prefer_inline.get_payload(None, True)),\n                # This padding is user facing, so don't overdo it.\n                chunksize=160)\n            status, enc = self._encrypt(message_text,\n                                        tokeys=to_keys,\n                                        armor=True)\n            if status == 0:\n                _update_text_payload(prefer_inline, enc)\n                self._update_crypto_status(prefer_inline)\n                return msg\n\n        else:\n            msg = self.prepare_wrap(msg)\n            if self.cleaner:\n                self.cleaner(msg)\n\n            message_text = self._add_padding(self.flatten(msg))\n            status, enc = self._encrypt(message_text,\n                                        tokeys=to_keys,\n                                        armor=True)\n            if status == 0:\n                self.enc_data.set_payload(enc)\n                self._update_crypto_status(self.enc_data)\n                return self.container\n\n        raise EncryptionFailureError(_('Failed to encrypt message!'), to_keys)\n\n\nif __name__ == \"__main__\":\n    import sys\n    import doctest\n\n    # FIXME: Add tests for the wrapping/unwrapping code. It's crazy that\n    #        we don't have such tests. :-(\n\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS)\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/crypto/records.py",
    "content": "#\n# NOTE: THIS CODE IS NOT BEING USED - YET.\n#       IT IS HERE TO FACILITATE REVIEW, COMMENTS AND EXPERIMENTS.\n#\n# FIXME:  Respond to following comment from Kai Michaelis\n#\n#    The dictionaries in records.py completely lack any message\n#    authentication and thus are vulnerable. The cryptographic checksums\n#    of the plaintext do not prevent this. The comments mention that this\n#    is intentional to limit destruction in case of bit flips. This is\n#    misguided in two ways. First, it's the responsibility of the file\n#    system and transport protocol to prevent these and second,\n#    cryptographic checksums are not designed for error detecting or\n#    correction.\n#    ...\n#    To derive keys for multiple shards in a EncryptedDictionary use a\n#    proper KDF like HKDF [2] or a CPRNG like those defined in NIST\n#    SP800-90a rev1 [3] (except Dual_EC DRBG :wink:).\n#\n#    Full discussion here: https://github.com/mailpile/Mailpile/pull/1684\n#\n# FIXME:  We've switched from AES-CBC to AES-CTR. We should review the\n#         IV logic and make sure it is still sane given the different\n#         properties of the underlying algorithms.\n#\n\"\"\"\nRecord-based AES encrypted data storage\n\nThis is a collection of modules designed to allow for easy Pythonic use\nof encrypted data stores.\n\nThe basics are provided by EncryptedRecordStore, which defines the\non-disk storage format (largely compatible with RFC2822) and takes care\nof the encryption and decryption of records.\n\nThe EncryptedBlobStore provides a subset of Pythonic list semantics,\nextending EncryptedRecordStore to allow for arbitrarily sized elements\nand splitting the storage accross multiple files to play nice with\nbackups, network-based storage and other environments where huge files\nmight be a problem.\n\nThe EncryptedDict provides a subset of Pythonic dict semantics, using a\nmixture of the above two classes.\n\nNotes:\n\n1. A simple SHA256 digest is used to derive an AES key. This would not\n   be considered sufficient for low entropy (human generated) keys.\n2. All data storage is record based, which implies nontrivial amounts\n   of disk space may be wasted. On the other hand, this is good for\n   security as it obscures the size of the data being stored.\n3. The EncryptedDict uses the encryption key as a salt to ensure the\n   hashing is not predictable to an attacker. Again, low entropy keys\n   should be avoided.\n4. No provisions are made to make it possible to change keys.\n5. No buffering or caching of any kind is done.\n6. Deleting entries is NOT supported anywhere. Overwriting works.\n\nPerformance thoughts:\n\nThe key to performance of these algorithms will ultimately depend on how\nwell the OS caches data. In general we can help with that by encouraging\nhot spots, trying to cluster frequently used values together. The other\nway we can help is to minimize wasted space within the records\nthemselves, so the OS doesn't waste RAM caching junk.\n\nFor the metadata index, we expect hot spot clustering to focus around\nrecently received mail. The default sorts are by date and most of the\ntime users are reading or organizing recently received mail. So the OS\nshould have a relatively easy time effectively caching records for\ncurrent metadata.\n\nThe case for posting lists, which naturally live in an EncryptedDict is\ndifferent but also promising. In general, the keyword index will grow\nlinearly with the number of index messages; in particular each message\nID is unique and will generate one or more new keywords in the index.\nThus the keyword index will have a very long tail of rarely used, small\nentries. The expected performance of such entries (reads and writes) is\ndominated by the disk seeks times. For these entries, we can save at\nleast one disk seek by storing the values along with the keys, but that\nis about all we can do.\n\nConversely, some keywords will have a very high frequency; for example\nvirtually all English language messages will contain the word \"the\".\nThese common keywords will become hot spots during the indexing process,\nso causing them to cluster together will again let us play nice with the\noperating system caches. A basic strategy for this is to allow larger\nentries to bump smaller ones from the front of each hash bucket to later\nstages, as entry size correlates with keyword popularity. Another\noptimization which is out of scope for this module, is they should\ncompress well as they will contain long sequential runs of message IDs.\n\nFrom a frequency point of view, the middle-of-the-road keywords barely\nmatter; 94% of all keywords match fewer than 5 messages, about 0.16%\nmatch over 1000 messages. On average about 17 keywords are generated\nper message.\n\nDetailed search keyword stats:\n\n  emails  keywords   __________ratios__________\n       2   4551468    88.21%  100.00%   88.21%\n       4    290686     5.63%   11.79%   93.84%\n       8    150924     2.92%    6.16%   96.77%\n      16     74239     1.44%    3.23%   98.21%\n      32     38341     0.74%    1.79%   98.95%\n      64     20552     0.40%    1.05%   99.35%\n     128     13178     0.26%    0.65%   99.60%\n     256      7731     0.15%    0.40%   99.75%\n     512      4640     0.09%    0.25%   99.84%\n    1024      2793     0.05%    0.16%   99.90%\n    2048      2141     0.04%    0.10%   99.94%\n   >2048      3084     0.06%    0.06%  100.00%\n          (sample size: ~300k emails)\n\n\"\"\"\nfrom __future__ import print_function\nfrom __future__ import absolute_import\nimport binascii\nimport hashlib\nimport os\nimport struct\nimport time\nimport threading\n\nfrom .aes_utils import getrandbits, aes_ctr_encrypt, aes_ctr_decrypt\n\n\nclass _SimpleList(object):\n    \"\"\"Some syntactic sugar for listalikes\"\"\"\n    def append(self, value):\n        with self._lock:\n            pos = len(self)\n            self[pos] = value\n        return pos\n\n    def extend(self, values):\n        for v in values:\n            self.append(v)\n\n    def __getslice__(self, i, j):\n        return [self[v] for v in range(i, j)]\n\n    def __iadd__(self, y):\n        self.extend(y)\n\n    def __iter__(self):\n        return (self[v] for v in range(0, len(self)))\n\n    def __reversed__(self):\n        return (self[v] for v in reversed(range(0, len(self))))\n\n\nclass EncryptedRecordStore(_SimpleList):\n    \"\"\"\n    This is an on-disk AES encrypted record storage. Data is written\n    out base64 encoded, with 64 characters per line + CRLF, as that is\n    likely to make it in and out of IMAP servers or other mail stores\n    unchanged, while giving us a round multiple of what both AES and\n    Base64 can handle without padding.\n    \"\"\"\n\n    _HEADER = ('X-Mailpile-Encrypted-Records: v1\\r\\n'\n               'From: Mailpile <encrypted@mailpile.is>\\r\\n'\n               'Subject: %(filename)s\\r\\n'\n               'cipher: aes-128-ctr\\r\\n'\n               'record-size: %(record_size)d\\r\\n'\n               'iv-seed: %(ivs)s\\r\\n'\n               '\\r\\n')\n\n    def __init__(self, fn, key,\n                 max_bytes=400, overwrite=False, default_value=None):\n        key_hash = bytes(hashlib.sha256(key.encode('utf-8')).digest())\n\n        self._NEWLINE = '\\r\\n'\n        self._max_bytes = max_bytes\n        self._calculate_constants()\n        self._default_value = default_value\n\n        self._iv_seed = getrandbits(48)\n        self._iv_seed_random = '%x' % getrandbits(64)\n        self._aes_key = key_hash[:16]\n        self._header_data = {\n            'record_size': self._RECORD_SIZE,\n            'filename': fn,\n            'ivs': '%12.12x' % self._iv_seed\n        }\n        self._lock = threading.RLock()\n\n        try:\n            self._fd = open(fn, 'wb+' if overwrite else 'rb+')\n        except (OSError, IOError):\n            self._fd = open(fn, 'wb+')\n\n        if not self._parse_header():\n            self._write_header()\n        if max_bytes > self._max_bytes:\n            raise AssertionError('max_bytes mismatch')\n\n    def _calculate_constants(self):\n        # Calculate our constants! Magic numbers:\n        #  - 48 is how much data fits in 64 byte of base64\n        #  - 16 is the size of the IV\n        #  - 7 is the minimum size of our checksum\n        self._RECORD_LINES = max(1, int((self._max_bytes + 7 + 16) / 48) + 1)\n        self._RECORD_SIZE = self._RECORD_LINES * 48\n        self._MAX_DATA_SIZE = self._RECORD_SIZE - (7 + 16)\n        self._RECORD_LINE_BYTES = 64\n        self._ZERO_RECORD = '\\0' * self._RECORD_SIZE\n        self._RECORD_BYTES =  self._RECORD_LINES * (self._RECORD_LINE_BYTES +\n                                                    len(self._NEWLINE))\n\n    def _parse_header(self):\n        try:\n            with self._lock:\n                self._fd.seek(0)\n                header = self._fd.read(len(self._HEADER % self._header_data)\n                                       + 1024)\n                if not header:\n                    return False\n\n            if '\\n' in header and self._NEWLINE not in header:\n                self._NEWLINE = '\\n'\n            header = header.split(self._NEWLINE + self._NEWLINE)[0]\n            headers = dict(hl.strip().split(': ', 1)\n                           for hl in header.splitlines())\n\n            self._header_skip = len(header) + len(self._NEWLINE) * 2\n            self._iv_seed = long(headers['iv-seed'], 16) + 10240020\n            self._max_bytes = int(headers['record-size']) - (7 + 16) - 1\n            self._header_data = {\n                'record_size': int(headers['record-size']),\n                'filename': headers['Subject'],\n                'ivs': headers['iv-seed']\n            }\n            self._calculate_constants()\n            return True\n\n        except IOError:\n            return False\n        except (KeyError, ValueError, AssertionError):\n            import traceback\n            traceback.print_exc()\n            return False\n\n    def _write_header(self):\n        with self._lock:\n            self._header_data['ivs'] = '%12.12x' % self._iv_seed\n            header = self._HEADER % self._header_data\n            self._fd.seek(0)\n            self._fd.write(header)\n            self._fd.flush()\n            self._header_skip = len(header)\n\n    def close(self):\n        self._write_header()\n        self._fd.close()\n\n    def _iv(self, pos):\n        # Here we generate an IV that should never repeat: the first 6 bytes\n        # are derived from a random counter created at program startup, the\n        # rest is pseudorandom crap.\n        with self._lock:\n            self._iv_seed += 1\n            self._iv_seed %= 0x1000000000000\n            if (self._iv_seed % 123456) == 0:\n                self._write_header()\n\n            self._iv_seed_random = hashlib.sha512(self._iv_seed_random\n                                                  ).digest()\n\n        iv = bytes(struct.pack('<Q', self._iv_seed)[:6] +\n                   self._iv_seed_random)\n\n        return iv[:16]\n\n    def save_record(self, pos, data):\n        if not isinstance(data, str):\n            raise ValueError('Data must be a str')\n        elif len(data) > self._MAX_DATA_SIZE:\n            raise ValueError('Data too big for record')\n\n        pos = int(pos)\n        if pos < 0:\n            raise KeyError('Negative record position')\n\n        iv = self._iv(pos)\n\n        # We're using MD5 as a checksum here to detect corruption.\n        cks = hashlib.md5(data).hexdigest()\n\n        # We use the checksum as padding, where we are guaranteed by the\n        # assertion above to have room for at least 6 nybbles of the MD5.\n        record = (data + ':' + cks + self._ZERO_RECORD\n                  )[:self._RECORD_SIZE - len(iv)]\n        encrypted = (iv + aes_ctr_encrypt(self._aes_key, iv, record)\n                     ).encode('base64').replace('\\n', '')\n\n        if len(encrypted) != self._RECORD_LINES * self._RECORD_LINE_BYTES:\n            raise AssertionError('%d: <%s> %s != %d*%d' %\n                                 (pos, encrypted, len(encrypted),\n                                  self._RECORD_LINES, self._RECORD_LINE_BYTES))\n\n        with self._lock:\n            self._fd.seek(self._header_skip + pos * self._RECORD_BYTES)\n            for i in range(0, self._RECORD_LINES):\n                self._fd.write(encrypted[i * self._RECORD_LINE_BYTES :\n                                         (i+1) * self._RECORD_LINE_BYTES])\n                self._fd.write(self._NEWLINE)\n\n    def save_zeros(self, pos, count):\n        zrecord = (('A' * self._RECORD_LINE_BYTES) + self._NEWLINE\n                   ) * self._RECORD_LINES\n        with self._lock:\n            self._fd.seek(self._header_skip + pos * self._RECORD_BYTES)\n            for i in range(0, count):\n                self._fd.write(zrecord)\n\n    def load_record(self, pos):\n        pos = int(pos)\n        if pos < 0:\n            raise KeyError('Negative record position')\n\n        with self._lock:\n            self._fd.seek(self._header_skip + pos * self._RECORD_BYTES)\n            try:\n                encrypted = self._fd.read(self._RECORD_BYTES).decode('base64')\n            except (IOError, binascii.Error):\n                encrypted = ''\n\n        if encrypted == '':\n            if self._default_value is not None:\n                return self._default_value\n            raise KeyError('Failed to read at %s' % pos)\n        if (len(encrypted) != self._RECORD_SIZE):\n            raise KeyError('Incorrect record size %s at %s'\n                           % (len(encrypted), pos))\n\n        iv, encrypted = encrypted[:16], encrypted[16:]\n        if iv == ('\\x00' * 16) and self._default_value is not None:\n            return self._default_value\n\n        plaintext, checksum = aes_ctr_decrypt(self._aes_key, iv, encrypted\n                                              ).rsplit(':', 1)\n\n        checksum = bytes(checksum.replace('\\0', ''))\n        if (len(checksum) < 6):\n            raise ValueError('Checksum too short')\n        if not hashlib.md5(plaintext).hexdigest().startswith(checksum):\n            raise ValueError('Checksum mismatch')\n\n        return plaintext\n\n    def __getitem__(self, pos):\n        try:\n            return self.load_record(pos)\n        except ValueError:\n            raise KeyError(pos)\n\n    def __setitem__(self, pos, data):\n        return self.save_record(pos, data)\n\n    def get(self, pos, default=None):\n        try:\n            return self[pos]\n        except KeyError:\n            return default\n\n    def __len__(self):\n        with self._lock:\n            self._fd.seek(0, 2)\n            size = self._fd.tell()\n        return (size - self._header_skip) // self._RECORD_BYTES\n\n\nclass EncryptedBlobStore(_SimpleList):\n    \"\"\"\n    This is an on-disk variable-sized, sharded AES encrypted blob\n    storage. It augments EncryptedRecordStore by placing bounds on\n    how large each encrypted file will become and allows data blobs\n    of arbitrary size by bumping \"large\" records to a secondary (or\n    tertiary, ...) store.\n    \"\"\"\n\n    _BIG_POINTER = '\\0B->'\n\n    def __init__(self, base_fn, key,\n                 max_bytes=400, shard_size=50000, big_ratio=10,\n                 overwrite=False):\n        self._base_fn = base_fn\n        self._key = key\n        self._max_bytes = max(max_bytes, 50)\n        self._shard_size = max(int(shard_size), 1024)\n        self._big_ratio = float(big_ratio)\n        self._overwrite = overwrite\n\n        self._lock = threading.RLock()\n\n        self._shards = []\n        self._load_next_shard()\n        while os.path.exists(self._next_shardname()):\n            self._load_next_shard()\n\n        self._big_map = {}\n        self._big_shard = None\n\n    s0 = property(lambda self: self._shards[0])\n\n    def _big(self):\n        with self._lock:\n            if self._big_shard is None:\n                self._big_shard = EncryptedBlobStore(\n                    '%s-b' % self._base_fn,\n                    self._key,\n                    max_bytes=self._max_bytes * self._big_ratio,\n                    shard_size=self._shard_size / self._big_ratio,\n                    big_ratio=self._big_ratio,\n                    overwrite=self._overwrite)\n            return self._big_shard\n\n    def _next_shardname(self):\n        return '%s-%s' % (self._base_fn, len(self._shards) + 1)\n\n    def _load_next_shard(self):\n        with self._lock:\n            self._shards.append(EncryptedRecordStore(\n                self._next_shardname(),\n                self._key,\n                max_bytes=self._max_bytes,\n                overwrite=self._overwrite))\n\n    def __getitem__(self, pos):\n        shard, pos = pos // self._shard_size, pos % self._shard_size\n        value = self._shards[shard][pos]\n        if value.startswith(self._BIG_POINTER):\n            self._big_map[pos] = int(value[len(self._BIG_POINTER):], 16)\n            return self._big()[self._big_map[pos]]\n        else:\n            return value\n\n    def __setitem__(self, pos, data):\n        shard, pos = pos // self._shard_size, pos % self._shard_size\n        with self._lock:\n            while shard >= len(self._shards):\n                self._load_next_shard()\n        if len(data) > self.s0._MAX_DATA_SIZE:\n            with self._lock:\n                bpos = self._big_map.get(pos, len(self._big()))\n                self._big()[bpos] = data\n                self._big_map[pos] = bpos\n                data = '%s%x' % (self._BIG_POINTER, bpos)\n        self._shards[shard][pos] = data\n\n    def __len__(self):\n        with self._lock:\n            return (self._shard_size * (len(self._shards) - 1) +\n                    len(self._shards[-1]))\n\n    def close(self):\n        with self._lock:\n            for s in self._shards:\n                s.close()\n            if self._big_shard is not None:\n                self._big_shard.close()\n            self._shards = []\n            self._big_shard = None\n\n\nclass EncryptedUnicodeStore(EncryptedBlobStore):\n    \"\"\"\n    An EncryptedBlobStore that only works with unicode data.\n    \"\"\"\n    def __setitem__(self, pos, data):\n        return EncryptedBlobStore.__setitem__(self, pos, data.encode('utf-8'))\n\n    def __getitem__(self, pos):\n        return EncryptedBlobStore.__getitem__(self, pos).decode('utf-8')\n\n\nclass EncryptedDict(object):\n    \"\"\"\n    This is a variable-sized, sharded AES encrypted dict. It uses\n    EncryptedRecordStore for hashing and EncryptedBlobStore for values\n    that do not fit alongside the keys. At the moment, data can be\n    overwritten, but not deleted.\n\n    TODO:\n        - Grow the dict by adding keysets\n        - Migrating \"exciting\" keys to the primary keyset\n    \"\"\"\n\n    DEFAULT_BUCKET_SIZE = 5\n    DEFAULT_DIGEST_SIZE = 16  # 128 bit hashes\n    _UNUSED = '\\0U'\n    _DELETED = '\\0D'\n\n    MIN_KEY_BYTES = (48 - 16 - 7 - 1)  # Magic number, smallest possible\n                                       # key record size.\n\n    DEFAULT_KEY_BYTES  = (48 - 16 - 7 - 1) + 1*48  # 2 lines per record\n    DEFAULT_DATA_BYTES = (48 - 16 - 7 - 1) + 8*48  # 9 lines ber record\n\n    def __init__(self, base_fn, key,\n                 key_bytes=None,\n                 data_bytes=None,\n                 big_ratio=5,\n                 shard_size=100000, min_shards=1, shard_ratio=2.0,\n                 bucket_size=None,\n                 digest_size=None,\n                 overwrite=False,\n                 init_zeros=True,\n                 sparse=False):\n        self._base_fn = base_fn\n        self._key = key\n        self._key_bytes = key_bytes or self.DEFAULT_KEY_BYTES\n        self._data_bytes = (self.DEFAULT_DATA_BYTES\n                            if (data_bytes is None) else data_bytes)\n\n        self._shard_ratio = shard_ratio  # Growth ratio when expanding\n        self._shard_size = max(int(shard_size), 1024)\n        self._big_ratio = float(big_ratio)\n        self._bucket_size = bucket_size or self.DEFAULT_BUCKET_SIZE\n        self._digest_size = digest_size or self.DEFAULT_DIGEST_SIZE\n        self._overwrite = overwrite\n        self._sparse = sparse\n        self._init_zeros = init_zeros\n\n        self._lock = threading.RLock()\n\n        self.load_factor = []\n        self.writes = []\n        self.reads = []\n        self._keys = []\n        while (os.path.exists(self._next_keyfile()[0]) or\n                len(self._keys) < min_shards):\n            self._load_next_keys()\n        self.reset_counters()\n\n        if big_ratio > 0:\n            self._values = EncryptedBlobStore(self._base_fn, self._key,\n                                              max_bytes=data_bytes,\n                                              shard_size=shard_size,\n                                              big_ratio=big_ratio,\n                                              overwrite=overwrite)\n        else:\n            self._values = None\n\n    def reset_counters(self):\n        with self._lock:\n            self.load_factor = [1.0 for keyset in self._keys]\n            self.writes = [0 for keyset in self._keys]\n            self.reads = [0 for keyset in self._keys]\n\n    def _keyfile_size(self, kfi):\n        return int(self._shard_size * (self._shard_ratio**kfi))\n\n    def _keyfile_bytes(self, kfi):\n        return self._key_bytes\n\n    def _next_keyfile(self):\n        pos = len(self._keys)\n        return '%s-k-%s' % (self._base_fn, pos + 1), pos\n\n    def _load_next_keys(self):\n        with self._lock:\n            kf, kfi = self._next_keyfile()\n            kb = self._keyfile_bytes(kfi)\n            ow = self._overwrite or not os.path.exists(kf)\n            self._keys.append(EncryptedRecordStore(kf, self._key,\n                max_bytes=kb, overwrite=ow, default_value=self._UNUSED))\n            self.load_factor.append(0)\n            self.writes.append(0)\n            self.reads.append(0)\n            if ow:\n                kfs = self._keyfile_size(kfi)\n                if self._sparse:\n                    self._keys[kfi][kfs - 1] = self._UNUSED\n                elif self._init_zeros:\n                    self._keys[kfi].save_zeros(0, kfs)\n                else:\n                    for i in range(0, kfs):\n                        self._keys[kfi][i] = self._UNUSED\n\n            return kfi, self._keys[kfi]\n\n    def _digest(self, key):\n        digest = hashlib.sha256(self._key)\n        if isinstance(key, unicode):\n            key = key.encode('utf-8')\n        elif not isinstance(key, str):\n            key = str(key)\n        digest.update(key)\n        return digest.digest()[:self._digest_size]\n\n    def _offset(self, digest):\n        return struct.unpack('<L', digest[:4])[0]\n\n    def _offset_and_digest(self, key):\n        digest = self._digest(key)\n        return self._offset(digest), digest\n\n    def load_records(self, kfi, keyset, count, pos, want=[]):\n        values = []\n        try:\n            for inc in range(0, count):\n                rpos = (pos + inc) % len(keyset)\n                rval = keyset.get(rpos, self._UNUSED)\n                values.append((rpos, rval))\n                for w in want:\n                    if rval.startswith(w):\n                        return values\n            return values\n        finally:\n            with self._lock:\n                self.load_factor[kfi] = (self.load_factor[kfi] * 9 +\n                                         len(values)) / 10\n\n    def load_record(self, key, **kwargs):\n        return self.load_digest_record(self._digest(key), **kwargs)\n\n    def load_digest_record(self, digest, want=None):\n        pos = self._offset(digest)\n        if want is None:\n            want = []\n        for kfi, keyset in enumerate(self._keys):\n            records = self.load_records(kfi, keyset, self._bucket_size, pos,\n                                        want=want+[digest])\n            if records[-1][1].startswith(digest):\n                self.reads[kfi] += 1\n                return (keyset, records[-1])\n            if [r for r in records if r[1] == self._UNUSED]:\n                # ...finding an unused marker means we can give up.\n                break\n        raise KeyError('Not found')\n\n    def _try_save(self, kfi, keyset, pos, digest, value, on_fail=None):\n        rpos = rdata = None\n        records = self.load_records(kfi, keyset, self._bucket_size, pos,\n                                    want=[digest])\n        if records[-1][1].startswith(digest):\n            rpos, rdata = records[-1]\n        else:\n            unused = [r for r in records\n                      if r[1] in (self._UNUSED, self._DELETED)]\n            if unused:\n                rpos, rdata = unused[0]\n\n        if rpos is not None:\n            record_data = ''.join([digest, '=', value])\n            if len(record_data) > keyset._MAX_DATA_SIZE:\n                if self._values is None:\n                    if on_fail is not None:\n                        return on_fail(self, kfi, keyset, pos, digest, value,\n                                       records)\n                    raise ValueError('Data too big for record')\n                rdata = keyset.get(rpos, self._UNUSED)\n                if rdata.startswith(digest+'>'):\n                    vpos = rdata[self._digest_size + 1:]\n                    vpos = struct.unpack('<II', vpos)[0]\n                    self._values[vpos] = value\n                    keyset[rpos] = ''.join([\n                        digest, struct.pack('<cII', '>', vpos, len(value))])\n                else:\n                    keyset[rpos] = ''.join([\n                        digest, struct.pack('<cII', '>',\n                                            self._values.append(value),\n                                            len(value))])\n            else:\n                keyset[rpos] = record_data\n            return rpos\n        elif on_fail is not None:\n            return on_fail(self, kfi, keyset, pos, digest, value, records)\n        else:\n            return None\n\n    def save_digest_record(self, digest, value, on_fail=None):\n        pos = self._offset(digest)\n        for kfi, keyset in enumerate(self._keys):\n            rpos = self._try_save(kfi, keyset, pos, digest, value,\n                                  on_fail=on_fail)\n            if rpos is not None:\n                self.writes[kfi] += 1\n                return keyset, rpos\n\n        # If we get this far, then we need to grow...\n        if self._shard_ratio > 0:\n            kfi, keyset = self._load_next_keys()\n            rpos = self._try_save(kfi, keyset, pos, digest, value,\n                                  on_fail=on_fail)\n            if rpos is not None:\n                self.writes[kfi] += 1\n                return keyset, rpos\n\n        raise KeyError('Save failed at %s' % pos)\n\n    def save_record(self, key, value, on_fail=None):\n        return self.save_digest_record(self._digest(key), value,\n                                       on_fail=on_fail)\n\n    def __setitem__(self, key, value):\n        self.save_record(key, value)\n\n    def __contains__(self, key):\n        try:\n            keyset, (rpos, rdata) = self.load_record(key)\n            return True\n        except (KeyError, ValueError, IndexError):\n            return False\n\n    def __delitem__(self, key):\n        try:\n            keyset, (rpos, rdata) = self.load_record(key)\n            keyset[rpos] = self._DELETED\n        except (KeyError, ValueError):\n            pass\n\n    def values(self):\n        for keyset in self._keys:\n            for val in keyset:\n                try:\n                    yield self.rdata_value(val)\n                except (KeyError, ValueError, IndexError):\n                    pass\n\n    def rdata_digest(self, rdata):\n        return rdata[:self._digest_size]\n\n    def rdata_value(self, rdata):\n        if rdata[self._digest_size] == '=':\n            return rdata[self._digest_size + 1:]\n        elif rdata[self._digest_size] == '>':\n            pos, dlen = struct.unpack('<II', rdata[self._digest_size + 1:])\n            return self._values[pos]\n        else:\n            raise KeyError('Not found, decode failed')\n\n    def __getitem__(self, key):\n        try:\n            keyset, (rpos, rdata) = self.load_record(key)\n        except ValueError as msg:\n            raise KeyError('%s: %s' % (key, msg))\n        return self.rdata_value(rdata)\n\n    def get(self, key, default=None, **kwargs):\n        try:\n            return self.__getitem__(key, **kwargs)\n        except KeyError:\n            return default\n\n    def close(self):\n        with self._lock:\n            for s in self._keys:\n                s.close()\n            self._keys = []\n\n\nclass EncryptedUnicodeDict(EncryptedDict):\n    \"\"\"\n    EncryptedDict which only deals in unicode values.\n    \"\"\"\n    def save_record(self, key, value):\n        value = value.encode('utf-8')\n        return EncryptedDict.save_record(self, key, value)\n\n    def rdata_value(self, rdata):\n        value = EncryptedDict.rdata_value(self, rdata)\n        return value.decode('utf-8')\n\n\nclass EncryptedIntDict(EncryptedDict):\n    \"\"\"\n    EncryptedDict which only deals in signed 64-bit int values.\n    This also adds a working keys() function.\n    \"\"\"\n    DATA_FORMAT = '<q'\n    DATA_BYTES = struct.calcsize(DATA_FORMAT)\n\n    def keys(self):\n        for keyset in self._keys:\n            for val in keyset:\n                try:\n                    yield EncryptedDict.rdata_value(self, val\n                                                    )[8:].decode('utf-8')\n                except (KeyError, ValueError, IndexError, UnicodeDecodeError):\n                    pass\n\n    def save_record(self, key, value):\n        value = (struct.pack(self.DATA_FORMAT, long(value))\n                 + key.encode('utf-8'))\n        return EncryptedDict.save_record(self, key, value)\n\n    def rdata_value(self, rdata):\n        value = EncryptedDict.rdata_value(self, rdata)\n        return long(struct.unpack(self.DATA_FORMAT, value[:self.DATA_BYTES]\n                                  )[0])\n\n\nif __name__ == '__main__':\n    import random\n\n    print('Creating EncryptedDict...')\n    eid = EncryptedIntDict('/tmp/test.aes', 'this is my secret key',\n                           shard_size=25, overwrite=True)\n    eid['hello'] = 99\n    assert('hello' in eid)\n    assert(eid['hello'] == 99)\n    try:\n        eid['hello'] = 'world'\n        assert('That should have failed')\n    except (ValueError, struct.error):\n        pass\n    assert(list(eid.keys()) == ['hello'])\n    assert(list(eid.values()) == [99])\n\n    for size in (16, 50, 100, 200, 400, 800, 1600):\n        er = EncryptedBlobStore('/tmp/tmp.aes', 'this is my secret key',\n                                max_bytes=size, overwrite=True)\n        print('Testing max_size=%d, real max=%d, lines=%d'\n              % (size, er.s0._MAX_DATA_SIZE, er.s0._RECORD_LINES))\n        for l in reversed(range(0, 20)):\n            er[l] = 'bjarni %s' % l\n        for l in range(0, 20):\n            assert(er[l] == 'bjarni %s' % l)\n        assert(len(er) == 20)\n\n    er = EncryptedBlobStore('/tmp/test.aes', 'this is my secret key',\n                            max_bytes=512, big_ratio=4, overwrite=True)\n    t0 = time.time()\n    count = 10 * 1024 + 4321\n    data = ('bjarni is a happy camper with plenty of stuff '\n            'we must pad this with gibberish and make it '\n            'a fair bit longer, so it makes a good test '\n            'to say about this and that and the other') * 100\n    for l in range(0, count):\n        if (l % 1024) == 5:\n            er[l] = data\n        else:\n            er[l] = data[:(l % 1024)]\n    done = time.time()\n    print ('10k record writes in %.2f (%.8f s/op)'\n           % (done - t0, (done - t0) / count))\n    assert(len(er) == count)\n\n    t0 = time.time()\n    for l in range(0, count):\n        if (l % 1024) == 5:\n            assert(er[l] == data)\n        else:\n            assert(er[l] == data[:(l % 1024)])\n\n    done = time.time()\n    print ('10k record reads in %.2f (%.8f s/op)'\n           % (done - t0, (done - t0) / count))\n    er.close()\n\n    print('Creating EncryptedDict...')\n    ed = EncryptedDict('/tmp/test.aes', 'another secret key',\n                       shard_size=(count * 2), min_shards=2, overwrite=True)\n\n    # Create a set of items to write to the Dict\n    items = list(range(0, count))\n    random.shuffle(items)\n\n    t0 = time.time()\n    for i in items:\n        if (i % 1024) == 5:\n            ed[str(i)] = data + str(i)\n        else:\n            ed[str(i)] = str(i)\n    done = time.time()\n    print ('10k dict writes in %.2f (%.8f s/op)\\n -- writes=%s lf=%s'\n           % (done - t0, (done - t0) / count, ed.writes, ed.load_factor))\n\n    # Add some non-existant entries to the list, shuffle\n    items += (['bogus'] * 1000)\n    random.shuffle(items)\n\n    t0 = time.time()\n    ed.reset_counters()\n    for i in items[:count]:\n        try:\n            if isinstance(i, int) and (i % 1024) == 5:\n                assert(ed.get(str(i)) == data + str(i))\n            else:\n                assert(ed[str(i)] == str(i))\n        except KeyError:\n            pass\n        except AssertionError:\n            print('FAILED, GOT: %s => %s' % (i, ed.get(str(i))))\n    done = time.time()\n    print ('10k dict reads in %.2f (%.8f s/op)\\n -- reads=%s lf=%s'\n           % (done - t0, (done - t0) / count, ed.reads, ed.load_factor))\n\n\n"
  },
  {
    "path": "mailpile/crypto/state.py",
    "content": "from __future__ import print_function\n#. Common crypto state and structure\nimport copy\n\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\n\n\nclass KeyLookupError(ValueError):\n    def __init__(self, message, missing):\n        ValueError.__init__(self, message)\n        self.missing = missing\n\n\n# Crypto state is a strange beast, it needs to flow down and then\n# back up again - parts need to inherit a crypto context from their\n# container, but if something interesting is discovered that has to\n# be overridden.\n#\n# The discoveries then usually have to bubble up to influence the\n# overall state of that container and the message itself (mixed-*\n# states, etc).\n#\nclass CryptoInfo(dict):\n    \"\"\"Base class for crypto-info classes\"\"\"\n    KEYS = [\"protocol\", \"status\", \"description\"]\n    STATUSES = [\"none\", \"mixed-error\", \"error\"]\n    DEFAULTS = {\"status\": \"none\"}\n\n    def __init__(self, parent=None, copy=None, bubbly=True):\n        self.parent = parent\n        self.bubbly = bubbly\n        self.filename = None\n        self.bubbles = []\n        self._status = None\n        if copy:\n            self.update(copy)\n            self._status = self.get(\"status\")\n        elif parent:\n            self.update(parent)\n            self._status = self.get(\"status\")\n        else:\n            self.update(self.DEFAULTS)\n\n    part_status = property(lambda self: (self._status or\n                                         self.DEFAULTS[\"status\"]),\n                           lambda self, v: self._set_status(v))\n\n    def _set_status(self, value):\n        if value not in self.STATUSES:\n            print('Bogus status for %s: %s' % (type(self), value))\n            raise ValueError('Invalid status: %s' % value)\n        self._status = value\n        self.mix_bubbles()\n\n    def __setitem__(self, item, value):\n        if item not in self.KEYS:\n            raise KeyError('Invalid key: %s' % item)\n        if item == \"status\":\n            if value not in self.STATUSES:\n                print('Bogus status for %s: %s' % (type(self), value))\n                raise ValueError('Invalid value for %s: %s' % (key, value))\n            if self._status is None:  # Capture initial value\n                self._status = value\n        dict.__setitem__(self, item, value)\n\n    def _overwrite_with(self, ci):\n        for k in self.keys():\n            del self[k]\n        self.update(ci)\n\n    def bubble_up(self, parent=None):\n        # Bubbling up adds this context to the list of contexts to be\n        # evaluated for all parent states.\n        if parent is not None and parent != self:\n            self.parent = parent\n\n        # Some contexts are neutral (pure MIME boilerplate) and do not\n        # bubble up at all.\n        if not self.bubbly:\n            return\n\n        parent = self.parent\n        while parent is not None:\n            parent.bubbles.append(self)\n            parent = parent.parent\n\n    def mix_bubbles(self):\n        # Reset visible status to initial state\n        self[\"status\"] = self.part_status\n        # Mix in all the bubbly bubbles\n        for bubble in self.bubbles:\n            if bubble.bubbly:\n                self._mix_in(bubble)\n\n    def _mix_in(self, ci):\n        \"\"\"\n        This generates a mixed state for the message. The most exciting state\n        is returned/explained, the status prefixed with \"mixed-\". How exciting\n        states are, is determined by the order of the STATUSES attribute.\n\n        This is lossy, but hopefully in a useful and non-harmful way.\n        \"\"\"\n        status = self[\"status\"]\n        if self.STATUSES.index(status) <= self.STATUSES.index(ci.part_status):\n            # ci is MORE or EQUALLY interesting\n            mix = copy.copy(ci)\n            if (self.bubbly and\n                   status != mix.part_status and\n                   not mix.part_status.startswith('mixed-')):\n                mix[\"status\"] = \"mixed-%s\" % mix.part_status\n            else:\n                mix[\"status\"] = mix.part_status\n            self._overwrite_with(mix)\n        elif not status.startswith(\"mixed-\"):\n            # ci is LESS interesting\n            self[\"status\"] = 'mixed-%s' % status\n\n\nclass EncryptionInfo(CryptoInfo):\n    \"\"\"Contains information about the encryption status of a MIME part\"\"\"\n    KEYS = (CryptoInfo.KEYS + [\"have_keys\", \"missing_keys\", \"locked_keys\"])\n    STATUSES = (CryptoInfo.STATUSES +\n                [\"mixed-decrypted\", \"decrypted\",\n                 \"mixed-missingkey\", \"missingkey\",\n                 \"mixed-lockedkey\", \"lockedkey\"])\n\n\nclass SignatureInfo(CryptoInfo):\n    \"\"\"Contains information about the signature status of a MIME part\"\"\"\n    KEYS = (CryptoInfo.KEYS + [\"name\", \"email\", \"keyinfo\", \"timestamp\"])\n    STATUSES = (CryptoInfo.STATUSES +\n                [\"mixed-unknown\", \"unknown\",\n                 \"mixed-changed\", \"changed\",  # TOFU; not the key we expected!\n                 \"mixed-unsigned\", \"unsigned\",  # TOFU; should be signed!\n                 \"mixed-expired\", \"expired\",\n                 \"mixed-revoked\", \"revoked\",\n                 \"mixed-unverified\", \"unverified\",\n                 \"mixed-signed\", \"signed\",  # TOFU; signature matches history\n                 \"mixed-verified\", \"verified\",\n                 \"mixed-invalid\", \"invalid\"])\n"
  },
  {
    "path": "mailpile/crypto/streamer.py",
    "content": "from __future__ import print_function\n#\n# This is code to stream data to or from encrypted storage. If the invoking\n# code us correctly written, it should be able to work with data far in\n# excess of available RAM.\n#\n# The storage format \"Mailpile Encrypted Storage\" takes pains to be either\n# valid RFC2822 (for direct storage in IMAP servers) or a delimited format\n# similar to OpenPGP armour. Files must use one style or the other, not a\n# mixture of both.\n#\n# By default this code prefers to use AES-128-CTR. This cipher is malleable,\n# which means that data corruption is localized and does not affect the\n# rest of the file (unlike CBC, for example). In order to detect corruption\n# or attacks, a SHA256-based MAC can be calculated on the plaintext or\n# the ciphertext (or both). When verifying the plaintext, the sum is written\n# into the header of the file, when verifying the ciphertext the sum is\n# expected to be stored somewhere else.\n#\n##############################################################################\n# FIXME:\n#\n# The decryption routines here support \"MEP v1\" which used AES-256-CBC\n# and MD5 sums (not a proper MAC).\n#\n# Very few people have data in this format, it would be nice to just\n# delete all of that code once users have had time to migrate. But\n# for that to happen there needs to be a migration that finds old data\n# and re-encrypts it automatically. We don't have that yet!\n#\n##############################################################################\n#\nimport base64\nimport os\nimport hashlib\nimport sys\nimport re\nimport threading\nimport time\nimport traceback\nfrom datetime import datetime\nfrom tempfile import NamedTemporaryFile\n\nimport mailpile.platforms\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.safe_popen import Popen, PIPE\nfrom mailpile.util import CryptoLock, safe_remove, safe_assert\nfrom mailpile.util import sha512b64 as genkey\n\nfrom mailpile.crypto.aes_utils import getrandbits\nfrom mailpile.crypto.aes_utils import aes_ctr_encryptor, aes_ctr_decryptor\n\nPREFERRED_CIPHER = 'aes-128-ctr'\n\n\n# This is for backwards compatibility with v1 of our storage format; we have\n# since corrected our silliness and v2 uses a SHA256-based MAC.\nLEN_MD5_SUM = len(hashlib.md5('testing').hexdigest())\nMD5_SUM_FORMAT = 'md5sum: %s'\nMD5_SUM_PLACEHOLDER = MD5_SUM_FORMAT % ('0' * LEN_MD5_SUM)\nMD5_SUM_RE = re.compile('(?m)^' + MD5_SUM_FORMAT % (r'[^\\n]+',))\n\nLEN_SHA_256 = len(hashlib.sha256('testing').hexdigest())\nSHA_256_FORMAT = 'sha256: %s'\nSHA_256_PLACEHOLDER = SHA_256_FORMAT % ('0' * LEN_SHA_256)\nSHA_256_RE = re.compile('(?m)^' + SHA_256_FORMAT % (r'[^\\n]+',))\n\nBLANK_LINE_RE = re.compile('^\\s*$')\n\n\n# This gets populated with all the obsolete data we see during\n# decryption, the app can check for this to trigger migrations.\nPREFERRED_FORMAT = 'v2:%s' % PREFERRED_CIPHER\nDETECTED_OBSOLETE_FORMATS = set([])\n\nOPENSSL_COMMAND = mailpile.platforms.GetDefaultOpenSSLCommand\nOPENSSL_MD_ALG = \"md5\"\n\n# FIXME: Why does Windows require this? Move to mailpile.platforms when\n#        we understand the underlying issue.\nif sys.platform.startswith(\"win\"):\n    FILTER_MD5 = True\nelse:\n    FILTER_MD5 = False\n\n\ndef mac_sha256(key, data):\n    mac = hashlib.sha256(key or '')\n    mac.update(data or '')\n    return mac.hexdigest()\n\n\nclass IOFilter(threading.Thread):\n    \"\"\"\n    This class will wrap a filehandle and spawn a background thread to\n    filter either the input or output.\n    \"\"\"\n    BLOCKSIZE = 16 * 1024\n\n    def __init__(self, fd, callback,\n                 name=None, error_callback=None, blocksize=None):\n        threading.Thread.__init__(self)\n        self.callback = callback\n        self.error_callback = error_callback\n        self.exc_info = None\n\n        self.blocksize = blocksize or self.BLOCKSIZE\n        self.fd = fd\n        self.writing = None\n        self.reading_from = None\n        self.writing_to = None\n\n        self.exposed_fd = None\n        self.my_pipe_fd = None\n\n        pipe = os.pipe()\n        self.pipe_reader = os.fdopen(pipe[0], 'rb', 0)\n        self.pipe_writer = os.fdopen(pipe[1], 'wb', 0)\n\n        self.info = 'Starting'\n        self.aborting = False\n        if name:\n            self.name = name\n\n    def __str__(self):\n        return '%s: %s' % (threading.Thread.__str__(self), self.info)\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.close()\n\n    def close(self):\n        fd, self.exposed_fd = self.exposed_fd, None\n        if fd is not None:\n            try:\n                fd.close()\n            except OSError:  # May already have been closed, that's fine\n                pass\n\n        if self.writing is False:\n            self.aborting = 'Closed reader'\n\n        self.join()\n\n    def join(self, aborting=None):\n        if aborting is not None:\n            self.aborting = aborting\n        return threading.Thread.join(self)\n\n    def writer(self):\n        if self.writing is None:\n            self.writing = True\n\n            self.reading_from = self.pipe_reader\n            self.writing_to = self.fd\n            self.my_pipe_fd = self.pipe_reader\n            self.exposed_fd = self.pipe_writer\n\n            self.start()\n        return self.pipe_writer\n\n    def reader(self):\n        if self.writing is None:\n            self.daemon = True\n            self.writing = False\n\n            self.reading_from = self.fd\n            self.writing_to = self.pipe_writer\n            self.my_pipe_fd = self.pipe_writer\n            self.exposed_fd = self.pipe_reader\n\n            self.start()\n        return self.pipe_reader\n\n    def _copy_loop(self):\n        while not self.aborting:\n            self.info = 'reading'\n            data = self.reading_from.read(self.blocksize)\n            if not self.aborting:\n                if len(data) == 0:\n                    self.info = 'writing, EOF'\n                    self.writing_to.write(self.callback(None) or '')\n                    break\n                else:\n                    self.info = 'writing'\n                    self.writing_to.write(self.callback(data))\n\n    def run(self):\n        okay = [AssertionError]\n        if self.writing is False:\n            # If we close early, we may get ValueErrors\n            okay.append(ValueError)\n\n        try:\n            self.info = 'Starting: %s' % self.writing\n            self._copy_loop()\n            self.info += ', done'\n        except tuple(okay):\n            self.info += ', okay'\n            pass\n        except:\n            self.info += ', failed'\n            self.exc_info = sys.exc_info()\n            traceback.print_exc()\n            if self.error_callback:\n                try:\n                    self.error_callback()\n                except:\n                    pass\n        finally:\n            fd, self.my_pipe_fd = self.my_pipe_fd, None\n            if fd is not None:\n                self.info = 'Closing'\n                fd.close()\n            self.info = 'Dead'\n\n\nclass ReadLineIOFilter(IOFilter):\n    \"\"\"\n    This is a line-based IOFilter, which can stop when it sees a\n    particular marker to hand off processing to others.\n    \"\"\"\n    def __init__(self, fd, callback,\n                 start_data=None, stop_check=None, **kwargs):\n        self.stop_check = stop_check\n        self.buffered = list(start_data)\n        self.buf_bytes = sum(len(s) for s in start_data)\n        IOFilter.__init__(self, fd, callback, **kwargs)\n\n    def _maybe_flush(self, eof=False):\n        if eof or (self.buf_bytes >= self.blocksize):\n            i, self.info = self.info, 'flushing'\n            self.writing_to.write(self.callback(''.join(self.buffered)))\n            self.buffered = []\n            self.buf_bytes = 0\n            while eof:\n                self.info = 'flushing EOF'\n                data = self.callback(None) or ''\n                self.writing_to.write(data)\n                if not data:\n                    break\n            self.info = i\n\n    def _copy_loop(self):\n        self.info = 'copying'\n        for line in self.reading_from:\n            if self.aborting: return\n\n            self.buffered.append(line)\n            if not re.match(BLANK_LINE_RE, line):\n                # Don't count blank lines\n                self.buf_bytes += len(line)\n                self._maybe_flush()\n\n            if self.aborting or (self.stop_check and self.stop_check(line)):\n                break\n\n        if not self.aborting:\n            self._maybe_flush(eof=True)\n\n\nclass IOCoprocess(object):\n    def __init__(self, command, fd, name=None, long_running=False):\n        self.stderr = ''\n        self._retval = None\n        self._reading = False\n        self.command = command\n        self.name = name\n        if command:\n            try:\n                self._proc, self._fd = self._popen(command, fd, long_running)\n            except:\n                print('Popen(%s, %s, %s)' % (command, fd, long_running))\n                traceback.print_exc()\n                print()\n                raise\n        else:\n            self._proc, self._fd = None, fd\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.close()\n\n    def close(self, *args):\n        if self._retval is None:\n            proc, fd, self._proc, self._fd = self._proc, self._fd, None, None\n            if proc and fd:\n                fd.close(*args)\n                self.stderr = proc.stderr.read()\n\n                # If we were reading from the process, not writing, then\n                # closing our FD above may not be enough to terminate it,\n                # and the following calls may hang. So kill kill kill.\n                if self._reading:\n                    count = 0\n                    while proc.poll() is None:\n                        time.sleep(0.01)\n                        if count == 4:\n                            print('TERM => %s' % proc)\n                            proc.terminate()\n                        elif count > 9:\n                            print('KILL => %s' % proc)\n                            proc.kill()\n                            break\n                        count += 1\n\n                self._retval = proc.wait()\n            else:\n                self._retval = 0\n        return self._retval\n\n\nclass OutputCoprocess(IOCoprocess):\n    \"\"\"\n    This class will stream data to an external coprocess.\n    \"\"\"\n    def _popen(self, command, fd, long_running):\n        proc = Popen(command, stdin=PIPE, stdout=fd, stderr=PIPE,\n                              bufsize=0, long_running=long_running)\n        return proc, proc.stdin\n\n    def _write_filter(self, data):\n        return data\n\n    def write(self, data, *args, **kwargs):\n        return self._fd.write(self._write_filter(data), *args, **kwargs)\n\n    def flush(self):\n        return self._fd.flush()\n\n\nclass InputCoprocess(IOCoprocess):\n    \"\"\"\n    This class will stream data from an external coprocess.\n    \"\"\"\n    def _popen(self, command, fd, long_running):\n        self._reading = True\n        proc = Popen(command, stdin=fd, stdout=PIPE, stderr=PIPE,\n                              bufsize=0, long_running=long_running)\n        return proc, proc.stdout\n\n    def _read_filter(self, data):\n        return data\n\n    def __iter__(self, *args):\n        return (self._read_filter(ln) for ln in self._fd.__iter__(*args))\n\n    def readline(self, *args):\n        return self._read_filter(self._fd.readline(*args))\n\n    def readlines(self, *args):\n        return [self._read_filter(ln) for ln in self._fd.readlines(*args)]\n\n    def read(self, *args):\n        return self._read_filter(self._fd.read(*args))\n\n\nclass ChecksummingStreamer(OutputCoprocess):\n    \"\"\"\n    This checksums and streams data to a named temporary file on disk, which\n    can then be read back or linked to a final location.\n    \"\"\"\n    FILTER_BLOCKSIZE = None\n\n    def __init__(self, dir=None, name=None, long_running=False,\n                       use_filter=FILTER_MD5):\n        self.tempfile, self.temppath = self._mk_tempfile_and_path(dir)\n        self.name = name\n\n        self.outer_sha256 = None\n        if use_filter:\n            self.outer_sha = hashlib.sha256()\n            self.shafilter = IOFilter(self.tempfile, self._sha256_callback,\n                                      name='%s/sha256' % (self.name or 'css'),\n                                      blocksize=self.FILTER_BLOCKSIZE)\n            self.fd = self.shafilter.writer()\n        else:\n            self.outer_sha = None\n            self.fd = self.tempfile\n\n        self.saved = False\n        self.finished = False\n        try:\n            self._write_preamble()\n            OutputCoprocess.__init__(self, self._mk_command(), self.fd,\n                                     name=self.name,\n                                     long_running=long_running)\n        except:\n            try:\n                self.fd.close()\n                if self.outer_sha is not None:\n                    self.shafilter.close()\n                    self.tempfile.close()\n                safe_remove(self.temppath)\n            except (IOError, OSError):\n                pass\n            raise\n\n    def _mk_tempfile_and_path(self, _dir):\n        ntf = NamedTemporaryFile(dir=_dir, delete=False)\n        return ntf, ntf.name\n\n    def _mk_command(self):\n        return None\n\n    def finish(self):\n        fin, self.finished = self.finished, True\n        if fin:\n            return\n        # Stop sending output to our coprocess, wait for it to finish\n        OutputCoprocess.close(self)\n\n        # Write postamble (the shafilter), close that too\n        self.tempfile.seek(0, 2)\n        self._write_postamble()\n\n        if self.outer_sha is None:\n            # If we weren't doing the SHA256 on the fly, do it now.\n            self.calculate_outer_sha256()\n        else:\n            # Otherwise, close our coprocess to trigger the calculation\n            self.fd.close()\n            self.shafilter.close()\n\n        # Reset our tempfile to the beginning for reading\n        self.tempfile.seek(0, 0)\n\n    def close(self):\n        self.finish()\n        self.tempfile.close()\n\n    def save(self, filename, finish=True, mode='wb'):\n        if finish:\n            self.finish()\n\n        # If no filename, return contents to caller\n        if filename is None:\n            if not self.saved:\n                safe_remove(self.temppath)\n                self.saved = True\n            self.tempfile.seek(0, 0)\n            return self.tempfile.read()\n\n        # 1st save just renames the tempfile\n        exists = os.path.exists(filename)\n        if (not self.saved and\n                (('a' not in mode) or not exists)):\n            try:\n                if exists:\n                    os.remove(filename)\n                os.rename(self.temppath, filename)\n                self.saved = True\n                return\n            except (OSError, IOError):\n                pass\n\n        # 2nd save (or append to existing) creates a copy\n        with open(filename, mode) as out:\n            self.save_copy(out)\n            if not self.saved:\n                safe_remove(self.temppath)\n        self.saved = True\n\n    def calculate_outer_sha256(self):\n        self.tempfile.seek(0, 0)\n        data = self.tempfile.read(4096)\n        self.outer_sha = outer_sha = hashlib.sha256()\n        while data != '':\n            # We calculate the MD5 sum as if the data used the CRLF linefeed\n            # convention, whether it's actually using that or not.\n            outer_sha.update(data.replace('\\r', '').replace('\\n', '\\r\\n'))\n            data = self.tempfile.read(4096)\n        self.outer_sha256 = outer_sha.hexdigest()\n\n    def outer_mac_sha256(self):\n        # Hm, we have no key, so this is a bit pointless\n        return mac_sha256('', self.outer_sha.digest())\n\n    def save_copy(self, ofd):\n        self.tempfile.seek(0, 0)\n        data = self.tempfile.read(4096)\n        while data != '':\n            ofd.write(data)\n            data = self.tempfile.read(4096)\n\n    def _sha256_callback(self, data):\n        if data is None:\n            # EOF...\n            self.outer_sha256 = self.outer_sha.hexdigest()\n            return ''\n        else:\n            # We calculate the MD5 sum as if the data used the CRLF linefeed\n            # convention, whether it's actually using that or not.\n            self.outer_sha.update(data.replace('\\r', '').replace('\\n', '\\r\\n'))\n            return data\n\n    def _write_preamble(self):\n        pass\n\n    def _write_postamble(self):\n        pass\n\n\nclass EncryptingDelimitedStreamer(ChecksummingStreamer):\n    \"\"\"\n    This class creates a coprocess for encrypting data. The data will\n    be streamed to a named temporary file on disk, which can then be\n    read back or linked to a final location.\n    \"\"\"\n    BEGIN_DATA = \"-----BEGIN MAILPILE ENCRYPTED DATA-----\\n\"\n    EXTRA_HEADERS = \"X-Mailpile-Encrypted-Data: v2\\n\"\n    EXTRA_DATA = {}\n    END_DATA = \"-----END MAILPILE ENCRYPTED DATA-----\\n\"\n\n    PREFERRED_CIPHER = None\n    FILTER_BLOCKSIZE = 19 * 3 * 16  # Make AES and Base64 happy\n\n    def __init__(self, key,\n                 dir=None, cipher=None, name=None, header_data=None,\n                 long_running=False, use_filter=FILTER_MD5):\n        self.cipher = cipher or self.PREFERRED_CIPHER or PREFERRED_CIPHER\n        self.nonce, self.key = self._nonce_and_mutated_key(key)\n        self.header_data = (header_data if header_data is not None\n                            else self.EXTRA_DATA)\n\n        self.encode_buffer = ''\n        if self.cipher == 'aes-128-ctr':\n            self.encryptor = aes_ctr_encryptor(self.key, self.nonce)\n            self.encoder = base64.encodestring\n            self.encode_batches = self.FILTER_BLOCKSIZE\n        elif self.cipher == 'none':\n            self.encryptor = lambda d: d\n            self.encoder = base64.encodestring\n            self.encode_batches = self.FILTER_BLOCKSIZE\n        elif self.cipher == 'broken':\n            self.encoder = self.encryptor = lambda d: d\n            self.encode_batches = None\n        else:\n            self.encoder = self.encryptor = None\n            self.encode_batches = None\n\n        ChecksummingStreamer.__init__(self, dir=dir, name=name,\n                                      long_running=long_running,\n                                      use_filter=use_filter)\n\n        # These are necessary for two reasons:\n        #\n        # 1. Prevent the public checksum from revealing what was encrypted.\n        # 2. When using malleable encryption modes (such as CTR), make it\n        #    infeasable for attackers to generate a checksum that validates\n        #    modified data.\n        #\n        # As this is for data at rest, we are not particularly concerned with\n        # oracle attacks. If an attacker has access to your mailpile on disk\n        # while it is in use (so they can inject new messages), there are\n        # other attacks which are both easier and more severe.\n        #\n        self.inner_sha256 = None\n        self.inner_sha = hashlib.sha256()\n        self.inner_sha.update(self.key)\n        self.inner_sha.update(self.nonce or '')\n\n        self._send_key()\n\n    def _write_filter(self, data):\n        if data:\n            self.inner_sha.update(data)\n        if self.encryptor and (data or self.encode_buffer):\n            if self.encode_batches:\n                eof = not data\n                if data: self.encode_buffer += data\n                data = ''\n                for i in (512, 128, 8, 1):\n                    batch = i * self.encode_batches\n                    while eof or (len(self.encode_buffer) >= batch):\n                        d = self.encode_buffer[:batch]\n                        b = self.encode_buffer[batch:]\n                        self.encode_buffer = b\n                        data += self.encoder(self.encryptor(d))\n                        eof = False\n        return data\n\n    def _sha256_callback(self, data):\n        return ChecksummingStreamer._sha256_callback(self, data)\n\n    def outer_mac_sha256(self):\n        return mac_sha256(self.key or '', self.outer_sha.digest())\n\n    def write_pad_and_flush(self, data, pad=' '):\n        if self.encryptor and (data or self.encode_buffer):\n            if self.encode_batches:\n                remainder = len(self.encode_buffer) + len(data)\n                remainder %= self.encode_batches\n                padding = self.encode_batches - remainder\n                data += (pad * padding)\n        self.write(data)\n        self.flush()\n\n    def finish(self, *args, **kwargs):\n        if not self.finished:\n            while self.encode_buffer:\n                self.write('')\n            rv = ChecksummingStreamer.finish(self, *args, **kwargs)\n            self._write_inner_sha256()\n            return rv\n        else:\n            return ChecksummingStreamer.finish(self, *args, **kwargs)\n\n    def _write_inner_sha256(self):\n        if not self.inner_sha256:\n            self.inner_sha256 = mac_sha256(self.key, self.inner_sha.digest())\n            pos = self.tempfile.tell()\n            self.tempfile.seek(0, 0)\n            old_data = self.tempfile.read(4096)\n\n            sha_256_header = SHA_256_FORMAT % (self.inner_sha256, )\n            new_data = re.sub(SHA_256_RE, sha_256_header, old_data)\n            if old_data != new_data:\n                self.tempfile.seek(0, 0)\n                self.tempfile.write(new_data)\n\n            self.tempfile.seek(pos, 0)\n\n    def _nonce_and_mutated_key(self, key):\n        # This generates a nonce which may be used as a salt, IV, or\n        # counter-prefix depending the algorithm and mode in use. We\n        # also use it to derive a mutated key for each message, thus\n        # reducing the risks of the (key, iv) pairs ever repeating even\n        # if a mistake is made somewhere else.\n        nonce = '%32.32x' % getrandbits(32 * 4)\n        return nonce, genkey(key, nonce)[:32].strip()\n\n    def _send_key(self):\n        # We talk directly to the underlying FD, to avoid corrupting the\n        # inner MD5 sum (calculated using the _write_filter() above).\n        if not self.encryptor:\n            self._fd.write('%s\\n' % self.key)\n\n    def _mk_command(self):\n        if self.encryptor:\n            return None\n        return [OPENSSL_COMMAND(), \"enc\", \"-e\", \"-a\", \"-%s\" % self.cipher,\n                \"-pass\", \"stdin\", \"-bufsize\", \"0\", \"-md\", OPENSSL_MD_ALG]\n\n    def _write_preamble(self):\n        self.fd.write(self.BEGIN_DATA)\n        self.fd.write('cipher: %s\\n' % self.cipher)\n        if self.nonce:\n            self.fd.write('nonce: %s\\n' % self.nonce)\n        self.fd.write(SHA_256_PLACEHOLDER + '\\n')\n        if self.EXTRA_HEADERS:\n            self.fd.write(self.EXTRA_HEADERS % self.header_data)\n        self.fd.write('\\n')\n        self.fd.flush()\n\n    def _write_postamble(self):\n        if self.END_DATA:\n            self.fd.write('\\n')\n            self.fd.write(self.END_DATA)\n        self.fd.flush()\n\n\nclass EncryptingUndelimitedStreamer(EncryptingDelimitedStreamer):\n    \"\"\"\n    This class creates a coprocess for encrypting data. The data will\n    be streamed to a named temporary file on disk, which can then be\n    read back or linked to a final location.\n    \"\"\"\n    BEGIN_DATA = \"X-Mailpile-Encrypted-Data: v2\\n\"\n    EXTRA_HEADERS = (\"From: Mailpile <encrypted@mailpile.is>\\n\"\n                     \"Subject: %(subject)s\\n\")\n    EXTRA_DATA = {'subject': 'Mailpile encrypted data'}\n    END_DATA = \"\"\n\n\ndef EncryptingStreamer(*args, **kwargs):\n    delimited = kwargs.get('delimited', False)\n    if 'delimited' in kwargs:\n        del kwargs['delimited']\n    if delimited:\n        return EncryptingDelimitedStreamer(*args, **kwargs)\n    else:\n        return EncryptingUndelimitedStreamer(*args, **kwargs)\n\n\nclass DecryptingStreamer(InputCoprocess):\n    \"\"\"\n    This class creates a coprocess for decrypting data.\n    \"\"\"\n    BEGIN_PGP = \"-----BEGIN PGP MESSAGE-----\"\n    END_PGP = \"-----END PGP MESSAGE-----\"\n    BEGIN_MED = \"-----BEGIN MAILPILE ENCRYPTED DATA-----\"\n    BEGIN_MED2 = \"X-Mailpile-Encrypted-Data: \"\n    END_MED = \"-----END MAILPILE ENCRYPTED DATA-----\"\n    PREFERRED_CIPHER = None\n\n    STATE_BEGIN = 0\n    STATE_HEADER = 1\n    STATE_DATA = 2\n    STATE_ONLY_DATA = 3\n    STATE_END = 4\n    STATE_RAW_DATA = 5\n    STATE_PGP_DATA = 6\n    STATE_ERROR = -1\n\n    @classmethod\n    def StartEncrypted(cls, line):\n        return (line.startswith(cls.BEGIN_MED) or\n                line.startswith(cls.BEGIN_MED2) or\n                line.startswith(cls.BEGIN_PGP))\n\n    @classmethod\n    def EndEncrypted(cls, line):\n        return (line.startswith(cls.END_MED) or\n                line.startswith(cls.END_PGP))\n\n    def __init__(self, fd,\n                 mep_key=None, gpg_pass=None, sha256=None, cipher=None,\n                 name=None, long_running=False, gpgi=None, md_alg=None):\n        self.expected_outer_sha256 = sha256\n        self.expected_inner_sha256 = None\n        self.expected_inner_md5sum = None\n        self.name = name\n        self.outer_sha = hashlib.sha256()\n        self.inner_sha = hashlib.sha256()\n        self.inner_md5 = hashlib.md5()\n        self.cipher = self.PREFERRED_CIPHER or PREFERRED_CIPHER\n        self.md_alg = md_alg or OPENSSL_MD_ALG\n        self.state = self.STATE_BEGIN\n        self.buffered = ''\n        self.mep_version = None\n        self.mep_mutated = None\n        self.mep_key = mep_key\n        self.gpg_pass = gpg_pass\n        self.gpgi = gpgi\n        self.decryptor = None\n        self.decoder = None\n        self.decoder_data_bytes = 0  # Not counting white-space\n\n        # Start reading our data...\n        self.startup_lock = CryptoLock()\n        self.startup_lock.acquire()\n        self.data_filter = self._mk_data_filter(fd, self._read_data,\n                                                self.startup_lock.release)\n        self.read_fd = self.data_filter.reader()\n        try:\n            # Once the header has been processed (_read_data() will release\n            # the lock), fork out our coprocess.\n            self.startup_lock.acquire()\n            InputCoprocess.__init__(self, self._mk_command(), self.read_fd,\n                                    name=name, long_running=long_running)\n        except:\n            try:\n                self.data_filter.join(aborting=True)\n                self.data_filter.close()\n            except (IOError, OSError):\n                pass\n            raise\n        finally:\n            self.startup_lock.release()\n            self.startup_lock = None\n\n    def _read_filter(self, data):\n        if data:\n            if self.expected_inner_sha256:\n                self.inner_sha.update(data)\n            if self.expected_inner_md5sum:\n                self.inner_md5.update(data)\n        return data\n\n    def close(self):\n        self.read_fd.close()\n        self.data_filter.join()\n        return InputCoprocess.close(self)\n\n    def verify(self, testing=False, _raise=None):\n        if self.close() != 0:\n            if testing:\n                print('Close returned nonzero')\n            if _raise:\n                raise _raise('Non-zero exit code from coprocess')\n            return False\n\n        if self.expected_inner_sha256:\n            mac = mac_sha256(self.mep_mutated, self.inner_sha.digest())\n            if self.expected_inner_sha256 != mac:\n                if testing:\n                    print('Inner %s != %s' % (self.expected_inner_sha256, mac))\n                if _raise:\n                    raise _raise('Invalid inner SHA256')\n                return False\n        elif self.expected_inner_md5sum:\n            if self.expected_inner_md5sum != self.inner_md5.hexdigest():\n                if testing:\n                    print('Inner %s != %s' % (self.expected_inner_md5sum,\n                                              self.inner_md5.hexdigest()))\n                if _raise:\n                    raise _raise('Invalid inner MD5 sum')\n                return False\n        elif testing and not self.expected_inner_md5sum:\n            print('No inner MD5 sum or SHA256 expected')\n\n        if self.expected_outer_sha256:\n            mac = mac_sha256(self.mep_mutated, self.outer_sha.digest())\n            if self.expected_outer_sha256 != mac:\n                if testing:\n                    print('Outer %s != %s' % (self.expected_outer_sha256, mac))\n                if _raise:\n                    raise _raise('Invalid outer SHA256')\n                return False\n        elif testing and not self.expected_outer_sha256:\n            print('No outer SHA256 expected')\n        return True\n\n    def _mk_data_filter(self, fd, cb, ecb):\n        return IOFilter(fd, cb, error_callback=ecb,\n                        name='%s/filter' % (self.name or 'ds'))\n\n    def _read_data(self, data):\n        def process(data):\n            if self.decryptor is not None:\n                eof = not data\n                if self.decoder_data_bytes and data:\n                    self.buffered += ''.join([c for c in data if c not in\n                                              (' ', '\\t', '\\r', '\\n')])\n                else:\n                    self.buffered += (data or '')\n                data = ''\n                for i in (256, 64, 8, 1):\n                    batch = max(1, i * self.decoder_data_bytes)\n                    while eof or (len(self.buffered) >= batch):\n                        if self.decoder_data_bytes:\n                            d = self.buffered[:batch]\n                            b = self.buffered[batch:]\n                            self.buffered = b\n                        else:\n                            d, self.buffered = self.buffered, ''\n                        try:\n                            data += self.decryptor(self.decoder(d))\n                            eof = False\n                        except TypeError:\n                            raise IOError('%s: Bad data, failed to decode'\n                                          % self.name)\n            return (data or '')\n\n        if data is None:\n            # EOF!\n            if self.state in (self.STATE_BEGIN, self.STATE_HEADER):\n                self.state = self.STATE_RAW_DATA\n                self.startup_lock.release()\n                data, self.buffered = self.buffered, ''\n                return process(data) + process(None)\n            return process(None)\n\n        if self.expected_outer_sha256:\n            # The outer MD5 sum is calculated over all data, but with any\n            # CRLF sequences normalized to only LF and the sha256 header\n            # itself replaced with a placeholder.\n            if self.state in (self.STATE_BEGIN, self.STATE_HEADER):\n                sum_data = re.sub(MD5_SUM_RE, MD5_SUM_PLACEHOLDER,\n                                  re.sub(SHA_256_RE, SHA_256_PLACEHOLDER, data))\n            else:\n                sum_data = data\n            sum_data = sum_data.replace('\\r', '').replace('\\n', '\\r\\n')\n            self.outer_sha.update(sum_data)\n\n        if self.state in (\n               self.STATE_RAW_DATA, self.STATE_PGP_DATA, self.STATE_ONLY_DATA):\n            return process(data)\n\n        if self.state == self.STATE_BEGIN:\n            self.buffered += data\n            if (len(self.buffered) >= len(self.BEGIN_PGP)\n                    and self.buffered.startswith(self.BEGIN_PGP)):\n                self.state = self.STATE_PGP_DATA\n                if self.gpg_pass:\n                    self.gpg_pass.seek(0, 0)\n                    passphrase, c = [], self.gpg_pass.read(1)\n                    while c != '':\n                        passphrase.append(c)\n                        c = self.gpg_pass.read(1)\n                    self.startup_lock.release()\n                    return ''.join(passphrase + ['\\n', self.buffered])\n                else:\n                    self.startup_lock.release()\n                    return self.buffered\n\n            # Note: The max() check is OK, because both formats add more\n            #       data which covers the difference.\n            if len(self.buffered) >= max(len(self.BEGIN_MED),\n                                         len(self.BEGIN_MED2)):\n                if not (self.buffered.startswith(self.BEGIN_MED) or\n                        self.buffered.startswith(self.BEGIN_MED2)):\n                    self.state = self.STATE_RAW_DATA\n                    self.startup_lock.release()\n                    return self.buffered\n                if '\\r\\n\\r\\n' in self.buffered:\n                    header, data = self.buffered.split('\\r\\n\\r\\n', 1)\n                    headlines = header.strip().split('\\r\\n')\n                    self.state = self.STATE_HEADER\n                elif '\\n\\n' in self.buffered:\n                    header, data = self.buffered.split('\\n\\n', 1)\n                    headlines = header.strip().split('\\n')\n                    self.state = self.STATE_HEADER\n                else:\n                    return ''\n            else:\n                return ''\n\n        if self.state == self.STATE_HEADER:\n            # State: header and data have been set, header is complete.\n            self.buffered = ''\n\n            headers = dict([l.split(': ', 1) for l in headlines if ': ' in l])\n            nonce = headers.get('nonce', '')\n            self.mep_mutated = self._mutate_key(self.mep_key, nonce)\n            self.mep_version = headers.get('X-Mailpile-Encrypted-Data', 'v1')\n            self.cipher = headers.get('cipher', self.cipher)\n\n            data_fmt = '%s:%s' % (self.mep_version, self.cipher)\n            if data_fmt != PREFERRED_FORMAT:\n                DETECTED_OBSOLETE_FORMATS.add(data_fmt)\n\n            eim = self.expected_inner_md5sum = headers.get('md5sum')\n            if eim and eim == ('0' * LEN_MD5_SUM):\n                self.expected_inner_md5sum = None\n            if self.expected_inner_md5sum:\n                self.inner_md5.update(self.mep_mutated)\n                self.inner_md5.update(nonce)\n\n            eis = self.expected_inner_sha256 = headers.get('sha256')\n            if eis and eis == ('0' * LEN_SHA_256):\n                self.expected_inner_sha256 = None\n            if self.expected_inner_sha256:\n                self.inner_sha.update(self.mep_mutated)\n                self.inner_sha.update(nonce)\n\n            if self.buffered.startswith(self.BEGIN_MED2):\n                self.state = self.STATE_ONLY_DATA\n            else:\n                self.state = self.STATE_DATA\n\n            if self.cipher == 'aes-128-ctr':\n                self.decryptor = aes_ctr_decryptor(self.mep_mutated, nonce)\n                self.decoder = base64.b64decode\n                # Decode data in chunks this big (multiple of 4 and 16);\n                # guarantees workable chunks for both AES-CTR and base64.\n                self.decoder_data_bytes = 32 * 1024\n            elif self.cipher == 'none':\n                self.decryptor = lambda d: d\n                self.decoder = base64.b64decode\n                self.decoder_data_bytes = 32 * 1024\n            elif self.cipher == 'broken':\n                self.decryptor = lambda d: d\n                self.decoder = lambda d: d\n                self.expected_inner_md5sum = None\n                self.expected_inner_sha256 = None\n            else:\n                self.decryptor = None\n                data = '\\n'.join((self.mep_mutated, data))\n\n            self.startup_lock.release()\n\n        if self.state == self.STATE_ONLY_DATA:\n            return process(data)\n\n        if self.state == self.STATE_DATA:\n            for delim in (self.END_MED, self.END_PGP):\n                if delim in data:\n                    for pf in ('\\r\\n', '\\n', ''):\n                        if pf + delim in data:\n                            data = data.split(pf + delim, 1)[0]\n                            self.state = self.STATE_END\n                            return process(data)\n            return process(data)\n\n        # Error, end and unknown states...\n        return ''\n\n    def _mutate_key(self, key, nonce):\n        return genkey(key or '', nonce)[:32].strip()\n\n    def _mk_command(self):\n        if self.state == self.STATE_RAW_DATA:\n            return None\n        elif self.decryptor is not None:\n            return None\n        elif self.state == self.STATE_PGP_DATA:\n            safe_assert(self.gpgi is not None)\n            if self.gpg_pass:\n                return self.gpgi.common_args(will_send_passphrase=True)\n            else:\n                return self.gpgi.common_args()\n        return [OPENSSL_COMMAND(), \"enc\", \"-d\", \"-a\", \"-%s\" % self.cipher,\n                \"-pass\", \"stdin\", \"-md\", self.md_alg]\n\n\nclass PartialDecryptingStreamer(DecryptingStreamer):\n    def __init__(self, start_data, *args, **kwargs):\n        self.start_data = start_data\n        DecryptingStreamer.__init__(self, *args, **kwargs)\n\n    def _mk_data_filter(self, fd, cb, ecb):\n        return ReadLineIOFilter(fd, cb,\n                                start_data=self.start_data,\n                                stop_check=self.EndEncrypted,\n                                error_callback=ecb,\n                                name='%s/rlfilter' % (self.name or 'ds'))\n\n\nif __name__ == \"__main__\":\n    import random  # See! Not in the main module!\n    import StringIO\n\n    def _assert(val, want=True, msg='assert'):\n        if isinstance(want, bool):\n            if (not val) == (not want):\n                want = val\n        if val != want:\n            raise AssertionError('%s(%s==%s)' % (msg, val, want))\n\n    LEGACY_TEST_KEY = 'test key'\n    LEGACY_PLAINTEXT = 'Hello world! This is great!\\nHooray, lalalalla!\\n'\n    LEGACY_TEST_1 = \"\"\"\\\nX-Mailpile-Encrypted-Data: v1\ncipher: aes-256-cbc\nnonce: SEefbOfc9UQmZeWWGWQMrb0n6czXY2Uv\nmd5sum: b07d3ed58b79a69ab5496cffcab5d878\nFrom: Mailpile <encrypted@mailpile.is>\nSubject: Mailpile encrypted data\n\nU2FsdGVkX18zVuMErdegtGziWDLhSvNRb7YRRxmYKMmygI1H3bp+mXffToii6lGB\nZ7Vlo78g20D8NAO6dpJfmA==\n\"\"\"\n    LEGACY_TEST_2 = \"\"\"\\\n-----BEGIN MAILPILE ENCRYPTED DATA-----\ncipher: aes-256-cbc\nnonce: SB+fmmM72oFpf/FO4wnaHhFBvhgzpbwW\nmd5sum: 90dfb2850da49c8a6027415521dadb3c\n\nU2FsdGVkX19U8G7SKp8QygUusdHZThlrLcI04+jZ9U5kwfsw7bJJ2721dwgIpCUh\n3wpQjsYtFF2dcKBjrG7xyw==\n\n-----END MAILPILE ENCRYPTED DATA-----\n\"\"\"\n\n    # Do this before checking for fd leaks, as it may open up /dev/urandom\n    # and keep it open.\n    b = getrandbits(128)\n\n    # Create a pipe, this tells us which FDs are available next\n    fdpair1 = os.pipe()\n    for fd in fdpair1:\n        os.close(fd)\n    def fdcheck(where):\n        fdpair2 = os.pipe()\n        try:\n            for fd in fdpair2:\n                if fd not in fdpair1:\n                    print('Probably have an FD leak at %s!' % where)\n                    print('Verify with: lsof -g %s' % os.getpid())\n                    import time\n                    time.sleep(900)\n                    return False\n            return True\n        finally:\n            for fd in fdpair2:\n                os.close(fd)\n\n    bc = [0]\n    def counter(data):\n        bc[0] += len(data or '')\n        return (data or '')\n\n    # Cleanup...\n    try:\n        os.unlink('/tmp/iofilter.tmp')\n    except OSError:\n        pass\n\n    print('Test the IOFilter in write mode')\n    with open('/tmp/iofilter.tmp', 'w') as bfd:\n        with IOFilter(bfd, counter) as iof:\n            iof.writer().write('Hello world!')\n    with open('/tmp/iofilter.tmp', 'r') as iof:\n        assert(iof.read() == 'Hello world!')\n    _assert(bc[0], 12)\n    _assert(fdcheck('IOFilter in write mode'))\n\n    print('Test the IOFilter in read mode')\n    bc[0] = 0\n    with open('/tmp/iofilter.tmp', 'r') as bfd:\n        with IOFilter(bfd, counter) as iof:\n            data = iof.reader().read()\n            _assert(data, 'Hello world!')\n            _assert(bc[0], 12)\n    _assert(fdcheck('IOFilter in read mode'))\n\n    print('Test the IOFilter in incomplete read mode')\n    bc[0] = 0\n    with open('/dev/urandom', 'r') as bfd:\n        with IOFilter(bfd, counter) as iof:\n            data = iof.reader().read(4096)\n    _assert(bc[0] >= 4096, msg='%s >= 4096' % bc[0])\n    _assert(len(data) == 4096)\n    _assert(fdcheck('IOFilter in incomplete read mode'))\n\n    print('Test the ReadLineIOFilter in incomplete read mode')\n    bc[0], daemonlogline = 0, ''\n    with open('/etc/passwd', 'r') as bfd:\n        with IOFilter(bfd, counter) as iof:\n            for line in iof.reader():\n                if 'daemon' in line:\n                    daemonlogline = line\n                    break\n    _assert(bc[0] > 80, msg='%s > 80' % bc[0])\n    _assert('daemon' in daemonlogline, msg='daemon in %s' % daemonlogline)\n    _assert(fdcheck('ReadLineIOFilter in incomplete read mode'))\n\n    print('Null decryption test, sha256 verification only')\n    outer_mac_sha256 = '7982970534e089b839957b7e174725ce1878731ed6d700766e59cb16f1c25e27'\n    with open('/tmp/iofilter.tmp', 'rb') as bfd:\n        with DecryptingStreamer(bfd,\n                                mep_key='test key',\n                                sha256=outer_mac_sha256\n                                ) as ds:\n            _assert('Hello world!', ds.read())\n            _assert(ds.verify(testing=True))\n    _assert(fdcheck('Decrypting test, sha256 verification'))\n\n    print('Legacy (MEP v1) decryption test')\n    for legacy in (LEGACY_TEST_1, LEGACY_TEST_2):\n        lfd = StringIO.StringIO(legacy)\n        with PartialDecryptingStreamer([], lfd,\n                                       mep_key=LEGACY_TEST_KEY) as ds:\n            plaintext = ''\n            d = ds.read(1)\n            while len(d) > 0:\n                plaintext += d\n                d = ds.read(random.randint(10, max(11, len(legacy))))\n            try:\n                _assert(plaintext, LEGACY_PLAINTEXT)\n                _assert(ds.verify(testing=True))\n            except AssertionError:\n                print('command=%s' % ds.command)\n                print('stderr=%s' % ds.stderr)\n                print('key=%s [%s]\\n%s' % (LEGACY_TEST_KEY, ds.mep_mutated, legacy))\n                raise\n\n    for cipher in ('none', 'broken', 'aes-128-ctr', 'aes-256-cbc'):\n      for filter_sha256 in (True, False):\n        for delim in (True, False):\n            print(('Encryption test, cipher=%s, delim=%s, filter_sha256=%s'\n                   ) % (cipher, delim, filter_sha256))\n\n            fn = '/tmp/enc-%s-%s-%s.tmp' % (cipher, delim, filter_sha256)\n            with open(fn, 'wb') as fd:\n                fd.write('junk')  # Make sure overwriting works\n\n            t0 = time.time()\n            mul = 123400\n            data = 'Hello world! This is great!\\nHooray, lalalalla!\\n' * mul\n            parts = [(data, 'wb')]\n            if delim:\n                for i in (2, 5, 10, mul // 10, mul):\n                    more = 'part two, yeaaaah\\n' * (mul // i)\n                    parts.append((more, 'ab'))\n                    data += more\n            encrypted = []\n            for part, mode in parts:\n                with EncryptingStreamer('test key', dir='/tmp',\n                                        delimited=delim,\n                                        cipher=cipher,\n                                        use_filter=filter_sha256) as es:\n                    d = part\n                    while d:\n                        i = min(len(d), random.randint(10, max(11, len(d))))\n                        es.write(d[:i])\n                        d = d[i:]\n                    es.finish()\n                    es.save(fn, mode=mode)\n                    encrypted.append(es.outer_mac_sha256())\n            _assert(fdcheck('Encrypted data, delimited=%s' % delim))\n\n            t1 = time.time()\n            print('Decryption test, delim=%s' % delim)\n            with open(fn, 'rb') as bfd:\n                new_data = ''\n                for ms in encrypted:\n                    if delim:\n                        ds = PartialDecryptingStreamer(\n                            [], bfd, mep_key='test key', sha256=ms)\n                    else:\n                        ds = DecryptingStreamer(\n                            bfd, mep_key='test key', sha256=ms)\n                    with ds:\n                        d = ds.read(9999)\n                        while d:\n                            new_data += d\n                            d = ds.read(random.randint(10, 102400))\n                        _assert(ds.verify(testing=True))\n                try:\n                    _assert(data, new_data)\n                except:\n                    print('OLD %d bytes vs. NEW %d bytes: \\n%s\\n' % (\n                        len(data), len(new_data), new_data[-100:]))\n                    raise\n            _assert(fdcheck('Decrypting test, delimited=%s' % delim))\n            t2 = time.time()\n            print (' => Elapsed: %.3fs + %.3fs = %.3fs (%.2f MB/s)'\n                   % (t1-t0, t2-t1, t2-t0, len(new_data)/(1024*1024*(t2-t0))))\n\n        # Cleanup\n        os.unlink(fn)\n      print()\n\n    _assert(len(DETECTED_OBSOLETE_FORMATS) > 0)\n    print('Obsolete formats detected: %s' % DETECTED_OBSOLETE_FORMATS)\n\n    os.unlink('/tmp/iofilter.tmp')\n    _assert(fdcheck('All done'))\n"
  },
  {
    "path": "mailpile/crypto/tor.py",
    "content": "import copy\nimport socket\nimport subprocess\nimport threading\nimport time\nimport traceback\nimport re\nimport sys\nimport os\n\nimport stem.process\nimport stem.control\n\nimport mailpile.util\nfrom mailpile.eventlog import Event\nfrom mailpile.platforms import RandomListeningPort, GetDefaultTorPath\nfrom mailpile.safe_popen import PresetSafePopenArgs, MakePopenSafe\nfrom mailpile.util import okay_random\n\nif 'pythonw' in sys.executable:\n    debug_target = open( os.devnull, 'w' )\nelse:\n    debug_target = sys.stdout\n\ndef debug( text ):\n    debug_target.write( text + '\\n' )\n    debug_target.flush()\n\n# Version check for STEM >= 1.4\nassert(int(stem.__version__[0]) > 1 or\n       (int(stem.__version__[0]) == 1 and int(stem.__version__[2]) >= 4))\n\n\nclass Tor(threading.Thread):\n    _instance = None\n    def __new__(cls, *args, **kwargs):\n        if not cls._instance:\n            cls._instance = super(Tor, cls).__new__(cls, *args, **kwargs)\n        return cls._instance\n\n    def __init__(self, session=None, config=None,\n                 socks_port=None, control_port=None, tor_binary=None,\n                 callbacks=None):\n        threading.Thread.__init__(self)\n        self.session = session\n        self.config = config or (session.config if session else None)\n        self.callbacks = callbacks\n\n        if self.config is None:\n            self.socks_port = None\n            self.control_port = None\n            self.control_password = okay_random(32)\n            self.tor_binary = tor_binary\n        else:\n            self.socks_port = self.config.sys.tor.socks_port\n            self.control_port = self.config.sys.tor.ctrl_port\n            self.control_password = self.config.sys.tor.ctrl_auth\n            self.tor_binary = tor_binary or self.config.sys.tor.binary or None\n\n        if socks_port is not None:\n            self.socks_port = socks_port\n        if control_port is not None:\n            self.control_port = control_port\n\n        self.event = Event(source=self, flags=Event.INCOMPLETE, data={})\n        self.lock = threading.Lock()\n        self.tor_process = None\n        self.tor_controller = None\n        self.hidden_services = {}\n        self.keep_looping = True\n        self.started = False\n\n    def run(self):\n        starts = 0\n        while self.keep_looping and not mailpile.util.QUITTING:\n            starts += 1\n            try:\n                self._run_once()\n            except OSError:\n                pass\n            for i in range(0, 5 * min(60, starts)):\n                if mailpile.util.QUITTING: break\n                time.sleep(0.2)\n\n    def _run_once(self):\n        try:\n            random_ports = RandomListeningPort(count=2)\n            with self.lock:\n                self.event.flags = Event.INCOMPLETE\n                self.tor_process = 'starting up'\n                self.tor_controller = None\n                self.started = True\n\n                if not self.socks_port:\n                    self.socks_port = random_ports[0]\n                if not self.control_port:\n                    self.control_port = random_ports[1]\n                if self.tor_binary is None:\n                    self.tor_binary = GetDefaultTorPath()\n\n                tor_process_config = {\n                    'SocksPort': str(self.socks_port),\n                    'ControlPort': str(self.control_port),\n                    'HashedControlPassword': self._hashed_control_password()}\n\n                self._log_line('Launching Tor (%s)' % self.tor_binary,\n                               notify=True)\n\n                PresetSafePopenArgs(long_running=True)\n                self.tor_process = stem.process.launch_tor_with_config(\n                    timeout=None,  # Required or signal.signal will raise\n                    tor_cmd=self.tor_binary,\n                    config= tor_process_config,\n                    init_msg_handler=self._log_line)\n\n                ctrl = stem.control.Controller.from_port(port=self.control_port)\n                ctrl.authenticate(password=self.control_password)\n                self.tor_controller = ctrl\n\n            self.event.flags = Event.RUNNING\n            self._log_line('Tor is live on socks=%d, control=%d'\n                           % (self.socks_port, self.control_port),\n                           notify=True)\n        finally:\n            MakePopenSafe()\n\n        # Relaunch all the hidden services, if our Tor process died on us.\n        self.relaunch_hidden_services()\n\n        # Invoke any on-startup callbacks\n        for cb in (self.callbacks or []):\n            try:\n                cb(self)\n            except:\n                self._log_line('Callback %s failed: %s'\n                               % (cb, traceback.format_exc()),\n                               notify=True)\n\n        # Finally, just wait until our child process terminates.\n        try:\n            self.tor_process.wait()\n        except:\n            pass\n        finally:\n            self.event.flags = Event.COMPLETE\n            self._log_line('Shut down', notify=True)\n            self.tor_controller = None\n            self.tor_process = None\n\n    def _hashed_control_password(self):\n        try:\n            hasher = subprocess.Popen(\n                [self.tor_binary, '--hush', '--hash-password',\n                str(self.control_password)],\n                stdout=subprocess.PIPE, bufsize=1)\n            hasher.wait()\n            expr = re.compile('([\\d]{2}:[\\w]{58})')\n            match = filter(None, map(expr.match, hasher.stdout))[0]\n            passhash = match.group(1)\n            return passhash\n        except:\n            return None\n\n    def _log_line(self, line, notify=False):\n        if self.session:\n            if notify:\n                self.session.ui.notify(line)\n            else:\n                self.session.ui.debug(line)\n        else:\n            debug('%s' % line)\n\n        log = self.event.data.get('log', [])\n        log.append(line.strip())\n        if len(log) > 100:\n            log[:10] = []\n        self.event.data['log'] = log\n        if notify:\n            self.event.message = line\n            if self.config and self.config.event_log:\n                self.config.event_log.log_event(self.event)\n\n    def relaunch_hidden_services(self):\n        hidden_services = copy.copy(self.hidden_services)\n        for onion, (portmap, key_t, key_c) in hidden_services.iteritems():\n            if key_t and key_c:\n                self.launch_hidden_service(portmap, key_t, key_c)\n            else:\n                self._log_line('Failed to relaunch: %s' % onion, notify=True)\n\n    def launch_hidden_service(self, portmap, key_type=None, key_content=None):\n        with self.lock:\n            aor = self.tor_controller.create_ephemeral_hidden_service(\n                portmap,\n                key_type=key_type if (key_content and key_type) else 'NEW',\n                key_content=key_content or 'BEST',\n                detached=True, await_publication=True)\n\n        self.hidden_services[aor.service_id] = (\n            portmap,\n            aor.private_key_type or key_type,\n            aor.private_key or key_content)\n        self._log_line('Listening on Onion: %s.onion' % aor.service_id,\n                       notify=True)\n        return aor\n\n    def stop_tor(self, wait=True):\n        self.keep_looping = False\n        if self.tor_process is not None:\n            try:\n                for onion in self.hidden_services:\n                    try:\n                        t.tor_controller.remove_ephemeral_hidden_service(onion)\n                    except:\n                        pass\n\n                with self.lock:\n                    self.tor_process.kill()\n                if wait:\n                    self.tor_process.wait()\n            except:\n                pass\n            Tor._instance = None\n\n    def isReady(self, wait=False):\n         while True:\n            if ((self.tor_process is not None) and\n                    (self.tor_controller is not None)):\n                return True\n            if not wait:\n                return False\n            with self.lock:\n                # If we have the lock, but self.tor_process is None, that\n                # means startup has failed or we are dead - stop waiting!\n                wait = (not self.started) or (self.tor_process is not None)\n            if not self.started:\n                time.sleep(0.1)\n\n    def isAlive(self):\n        # FIXME: This is inaccurate\n        return (self.tor_process is not None)\n\n    def quit(self, join=True):\n        self.stop_tor(wait=join)\n\n\nif __name__ == \"__main__\":\n    TEST_KEY = \"RSA1024:MIICXQIBAAKBgQDQ/+aYFpSvZZ5Ce2cpsuJz1epCcY9n+HZx/bC/D7mqEXCdDB9W13FMuwwK9FvjjXdfJzkdJ1GEcppEzd69C5xPZo2k+klKDhMONYhGHcm+CGu+JWNbqrcInNfZageu1Hg8g5Kz2h+/xCmuqKLSxGwJGvIoYfZupyn3DaxGnZv/2QIDAQABAoGAYs13L9MM+1Yo2PkJrhbZIzWvhzW0O8ykAgOSeOBwP0v7VuMSNbWn5ERQzyTyA8Mu+ZbLU1LxIJIlB/3jHK/Odoe2kkPjjaeKKVXGM+NMefps/YPs8abql06YoWN6KshY0BYkzkmlF/Xxl4t+jjvDG9Fsx6kJV6LKRwm6BFVzUTkCQQD2ujGQs1I1fsuCCHZXcnyoLO/hJaJLoj3clCiYcWhnfdpgkv0+CdIYE+DPZNsiIfruQQYZzZjKtO2xx0fvqKuPAkEA2Nq46nR1L0ISJizSfTYkz+KXuV8kgkMxwzkZC6l+DAqf4qxjFcDOwrIw9f75N8DcveHLD4R/fyMaesW6SK8KFwJAZ4Om1/bkPt17tIqoW/gEpOp1mhiYBvOC0NC4V3z9OK5suKfy59xm8QMmBt1hsuhexycw0BKaUDGoqDXb0IkLsQJBANQaUsWXdMrtX90Q+CxaGfVvVyGL6qSyXmjpXxLmDBBxD+Ng42VyeYk7SuJBKreanw3mXHvoB+BtkEfHQCY5dq8CQQCofoToQr5mTrlomus6/ei22Ein/BS9s0YUPCOpMkZfSp/GaWyEH7QjxatM/LoaMRlH/Y/wGMEK8P05F9DGBtSP\"\n    t = Tor()\n    try:\n        debug_target = open( sys.argv[1], 'w' )\n    except IndexError:\n        pass\n    try:\n        debug( \"*** Starting Tor\" )\n        t.start()\n        if t.isReady(wait=True):\n            debug( \"*** Creating hidden services...\" )\n            key_type, key_content = TEST_KEY.split(':', 1)\n            aor = t.launch_hidden_service({80: 80}, key_type, key_content)\n            aor = t.launch_hidden_service(443)\n\n            #raw_input(\"*** Hit enter to disable service and shutdown ***\")\n            debug(\"waiting...\")\n            time.sleep(5)\n    finally:\n        debug(\"quiting!?!\")\n        t.quit()\n        debug(\"exiting!?!\")\n"
  },
  {
    "path": "mailpile/eventlog.py",
    "content": "from __future__ import print_function\nimport copy\nimport datetime\nimport json\nimport os\nimport threading\nimport time\nfrom email.utils import formatdate, parsedate_tz, mktime_tz\n\nfrom mailpile.crypto.streamer import EncryptingStreamer, DecryptingStreamer\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.util import EventRLock, EventLock, CleanText, json_helper\nfrom mailpile.util import safe_remove, thread_context\n\n\nEVENT_COUNTER_LOCK = threading.Lock()\nEVENT_COUNTER = 0\n\n\n\ndef NewEventId():\n    \"\"\"\n    This is guaranteed to generate unique event IDs for up to 1 million\n    events per second. Beyond that, all bets are off. :-P\n    \"\"\"\n    global EVENT_COUNTER\n    with EVENT_COUNTER_LOCK:\n        EVENT_COUNTER = EVENT_COUNTER+1\n        EVENT_COUNTER %= 0x100000\n        return '%8.8x-%5.5x-%x' % (time.time(), EVENT_COUNTER, os.getpid())\n\n\ndef _ClassName(obj, ignore_regexps=False):\n    if isinstance(obj, (str, unicode)):\n        return str(obj).replace('mailpile.', '.')\n    elif hasattr(obj, '__classname__'):\n        return str(obj.__classname__).replace('mailpile.', '.')\n    elif ignore_regexps and 'SRE_Pattern' in str(obj.__class__):\n        return obj\n    else:\n        module = str(obj.__class__.__module__)\n        if module.startswith('mailpile.'):\n            module = module[len('mailpile'):]\n        return '%s.%s' % (module, str(obj.__class__.__name__))\n\n\nclass Event(object):\n    \"\"\"\n    This is a single event in the event log. Actual interpretation and\n    rendering of events should be handled by the respective source class.\n    \"\"\"\n    RUNNING = 'R'\n    COMPLETE = 'c'\n    INCOMPLETE = 'i'\n    FUTURE = 'F'\n\n    # For now these live here, we may templatize this later.\n    PREAMBLE_HTML = '<ul class=\"events\">'\n    PUBLIC_HTML = ('<li><span class=\"event_date\">%(date)s</span> '\n                   '<b class=\"event_message\">%(message)s</b></li>')\n    PRIVATE_HTML = PUBLIC_HTML\n    POSTAMBLE_HTML = '</ul>'\n\n    @classmethod\n    def Parse(cls, json_string):\n        try:\n            return cls(*json.loads(json_string))\n        except:\n            return cls()\n\n    def __init__(self,\n                 ts=None, event_id=None, flags='c', message='',\n                 source=None, data=None, private_data=None):\n        self._data = [\n            '',\n            (event_id or NewEventId()).replace('.', '-'),\n            flags,\n            message,\n            _ClassName(source),\n            data or {},\n            private_data or {},\n        ]\n        self._set_ts(ts or time.time())\n\n    def __str__(self):\n        return json.dumps(self._data, default=json_helper)\n\n    def _set_ts(self, ts):\n        if hasattr(ts, 'timetuple'):\n            self._ts = int(time.mktime(ts.timetuple()))\n        elif isinstance(ts, (str, unicode)):\n            self._ts = int(mktime_tz(parsedate_tz(ts)))\n        else:\n            self._ts = float(ts)\n        self._data[0] = formatdate(self._ts)\n\n    def _set(self, col, value):\n        self._set_ts(time.time())\n        self._data[col] = value\n\n    def _get_source_class(self):\n        try:\n            module_name, class_name = CleanText(self.source,\n                                                banned=CleanText.NONDNS\n                                                ).clean.rsplit('.', 1)\n            if module_name.startswith('.'):\n                module_name = 'mailpile' + module_name\n            module = __import__(module_name, globals(), locals(), class_name)\n            return getattr(module, class_name)\n        except (ValueError, AttributeError, ImportError):\n            return None\n\n    date = property(lambda s: s._data[0], lambda s, v: s._set_ts(v))\n    ts = property(lambda s: s._ts, lambda s, v: s._set_ts(v))\n    event_id = property(lambda s: s._data[1], lambda s, v: s._set(1, v))\n    flags = property(lambda s: s._data[2], lambda s, v: s._set(2, v))\n    message = property(lambda s: s._data[3], lambda s, v: s._set(3, v))\n    source = property(lambda s: s._data[4],\n                      lambda s, v: s._set(4, _ClassName(v)))\n    data = property(lambda s: s._data[5], lambda s, v: s._set(5, v))\n    private_data = property(lambda s: s._data[6], lambda s, v: s._set(6, v))\n    source_class = property(_get_source_class)\n\n    def as_dict(self, private=True):\n        try:\n            return self.source_class.EventAsDict(self, private=private)\n        except (AttributeError, NameError):\n            data = {\n                'ts': self.ts,\n                'date': self.date,\n                'event_id': self.event_id,\n                'message': self.message,\n                'flags': self.flags,\n                'source': self.source,\n                'data': self.data\n            }\n            if private:\n                data['private_data'] = self.private_data\n            return data\n\n    def as_text(self, private=True, compact=False):\n        try:\n            return self.source_class.EventAsText(self, private=private,\n                                                       compact=True)\n        except (AttributeError, NameError):\n            if compact:\n                return '%s=%s:%s %s' % (self.event_id,\n                                        self.source.split('.')[-1],\n                                        self.flags, self.message)\n            else:\n                return json.dumps(self.as_dict(private=private),\n                                  default=json_helper)\n\n    def as_json(self, private=True):\n        try:\n            return self.source_class.EventAsJson(self, private=private)\n        except (AttributeError, NameError):\n            return json.dumps(self.as_dict(private=private))\n\n    def as_html(self, private=True):\n        try:\n            return self.source_class.EventAsHtml(self, private=private)\n        except (AttributeError, NameError):\n            if private:\n                return self.PRIVATE_HTML % self.as_dict(private=True)\n            else:\n                return self.PUBLIC_HTML % self.as_dict(private=False)\n\n\ndef GetThreadEvent(create=False, message=None, source=None):\n    ctx = thread_context()\n    if ctx and 'event' in ctx[-1]:\n        return ctx[-1]['event']\n    elif create:\n        return Event(message=message, source=source)\n    else:\n        return None\n\n\nclass EventLog(object):\n    \"\"\"\n    This is the Mailpile Event Log.\n\n    The log is written encrypted to disk on an ongoing basis (rotated\n    every N lines), but entries are kept in RAM as well. The event log\n    allows for recording of incomplete events, to help different parts\n    of the app \"remember\" tasks which have yet to complete or may need\n    to be retried.\n    \"\"\"\n    KEEP_LOGS = 2\n\n    def __init__(self, logdir, decryption_key_func, encryption_key_func,\n                 rollover=1024):\n        self.logdir = logdir\n        self.decryption_key_func = decryption_key_func or (lambda: None)\n        self.encryption_key_func = encryption_key_func or (lambda: None)\n        self.rollover = rollover\n\n        self._events = {}\n\n        # Internals...\n        self._watching_uis = []\n        self._waiter = threading.Condition(EventRLock())\n        self._lock = EventLock()\n        self._log_fd = None\n\n    def _notify_waiters(self):\n        with self._waiter:\n            self._waiter.notifyAll()\n\n    def wait(self, timeout=None):\n        with self._waiter:\n            self._waiter.wait(timeout)\n\n    def _save_filename(self):\n        return os.path.join(self.logdir, self._log_start_id)\n\n    def _open_log(self):\n        if self._log_fd:\n            self._log_fd.close()\n\n        if not os.path.exists(self.logdir):\n            os.mkdir(self.logdir)\n\n        self._log_start_id = NewEventId()\n        enc_key = self.encryption_key_func()\n        if enc_key:\n            self._log_fd = EncryptingStreamer(enc_key,\n                                              dir=self.logdir,\n                                              name='EventLog/ES',\n                                              use_filter=False,\n                                              long_running=True)\n            self._log_fd.save(self._save_filename(), finish=False)\n            self._log_write = self._log_fd.write_pad_and_flush\n        else:\n            self._log_fd = open(self._save_filename(), 'wb', 0)\n            self._log_write = self._log_fd.write\n\n        # Write any incomplete events to the new file\n        for e in self.incomplete():\n            self._log_write('%s\\n')\n\n        # We're starting over, incomplete events don't count\n        self._logged = 0\n\n    def _maybe_rotate_log(self):\n        if self._logged > self.rollover:\n            self._log_fd.close()\n            kept_events = {}\n            for e in self.incomplete():\n                kept_events[e.event_id] = e\n            self._events = kept_events\n            self._open_log()\n            self.purge_old_logfiles()\n\n    def _list_logfiles(self):\n        return sorted([l for l in os.listdir(self.logdir)\n                       if not l.startswith('.')])\n\n    def _save_events(self, events, recursed=False):\n        if not self._log_fd:\n            self._open_log()\n        events.sort(key=lambda ev: ev.ts)\n        try:\n            for event in events:\n                self._log_write('%s\\n' % event)\n                self._events[event.event_id] = event\n        except IOError:\n            if recursed:\n                raise\n            else:\n                self._unlocked_close()\n                return self._save_events(events, recursed=True)\n\n    def _load_logfile(self, lfn):\n        enc_key = self.decryption_key_func()\n        with open(os.path.join(self.logdir, lfn)) as fd:\n            if enc_key:\n                with DecryptingStreamer(fd, mep_key=enc_key,\n                                        name='EventLog/DS(%s)' % lfn\n                                        ) as streamer:\n                    lines = streamer.read()\n                    streamer.verify(_raise=IOError)\n            else:\n                lines = fd.read()\n            if lines:\n                for line in lines.splitlines():\n                    event = Event.Parse(line.strip())\n                    self._events[event.event_id] = event\n\n    def _match(self, event, filters):\n        def compare(val, rule):\n            if isinstance(rule, (str, unicode)):\n                return unicode(val) == unicode(rule)\n            else:\n                return rule.match(unicode(val)) is not None\n        for kw, rule in filters.iteritems():\n            if kw.endswith('!'):\n                truth, okw, kw = False, kw, kw[:-1]\n            else:\n                truth, okw = True, kw\n            if kw == 'source':\n                if truth != compare(event.source,\n                                    _ClassName(rule, ignore_regexps=True)):\n                    return False\n            elif kw == 'flag':\n                if truth != (rule in event.flags):\n                    return False\n            elif kw == 'flags':\n                if truth != compare(event.flags, rule):\n                    return False\n            elif kw == 'event_id':\n                if truth != compare(event.event_id, rule):\n                    return False\n            elif kw == 'since':\n                when = float(rule)\n                if when < 0:\n                    when += time.time()\n                if truth != (event.ts > when):\n                    return False\n            elif kw.startswith('data_'):\n                if truth != compare(event.data.get(kw[5:]), rule):\n                    return False\n            elif kw.startswith('private_data_'):\n                if truth != compare(event.data.get(kw[13:]), rule):\n                    return False\n            else:\n                # Unknown keywords match nothing...\n                print('Unknown keyword: `%s=%s`' % (okw, rule))\n                return False\n        return True\n\n    def incomplete(self, **filters):\n        \"\"\"Return all the incomplete events, in order.\"\"\"\n        if 'event_id' in filters:\n            ids = [filters['event_id']]\n        else:\n            ids = sorted(self._events.keys())\n        for ek in ids:\n            e = self._events.get(ek, None)\n            if (e is not None and\n                    Event.COMPLETE not in e.flags and\n                    self._match(e, filters)):\n                yield e\n\n    def since(self, ts, **filters):\n        \"\"\"Return all events since a given time, in order.\"\"\"\n        if ts < 0:\n            ts += time.time()\n        if 'event_id' in filters and filters['event_id'][:1] != '!':\n            ids = [filters['event_id']]\n        else:\n            ids = sorted(self._events.keys())\n        for ek in ids:\n            e = self._events.get(ek, None)\n            if (e is not None and\n                    e.ts >= ts and\n                    self._match(e, filters)):\n                yield e\n\n    def events(self, **filters):\n        return self.since(0, **filters)\n\n    def get(self, event_id, default=None):\n        return self._events.get(event_id, default)\n\n    def log_event(self, event):\n        \"\"\"Log an Event object.\"\"\"\n        with self._lock:\n            self._save_events([event])\n            self._logged += 1\n            self._maybe_rotate_log()\n            self._notify_waiters()\n            for ui in self._watching_uis:\n                ui.notify(event.as_text(compact=True))\n        return event\n\n    def log(self, *args, **kwargs):\n        \"\"\"Log a new event.\"\"\"\n        return self.log_event(Event(*args, **kwargs))\n\n    def close(self):\n        with self._lock:\n            return self._unlocked_close()\n\n    def _unlocked_close(self):\n        try:\n            self._log_fd.close()\n            self._log_fd = None\n        except (OSError, IOError):\n            pass\n\n    def _prune_completed(self):\n        for event_id in self._events.keys():\n            if Event.COMPLETE in self._events[event_id].flags:\n                del self._events[event_id]\n\n    def ui_watch(self, ui):\n        while ui.log_parent is not None:\n            ui = ui.log_parent\n        if ui not in self._watching_uis:\n            self._watching_uis.append(ui)\n            return True\n        else:\n            return False\n\n    def ui_unwatch(self, ui):\n        while ui.log_parent is not None:\n            ui = ui.log_parent\n        try:\n            self._watching_uis.remove(ui)\n        except ValueError:\n            pass\n\n    def load(self):\n        with self._lock:\n            self._open_log()\n            for lf in self._list_logfiles()[-4:]:\n                try:\n                    self._load_logfile(lf)\n                except (OSError, IOError):\n                    # Nothing we can do, no point complaining...\n                    pass\n            self._prune_completed()\n            self._save_events(self._events.values())\n            return self\n\n    def purge_old_logfiles(self, keep=None):\n        keep = keep or self.KEEP_LOGS\n        for lf in self._list_logfiles()[:-keep]:\n            try:\n                safe_remove(os.path.join(self.logdir, lf))\n            except OSError:\n                pass\n"
  },
  {
    "path": "mailpile/httpd.py",
    "content": "#\n# Mailpile's built-in HTTPD\n#\n###############################################################################\nimport Cookie\nimport cStringIO\nimport hashlib\nimport gzip\nimport mimetypes\nimport os\nimport random\nimport select\nimport socket\nimport SocketServer\nimport time\nimport threading\nimport traceback\nfrom SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler\nfrom urllib import quote, unquote\nfrom urlparse import parse_qs, urlparse\n\nimport mailpile.util\nimport mailpile.security as security\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.urlmap import UrlMap\nfrom mailpile.util import *\nfrom mailpile.ui import *\n\nglobal WORD_REGEXP, STOPLIST, BORING_HEADERS, DEFAULT_PORT\n\nDEFAULT_PORT = 33411\n\nBLOCK_HTTPD_LOCK = UiRLock()\nLIVE_HTTP_REQUESTS = 0\n\n\ndef Idle_HTTPD(allowed=1):\n    with BLOCK_HTTPD_LOCK:\n        sleep = 100\n        while (sleep and\n                not mailpile.ui.QUITTING and\n                LIVE_HTTP_REQUESTS > allowed):\n            time.sleep(0.05)\n            sleep -= 1\n        return BLOCK_HTTPD_LOCK\n\n\nclass HttpRequestHandler(SimpleXMLRPCRequestHandler):\n    # Allow persistent HTTP/1.1 connections\n    protocol_version = 'HTTP/1.1'\n\n    # We always recognize these extensions, no matter what the Python\n    # mimetype module thinks.\n    _MIMETYPE_MAP = dict([(ext, 'text/plain') for ext in (\n        'c', 'cfg', 'conf', 'cpp', 'csv', 'h', 'hpp', 'log', 'md', 'me',\n        'py', 'rb', 'rc', 'txt'\n    )] + [(ext, 'application/x-font') for ext in (\n        'pfa', 'pfb', 'gsf', 'pcf'\n    )] + [\n        ('css', 'text/css'),\n        ('eot', 'application/vnd.ms-fontobject'),\n        ('gif', 'image/gif'),\n        ('html', 'text/html'),\n        ('htm', 'text/html'),\n        ('ico', 'image/x-icon'),\n        ('jpg', 'image/jpeg'),\n        ('jpeg', 'image/jpeg'),\n        ('js', 'text/javascript'),\n        ('json', 'application/json'),\n        ('otf', 'font/otf'),\n        ('png', 'image/png'),\n        ('rss', 'application/rss+xml'),\n        ('tif', 'image/tiff'),\n        ('tiff', 'image/tiff'),\n        ('ttf', 'font/ttf'),\n        ('svg', 'image/svg+xml'),\n        ('svgz', 'image/svg+xml'),\n        ('woff', 'application/font-woff'),\n    ])\n\n    _ERROR_CONTEXT = {'lastq': '', 'csrf': '', 'path': ''},\n    _NEWLINE_RE = re.compile('[\\r\\n]+')\n    _HTML_RE = re.compile('[<>\\'\\\"]+')\n\n    def assert_no_newline(self, data):\n        if re.search(self._NEWLINE_RE, str(data) or '') is not None:\n            raise ValueError()\n\n    def assert_no_html(self, data):\n        if re.search(self._HTML_RE, data or '') is not None:\n            raise ValueError()\n\n    def send_header(self, hdr, value):\n        self.assert_no_newline(value)\n        return SimpleXMLRPCRequestHandler.send_header(self, hdr, value)\n\n    def http_host(self):\n        \"\"\"Return the current server host, e.g. 'localhost'\"\"\"\n        try:\n            # rsplit removes port\n            return self.headers.get('host', 'localhost').rsplit(':', 1)[0]\n        except AttributeError:\n            return 'unknown'\n\n    def _load_cookies(self):\n        \"\"\"Robustified cookie parser that silently drops invalid cookies.\"\"\"\n        cookies = Cookie.SimpleCookie()\n        for fragment in self.headers.get('cookie', '').split('; '):\n            if fragment:\n                try:\n                    cookies.load(fragment)\n                except Cookie.CookieError:\n                    pass\n        return cookies\n\n    def http_session(self):\n        \"\"\"Fetch the session ID from a cookie, or assign a new one\"\"\"\n        session_id = self._load_cookies().get(self.server.session_cookie)\n        if session_id:\n            session_id = session_id.value\n            self.assert_no_newline(session_id)\n        else:\n            session_id = self.server.make_session_id(self)\n        return session_id\n\n    def server_url(self):\n        \"\"\"Return the current server URL, e.g. 'http://localhost:33411/'\"\"\"\n        try:\n            surl = '%s://%s' % (self.headers.get('x-forwarded-proto', 'http'),\n                                self.headers.get('host', 'localhost'))\n            self.server.server_url = surl\n        except AttributeError:\n            surl = self.server.server_url\n        return surl\n\n    def send_http_response(self, code, msg):\n        \"\"\"Send the HTTP response header\"\"\"\n        msg = '%s %s' % (code, msg)\n        self.assert_no_newline(msg)\n        self.wfile.write('HTTP/1.1 %s\\r\\n' % msg)\n\n    def send_http_redirect(self, destination):\n        # We don't re-encode things here, we expect our input to already\n        # be well formed. However, this is the last chance to block any\n        # exploits, so we do check to make sure.\n        self.assert_no_newline(destination)\n        self.assert_no_html(destination)\n        self.send_http_response(302, 'Found')\n        body = ('<h1><a href=\"%s\">Please look here!</a></h1>\\n'\n                ) % (destination,)\n        self.wfile.write(('Location: %s\\r\\n'\n                          'Content-Length: %d\\r\\n\\r\\n'\n                          ) % (destination, len(body)))\n        self.wfile.write(body)\n\n    def send_standard_headers(self,\n                              header_list=[],\n                              cachectrl='private',\n                              mimetype='text/html',\n                              x_dns_prefetch='off'):\n        \"\"\"\n        Send common HTTP headers plus a list of custom headers:\n        - Cache-Control\n        - Content-Type\n        - X-DNS-Prefetch-Control\n\n        This function does not send the HTTP/1.1 header, so\n        ensure self.send_http_response() was called before\n\n        Keyword arguments:\n        header_list  -- A list of custom headers to send, containing\n                        key-value tuples\n        cachectrl    -- The value of the 'Cache-Control' header field\n        mimetype     -- The MIME type to send as 'Content-Type' value\n        \"\"\"\n        if mimetype.startswith('text/') and ';' not in mimetype:\n            mimetype += ('; charset = utf-8')\n        self.send_header('Cache-Control', cachectrl)\n        self.send_header('Content-Security-Policy',\n                         security.http_content_security_policy(self.server))\n        self.send_header('Content-Type', mimetype)\n        self.send_header('X-DNS-Prefetch-Control', x_dns_prefetch)\n        self.send_header('X-UA-Compatible', 'IE=Edge')  # For old Windowses\n        for header in header_list:\n            self.send_header(header[0], header[1])\n        session_id = self.session.ui.html_variables.get('http_session')\n        if session_id:\n            cookies = Cookie.SimpleCookie()\n            cookies[self.server.session_cookie] = session_id\n            cookies[self.server.session_cookie]['path'] = '/'\n            cookies[self.server.session_cookie]['max-age'] = 24 * 3600\n            self.send_header(*cookies.output().split(': ', 1))\n        if mailpile.util.QUITTING:\n            self.send_header('Connection', 'close')\n        self.end_headers()\n\n    def send_full_response(self, message,\n                           code=200, msg='OK',\n                           mimetype='text/html', header_list=[],\n                           cachectrl=None,\n                           suppress_body=False):\n        \"\"\"\n        Sends the HTTP header and a response list\n\n        message       -- The body of the response to send\n        header_list   -- A list of custom headers to send,\n                         containing key-value tuples\n        code          -- The HTTP response code to send\n        mimetype      -- The MIME type to send as 'Content-Type' value\n        suppress_body -- Set this to True to ignore the message parameter\n                              and not send any response body\n        \"\"\"\n        message = unicode(message).encode('utf-8')\n        self.log_request(code, message and len(message) or '-')\n        # Send HTTP/1.1 header\n        self.send_http_response(code, msg)\n        # Send all headers\n        if code == 401:\n            self.send_header('WWW-Authenticate',\n                             'Basic realm = MP%d' % (time.time() / 3600))\n        # If suppress_body == True, we don't know the content length\n        headers = []\n        if not suppress_body:\n            message, headers = self._maybe_gzip(message, len(message or ''), [])\n        self.send_standard_headers(header_list=(header_list + headers),\n                                   mimetype=mimetype,\n                                   cachectrl=(cachectrl or \"no-cache\"))\n        # Response body\n        if not suppress_body:\n            self.wfile.write(message or '')\n\n    def guess_mimetype(self, fpath):\n        ext = os.path.basename(fpath).rsplit('.')[-1]\n        return (self._MIMETYPE_MAP.get(ext.lower()) or\n                mimetypes.guess_type(fpath, strict=False)[0] or\n                'application/octet-stream')\n\n    def _mk_etag(self, *args):\n        # This ETag varies by whatever args we give it (e.g. size, mtime,\n        # etc), but is unique per Mailpile instance and should leak nothing\n        # about the actual server configuration.\n        data = '%s-%s' % (self.server.secret, '-'.join((str(a) for a in args)))\n        return hashlib.md5(data).hexdigest()\n\n    def _maybe_gzip(self, data, msg_size, headers):\n        if (data and\n                (len(data) > 1400) and\n                (data[:2] not in ('\\xff\\xd8', '\\x89\\x50', # JPEG, PNG\n                                  '\\x1f\\x8b', 'BZ', 'PK' # GZIP, BZIP, PKZIP\n                                  )) and\n                ('gzip' in self.headers.get('accept-encoding', ''))):\n            gzipped = cStringIO.StringIO()\n            with gzip.GzipFile(fileobj=gzipped, mode='w') as fd:\n                fd.write(data)\n            gzipped = gzipped.getvalue()\n            if len(data) > len(gzipped):\n                headers.extend([('Content-Length', '%s' % len(gzipped)),\n                                ('X-Full-Size', '%s' % msg_size),\n                                ('Content-Encoding', 'gzip')])\n                return gzipped, headers\n        headers.append(('Content-Length', '%s' % msg_size))\n        return data, headers\n\n    def send_file(self, config, filename, suppress_body=False):\n        # FIXME: Do we need more security checks?\n        if '..' in filename:\n            code, msg = 403, \"Access denied\"\n        else:\n            try:\n                tpl = config.sys.path.get(self.http_host(), 'html_theme')\n                fpath, fd, mt = config.open_file(tpl, filename)\n                with fd:\n                    mimetype = mt or self.guess_mimetype(fpath)\n                    msg_size = os.path.getsize(fpath)\n                    if not suppress_body:\n                        message = fd.read()\n                    else:\n                        message = None\n                code, msg = 200, \"OK\"\n            except IOError as e:\n                mimetype = 'text/plain'\n                if e.errno == 2:\n                    code, msg = 404, \"File not found\"\n                elif e.errno == 13:\n                    code, msg = 403, \"Access denied\"\n                else:\n                    code, msg = 500, \"Internal server error\"\n                message = None\n                msg_size = 0\n\n        # Note: We assume the actual static content almost never varies\n        #       on a given Mailpile instance, thuse the long TTL and no\n        #       ETag for conditional loads.\n\n        message, headers = self._maybe_gzip(message, msg_size, [])\n        self.log_request(code, msg_size if (message is not None) else '-')\n        self.send_http_response(code, msg)\n        self.send_standard_headers(header_list=headers,\n                                   mimetype=mimetype,\n                                   cachectrl='must-revalidate, max-age=36000')\n        self.wfile.write(message or '')\n\n    def do_POST(self, method='POST'):\n        (scheme, netloc, path, params, query, frag) = urlparse(self.path)\n        if path.startswith('/::XMLRPC::/'):\n            raise ValueError(_('XMLRPC has been disabled for now.'))\n            #return SimpleXMLRPCRequestHandler.do_POST(self)\n\n        # Update thread name for debugging purposes\n        threading.current_thread().name = 'POST:%s' % self.path.split('?')[0]\n\n        self.session, config = self.server.session, self.server.session.config\n        post_data = {}\n        try:\n            ue = 'application/x-www-form-urlencoded'\n            clength = int(self.headers.get('content-length', 0))\n            ctype, pdict = cgi.parse_header(self.headers.get('content-type',\n                                                             ue))\n            if ctype == 'multipart/form-data':\n                post_data = cgi.FieldStorage(\n                    fp=self.rfile,\n                    headers=self.headers,\n                    environ={'REQUEST_METHOD': method,\n                             'CONTENT_TYPE': self.headers['Content-Type']}\n                )\n            elif ctype == ue:\n                if clength > 5 * 1024 * 1024:\n                    raise ValueError(_('OMG, input too big'))\n                post_data = cgi.parse_qs(self.rfile.read(clength), 1)\n            else:\n                raise ValueError(_('Unknown content-type'))\n\n        except (IOError, ValueError) as e:\n            self.send_full_response(self.server.session.ui.render_page(\n                config, self._ERROR_CONTEXT,\n                body='POST geborked: %s' % e,\n                title=_('Internal Error')\n            ), code=500)\n            return None\n        return self.do_GET(post_data=post_data, method=method)\n\n    def do_GET(self, *args, **kwargs):\n        global LIVE_HTTP_REQUESTS\n        try:\n            path = self.path.split('?')[0]\n\n            threading.current_thread().name = 'WAIT:%s' % path\n            with BLOCK_HTTPD_LOCK:\n                LIVE_HTTP_REQUESTS += 1\n\n            threading.current_thread().name = 'WORK:%s' % path\n            return self._real_do_GET(*args, **kwargs)\n        finally:\n            threading.current_thread().name = 'DONE:%s' % path\n            LIVE_HTTP_REQUESTS -= 1\n            if mailpile.util.QUITTING:\n                self.wfile.close()\n\n    def _real_do_GET(self, post_data={}, suppress_body=False, method='GET'):\n        (scheme, netloc, path, params, query, frag) = urlparse(self.path)\n        query_data = parse_qs(query)\n        opath = path = unquote(path)\n\n        # HTTP is stateless, so we create a new session for each request.\n        self.session, config = self.server.session, self.server.session.config\n        server_session = self.server.session\n\n        # Debugging...\n        if 'httpdata' in config.sys.debug:\n            self.wfile = DebugFileWrapper(sys.stderr, self.wfile)\n\n        # Path manipulation...\n        if path == '/favicon.ico':\n            path = '%s/static/favicon.ico' % (config.sys.http_path or '')\n        if config.sys.http_path:\n            if not path.startswith(config.sys.http_path):\n                self.send_full_response(_(\"File not found (invalid path)\"),\n                                        code=404, mimetype='text/plain')\n                return None\n            path = path[len(config.sys.http_path):]\n        if path.startswith('/_/'):\n            path = path[2:]\n        for static in ('/static/', '/bower_components/'):\n            if path.startswith(static):\n                return self.send_file(config, path[len(static):],\n                                      suppress_body=suppress_body)\n\n        self.session = session = Session(config)\n        session.ui = HttpUserInteraction(self, config,\n                                         log_parent=server_session.ui)\n        if 'context' in post_data:\n            session.load_context(post_data['context'][0])\n        elif 'context' in query_data:\n            session.load_context(query_data['context'][0])\n\n        mark_name = 'Processing HTTP API request at %s' % time.time()\n        session.ui.start_command(mark_name, [], {})\n\n        if 'http' in config.sys.debug:\n            session.ui.warning = server_session.ui.warning\n            session.ui.notify = server_session.ui.notify\n            session.ui.error = server_session.ui.error\n            session.ui.debug = server_session.ui.debug\n            session.ui.debug('%s: %s qs = %s post = %s'\n                             % (method, opath, query_data, post_data))\n\n        idx = session.config.index\n        if session.config.loaded_config:\n            name = session.config.get_profile().get('name', 'Chelsea Manning')\n        else:\n            name = 'Chelsea Manning'\n\n        http_headers = []\n        http_session = self.http_session()\n        csrf_token = security.make_csrf_token(self.server.secret, http_session)\n        session.ui.html_variables = {\n            'csrf_token': csrf_token,\n            'csrf_field': ('<input type=\"hidden\" name=\"csrf\" value=\"%s\">'\n                           % csrf_token),\n            'http_host': self.headers.get('host', 'localhost'),\n            'http_hostname': self.http_host(),\n            'http_method': method,\n            'http_session': http_session,\n            'http_request': self,\n            'http_response_headers': http_headers,\n            'message_count': (idx and len(idx.INDEX) or 0),\n            'name': name,\n            'title': 'Mailpile dummy title',\n            'url_protocol': self.headers.get('x-forwarded-proto', 'http'),\n            'mailpile_size': idx and len(idx.INDEX) or 0\n        }\n        session.ui.valid_csrf_token = lambda token: security.valid_csrf_token(\n            self.server.secret, http_session, token)\n\n        try:\n            try:\n                need_auth = not (mailpile.util.TESTING or\n                                 session.config.sys.http_no_auth)\n                commands = UrlMap(session).map(\n                    self, method, path, query_data, post_data,\n                    authenticate=need_auth)\n            except UsageError:\n                if (not path.endswith('/') and\n                        not session.config.sys.debug and\n                        method == 'GET'):\n                    commands = UrlMap(session).map(self, method, path + '/',\n                                                   query_data, post_data)\n                    url = quote(path) + '/'\n                    if query:\n                        url += '?' + query\n                    return self.send_http_redirect(url)\n                else:\n                    raise\n\n            cachectrl = None\n            if 'http' not in config.sys.debug:\n                etag_data = []\n                max_ages = []\n                have_ed = 0\n                for c in commands:\n                    max_ages.append(c.max_age())\n                    ed = c.etag_data()\n                    have_ed += 1 if ed else 0\n                    etag_data.extend(ed)\n                if have_ed == len(commands):\n                    etag = self._mk_etag(*etag_data)\n                    conditional = self.headers.get('if-none-match')\n                    if conditional == etag:\n                        self.send_full_response('OK', code=304,\n                                                msg='Unmodified')\n                        return None\n                    else:\n                        http_headers.append(('ETag', etag))\n                max_age = min(max_ages) if max_ages else 10\n                if max_age:\n                    cachectrl = 'must-revalidate, max-age=%d' % max_age\n                else:\n                    cachectrl = 'must-revalidate, no-store, max-age=0'\n\n            global LIVE_HTTP_REQUESTS\n            hang_fix = 1 if ([1 for c in commands if c.IS_HANGING_ACTIVITY]\n                             ) else 0\n            try:\n                LIVE_HTTP_REQUESTS -= hang_fix\n\n                session.ui.mark('Running %d commands' % len(commands))\n                results = [cmd.run() for cmd in commands]\n\n                session.ui.mark('Displaying final result')\n                session.ui.display_result(results[-1])\n            finally:\n                LIVE_HTTP_REQUESTS += hang_fix\n\n            session.ui.mark('Rendering response')\n            mimetype, content = session.ui.render_response(session.config)\n\n            session.ui.mark('Sending response')\n            self.send_full_response(content,\n                                    mimetype=mimetype,\n                                    header_list=http_headers,\n                                    cachectrl=cachectrl)\n\n        except UrlRedirectException as e:\n            return self.send_http_redirect(e.url)\n        except SuppressHtmlOutput:\n            return None\n        except AccessError:\n            self.send_full_response(_('Access Denied'),\n                                    code=403, mimetype='text/plain')\n            return None\n        except:\n            e = traceback.format_exc()\n            session.ui.debug(e)\n            if not session.config.sys.debug:\n                e = _('Internal Error')\n            self.send_full_response(e, code=500, mimetype='text/plain')\n            return None\n\n        finally:\n            session.ui.report_marks(\n                details=('timing' in session.config.sys.debug))\n            session.ui.finish_command(mark_name)\n\n    def do_PUT(self):\n        return self.do_POST(method='PUT')\n\n    def do_UPDATE(self):\n        return self.do_POST(method='UPDATE')\n\n    def do_HEAD(self):\n        return self.do_GET(suppress_body=True, method='HEAD')\n\n    def log_message(self, fmt, *args):\n        if 'http' in self.server.session.config.sys.debug:\n            self.server.session.ui.notify(self.server_url() +\n                                          ' ' + (fmt % args))\n\n\nclass HttpServer(SocketServer.ThreadingMixIn, SimpleXMLRPCServer):\n    def __init__(self, session, sspec, handler):\n        SimpleXMLRPCServer.__init__(self, sspec[:2], handler)\n        self.daemon_threads = True\n        self.session = session\n        self.sessions = {}\n        self.session_cookie = None\n\n        # Duplicates from SocketServer.py, so our overrides work\n        self.__is_shut_down = threading.Event()\n        self.__shutdown_request = False\n\n        # This lets us create new HTTPDs withut waiting for this one to\n        # completely shut down.\n        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n\n        # We set a large sending buffer to avoid blocking, because the GIL and\n        # scheduling interact badly when we have busy background jobs.\n        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 128 * 1024)\n        self.server_url = 'http://UNKNOWN/'\n        self.sspec = (sspec[0] or 'localhost',\n                      self.socket.getsockname()[1],\n                      sspec[2])\n\n        # This hash includes the index ofuscation master key, which means\n        # it should be very strongly unguessable.\n        self.secret = okay_random(64, session.config.get_master_key())\n\n        # Generate a new unguessable session cookie name on startup\n        while not self.session_cookie:\n            self.session_cookie = okay_random(12, self.secret)\n\n    def serve_forever(self, poll_interval=0.5, tick_func=None):\n        \"\"\"\n        Override SocketServer.serve_forever to allow other things to happen.\n        \"\"\"\n        if self.__is_shut_down is None:\n            return\n        self.__is_shut_down.clear()\n        try:\n            while not (self.__shutdown_request or mailpile.util.QUITTING):\n                # FIXME: Let's add a global FD to interrupt this, so we can\n                #        be more responsive AND lengthen our timeouts.\n                r, w, e = SocketServer._eintr_retry(\n                    select.select, [self], [], [], poll_interval)\n                if self in r:\n                    self._handle_request_noblock()\n                elif not (mailpile.util.QUITTING or tick_func is None):\n                    tick_func(self)\n        finally:\n            self.__shutdown_request = False\n            if self.__is_shut_down is not None:\n                self.__is_shut_down.set()\n\n    def shutdown(self, join=True):\n        self.__shutdown_request = True\n        if join and (self.__is_shut_down is not None):\n            self.__is_shut_down.wait()\n            self.__is_shut_down = None\n\n    def make_session_id(self, request):\n        \"\"\"Generate an unguessable and unauthenticated new session ID.\"\"\"\n        session_id = None\n        while session_id in self.sessions or session_id is None:\n            session_id = okay_random(32, self.secret,\n                                     '%s' % (request and request.headers))\n        return session_id\n\n    def finish_request(self, request, client_address):\n        try:\n            SimpleXMLRPCServer.finish_request(self, request, client_address)\n        except (socket.error, AttributeError):\n            # AttributeError may get thrown if the underlying socket has\n            # already been closed elsewhere and _sock = None.\n            pass\n        finally:\n            if mailpile.util.QUITTING:\n                self.shutdown()\n\n\nclass HttpWorker(threading.Thread):\n    def __init__(self, session, sspec):\n        threading.Thread.__init__(self)\n        self.httpd = HttpServer(session, sspec, HttpRequestHandler)\n        self.daemon = True\n        self.session = session\n\n\n    def idle_tick(self, httpd):\n        pass\n\n    def run(self):\n        while self.httpd is not None:\n            try:\n                self.httpd.serve_forever(\n                    poll_interval=1.0, tick_func=self.idle_tick)\n            except KeyboardInterrupt:\n                return\n            except socket.error:\n                pass\n            except:\n                time.sleep(1)\n                if self.httpd:\n                    traceback.print_exc()\n\n    def quit(self, join=False):\n        if self.httpd:\n            try:\n                self.httpd.server_close()\n            except (OSError, IOError):\n                pass\n            self.httpd.shutdown(join=join)\n        self.httpd = None\n"
  },
  {
    "path": "mailpile/i18n.py",
    "content": "import gettext\nimport os\nimport threading\nfrom gettext import translation, gettext, NullTranslations\nfrom jinja2 import Environment, BaseLoader, TemplateNotFound\n\n\nACTIVE_TRANSLATION = None\n\nRECENTLY_TRANSLATED_LOCK = threading.Lock()\nRECENTLY_TRANSLATED = []\n\nFORMAT_CHECKED = {}\n\n\n# This little doodad will on-the-fly check whether our translators\n# messed up our format strings in various ways, and suppress the\n# translation if it is obviously broken.\ndef _fmt_safe(translation, original):\n    global FORMAT_CHECKED\n    if translation in FORMAT_CHECKED:\n        return FORMAT_CHECKED[translation]\n    if '%' in original:\n        try:\n            safe_assert(len([c for c in translation if c == '%'])\n                        == len([c for c in original if c == '%']))\n            bogon = translation % 1\n            FORMAT_CHECKED[translation] = translation\n        except TypeError:\n            # This just means we gave the wrong argument or the wrong\n            # number of arguments - so the format string itself is OK.\n            FORMAT_CHECKED[translation] = translation\n        except:\n            FORMAT_CHECKED[translation] = original\n    else:\n        FORMAT_CHECKED[translation] = translation\n    return FORMAT_CHECKED[translation]\n\n\ndef gettext(string):\n    with RECENTLY_TRANSLATED_LOCK:\n        if isinstance(string, str):\n            global RECENTLY_TRANSLATED\n            RECENTLY_TRANSLATED = [t for t in RECENTLY_TRANSLATED[-100:]\n                                   if t != string] + [string]\n    if not ACTIVE_TRANSLATION:\n        return string\n\n    # FIXME: What if our input is utf-8?  Does gettext want us to\n    #        encode it first, or send the UTF-8 string?  Since we are\n    #        not encoding it, the decode below may fail. :(\n    translation = ACTIVE_TRANSLATION.org_gettext(string)\n    try:\n        translation = translation.decode('utf-8')\n    except UnicodeEncodeError:\n        pass\n\n    return _fmt_safe(translation, string)\n\n\ndef ngettext(string1, string2, n):\n    with RECENTLY_TRANSLATED_LOCK:\n        global RECENTLY_TRANSLATED\n        RECENTLY_TRANSLATED = [t for t in RECENTLY_TRANSLATED[-100:]\n                               if t not in (string1, string2)\n                               ] + [string1, string2]\n\n    default = string1 if (n == 1) else string2\n    if not ACTIVE_TRANSLATION:\n        return default\n\n    # FIXME: What if our input is utf-8?  Does gettext want us to\n    #        encode it first, or send the UTF-8 string?  Since we are\n    #        not encoding it, the decode below may fail. :(\n    translation = ACTIVE_TRANSLATION.org_ngettext(string1, string2, n)\n    try:\n        translation = translation.decode('utf-8')\n    except UnicodeEncodeError:\n        pass\n\n    return _fmt_safe(translation, default)\n\n\nclass i18n_disabler:\n    def __init__(self):\n        self.stack = []\n\n    def __enter__(self):\n        global ACTIVE_TRANSLATION\n        self.stack.append(ACTIVE_TRANSLATION)\n        ACTIVE_TRANSLATION = None\n\n    def __exit__(self, *args, **kwargs):\n        global ACTIVE_TRANSLATION\n        ACTIVE_TRANSLATION = self.stack.pop(-1)\n\n\ni18n_disabled = i18n_disabler()\n\n\ndef ActivateTranslation(session, config, language, localedir=None):\n    global ACTIVE_TRANSLATION, RECENTLY_TRANSLATED\n\n    if not language:\n        language = os.getenv('LANG', None)\n\n    if not localedir:\n        import mailpile.config.paths\n        localedir = mailpile.config.paths.DEFAULT_LOCALE_DIRECTORY()\n\n    trans = None\n    if (not language) or language[:5].lower() in ('en', 'en_us', 'c'):\n        trans = NullTranslations()\n    elif language:\n        try:\n            trans = translation(\"mailpile\", localedir,\n                                [language], codeset=\"utf-8\")\n        except IOError:\n            if session:\n                session.ui.debug('Failed to load language %s' % language)\n\n    if not trans:\n        trans = translation(\"mailpile\", localedir,\n                            codeset='utf-8', fallback=True)\n\n        if session:\n            session.ui.debug('Failed to configure i18n (%s). '\n                             'Using fallback.' % language)\n\n    if trans:\n        with RECENTLY_TRANSLATED_LOCK:\n            RECENTLY_TRANSLATED = []\n\n        ACTIVE_TRANSLATION = trans\n        trans.org_gettext = trans.gettext\n        trans.org_ngettext = trans.ngettext\n        trans.gettext = lambda t, g: gettext(g)\n        trans.ngettext = lambda t, s1, s2, n: ngettext(s1, s2, n)\n        trans.set_output_charset(\"utf-8\")\n\n        if hasattr(config, 'jinja_env'):\n            config.jinja_env.install_gettext_translations(trans,\n                                                          newstyle=True)\n\n        if session and language and not isinstance(trans, NullTranslations):\n            session.ui.debug(gettext('Loaded language %s') % language)\n\n    return trans\n\n\ndef ListTranslations(config, localedir=None):\n    if not localedir:\n        import mailpile.config.paths\n        localedir = mailpile.config.paths.DEFAULT_LOCALE_DIRECTORY()\n    languages = {\n        'C': 'English (Mailpile default)'\n    }\n    for lang in os.listdir(localedir):\n        langdir = os.path.join(localedir, lang, 'LC_MESSAGES')\n        if not os.path.exists(os.path.join(langdir, 'mailpile.mo')):\n            continue\n        try:\n            with open(os.path.join(langdir, 'mailpile.po')) as fd:\n                for line in fd.read(8192).splitlines():\n                    line = line.decode('utf-8')\n                    if line[1:].startswith('Language-Team: '):\n                        languages[lang] = ' '.join([word for word in\n                                                    line[1:-2].split()[1:-1]]\n                                                   ).replace('LANGUAGE', lang)\n        except (IOError, OSError, UnicodeDecodeError):\n            pass\n    return languages\n"
  },
  {
    "path": "mailpile/index/__init__.py",
    "content": ""
  },
  {
    "path": "mailpile/index/base.py",
    "content": "from __future__ import print_function\nimport copy\nimport json\nimport random\nimport rfc822\nimport time\nimport traceback\n\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.index.msginfo import MessageInfoConstants\nfrom mailpile.index.search import SearchResultSet\nfrom mailpile.mailutils import MBX_ID_LEN\nfrom mailpile.mailutils.addresses import AddressHeaderParser\nfrom mailpile.mailutils.safe import *\nfrom mailpile.util import *\n\n\nclass BaseIndex(MessageInfoConstants):\n\n    MAX_CACHE_ENTRIES = 250\n\n    CAN_SEARCH = 'can_search'  # Can search message contents\n    CAN_SORT = 'can_sort'      # Can sort search results\n    HAS_UNREAD = 'has_unread'  # Can filter messages by read/unread\n    HAS_ATTS = 'has_atts'      # Can filter messages by attachments or no\n    HAS_TAGS = 'has_tags'      # Can filter messages by tags and/or apply tags\n\n    # What is this Index capable of?  A list or set of the above.\n    CAPABILITIES = []\n\n    def __init__(self, config):\n        self.config = config\n        self.CACHE = {}\n        self.EMAILS = []\n        self.EMAIL_IDS = {}\n\n    ### Known e-mail addresses #############################################\n\n    # NOTE: This is all probably a misfeature and should probably go away.\n\n    def add_email(self, email, name=None, eid=None):\n        if eid is None:\n            eid = len(self.EMAILS)\n            self.EMAILS.append('')\n        self.EMAILS[eid] = '%s (%s)' % (email, name or email)\n        self.EMAIL_IDS[email.lower()] = eid\n        # FIXME: This needs to get written out...\n        return eid\n\n    def update_email(self, email, name=None, change_name=True):\n        eid = self.EMAIL_IDS.get(email.lower())\n        if (eid is not None) and not change_name:\n            el = self.EMAILS[eid].split(' ')\n            if len(el) == 2:\n                en = el[1][1:-1]\n                if '@' not in en:\n                    name = en\n        return self.add_email(email, name=name, eid=eid)\n\n    def compact_to_list(self, msg_to):\n        eids = []\n        for ai in msg_to:\n            email = ai.address\n            eid = self.EMAIL_IDS.get(email.lower())\n            if eid is None:\n                eid = self.add_email(email, name=ai.fn)\n            elif ai.fn and ai.fn != email:\n                self.update_email(email, name=ai.fn, change_name=False)\n            eids.append(eid)\n        return ','.join([b36(e) for e in set(eids)])\n\n    def expand_to_list(self, msg_info, field=None):\n        eids = msg_info[field if (field is not None) else self.MSG_TO]\n        eids = [e for e in eids.strip().split(',') if e]\n        return [self.EMAILS[int(e, 36)] for e in eids]\n\n\n    ### Tags & filters #####################################################\n\n    def remove_tag(self, session, tid, msg_idxs=None):\n        pass\n\n    def add_tag(self, session, tid, msg_idxs=None):\n        pass\n\n    def apply_filters(self, session, fid, msg_idxs=None):\n        pass\n\n\n    ### Searching & sorting ################################################\n\n    def search(self, session, terms, context=None):\n        return SearchResultSet(self, terms, [], [])\n\n    def sort_results(self, session, results, sort_order):\n        pass\n\n    def get_conversation(self, msg_idx=None):\n        return []\n\n\n    ### Loading data: subclasses override these ############################\n\n    def get_msg_at_idx_pos_uncached(self, msg_idx):\n        raise IndexError('Unimplemented')\n\n    def open_mailbox_by_ptr(self, msg_ptr):\n        return self.config.open_mailbox(None, msg_ptr[:MBX_ID_LEN])\n\n\n    ### Loading data: higher level methods #################################\n\n    def unique_mbox_ids(self, msg_info):\n        return set([\n            p[:MBX_ID_LEN] for p in msg_info[self.MSG_PTRS].split(',') if p])\n\n    def enumerate_ptrs_mboxes_fds(self, msg_info):\n        for msg_ptr in self._sorted_msg_ptrs(msg_info):\n            mbox = fd = None\n            try:\n                mbox = self.open_mailbox_by_ptr(msg_ptr)\n                fd = mbox.get_file_by_ptr(msg_ptr)\n            except (IOError, OSError, KeyError, ValueError, IndexError):\n                if 'sources' in self.config.sys.debug:\n                    traceback.print_exc()\n                    print('WARNING: %s not found' % msg_ptr)\n            yield (msg_ptr, mbox, fd)\n\n    ### ... ################################################################\n\n    def _sorted_msg_ptrs(self, msg_info):\n        ptrs = (p.strip() for p in msg_info[self.MSG_PTRS].split(','))\n        # FIXME: Prefer local data? Prefer some mailbox types? Hmm.\n        #        Doing this well would speed things up and ensure the\n        #        `message/delete --keep` deduplication works nicely.\n        return sorted([p for p in ptrs if p])\n\n    def _encode_msg_id(self, msg_id):\n        \"\"\"Normalize and hash a message ID for the metadata index\"\"\"\n        if '<' in msg_id:\n            new_msg_id = '<%s>' % msg_id.split('<')[1].split('>')[0]\n            if len(new_msg_id) > 2:\n                msg_id = new_msg_id\n        return b64c(sha1b64(msg_id.strip()))\n\n    def get_msg_id(self, msg, msg_ptr):\n        return self._encode_msg_id(safe_get_msg_id(msg) or msg_ptr)\n\n    def _message_to_msg_info(self, msg_idx_pos, msg_ptr, msg):\n        msg_mid = b36(msg_idx_pos)\n        msg_to = AddressHeaderParser(msg.get('to'))\n        msg_cc = AddressHeaderParser(msg.get('cc'))\n        msg_cc += AddressHeaderParser(msg.get('bcc'))\n        return [\n            msg_mid,\n            msg_ptr,                          # Message PTR\n            self.get_msg_id(msg, msg_ptr),    # Message ID\n            b36(safe_message_ts(msg)),        # Message timestamp\n            safe_decode_hdr(msg, 'from'),     # Message from\n            self.compact_to_list(msg_to),     # Compacted to-list\n            self.compact_to_list(msg_cc),     # Compacted cc/bcc-list\n            b36(len(msg) // 1024),            # Message size\n            safe_decode_hdr(msg, 'subject'),  # Subject\n            self.MSG_BODY_LAZY,               # Body snippets come later\n            '',                               # Tags\n            '',                               # Replies\n            msg_mid]                          # Thread\n\n    def get_msg_at_idx_pos(self, msg_idx):\n        try:\n            crv = self.CACHE.get(msg_idx, {})\n            if 'msg_info' in crv:\n                return crv['msg_info']\n\n            if len(self.CACHE) > self.MAX_CACHE_ENTRIES:\n                try:\n                    for k in random.sample(\n                            self.CACHE.keys(), self.MAX_CACHE_ENTRIES/20):\n                        del self.CACHE[k]\n                except KeyError:\n                    pass\n            rv = self.get_msg_at_idx_pos_uncached(msg_idx)\n            crv['msg_info'] = rv\n            self.CACHE[msg_idx] = crv\n            return rv\n\n        except (IndexError, ValueError):\n            return copy.copy(self.BOGUS_METADATA)\n\n    def set_msg_at_idx_pos(self, msg_idx, msg_info):\n        pass\n\n        return []  # FIXME\n\n    @classmethod\n    def get_body(self, msg_info):\n        msg_body = msg_info[self.MSG_BODY]\n        if msg_body.startswith('{'):\n            if msg_body == self.MSG_BODY_LAZY:\n                return {'snippet': _('(unprocessed)'), 'lazy': True}\n            elif msg_body == self.MSG_BODY_GHOST:\n                return {'snippet': _('(ghost)'), 'ghost': True}\n            elif msg_body == self.MSG_BODY_DELETED:\n                return {'snippet': _('(deleted)'), 'deleted': True}\n            try:\n                return json.loads(msg_body)\n            except ValueError:\n                pass\n        return {\n            'snippet': msg_body\n        }\n\n    @classmethod\n    def truncate_body_snippet(self, body, max_chars):\n        if 'snippet' in body:\n            delta = len(self.encode_body(body)) - max_chars\n            if delta > 0:\n                body['snippet'] = body['snippet'][:-delta].rsplit(' ', 1)[0]\n\n    @classmethod\n    def encode_body(self, d, **kwargs):\n        for k, v in kwargs:\n            if v is None:\n                if k in d:\n                    del d[k]\n            else:\n                d[k] = v\n        if len(d) == 1 and 'snippet' in d:\n            snippet = d['snippet']\n            if snippet[:3] in self.MSG_BODY_MAGIC or snippet[:1] != '{':\n                return d['snippet']\n        return json.dumps(d, indent=None, separators=(',', ':'))\n\n    @classmethod\n    def set_body(self, msg_info, **kwargs):\n        d = self.get_body(msg_info)\n        msg_info[self.MSG_BODY] = self.encode_body(d, **kwargs)\n\n\nif __name__ == '__main__':\n    import doctest\n    import sys\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                              extraglobs={})\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/index/mailboxes.py",
    "content": "from __future__ import print_function\nimport email.parser\nimport json\nimport traceback\n\nfrom mailpile.util import *\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.index.base import BaseIndex\nfrom mailpile.index.msginfo import MessageInfoConstants\nfrom mailpile.index.search import SearchResultSet\nfrom mailpile.mailutils import MBX_ID_LEN\n\n\nclass MailboxIndex(BaseIndex):\n\n    FAKE_MBX_ID = ('Z' * MBX_ID_LEN)\n\n    def __init__(self, config, mailbox, mbx_mid=None):\n        BaseIndex.__init__(self, config)\n        self.mbx_mid = mbx_mid or self.FAKE_MBX_ID\n        self.mailbox = mailbox\n        self.ptrset = set([])\n        self.idxmap = []\n\n    def get_msg_at_idx_pos_uncached(self, msg_idx_pos):\n        msg_ptr = self.idxmap[msg_idx_pos]\n        msg_raw = self.mailbox.get_file_by_ptr(msg_ptr)\n        message = email.parser.Parser().parse(msg_raw, True)\n        return self._message_to_msg_info(msg_idx_pos, msg_ptr, message)\n\n    def open_mailbox_by_ptr(self, msg_ptr):\n        if msg_ptr[:MBX_ID_LEN] == self.mbx_mid:\n            return self.mailbox\n        else:\n            return BaseIndex.open_mailbox_by_ptr(self, msg_ptr)\n\n    def _update_keymap(self):\n        try:\n            mailbox_keys = self.mailbox.keys()\n            mailbox_ptrs = [self.mailbox.get_msg_ptr(self.mbx_mid, i)\n                            for i in mailbox_keys]\n            for ptr in mailbox_ptrs:\n                if ptr not in self.ptrset:\n                    self.idxmap.append(ptr)\n            self.ptrset = set(mailbox_ptrs)\n        except:\n            traceback.print_exc()\n\n    def search(self, session, terms, context=None):\n        if not terms or terms == ['all:mail']:\n            self._update_keymap()\n            result = [i for i, ptr in enumerate(self.idxmap)\n                      if ptr in self.ptrset]\n            result.reverse()\n        else:\n            print('FIXME! %s: search %s' % (self, terms))\n            result = []\n        return SearchResultSet(self, terms, result, [])\n\n\nif __name__ == '__main__':\n    import doctest\n    import sys\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                              extraglobs={})\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/index/msginfo.py",
    "content": "from __future__ import print_function\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\n\n\nclass MessageInfoConstants(object):\n    MSG_MID = 0\n    MSG_PTRS = 1\n    MSG_ID = 2\n    MSG_DATE = 3\n    MSG_FROM = 4\n    MSG_TO = 5\n    MSG_CC = 6\n    MSG_KB = 7\n    MSG_SUBJECT = 8\n    MSG_BODY = 9\n    MSG_TAGS = 10\n    MSG_REPLIES = 11\n    MSG_THREAD_MID = 12\n\n    MSG_FIELDS_V1 = 11\n    MSG_FIELDS_V2 = 13\n\n    MSG_BODY_LAZY = '{L}'\n    MSG_BODY_GHOST = '{G}'\n    MSG_BODY_DELETED = '{D}'\n    MSG_BODY_UNSCANNED = (MSG_BODY_LAZY, MSG_BODY_GHOST)\n    MSG_BODY_MAGIC = (MSG_BODY_LAZY, MSG_BODY_GHOST, MSG_BODY_DELETED)\n\n    BOGUS_METADATA = [None, '', None, '0', '(no sender)', '', '', '0',\n                      '(not in index)', '', '', '', '-1']\n\n\nif __name__ == '__main__':\n    import doctest\n    import sys\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                              extraglobs={})\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/index/search.py",
    "content": "from __future__ import print_function\nimport time\n\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\n\n\nclass SearchResultSet:\n    \"\"\"\n    Search results!\n    \"\"\"\n    def __init__(self, idx, terms, results, exclude):\n        self.terms = set(terms)\n        self._index = idx\n        self.set_results(results, exclude)\n\n    def set_results(self, results, exclude):\n        self._results = {\n            'raw': set(results),\n            'excluded': set(exclude) & set(results)\n        }\n        return self\n\n    def __len__(self):\n        return len(self._results.get('raw', []))\n\n    def as_set(self, order='raw'):\n        return self._results[order] - self._results['excluded']\n\n    def excluded(self):\n        return self._results['excluded']\n\n\nSEARCH_RESULT_CACHE = {}\n\n\nclass CachedSearchResultSet(SearchResultSet):\n    \"\"\"\n    Cached search result.\n    \"\"\"\n    def __init__(self, idx, terms):\n        global SEARCH_RESULT_CACHE\n        self.terms = set(terms)\n        self._index = idx\n        self._results = SEARCH_RESULT_CACHE.get(self._skey(), {})\n        self._results['_last_used'] = time.time()\n\n    def _skey(self):\n        return ' '.join(self.terms)\n\n    def set_results(self, *args):\n        global SEARCH_RESULT_CACHE\n        SearchResultSet.set_results(self, *args)\n        SEARCH_RESULT_CACHE[self._skey()] = self._results\n        self._results['_last_used'] = time.time()\n        return self\n\n    @classmethod\n    def DropCaches(cls, msg_idxs=None, tags=None):\n        # FIXME: Make this more granular\n        global SEARCH_RESULT_CACHE\n        SEARCH_RESULT_CACHE = {}\n\n\nif __name__ == '__main__':\n    import doctest\n    import sys\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                              extraglobs={})\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/mail_source/__init__.py",
    "content": "import datetime\nimport os\nimport random\nimport re\nimport thread\nimport threading\nimport traceback\nimport time\n\nimport mailpile.util\nimport mailpile.vfs\nfrom mailpile.eventlog import Event\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.mailboxes import *\nfrom mailpile.mailutils import FormatMbxId\nfrom mailpile.util import *\nfrom mailpile.vfs import vfs, FilePath, MailpileVfsBase\n\n\n__all__ = ['local', 'imap', 'pop3']\n\n\nclass BaseMailSource(threading.Thread):\n    \"\"\"\n    MailSources take care of managing a group of mailboxes, synchronizing\n    the source with Mailpile's local metadata and/or caches.\n    \"\"\"\n    DEFAULT_JITTER = 15         # Fudge factor to tame thundering herds\n    SAVE_STATE_INTERVAL = 3600  # How frequently we pickle our state\n    INTERNAL_ERROR_SLEEP = 900  # Pause time on error, in seconds\n    RESCAN_BATCH_SIZE = 200     # Index at most this many new e-mails at once\n    MAX_PATHS = 2000            # Limit how many directories we scan at once\n\n    # This is a helper for the events.\n    __classname__ = 'mailpile.mail_source.BaseMailSource'\n\n\n    class MailSourceVfs(MailpileVfsBase):\n        \"\"\"Generic VFS layer for this mail source.\"\"\"\n        def __init__(self, config, source, *args, **kwargs):\n            MailpileVfsBase.__init__(self, *args, **kwargs)\n            self.config = config\n            self.source = source\n            self.root = FilePath('/src:%s' % self.source.my_config._key)\n\n        def _get_mbox_id(self, path):\n            return path[len(self.root.raw_fp)+1:]\n\n        def Handles(self, path):\n            path = FilePath(path)\n            return (self.root == path or\n                    path.raw_fp.startswith(self.root.raw_fp))\n\n        def glob_(self, *args, **kwargs):\n            return self.listdir_(*args, **kwargs)\n\n        def listdir_(self, where, **kwargs):\n            return [m for m in self.source.my_config.mailbox.keys()]\n\n        def open_(self, fp, *args, **kwargs):\n            raise IOError('Cannot open Mail Source entries (yet)')\n\n        def abspath_(self, path):\n            if not path.startswith(self.root.raw_fp):\n                path = self.root.join(path).raw_fp\n            if path == self.root:\n                return path\n            try:\n                mbox_id = self._get_mbox_id(path)\n                path = self.config.sys.mailbox[mbox_id]\n                if path.startswith('src:'):\n                    return '/%s' % path\n                return path\n            except (ValueError, KeyError, IndexError):\n                raise OSError('Not found: %s' % path)\n\n        def isdir_(self, fp):\n            return (self.root == fp)\n\n        def ismailsource_(self, fp):\n            return (self.root == fp)\n\n        def mailbox_type_(self, fp, config):\n            return False if (fp == self.root) else 'source'  # Fixme\n\n        def getsize_(self, path):\n            return None\n\n        def display_name_(self, path, config):\n            if (self.root == path):\n                return (self.source.my_config.name or\n                        self.source.my_config._key)\n            try:\n                mbox_id = self._get_mbox_id(path)\n                return self.source.my_config.mailbox[mbox_id].name\n            except (ValueError, KeyError, IndexError):\n                raise OSError('Not found: %s' % path)\n\n        def exists_(self, fp):\n            return ((self.root == fp) or\n                    (fp[len(self.root)+1:] in self.source.my_config.mailbox))\n\n\n    def __init__(self, session, my_config):\n        threading.Thread.__init__(self)\n        self.daemon = mailpile.util.TESTING\n        self._lock = MSrcRLock()\n        self.my_config = my_config\n        self.name = my_config.name\n        self.session = session\n        self.alive = None\n        self.event = None\n        self.jitter = self.DEFAULT_JITTER\n        self._state = 'Idle'\n        self._sleeping = None\n        self._interrupt = None\n        self._rescanning = False\n        self._rescan_waiters = []\n        self._rescan_forced = False\n        self._loop_count = 0\n        self._last_rescan_count = (0, 0)\n        self._last_rescan_completed = False\n        self._last_rescan_failed = False\n        self._last_saved = time.time()  # Saving right away would be silly\n        ms_vfs = self.MailSourceVfs(session.config, self)\n        mailpile.vfs.register_handler(5000, ms_vfs)\n\n    def __str__(self):\n        rv = ': '.join([threading.Thread.__str__(self), self._state])\n        if self._sleeping > 0:\n            rv += '(%s)' % self._sleeping\n        return rv\n\n    def _pfn(self):\n        return 'mail-source.%s' % self.my_config._key\n\n    def _load_state(self):\n        with self._lock:\n            config, my_config = self.session.config, self.my_config\n            events = list(config.event_log.incomplete(source=self,\n                                                      data_id=my_config._key))\n            if events:\n                self.event = events[0]\n                if my_config.enabled:\n                    self.event.message = _('Starting up')\n                else:\n                    self.event.message = _('Disabled')\n            else:\n                self.event = config.event_log.log(\n                    source=self,\n                    flags=Event.RUNNING,\n                    message=_('Starting up'),\n                    data={'id': my_config._key})\n            self.event.data['name'] = my_config.name or _('Mail Source')\n            if 'counters' not in self.event.data:\n                self.event.data['counters'] = {}\n            for c in ('copied_messages',\n                      'indexed_messages',\n                      'unknown_policies'):\n                if c not in self.event.data['counters']:\n                    self.event.data['counters'][c] = 0\n\n    def _save_state(self):\n        self.session.config.event_log.log_event(self.event)\n\n    def _save_config(self):\n        self.session.config.save_worker.add_unique_task(\n            self.session, 'Save config', self.session.config.save)\n\n    def _log_status(self, message, clear_errors=False):\n        # If the user renames our parent_tag, we assume the name change too.\n        self.update_name_to_match_tag()\n        if clear_errors:\n            err = self.event.data.get('connection', {}).get('error', [False])\n            if err[0]:\n                err[:] = [False, _('Nothing is wrong')]\n        self.event.message = message\n        self.session.config.event_log.log_event(self.event)\n        self.session.ui.mark(message)\n        if 'sources' in self.session.config.sys.debug:\n            self.session.ui.debug('%s: %s' % (self, message))\n\n    def open(self):\n        \"\"\"Open mailboxes or connect to the remote mail source.\"\"\"\n        raise NotImplemented('Please override open in %s' % self)\n\n    def close(self):\n        \"\"\"Close mailboxes or disconnect from the remote mail source.\"\"\"\n        raise NotImplemented('Please override open in %s' % self)\n\n    def _has_mailbox_changed(self, mbx, state):\n        \"\"\"For the default sync_mail routine, report if mailbox changed.\"\"\"\n        raise NotImplemented('Please override _has_mailbox_changed in %s'\n                             % self)\n\n    def _mark_mailbox_rescanned(self, mbx, state):\n        \"\"\"For the default sync_mail routine, note mailbox was rescanned.\"\"\"\n        raise NotImplemented('Please override _mark_mailbox_rescanned in %s'\n                             % self)\n\n    def _path(self, mbx):\n        if mbx.path.startswith('@'):\n            return self.session.config.sys.mailbox[mbx.path[1:]]\n        else:\n            return mbx.path\n\n    def _check_interrupt(self, log=True, clear=True):\n        if not self._interrupt:\n            full_path = self.session.config.need_more_disk_space()\n            if full_path is not None:\n                self._interrupt = _('Insufficient free space in %s'\n                                    ) % full_path\n        if (mailpile.util.QUITTING or\n                self._interrupt or\n                not self.my_config.enabled):\n            if log:\n                self._log_status(_('Interrupted: %s')\n                                     % (self._interrupt or _('Shutting down')),\n                                 clear_errors=(not self._interrupt))\n            if clear:\n                self._interrupt = None\n            return True\n        else:\n            return False\n\n    def _mailbox_sort_key(self, m):\n        return md5_hex(str(self._loop_count), m.name)\n\n    def _sorted_mailboxes(self):\n        mailboxes = self.my_config.mailbox.values()\n        mailboxes.sort(key=lambda m: (\n            'inbox' in m.name.lower() and 1 or 2,\n            'sent' in m.name.lower() and 1 or 2,\n            'spam' in m.name.lower() and 1 or 2,  # For training filters!\n            '[Gmail]' in m.name and 2 or 1,       # This goes last...\n            self._mailbox_sort_key(m)))\n        return mailboxes\n\n    def _policy(self, mbx_cfg):\n        policy = mbx_cfg.policy\n        if policy == 'inherit':\n            return self.my_config.discovery.policy\n        return policy\n\n    def update_name_to_match_tag(self):\n        parent_tag_id = self.my_config.discovery.parent_tag\n        if parent_tag_id and parent_tag_id != '!CREATE':\n            tag = self.session.config.get_tag(parent_tag_id)\n            if tag and self.name != tag.name:\n                self.name = self.my_config.name = tag.name\n                if self.event:\n                    self.event.data['name'] = self.name\n\n    def sync_mail(self):\n        \"\"\"Iterates through all the mailboxes and scans if necessary.\"\"\"\n        config = self.session.config\n        rescanned = messages = errors = 0\n        self._last_rescan_count = (0, 0)\n        self._last_rescan_completed = False\n        self._last_rescan_failed = False\n        self._interrupt = None\n        batch = min(self._loop_count * 20, self.RESCAN_BATCH_SIZE)\n        errors = rescanned = 0\n        all_completed = True\n        ostate = self._state\n\n        if not self._check_interrupt(clear=False):\n            self._state = 'Waiting... (disco)'\n            discovered = self.discover_mailboxes()\n        else:\n            discovered = 0\n\n        plan = self._sorted_mailboxes()\n        self.event.data['plan'] = [[m._key, _('Pending'), m.name] for m in plan]\n        event_plan = dict((mp[0], mp) for mp in self.event.data['plan'])\n        if plan and random.randint(0, 20) == 1:\n            random_plan = [m._key for m in random.sample(plan, 1)]\n        else:\n            random_plan = []\n\n        for mbx_cfg in plan:\n            if not self._rescan_forced:\n                play_nice_with_threads(weak=True)\n\n            if self._check_interrupt(clear=False):\n                all_completed = False\n                break\n            try:\n                with self._lock:\n                    mbx_key = FormatMbxId(mbx_cfg._key)\n                    path = self._path(mbx_cfg)\n                    policy = self._policy(mbx_cfg)\n                    if (path in ('/dev/null', '', None)\n                            or policy in ('ignore', 'unknown')):\n                        event_plan[mbx_cfg._key][1] = _('Skipped')\n                        continue\n\n                # Generally speaking, we only rescan if a mailbox looks like\n                # it has changed. However, every once in a while (see logic\n                # around random_mailboxes above) we check anyway just in case\n                # looks are deceiving.\n                state = {}\n                if batch < 1:\n                    event_plan[mbx_cfg._key][1] = _('Postponed')\n\n                elif (self._has_mailbox_changed(mbx_cfg, state) or\n                        self._rescan_forced or\n                        mbx_cfg.local == '!CREATE' or\n                        mbx_cfg._key in random_plan):\n                    event_plan[mbx_cfg._key][1] = _('Working ...')\n\n                    this_batch = max(5, int(0.7 * batch))\n                    self._state = 'Waiting... (rescan)'\n                    if self._check_interrupt(clear=False):\n                        all_completed = False\n                        break\n                    count = self.rescan_mailbox(mbx_key, mbx_cfg, path,\n                                                stop_after=this_batch)\n\n                    if count >= 0:\n                        self.event.data['counters'\n                                        ]['indexed_messages'] += count\n                        batch -= count\n                        this_batch -= count\n                        messages += count\n                        complete = ((count == 0 or this_batch > 0) and\n                                    not self._interrupt and\n                                    not mailpile.util.QUITTING)\n                        if complete:\n                            rescanned += 1\n\n                        # If there was a copy, check if it completed\n                        cstate = self.event.data.get('copying') or {}\n                        if not cstate.get('complete', True):\n                            complete = False\n\n                        # If there was a rescan, check if it completed\n                        rstate = self.event.data.get('rescan') or {}\n                        if not rstate.get('complete', True):\n                            complete = False\n\n                        # OK, everything looks complete, mark it!\n                        if complete:\n                            event_plan[mbx_cfg._key][1] = _('Completed')\n                            self._mark_mailbox_rescanned(mbx_cfg, state)\n                        else:\n                            event_plan[mbx_cfg._key][1] = _('Indexed %d'\n                                                            ) % count\n                            all_completed = False\n                            if count == 0 and ('sources' in config.sys.debug):\n                                time.sleep(60)\n                    else:\n                        event_plan[mbx_cfg._key][1] = _('Failed')\n                        self._last_rescan_failed = True\n                        all_completed = False\n                        errors += 1\n\n                else:\n                    event_plan[mbx_cfg._key][1] = _('Unchanged')\n\n            except (NoSuchMailboxError, IOError, OSError) as e:\n                event_plan[mbx_cfg._key][1] = '%s: %s' % (_('Error'), e)\n                self._last_rescan_failed = True\n                errors += 1\n            except Exception as e:\n                event_plan[mbx_cfg._key][1] = '%s: %s' % (\n                    _('Internal error'), e)\n                self._rescan_forced = False\n                self._last_rescan_failed = True\n                self._log_status(_('Internal error'))\n                raise\n\n        self._last_rescan_completed = all_completed\n\n        self._state = 'Done'\n        status = []\n        if discovered > 0:\n            status.append(_('Discovered %d mailboxes') % discovered)\n            self._last_rescan_completed = False\n        if rescanned > 0:\n            status.append(_('Processed %d mailboxes') % rescanned)\n        if errors:\n            status.append(_('Failed to process %d') % errors)\n        if not status:\n            status.append(_('No new mail at %s'\n                            ) % datetime.datetime.today().strftime('%H:%M'))\n\n        self._rescan_forced = False\n        self._log_status(', '.join(status))\n        self._last_rescan_count = (messages, rescanned)\n        self._state = ostate\n        return rescanned\n\n    def _jitter(self, seconds):\n        return seconds + random.randint(0, self.jitter)\n\n    def _sleeping_is_ok(self, slept):\n        return True\n\n    def _sleep(self, seconds):\n        enabled = self.my_config.enabled\n        if self._rescan_forced:\n            seconds = 0\n        else:\n            play_nice_with_threads()\n        if 'sources' in self.session.config.sys.debug:\n            self.session.ui.debug('Sleeping up to %d seconds...' % seconds)\n        if self._sleeping is None:\n            self._sleeping = seconds\n            while (self.alive and\n                    (self._sleeping > 0) and\n                    self._sleeping_is_ok(seconds - self._sleeping) and\n                    (enabled == self.my_config.enabled) and\n                    not mailpile.util.QUITTING):\n                time.sleep(max(0, min(1, self._sleeping)))\n                self._sleeping -= 1\n        self._sleeping = None\n        return (self.alive and not mailpile.util.QUITTING)\n\n    def _existing_mailboxes(self):\n        return set(self.session.config.sys.mailbox +\n                   [mbx_cfg.local\n                    for mbx_cfg in self.my_config.mailbox.values()\n                    if mbx_cfg.local])\n\n    def _update_unknown_state(self):\n        have_unknown = 0\n        for mailbox in self.my_config.mailbox.values():\n            if mailbox.policy == 'unknown':\n                have_unknown += 1\n        self.event.data['counters']['unknown_policies'] = have_unknown\n        self.event.data['have_unknown'] = (have_unknown > 0)\n\n    def reset_event_discovery_state(self):\n        for k in ('discovery_error', 'discovery_limit', 'discovery_state'):\n            if k in self.event.data:\n                del self.event.data[k]\n\n    def set_event_discovery_state(self, state=None, error=None, status=None):\n        self.event.data['discovery_limit'] = (\n            self.my_config.discovery.max_mailboxes)\n        if state is not None:\n            self.event.data['discovery_state'] = state\n        if error is not None:\n            self.event.data['discovery_error'] = error\n        if status is not None:\n            self._log_status(status)\n\n    def on_event_discovery_starting(self):\n        ostate, self._state = self._state, 'Discovery'\n        self.reset_event_discovery_state()\n        self.set_event_discovery_state(\n            'scanning', status=_('Checking for new mailboxes'))\n        return ostate\n\n    def on_event_discovery_toomany(self):\n        self.set_event_discovery_state(\n            error='toomany',\n            status=_('Too many mailboxes found! Raise limits to continue.'))\n\n        old_limit = self.my_config.discovery.max_mailboxes\n        for i in range(0, 15):\n            if old_limit == self.my_config.discovery.max_mailboxes:\n                self._sleep(2)\n            else:\n                return True\n        return False\n\n    def on_event_discovery_done(self, ostate):\n        self.set_event_discovery_state('done')\n        self._state = ostate\n\n    def discover_mailboxes(self, paths=None):\n        config = self.session.config\n        ostate = self.on_event_discovery_starting()\n        try:\n            existing = self._existing_mailboxes()\n            max_mailboxes = self.my_config.discovery.max_mailboxes\n            max_mailboxes -= len(self.my_config.mailbox)\n            adding = []\n            paths = [(p.encode('utf-8') if isinstance(p, unicode) else p)\n                     for p in (paths or self.my_config.discovery.paths)]\n            paths.sort()\n            while paths:\n                raw_fn = paths.pop(0)\n                if 'sources' in config.sys.debug:\n                    self.session.ui.mark(_('Checking for new mailboxes in %s'\n                                           ) % raw_fn.decode('utf-8'))\n\n                fn = os.path.normpath(os.path.expanduser(raw_fn))\n                fn = os.path.abspath(fn)\n                if not os.path.exists(fn):\n                    continue\n\n                is_mailbox = False\n                if (raw_fn not in existing and\n                        fn not in existing and\n                        fn not in adding):\n                    if self.is_mailbox(fn):\n                        adding.append(fn)\n                        is_mailbox = True\n                    if len(adding) and (len(adding) > max_mailboxes):\n                        break\n\n                if os.path.isdir(fn):\n                    try:\n                        max_paths = self.MAX_PATHS - len(paths)\n                        subdirs = [f for f in os.listdir(fn)\n                                   if f not in ('.', '..')]\n\n                        if len(subdirs) > (max_paths/2):\n                            # If we are hitting our limits, randomize.\n                            random.shuffle(subdirs)\n                        else:\n                            # Otherwise, do things in an orderly fashion.\n                            subdirs.sort()\n\n                        for f in subdirs[:max_paths/2]:\n                            nfn = os.path.join(fn, f)\n                            if is_mailbox and f in ('cur', 'new', 'tmp'):\n                                pass  # Skip Maildir special directories\n                            elif (len(paths) <= self.MAX_PATHS and\n                                    os.path.isdir(nfn)):\n                                paths.append(nfn)\n                            elif self.is_mailbox(nfn):\n                                paths.append(nfn)\n                            play_nice_with_threads(weak=True)\n                    except OSError:\n                        pass\n\n                    # This may have been a bit of work, take a break.\n                    play_nice_with_threads()\n\n                if len(adding) and (len(adding) > max_mailboxes):\n                    break\n\n            if len(adding) and (len(adding) > max_mailboxes):\n                if self.on_event_discovery_toomany():\n                    return self.discover_mailboxes(paths=paths)\n                adding = adding[:max(0, max_mailboxes)]\n\n            if adding:\n                self.set_event_discovery_state('adding')\n                play_nice_with_threads()\n                new = {}\n                for path in adding:\n                    new[config.sys.mailbox.append(path)] = path\n                for mailbox_idx in new.keys():\n                    mbx_cfg = self.take_over_mailbox(mailbox_idx, save=False)\n                    if self._policy(mbx_cfg) != 'unknown':\n                        del new[mailbox_idx]\n\n                self._save_config()\n\n            return len(adding)\n        except:\n            if config.sys.debug:\n                self.session.ui.debug('%s' % traceback.format_exc())\n            raise\n        finally:\n            self.on_event_discovery_done(ostate)\n\n    def _default_policy(self, mbx_cfg):\n        return 'inherit'\n\n    def take_over_mailbox(self, mailbox_idx,\n                          policy=None, create_local=None, save=True,\n                          guess_tags=None, apply_tags=None):\n        config = self.session.config\n        disco_cfg = self.my_config.discovery  # Stayin' alive! Stayin' alive!\n        with self._lock:\n            mailbox_idx = FormatMbxId(mailbox_idx)\n            self.my_config.mailbox[mailbox_idx] = {\n                'path': '@%s' % mailbox_idx,\n                'policy': policy or 'inherit',\n                'process_new': disco_cfg.process_new,\n                'local': '!CREATE' if create_local else '',\n            }\n            mbx_cfg = self.my_config.mailbox[mailbox_idx]\n            mbx_cfg.apply_tags.extend(disco_cfg.apply_tags)\n            if apply_tags:\n                mbx_cfg.apply_tags.extend(t for t in apply_tags if t)\n        mbx_cfg.policy = policy or self._default_policy(mbx_cfg)\n        mbx_cfg.name = self._mailbox_name(self._path(mbx_cfg))\n        if guess_tags is None:\n            guess_tags = disco_cfg.guess_tags\n        if guess_tags:\n            self._guess_tags(mbx_cfg)\n        self._create_primary_tag(mbx_cfg, save=False)\n        self._create_local_mailbox(mbx_cfg, save=False)\n        if save:\n            self._save_config()\n        return mbx_cfg\n\n    def _guess_tags(self, mbx_cfg):\n        if not mbx_cfg.name:\n            return\n        mbx_cfg.apply_tags = sorted(list(\n            set(mbx_cfg.apply_tags) |\n            self.session.config.guess_tags(mbx_cfg.name)))\n\n    def _strip_file_extension(self, path):\n        return path.rsplit('.', 1)[0]\n\n    def _mailbox_path_split(self, path):\n        return ('/' in path) and path.split('/') or path.split('\\\\')\n\n    def _mailbox_name(self, path):\n        return self._mailbox_path_split(path)[-1]\n\n    def _create_local_mailbox(self, mbx_cfg, save=True):\n        config = self.session.config\n        disco_cfg = self.my_config.discovery\n\n        if mbx_cfg.local and mbx_cfg.local != '!CREATE':\n            if not vfs.exists(mbx_cfg.local):\n                config.flush_mbox_cache(self.session)\n                path, wervd = config.create_local_mailstore(self.session,\n                                                            name=mbx_cfg.local)\n                wervd.is_local = mbx_cfg._key\n                mbx_cfg.local = path\n                if save:\n                    self._save_config()\n\n        elif mbx_cfg.local == '!CREATE' or disco_cfg.local_copy:\n            config.flush_mbox_cache(self.session)\n            path, wervd = config.create_local_mailstore(self.session)\n            wervd.is_local = mbx_cfg._key\n            mbx_cfg.local = path\n            if save:\n                self._save_config()\n\n        return mbx_cfg\n\n    def _create_parent_tag(self, save=True):\n        disco_cfg = self.my_config.discovery\n        if disco_cfg.parent_tag:\n            if disco_cfg.parent_tag == '!CREATE':\n                name = (self.my_config.name or\n                        (self.my_config.username or '').split('@')[-1] or\n                        (disco_cfg.paths and\n                         os.path.basename(disco_cfg.paths[0])) or\n                        self.my_config._key)\n                if len(name) < 4:\n                    name = _('Mail: %s') % name\n                disco_cfg.parent_tag = name\n            if disco_cfg.parent_tag not in self.session.config.tags.keys():\n                from mailpile.plugins.tags import Slugify\n                disco_cfg.parent_tag = self._create_tag(\n                    disco_cfg.parent_tag,\n                    use_existing=False,\n                    icon='icon-mailsource',\n                    slug=Slugify(\n                        self.my_config.name, tags=self.session.config.tags),\n                    unique=False)\n                if save:\n                    self._save_config()\n            return disco_cfg.parent_tag\n        else:\n            return None\n\n    def _create_primary_tag(self, mbx_cfg, save=True):\n        config = self.session.config\n        if mbx_cfg.primary_tag and (mbx_cfg.primary_tag in config.tags):\n            return\n\n        # Stayin' alive! Stayin' alive!\n        disco_cfg = self.my_config.discovery\n        if not disco_cfg.create_tag:\n            return\n\n        # Make sure we have a parent tag, as that maybe useful when creating\n        # tag names or the primary tag itself.\n        parent = self._create_parent_tag(save=False)\n\n        # We configure the primary_tag with a name, if it doesn't have\n        # one already.\n        if not mbx_cfg.primary_tag:\n            mbx_cfg.primary_tag = self._create_tag_name(self._path(mbx_cfg))\n\n        # If we have a policy for this mailbox, we really go and create\n        # tags. The gap here allows the user to edit the primary_tag\n        # proposal before changing the policy from 'unknown'.\n        if self._policy(mbx_cfg) != 'unknown':\n            try:\n                mbx_cfg.primary_tag = self._create_tag(\n                    mbx_cfg.primary_tag,\n                    use_existing=False,\n                    visible=disco_cfg.visible_tags,\n                    unique=False,\n                    parent=parent)\n            except (ValueError, IndexError):\n                self.session.ui.debug(traceback.format_exc())\n\n        if save:\n            self._save_config()\n\n    BORING_FOLDER_RE = re.compile('(?i)^(home|mail|data|user\\S*|[^[:alpha:]]+)$', re.UNICODE)\n    TAGNAME_STRIP_RE = re.compile('[{}\\\\[\\\\]]', re.UNICODE)\n\n    def _path_to_tagname(self, path):  # -> tag name\n        \"\"\"This converts a path to a tag name.\"\"\"\n        parts = self._mailbox_path_split(path)\n        parts = [p for p in parts if not re.match(self.BORING_FOLDER_RE, p)]\n        if not parts:\n            return _('Unnamed')\n        tagname = self._strip_file_extension(parts.pop(-1))\n        while tagname[:1] == '.':\n            tagname = tagname[1:]\n        return re.sub(self.TAGNAME_STRIP_RE, '', tagname.replace('_', ' '))\n\n    def _unique_tag_name(self, tagname):  # -> unused tag name\n        \"\"\"Make sure a tagname really is unused, unless we have a parent\"\"\"\n        if self.my_config.discovery.parent_tag:\n            return tagname\n        tagnameN, count = tagname, 2\n        while self.session.config.get_tags(tagnameN):\n            tagnameN = '%s (%s)' % (tagname, count)\n            count += 1\n        return tagnameN\n\n    def _create_tag_name(self, path):  # -> unique tag name\n        \"\"\"Convert a path to a unique tag name.\"\"\"\n        return self._unique_tag_name(self._path_to_tagname(path))\n\n    def _create_tag(self, tag_name_or_id,\n                    use_existing=True,\n                    unique=False,\n                    label=False,\n                    visible=True,\n                    slug=None,\n                    icon=None,\n                    parent=None):  # -> tag ID\n        if tag_name_or_id in self.session.config.tags:\n            # Short circuit if this is a tag ID for an existing tag\n            return tag_name_or_id\n        else:\n            tag_name = tag_name_or_id\n\n        tags = self.session.config.get_tags(tag_name)\n        if tags and unique:\n            raise ValueError('Tag name is not unique!')\n        elif len(tags) == 1 and use_existing:\n            tag_id = tags[0]._key\n        else:\n            if slug is None:\n                from mailpile.plugins.tags import Slugify\n                if self.my_config.name:\n                    slug = Slugify('/'.join([self.my_config.name, tag_name]),\n                                   tags=self.session.config.tags)\n                else:\n                    slug = Slugify(tag_name, tags=self.session.config.tags)\n            tag_id = self.session.config.tags.append({\n                'name': tag_name,\n                'slug': slug,\n                'type': 'mailbox',\n                'parent': parent or '',\n                'label': label,\n                'flag_allow_add': False,\n                'flag_allow_del': False,\n                'icon': icon or 'icon-tag',\n                'display': 'tag' if visible else 'archive',\n            })\n            if parent and visible:\n                self.session.config.tags[parent].display = 'tag'\n        return tag_id\n\n    def interrupt_rescan(self, reason):\n        self._interrupt = reason or _('Aborted')\n        if self._rescanning:\n            self.session.config.index.interrupt = reason\n\n    def _process_new(self, mbx_key, mbx_cfg, mbox,\n                     msg, msg_metadata_kws, msg_ts, keywords, snippet):\n        # Here subclasses could use mbx_key, mbx_cfg or mbox to grab the\n        # mailbox itself, in case it has metadata (like Maildir). The\n        # default just looks at the Status: headers of the mail itself.\n        return ProcessNew(self.session, msg, msg_metadata_kws, msg_ts,\n                          keywords, snippet)\n\n    def _msg_key_order(self, key):\n        return key\n\n    def _copy_new_messages(self, mbx_key, mbx_cfg, src,\n                           stop_after=-1, scan_args=None, deadline=None):\n        session, config = self.session, self.session.config\n        self.event.data['copying'] = progress = {\n            'running': True,\n            'mailbox_id': mbx_key,\n            'copied_messages': 0,\n            'copied_bytes': 0,\n            'deleting': False,\n            'complete': False}\n        scan_args = scan_args or {}\n        policy = self._policy(mbx_cfg)\n        count = 0\n\n        def maybe_delete_from_server(loc, src):\n            # Delete from source, if that's our policy.\n            if policy != 'move':\n                return\n\n            # Messages we have downloaded are candidates for deletion.\n            downloaded = list(set(src.keys()) & set(loc.source_map.keys()))\n            downloaded.sort(key=self._msg_key_order)\n\n            # If prefs.deletion_ratio is less than 1.0, leave the most\n            # recent messages on the server (so other clients have a\n            # chance to see them too).\n            #\n            # FIXME: This is a hack. It would be better to check the\n            #        timestamps of the messages and always leave on the\n            #        server for a fixed, configurable amount of time.\n            #\n            if config.prefs.deletion_ratio < 1.0:\n                random_ratio = random.random() * config.prefs.deletion_ratio\n                cutoff = int(max(\n                    random_ratio * len(downloaded),\n                    # Jitter. Without this, the last message never gets deleted:\n                    random.randint(-10, 1)))\n            else:\n                cutoff = len(downloaded)\n\n            should = _('Should delete %d messages') % cutoff\n            if 'sources' in config.sys.debug and downloaded:\n                session.ui.debug(should)\n\n            if config.prefs.allow_deletion:\n                try:\n                    for i, key in enumerate(downloaded):\n                        if i >= cutoff:\n                            break\n                        progress['deleting'] = '%d/%d' % (i+1, cutoff)\n                        src.remove(key)\n                    src.flush()\n                except:\n                    # Just ignore errors for now, we'll try again later.\n                    if 'sources' in config.sys.debug:\n                        session.ui.debug(traceback.format_exc())\n            else:\n                progress['deleting'] = '. '.join([\n                    _('Deletion is disabled'), should])\n\n        try:\n            # Lock the source mailbox while we work with it\n            src.lock()\n\n            with self._lock:\n                loc = config.open_mailbox(session, mbx_key, prefer_local=True)\n            if src == loc:\n                return count\n\n            # Perform housekeeping on the source_map, to make sure it does\n            # not grow without bounds or misrepresent things.\n            gone = []\n            src_keys = set(src.keys())\n            loc_keys = set(loc.keys())\n            for key, val in loc.source_map.iteritems():\n                if (val not in loc_keys) or (key not in src_keys):\n                    gone.append(key)\n            for key in gone:\n                del loc.source_map[key]\n\n            # Figure out what actually needs to be downloaded, log it\n            keys = list(src_keys - set(loc.source_map.keys()))\n            keys.sort(key=self._msg_key_order)\n            progress.update({\n                'total': len(src_keys),\n                'total_local': len(loc_keys),\n                'uncopied': len(keys),\n                'batch_size': stop_after if (stop_after > 0) else len(keys)})\n\n            # Go download!\n            key_errors = []\n            for key in reversed(keys):\n                if self._check_interrupt(log=False, clear=False):\n                    progress['interrupted'] = True\n                    return count\n\n                session.ui.mark(_('Copying message: %s') % key)\n                progress['copying_src_id'] = key\n                try:\n                    mkws = src.get_metadata_keywords(key)\n                    data = src.get_bytes(key)\n                except KeyError:\n                    progress['key_errors'] = key_errors\n                    key_errors.append(key)\n                    # Ignore, in case this is a problem with just this\n                    # individual message...\n                    continue\n\n                loc_key = loc.add_from_source(key, mkws, data)\n                self.event.data['counters']['copied_messages'] += 1\n                del progress['copying_src_id']\n                progress['copied_messages'] += 1\n                progress['copied_bytes'] += len(data)\n                progress['uncopied'] -= 1\n                count += 1\n\n                # This forks off a scan job to index the message\n                config.index.scan_one_message(\n                    session, mbx_key, loc, loc_key,\n                    wait=False, msg_data=data, msg_metadata_kws=mkws,\n                    **scan_args)\n\n                stop_after -= 1\n                if (stop_after == 0) or (deadline and time.time() > deadline):\n                    maybe_delete_from_server(loc, src)\n                    progress['stopped'] = True\n                    return count\n            progress['complete'] = True\n\n        except IOError:\n            # These just abort the download/read, which we're going to just\n            # take in stride for now.\n            if 'sources' in config.sys.debug:\n                session.ui.debug(traceback.format_exc())\n            progress['ioerror'] = True\n        except:\n            if 'sources' in config.sys.debug:\n                session.ui.debug(traceback.format_exc())\n            progress['raised'] = True\n            raise\n        finally:\n            progress['running'] = False\n            src.unlock()\n\n        maybe_delete_from_server(loc, src)\n        return count\n\n    def rescan_mailbox(self, mbx_key, mbx_cfg, path, stop_after=None):\n        session, config = self.session, self.session.config\n\n        with self._lock:\n            if self._rescanning:\n                return -1\n            self._rescanning = True\n\n        mailboxes = min(1, len([m for m in self.my_config.mailbox.values()\n                                if self._policy(m) not in ('ignore',\n                                                           'unknown')]))\n        try:\n            ostate = self._state  # Set this in case locking fails\n            with self._lock:\n                new_state = 'Rescan(%s, %s)' % (mbx_key, stop_after)\n                ostate, self._state = self._state, new_state\n\n                apply_tags = mbx_cfg.apply_tags[:]\n\n                parent = self._create_parent_tag(save=True)\n                if parent:\n                    tid = config.get_tag_id(parent)\n                    if tid:\n                        apply_tags.append(tid)\n\n                self._create_primary_tag(mbx_cfg)\n                if mbx_cfg.primary_tag:\n                    tid = config.get_tag_id(mbx_cfg.primary_tag)\n                    if tid:\n                        apply_tags.append(tid)\n\n            with self._lock:\n                mbox = config.open_mailbox(session, mbx_key,\n                                           prefer_local=False)\n            def process_new(msg, msg_metadata_kws, msg_ts, keywords, snippet):\n                return self._process_new(mbx_key, mbx_cfg, mbox,\n                                         msg, msg_metadata_kws, msg_ts,\n                                         keywords, snippet)\n            scan_mailbox_args = {\n                'process_new': (process_new if mbx_cfg.process_new else False),\n                'apply_tags': (apply_tags or []),\n                'stop_after': stop_after,\n                'event': self.event}\n            copied = count = 0\n\n            if mbx_cfg.local or self.my_config.discovery.local_copy:\n                # Note: We copy fewer messages than the batch allows for,\n                # because we might have been aborted on an earlier run and\n                # the rescan may need to catch up.\n                self._create_local_mailbox(mbx_cfg)\n                max_copy = max(min(stop_after, 5), int(0.8 * stop_after))\n                self._state = '%s: %s' % (new_state, _('Copying'))\n                self._log_status(_('Copying up to %d e-mails from %s'\n                                   ) % (max_copy, self._mailbox_name(path)))\n                copied = self._copy_new_messages(mbx_key, mbx_cfg, mbox,\n                                                 stop_after=max_copy,\n                                                 scan_args=scan_mailbox_args)\n                count += copied\n\n            if self._check_interrupt(clear=False):\n                if 'rescan' in self.event.data:\n                    self.event.data['rescan']['running'] = False\n                return count\n\n            self._state = '%s: %s' % (new_state, _('Working'))\n            self._log_status(_('Updating search engine for %s'\n                               ) % self._mailbox_name(path))\n            # Wait for background message scans to complete...\n            config.scan_worker.do(session, 'Wait:%s' % path, lambda: 1)\n\n            if 'rescans' in self.event.data:\n                self.event.data['rescans'][:-mailboxes] = []\n\n            return count + config.index.scan_mailbox(session,\n                                                     mbx_key,\n                                                     mbx_cfg.local or path,\n                                                     config.open_mailbox,\n                                                     force=self._rescan_forced,\n                                                     **scan_mailbox_args)\n        except ValueError:\n            session.ui.debug(traceback.format_exc())\n            return -1\n        finally:\n            self._state = ostate\n            self._rescanning = False\n\n    def open_mailbox(self, mbx_id, fn):\n        # This allows mail sources to override the default mailbox\n        # opening mechanism.  Returning false respectfully declines.\n        return None\n\n    def is_mailbox(self, fn):\n        return False\n\n    def _summarize_auth(self):\n        return sha1b64(self.my_config.auth_type, '-',\n                       self.my_config.username, '-',\n                       self.my_config.password)\n\n    def run(self):\n        play_nice(18)  # Reduce priority quite a lot\n\n        with self.session.config.index_check:\n            self.alive = True\n\n        self._load_state()\n        _original_session = self.session\n\n        def sleeptime():\n            if not self.my_config.enabled:\n                return 24 * 3600\n            elif self._last_rescan_completed or self._last_rescan_failed:\n                return self.my_config.interval\n            else:\n                return 1\n\n        def have_invalid_auth():\n            conn_err = self.event.data.get('connection', {}).get('error')\n            if conn_err and conn_err[0] in ('oauth2', 'auth'):\n                if ((self._loop_count % 100 != 0)\n                        and self._summarize_auth() == conn_err[-1]):\n                    self.session.ui.debug('Auth unchanged, doing nothing')\n                    return True\n                else:\n                    self._log_status(_('Checking new credentials'),\n                                     clear_errors=True)\n            return False\n\n        self._loop_count = 0\n        while self._loop_count == 0 or self._sleep(self._jitter(sleeptime())):\n            self.event.data['enabled'] = self.my_config.enabled\n            self.event.data['profile_id'] = self.my_config.profile\n            if self.my_config.enabled:\n                self.event.flags = Event.RUNNING\n                self._loop_count += 1\n            else:\n                if self._loop_count > 1:\n                    self._log_status(_('Disabled'), clear_errors=True)\n                self._loop_count = 1\n                self.close()\n                continue\n\n            self.name = self.my_config.name  # In case the config changes\n            self._update_unknown_state()\n            if not self.session.config.index:\n                continue\n\n            if have_invalid_auth():\n                continue\n\n            waiters, self._rescan_waiters = self._rescan_waiters, []\n            for b, e, s in waiters:\n                try:\n                    b.release()\n                except thread.error:\n                    pass\n                if s:\n                    self.session = s\n            try:\n                if 'traceback' in self.event.data:\n                    del self.event.data['traceback']\n                if self.open():\n                    self.sync_mail()\n                else:\n                    self._log_conn_errors()\n\n                next_save_time = self._last_saved + self.SAVE_STATE_INTERVAL\n                if self.alive and time.time() >= next_save_time:\n                    self._save_state()\n                    self._check_keepalive()\n                elif self._last_rescan_completed:\n                    self._check_keepalive()\n            except:\n                self.event.data['traceback'] = traceback.format_exc()\n                self.session.ui.debug(self.event.data['traceback'])\n                self._log_status(_('Internal error!  Sleeping...'))\n                self._sleep(self.INTERNAL_ERROR_SLEEP)\n            finally:\n                for b, e, s in waiters:\n                    try:\n                        e.release()\n                    except thread.error:\n                        pass\n                self.session = _original_session\n            self._update_unknown_state()\n        self.close()\n        self._log_status(_('Shutdown'), clear_errors=True)\n        self._save_state()\n\n    def _check_keepalive(self):\n        if not self.my_config.keepalive:\n            self.close()\n\n    def _log_conn_errors(self):\n        if 'connection' in self.event.data:\n            cinfo = self.event.data['connection']\n            if not cinfo.get('live'):\n                err_msg = cinfo.get('error', [None, None])[1]\n                if err_msg:\n                    self._log_status(err_msg)\n\n    def wake_up(self):\n        if 'wakeup' in self.session.config.sys.debug:\n            self.session.ui.debug('%s' % traceback.format_stack())\n        self._sleeping = -1\n\n    def notify_config_changed(self):\n        # FIXME: It would be nice to check if the changes apply to us and\n        #        stay asleep otherwise.\n        self.wake_up()\n\n    def rescan_now(self, session=None, started_callback=None):\n        if not self.my_config.enabled:\n            return (0, 0)\n\n        begin, end = MSrcLock(), MSrcLock()\n        for l in (begin, end):\n            l.acquire()\n        try:\n            self._rescan_waiters.append((begin, end, session))\n            self._rescan_forced = True\n            self._interrupt = 'Rescan forced'\n            self.wake_up()\n            while not begin.acquire(False):\n                time.sleep(1)\n                if mailpile.util.QUITTING:\n                    return self._last_rescan_count\n            if started_callback:\n                started_callback()\n            while not end.acquire(False):\n                time.sleep(1)\n                if mailpile.util.QUITTING:\n                    return self._last_rescan_count\n            return self._last_rescan_count\n        except KeyboardInterrupt:\n            self.interrupt_rescan(_('User aborted'))\n            raise\n        finally:\n            for l in (begin, end):\n                try:\n                    l.release()\n                except thread.error:\n                    pass\n\n    def quit(self, join=False):\n        self.interrupt_rescan(_('Shutdown'))\n        self.alive = False\n        self.wake_up()\n        if join and self.isAlive():\n            self.join()\n\n\ndef ProcessNew(session, msg, msg_metadata_kws, msg_ts, keywords, snippet):\n    if False and ('dsn:has' in keywords or 'mdn:has' in keywords):\n        # FIXME: This is a delivery notfication of some sort!\n        # TODO:  Figure out what it is telling us, do not mark as \"new\".\n        return False\n\n    if ('s:maildir' in msg_metadata_kws                  # Seen=read, maildir\n            or 'r:maildir' in msg_metadata_kws           # Replied, maildir\n            or 'r' in msg.get('status', '').lower()      # Read, mbox\n            or 'a' in msg.get('x-status', '').lower()):  # PINE, answered\n        return False\n\n    keywords.update(['%s:in' % tag._key for tag in\n                     session.config.get_tags(type='unread')])\n    return True\n\n\ndef MailSource(session, my_config):\n    # FIXME: check the plugin and instanciate the right kind of mail source\n    #        for this config section.\n    if my_config.protocol in ('mbox', 'maildir', 'local'):\n        from mailpile.mail_source.local import LocalMailSource\n        return LocalMailSource(session, my_config)\n    elif my_config.protocol in ('imap', 'imap_ssl', 'imap_tls'):\n        from mailpile.mail_source.imap import ImapMailSource\n        return ImapMailSource(session, my_config)\n    elif my_config.protocol in ('pop3', 'pop3_ssl'):\n        from mailpile.mail_source.pop3 import Pop3MailSource\n        return Pop3MailSource(session, my_config)\n    raise ValueError(_('Unknown mail source protocol: %s'\n                       ) % my_config.protocol)\n"
  },
  {
    "path": "mailpile/mail_source/imap.py",
    "content": "from __future__ import print_function\n# This implements our IMAP mail source. It has been tested against the\n# following IMAP implementations:\n#\n#   * Google's GMail (july 2014)\n#   * UW IMAPD (10.1.legacy from 2001)\n#\n#\n# IMAP resonses seen in the wild:\n#\n# GMail:\n#\n#    Message flags: \\* \\Answered \\Flagged \\Draft \\Deleted \\Seen\n#                   $Phishing receipt-handled $NotPhishing Junk\n#\n#    LIST (\\HasNoChildren) \"/\" \"Travel\"\n#    LIST (\\Noselect \\HasChildren) \"/\" \"[Gmail]\"\n#\n# UW IMAPD 10.1:\n#\n#    Message flags: \\* \\Answered \\Flagged \\Deleted \\Draft \\Seen\n#\n#    LIST (\\NoSelect) \"/\" 17.03.2002\n#    LIST (\\NoInferiors \\Marked) \"/\" in\n#    LIST (\\NoInferiors \\UnMarked) \"/\" todays-junk\n#\n# Fastmail.fm:\n#\n#    Message flags: \\Answered \\Flagged \\Draft \\Deleted \\Seen $X-ME-Annot-2\n#                   $IsMailingList $IsNotification $HasAttachment $HasTD\n#\n#    LIST (\\Noinferiors \\HasNoChildren) \".\" INBOX\n#    LIST (\\HasNoChildren \\Archive) \".\" Archive\n#    LIST (\\HasNoChildren \\Drafts) \".\" Drafts\n#    LIST (\\HasNoChildren \\Junk) \".\" \"Junk Mail\"\n#    LIST (\\HasNoChildren \\Sent) \".\" \"Sent Items\"\n#    LIST (\\HasNoChildren \\Trash) \".\" Trash\n#\n# Mykolab.com:\n#\n#\n#\nimport copy\nimport imaplib\nimport os\nimport re\nimport socket\nimport select\nimport ssl\nimport traceback\nimport time\nfrom imaplib import IMAP4_SSL, CRLF\nfrom mailbox import Mailbox, Message\nfrom urllib import quote, unquote\n\ntry:\n    import cStringIO as StringIO\nexcept ImportError:\n    import StringIO\n\nimport mailpile.mail_source.imap_utf7\nfrom mailpile.auth import IndirectPassword\nfrom mailpile.conn_brokers import Master as ConnBroker\nfrom mailpile.eventlog import Event\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.index.mailboxes import MailboxIndex\nfrom mailpile.mail_source import BaseMailSource\nfrom mailpile.mail_source.imap_starttls import IMAP4\nfrom mailpile.mailutils import FormatMbxId, MBX_ID_LEN\nfrom mailpile.plugins.oauth import OAuth2\nfrom mailpile.util import *\nfrom mailpile.vfs import FilePath\n\n\n# Raise imaplib's default maximum line length to something long and\n# silly. Some versions of Python ship with this set too low for the\n# Real World (no matter what the RFCs say).\nimaplib._MAXLINE = 10 * 1024 * 1024\n\n\nIMAP_TOKEN = re.compile('(\"[^\"]*\"'\n                        '|[\\\\(\\\\)]'\n                        '|[^\\\\(\\\\)\"\\\\s]+'\n                        '|\\\\s+)')\n\n# These are mailbox names we avoid downloading (by default)\nBLACKLISTED_MAILBOXES = (\n    'drafts',\n    'chats',\n    '[gmail]/all mail',\n    '[gmail]/important',\n    '[gmail]/starred',\n    'openpgp_keys')\n\n\nclass IMAP_IOError(IOError):\n    pass\n\n\nclass WithaBool(object):\n    def __init__(self, v): self.v = v\n    def __nonzero__(self): return self.v\n    def __enter__(self, *a, **kw): return self.v\n    def __exit__(self, *a, **kw): return self.v\n\n\ndef _parse_imap(reply):\n    \"\"\"\n    This routine will parse common IMAP4 responses into Pythonic data\n    structures.\n\n    >>> _parse_imap(('OK', ['One (Two (Th ree)) \"Four Five\"']))\n    (True, ['One', ['Two', ['Th', 'ree']], 'Four Five'])\n\n    >>> _parse_imap(('BAD', ['Sorry']))\n    (False, ['Sorry'])\n    \"\"\"\n    if not reply or len(reply) < 2:\n        return False, []\n    stack = []\n    pdata = []\n    for dline in reply[1]:\n        while True:\n            if isinstance(dline, (str, unicode)):\n                m = IMAP_TOKEN.match(dline)\n            else:\n                print('WARNING: Unparsed IMAP response data: %s' % (dline,))\n                m = None\n            if m:\n                token = m.group(0)\n                dline = dline[len(token):]\n                if token[:1] == '\"':\n                    pdata.append(token[1:-1])\n                elif token[:1] == '(':\n                    stack.append(pdata)\n                    pdata.append([])\n                    pdata = pdata[-1]\n                elif token[:1] == ')':\n                    pdata = stack.pop(-1)\n                elif token[:1] not in (' ', '\\t', '\\n', '\\r'):\n                    pdata.append(token)\n            else:\n                break\n    return (reply[0].upper() == 'OK'), pdata\n\n\nclass ImapMailboxIndex(MailboxIndex):\n    pass\n\n\nclass SharedImapConn(threading.Thread):\n    \"\"\"\n    This is a wrapper around an imaplib.IMAP4 connection which facilitates\n    sharing of the same conn between different parts of the app.\n\n    If nobody is using the connection and an IDLE callback is specified,\n    it will switch to IMAP IDLE mode when not otherwise in use.\n\n    Callers are expected to use the \"with sharedconn as conn: ...\" syntax.\n    \"\"\"\n    def __init__(self, session, conn, idle_mailbox=None, idle_callback=None):\n        threading.Thread.__init__(self)\n        self.daemon = True\n        self.session = session\n        self._lock = MSrcLock()\n        self._conn = conn\n        self._idle_mailbox = idle_mailbox\n        self._idle_callback = idle_callback\n        self._can_idle = False\n        self._idling = False\n        self._selected = None\n\n        for meth in ('append', 'add', 'authenticate', 'capability', 'fetch',\n                     'noop', 'store', 'expunge', 'close',\n                     'list', 'login', 'logout', 'namespace', 'search', 'uid'):\n            self.__setattr__(meth, self._mk_proxy(meth))\n\n        self._update_name()\n        self.start()\n\n    def _mk_proxy(self, method):\n        def proxy_method(*args, **kwargs):\n            try:\n                safe_assert(self._lock.locked())\n                if 'mailbox' in kwargs:\n                    # We're sharing this connection, so all mailbox methods\n                    # need to tell us which mailbox they're operating on.\n                    typ, data = self.select(kwargs['mailbox'])\n                    if typ.upper() != 'OK':\n                        return (typ, data)\n                    del kwargs['mailbox']\n                if 'imap' in self.session.config.sys.debug:\n                    self.session.ui.debug('%s(%s %s)' % (method, args, kwargs))\n                rv = getattr(self._conn, method)(*args, **kwargs)\n                if 'imap' in self.session.config.sys.debug:\n                    self.session.ui.debug((' => %s' % (rv,))[:240])\n                return rv\n\n            # This is annoyingly repetetive because the imaplib error classes\n            # are subclassed in a strange way.\n            #\n            # In short, we convert imaplib's error, abort and readonly into\n            # a subclass of IOError, so Mailplie's common logic can handle\n            # things gracefully. In the case of abort, we also kill the\n            # connection because it's probably in an unworkable state.\n            #\n            except IMAP4.readonly:\n                if 'imap' in self.session.config.sys.debug:\n                    self.session.ui.debug('%s' % traceback.format_exc())\n                raise IMAP_IOError('Readonly: %s(%s %s)' % (method, args, kwargs))\n            except IMAP4.abort:\n                if 'imap' in self.session.config.sys.debug:\n                    self.session.ui.debug('%s' % traceback.format_exc())\n                self._shutdown()\n                raise IMAP_IOError('Abort: %s(%s %s)' % (method, args, kwargs))\n            except IMAP4.error:\n                if 'imap' in self.session.config.sys.debug:\n                    self.session.ui.debug('%s' % traceback.format_exc())\n                raise IMAP_IOError('Error: %s(%s %s)' % (method, args, kwargs))\n            except:\n                # Default is no-op, just re-raise the exception. This includes\n                # the assertions above; they're logic errors we don't want to\n                # suppress.\n                raise\n        return proxy_method\n\n    def _shutdown(self):\n        if self._conn:\n            self._conn.shutdown()\n            self._conn = None\n            self._update_name()\n\n    def close(self):\n        safe_assert(self._lock.locked())\n        self._selected = None\n        if '(closed)' not in self.name:\n            self.name += ' (closed)'\n        return self._conn.close()\n\n    def select(self, mailbox='INBOX', readonly=False):\n        # This routine caches the SELECT operations, because we will be\n        # making lots and lots of superfluous ones \"just in case\" as part\n        # of multiplexing one IMAP connection for multiple mailboxes.\n        safe_assert(self._lock.locked())\n        if self._selected and self._selected[0] == (mailbox, readonly):\n            return self._selected[1]\n        elif self._selected:\n            try:\n                self._conn.close()\n            except IMAP4.error:\n                # This happens if we haven't previously selected a mailbox\n                pass\n        rv = self._conn.select(mailbox='\"%s\"' % mailbox, readonly=readonly)\n        if rv[0].upper() == 'OK':\n            info = dict(self._conn.response(f) for f in\n                        ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'))\n            self._selected = ((mailbox, readonly), rv, info)\n        else:\n            info = '(error)'\n        if 'imap' in self.session.config.sys.debug:\n            self.session.ui.debug('select(%s, %s) = %s %s'\n                                  % (mailbox, readonly, rv, info))\n        return rv\n\n    def mailbox_info(self, k, default=None):\n        if not self._selected or not self._selected[2]:\n            return default\n        return self._selected[2].get(k, default)\n\n    def _update_name(self):\n        name = self._conn and self._conn.host\n        if name:\n            self.name = name\n        elif '(dead)' not in self.name:\n            self.name += ' (dead)'\n\n    def __enter__(self):\n        if not self._conn:\n            raise IOError('I am dead')\n        self._stop_idling()\n        self._lock.acquire()\n        self._stop_idling()\n        return self\n\n    def __exit__(self, type, value, traceback):\n        self._start_idling()\n        self._lock.release()\n\n    def _start_idling(self):\n        self._can_idle = True\n\n    def _stop_idling(self):\n        self._can_idle = False\n\n    def _imap_idle(self):\n        if not self._conn:\n            return\n\n        self._can_idle = True\n        self.select(self._idle_mailbox)\n        if 'imap' in self.session.config.sys.debug:\n            logger = self.session.ui.debug\n        else:\n            logger = lambda x: True\n\n        def send_line(data):\n            logger('> %s' % data)\n            self._conn.send('%s%s' % (data, CRLF))\n\n        def get_line():\n            data = self._conn._get_line().rstrip()\n            logger('< %s' % data)\n            return data\n\n        try:\n            send_line('%s IDLE' % self._conn._new_tag())\n            while self._can_idle and not get_line().startswith('+ '):\n                pass\n            while True:\n                rl = wl = xl = None\n                try:\n                    rl, wl, xl = select.select([self._conn.sock], [], [], 1)\n                except socket.error:\n                    pass\n                if mailpile.util.QUITTING or not self._can_idle:\n                    break\n                elif rl and self._idle_callback(get_line()):\n                    self._selected = None\n                    break\n            send_line('DONE')\n            # Note: We let the IDLE response drop on the floor, don't care.\n        except (socket.error, OSError) as val:\n            raise self._conn.abort('socket error: %s' % val)\n\n    def quit(self):\n        self._can_idle = False  # Required to avoid deadlock below\n        with self._lock:\n            try:\n                if self._conn and self._conn.file:\n                    if self._selected:\n                        self._conn.close()\n                    self.logout()\n            except (IOError, IMAP4.error, AttributeError):\n                pass\n            self._can_idle = False\n            self._conn = None\n            self._update_name()\n\n    def run(self):\n        try:\n            idle_counter = 0\n            while self._conn:\n                # By default, all this does is send a NOOP every 120 seconds\n                # to keep the connection alive (or detect errors).\n                for t in range(0, 120):\n                    time.sleep(1 if self._conn else 0)\n                    if self._can_idle and self._idle_mailbox:\n                        idle_counter += 1\n                        # Once we've been in \"can idle\" state for 5: IDLE!\n                        if idle_counter >= 5:\n                            with self as raw_conn:\n                                self._imap_idle()\n                    else:\n                        idle_counter = 0\n\n                if self._conn:\n                    with self as raw_conn:\n                        raw_conn.noop()\n        except:\n            if 'imap' in self.session.config.sys.debug:\n                self.session.ui.debug('%s' % traceback.format_exc())\n        finally:\n            self.quit()\n\n\nclass SharedImapMailbox(Mailbox):\n    \"\"\"\n    This implements a Mailbox view of an IMAP folder. The IMAP connection\n    itself is obtained as a SharedImapConn from a particular mail source.\n\n    >>> imap = ImapMailSource(session, imap_config)\n    >>> mailbox = SharedImapMailbox(session, imap, conn_cls=_MockImap)\n    >>> #mailbox.add('From: Bjarni\\\\r\\\\nBarely a message')\n    \"\"\"\n\n    def __init__(self, session, mail_source,\n                 mailbox_path='INBOX', conn_cls=None):\n        self.config = session\n        self.source = mail_source\n        self.editable = False  # FIXME: this is technically not true\n        self.path = mailbox_path\n        self.conn_cls = conn_cls\n        self._last_updated = None\n        self._index = None\n        self._factory = None  # Unused, for Mailbox compatibility\n        self._broken = None\n\n    def open_imap(self):\n        return self.source.open(throw=IMAP_IOError, conn_cls=self.conn_cls)\n\n    def timed_imap(self, *args, **kwargs):\n        return self.source.timed_imap(*args, **kwargs)\n\n    def last_updated(self):\n        return self._last_updated\n\n    def _assert(self, test, error):\n        if not test:\n            raise IMAP_IOError(error)\n\n    def __nonzero__(self):\n        if self._broken is not None:\n            return not self._broken\n        try:\n            with self.open_imap() as imap:\n                ok, data = self.timed_imap(imap.noop, mailbox=self.path)\n                self._broken = False\n        except (IOError, AttributeError):\n            self._broken = True\n        return not self._broken\n\n    def add(self, message):\n        raise Exception('FIXME: Need to RETURN AN ID.')\n        self._broken = None\n        with self.open_imap() as imap:\n            ok, data = self.timed_imap(imap.append, self.path, message=message)\n            self._last_updated = time.time()\n            self._assert(ok, _('Failed to add message'))\n        self._broken = False\n\n    def remove(self, key):\n        self._broken = None\n        with self.open_imap() as imap:\n            uidv, uid = (int(k, 36) for k in key.split('.'))\n            ok, data = self.timed_imap(imap.uid, 'STORE', uid,\n                                       '+FLAGS', '(\\Deleted)',\n                                       mailbox=self.path)\n            self._last_updated = time.time()\n            self._assert(ok, _('Failed to remove message'))\n        self._broken = False\n\n    def mailbox_info(self, k, default=None):\n        self._broken = None\n        with self.open_imap() as imap:\n            imap.select(self.path)\n            return imap.mailbox_info(k, default=default)\n        self._broken = False\n\n    def get_info(self, key):\n        self._broken = None\n        with self.open_imap() as imap:\n            uidv, uid = (int(k, 36) for k in key.split('.'))\n            ok, data = self.timed_imap(imap.uid, 'FETCH', uid,\n                                       # Note: It seems that either python's\n                                       #       imaplib, or our parser cannot\n                                       #       handle dovecot's ENVELOPE\n                                       #       details. So omit that for now.\n                                       '(RFC822.SIZE FLAGS)',\n                                       mailbox=self.path)\n            if not ok:\n                raise KeyError(key)\n            self._assert(str(uidv) in imap.mailbox_info('UIDVALIDITY', ['0']),\n                         _('Mailbox is out of sync'))\n            info = dict(zip(*[iter(data[1])]*2))\n            info['UIDVALIDITY'] = uidv\n            info['UID'] = uid\n        self._broken = False\n        return info\n\n    def get(self, key, _bytes=None):\n        info = self.get_info(key)\n        if 'UID' not in info:\n            raise KeyError(key)\n\n        # FIXME: This will hard fail to download mail, if our internet\n        #        connection averages 8 kbps or worse. Better would be to\n        #        adapt the chunk size here to actual network performance.\n        #\n        chunk_size = self.source.timeout * 1024\n        chunk = 0\n        msg_data = []\n        if _bytes and chunk_size > _bytes:\n            chunk_size = _bytes\n\n        # Some IMAP servers misreport RFC822.SIZE, so we cannot really know\n        # how much data to expect. So we just FETCH chunk until one comes up\n        # short or empty and assume that's it...\n        while chunk >= 0:\n            req = '(BODY.PEEK[]<%d.%d>)' % (chunk * chunk_size, chunk_size)\n            with self.open_imap() as imap:\n                # Note: use the raw method, not the convenient parsed version.\n                typ, data = self.source.timed(imap.uid,\n                                              'FETCH', info['UID'], req,\n                                              mailbox=self.path)\n            self._assert(typ == 'OK',\n                         _('Fetching chunk %d failed') % chunk)\n            msg_data.append(data[0][1])\n            if len(data[0][1]) < chunk_size:\n                chunk = -1\n            else:\n                chunk += 1\n            if _bytes and chunk * chunk_size > _bytes:\n                chunk = -1\n\n        # FIXME: Should we add a sanity check and complain if we got\n        #        significantly less data than expected via. RFC822.SIZE?\n        return info, ''.join(msg_data)\n\n    def get_message(self, key):\n        info, payload = self.get(key)\n        return Message(payload)\n\n    def get_bytes(self, key, *args):\n        info, payload = self.get(key, *args)\n        return payload\n\n    def get_file(self, key):\n        info, payload = self.get(key)\n        return StringIO.StringIO(payload)\n\n    def iterkeys(self):\n        self._broken = None\n        with self.open_imap() as imap:\n            ok, data = self.timed_imap(imap.uid, 'SEARCH', None, 'ALL',\n                                       mailbox=self.path)\n            self._assert(ok, _('Failed to list mailbox contents'))\n            validity = imap.mailbox_info('UIDVALIDITY', ['0'])[0]\n        self._broken = False\n        return ('%s.%s' % (b36(int(validity)), b36(int(k)))\n                for k in sorted(data))\n\n    def keys(self):\n        return list(self.iterkeys())\n\n    def update_toc(self):\n        self._last_updated = time.time()\n\n    def get_msg_ptr(self, mboxid, key):\n        return '%s%s' % (mboxid, quote(key))\n\n    def get_file_by_ptr(self, msg_ptr):\n        return self.get_file(unquote(msg_ptr[MBX_ID_LEN:]))\n\n    def remove_by_ptr(self, msg_ptr):\n        return self.remove(unquote(msg_ptr[MBX_ID_LEN:]))\n\n    def get_msg_size(self, key):\n        return long(self.get_info(key).get('RFC822.SIZE', 0))\n\n    def get_metadata_keywords(self, key):\n        # Translate common IMAP flags into the maildir vocabulary\n        flags = [f.lower() for f in self.get_info(key).get('FLAGS', '')]\n        mkws = []\n        for char, flag in (('s', '\\\\seen'),\n                           ('r', '\\\\answered'),\n                           ('d', '\\\\draft'),\n                           ('f', '\\\\flagged'),\n                           ('t', '\\\\deleted')):\n           if flag in flags:\n               mkws.append('%s:maildir' % char)\n        return mkws\n\n    def __contains__(self, key):\n        try:\n            self.get_info(key)\n            return True\n        except (KeyError):\n            return False\n\n    def __len__(self):\n        self._broken = None\n        with self.open_imap() as imap:\n            ok, data = self.timed_imap(imap.noop, mailbox=self.path)\n            return imap.mailbox_info('EXISTS', ['0'])[0]\n        self._broken = False\n\n    def flush(self):\n        pass\n\n    def close(self):\n        pass\n\n    def lock(self):\n        pass\n\n    def unlock(self):\n        pass\n\n    def save(self, *args, **kwargs):\n        # SharedImapMailboxes are never pickled to disk.\n        pass\n\n    def get_index(self, config, mbx_mid=None):\n        if self._index is None:\n            self._index = ImapMailboxIndex(config, self, mbx_mid=mbx_mid)\n        return self._index\n\n    def __unicode__(self):\n        if self:\n            return _(\"IMAP: %s\") % self.path\n        else:\n            return _(\"IMAP: %s (not logged in)\") % self.path\n\n    def describe_msg_by_ptr(self, msg_ptr):\n        if self:\n            return _(\"e-mail with ID %s\") % unquote(msg_ptr[MBX_ID_LEN:])\n        else:\n            return _(\"remote mailbox is inavailable\")\n\n\ndef _connect_imap(session, settings, event,\n                  conn_cls=None, timeout=30, throw=False,\n                  logged_in_cb=None, source=None):\n\n    def timed(*args, **kwargs):\n        if source is not None:\n            kwargs['unique_thread'] = 'imap/%s' % (source.my_config._key,)\n        return RunTimed(timeout, *args, **kwargs)\n\n    def timed_imap(*args, **kwargs):\n        if source is not None:\n            kwargs['unique_thread'] = 'imap/%s' % (source.my_config._key,)\n        return _parse_imap(RunTimed(timeout, *args, **kwargs))\n\n    conn = None\n    try:\n        # Prepare the data section of our event, for keeping state.\n        for d in ('mailbox_state',):\n            if d not in event.data:\n                event.data[d] = {}\n        ev = event.data['connection'] = {\n            'live': False,\n            'error': [False, _('Nothing is wrong')]\n        }\n\n        # If we are given a conn class, use that - this allows mocks for\n        # testing.\n        if not conn_cls:\n            req_stls = (settings.get('protocol') == 'imap_tls')\n            want_ssl = (settings.get('protocol') == 'imap_ssl')\n            conn_cls = IMAP4_SSL if want_ssl else IMAP4\n        else:\n            req_stls = want_ssl = False\n\n        def mkconn():\n            if want_ssl:\n                need = [ConnBroker.OUTGOING_IMAPS]\n            else:\n                need = [ConnBroker.OUTGOING_IMAP]\n            with ConnBroker.context(need=need):\n                return conn_cls(settings.get('host'),\n                                int(settings.get('port')))\n        conn = timed(mkconn)\n        if hasattr(conn, 'sock'):\n            conn.sock.settimeout(120)\n        conn.debug = ('imaplib' in session.config.sys.debug) and 4 or 0\n\n        ok, data = timed_imap(conn.capability)\n        if ok:\n            capabilities = set(' '.join(data).upper().split())\n        else:\n            capabilities = set()\n\n        if req_stls or ('STARTTLS' in capabilities and not want_ssl):\n            try:\n                ok, data = timed_imap(conn.starttls)\n                if ok:\n                    # Fetch capabilities again after STARTTLS\n                    ok, data = timed_imap(conn.capability)\n                    capabilities = set(' '.join(data).upper().split())\n                    # Update the protocol to avoid getting downgraded later\n                    if settings.get('protocol', '') != 'imap_ssl':\n                        settings['protocol'] = 'imap_tls'\n            except (IMAP4.error, IOError, socket.error):\n                ok = False\n            if not ok:\n                ev['error'] = [\n                    'tls',\n                    _('Failed to make a secure TLS connection'),\n                    '%s:%s' % (settings.get('host'), settings.get('port'))]\n                if throw:\n                    raise throw(ev['error'][1])\n                return WithaBool(False)\n\n        username = password = \"\"\n        try:\n            error_type = 'login'\n            error_msg = _('IMAP Login error')\n            auth_error_type = auth_error_msg = None\n            username = settings.get('username', '').encode('utf-8')\n            password = IndirectPassword(\n                session.config,\n                settings.get('password', '')\n                ).encode('utf-8')\n\n            if (settings.get('auth_type', '').lower() == 'oauth2'\n                    and 'AUTH=XOAUTH2' in capabilities):\n                auth_error_type = 'oauth2'\n                auth_error_msg = _('Access denied by mail server')\n                token_info = OAuth2.GetFreshTokenInfo(session, username)\n                if not (username and token_info and token_info.access_token):\n                    raise ValueError(\"Missing configuration\")\n                ok, data = timed_imap(\n                    conn.authenticate, 'XOAUTH2',\n                    lambda challenge: OAuth2.XOAuth2Response(username,\n                                                             token_info))\n                if not ok:\n                    token_info.access_token = ''\n\n            else:\n                auth_error_type = 'auth'\n                auth_error_msg = _('Invalid username or password')\n                ok, data = timed_imap(conn.login, username, password)\n                \n        except IMAP4.error as save_error:\n            error_str = save_error.__str__()\n            ok, data = False, error_str\n            if auth_error_type:\n                # Is this a password error or some other kind of error?\n                # If the response contains any RFC5530 response code\n                # check for an RFC5530 auth error code, otherwise check for\n                # \"password\" (case independent) in the response string.\n                if (re.search('\\[AUTHENTICATIONFAILED\\]|'\n                                '\\[AUTHORIZATIONFAILED\\]|'\n                                '\\[EXPIRED\\]',     error_str) or\n                        (not re.search('\\[([a-zA-Z]*)\\]', error_str) and\n                            re.search('(?i)password', error_str) ) ):\n                    error_type = auth_error_type\n                    error_msg = auth_error_msg\n                    \n        except (UnicodeDecodeError, ValueError):\n            ok, data = False, None\n        if not ok:\n            auth_summary = ''\n            if source is not None:\n                auth_summary = source._summarize_auth()\n            ev['error'] = [error_type, error_msg, username, auth_summary]\n            if throw:\n                raise throw(ev['error'][1])\n            return WithaBool(False)\n\n        if logged_in_cb is not None:\n            logged_in_cb(conn, ev, capabilities)\n\n        return conn\n\n    except TimedOut:\n        if 'imap' in session.config.sys.debug:\n            session.ui.debug('%s' % traceback.format_exc())\n        ev['error'] = ['timeout', _('Connection timed out')]\n    except (ssl.CertificateError, ssl.SSLError):\n        if 'imap' in session.config.sys.debug:\n            session.ui.debug('%s' % traceback.format_exc())\n        ev['error'] = ['tls', _('Failed to make a secure TLS connection'),\n                       '%s:%s' % (settings.get('host'), settings.get('port'))]\n    except (IMAP_IOError, IMAP4.error):\n        if 'imap' in session.config.sys.debug:\n            session.ui.debug('%s' % traceback.format_exc())\n        ev['error'] = ['protocol', _('An IMAP protocol error occurred')]\n    except (IOError, AttributeError, socket.error):\n        if 'imap' in session.config.sys.debug:\n            session.ui.debug('%s' % traceback.format_exc())\n        ev['error'] = ['network', _('A network error occurred')]\n\n    try:\n        if conn:\n            # Close the socket directly, in the hopes this will boot\n            # any timed-out operations out of a hung state.\n            conn.socket().shutdown(socket.SHUT_RDWR)\n            conn.file.close()\n    except (AttributeError, IOError, socket.error):\n        pass\n    if throw:\n        raise throw(ev['error'])\n\n    return None\n\n\nclass ImapMailSource(BaseMailSource):\n    \"\"\"\n    This is a mail source that connects to an IMAP server.\n\n    A single connection is made to the IMAP server, which is then shared\n    between the ImapMailSource job and individual mailbox instances.\n    \"\"\"\n    # This is a helper for the events.\n    __classname__ = 'mailpile.mail_source.imap.ImapMailSource'\n\n    TIMEOUT_INITIAL = 60\n    TIMEOUT_LIVE = 120\n    CONN_ERRORS = (IOError, IMAP_IOError, IMAP4.error, TimedOut)\n\n\n    class MailSourceVfs(BaseMailSource.MailSourceVfs):\n        \"\"\"Expose the IMAP tree to the VFS layer.\"\"\"\n        def _imap_path(self, path):\n            if path[:1] == '/':\n                path = path[1:]\n            return path[len(self.root.raw_fp):]\n\n        def _imap(self, *args, **kwargs):\n            return self.source.timed_imap(*args, **kwargs)\n\n        def listdir_(self, where, **kwargs):\n            results = []\n            path = self._imap_path(where)\n            prefix, pathsep = self.source._namespace_info(path)\n            with self.source.open() as conn:\n                if not conn:\n                    raise socket.error(_('Not connected to IMAP server.'))\n                if path:\n                    ok, data = self._imap(conn.list, path + pathsep, '%')\n                else:\n                    ok, data = self._imap(conn.list, '', '%')\n                while ok and len(data) >= 3:\n                    (flags, sep, path), data[:3] = data[:3], []\n                    if path.lower() not in BLACKLISTED_MAILBOXES:\n                        flags = [f.lower() for f in flags]\n                        self.source._cache_flags(path, flags)\n                        results.append('/' + self.source._fmt_path(path))\n            return results\n\n        def getflags_(self, fp, cfg):\n            if self.root == fp:\n                return BaseMailSource.MailSourceVfs.getflags_(self, fp, cfg)\n            flags = [flag.lower().replace('\\\\', '') for flag in\n                     self.source._cache_flags(self._imap_path(fp)) or []]\n            if not ('hasnochildren' in flags or 'noinferiors' in flags):\n                flags.append('Directory')\n            if not ('noselect' in flags):\n                flags.append('Mailbox')\n            return flags\n\n        def abspath_(self, fp):\n            return fp\n\n        def display_name_(self, fp, config):\n            return FilePath(fp).display_basename()\n\n        def isdir_(self, fp):\n            if self.root == fp:\n                return True\n            flags = self.source._cache_flags(self._imap_path(fp)) or []\n            return not ('hasnochildren' in flags or 'noinferiors' in flags)\n\n        def getsize_(self, path):\n            return None\n\n\n    def __init__(self, *args, **kwargs):\n        BaseMailSource.__init__(self, *args, **kwargs)\n        self.timeout = self.TIMEOUT_INITIAL\n        self.last_op = 0\n        self.watching = -1\n        self.capabilities = set()\n        self.logged_in_at = None\n        self.namespaces = {'private': []}\n        self.flag_cache = {}\n        self.conn = None\n        self.conn_id = ''\n\n    @classmethod\n    def Tester(cls, conn_cls, *args, **kwargs):\n        tcls = cls(*args, **kwargs)\n        return tcls.open(conn_cls=conn_cls) and tcls or False\n\n    def timed(self, *args, **kwargs):\n        return RunTimed(self.timeout, *args, **kwargs)\n\n    def timed_imap(self, *args, **kwargs):\n        return _parse_imap(RunTimed(self.timeout, *args, **kwargs))\n\n    def _conn_id(self):\n        def e(s):\n            try:\n                return unicode(s).encode('utf-8')\n            except UnicodeDecodeError:\n                return unicode(s).encode('utf-8', 'replace')\n        return md5_hex('\\n'.join([e(self.my_config[k]) for k in\n                                  ('host', 'port', 'password', 'username')]))\n\n    def close(self):\n        with self._lock:\n            if self.conn:\n                self.event.data['connection'] = {\n                    'live': False,\n                    'error': [False, _('Nothing is wrong')]\n                }\n                self.conn.quit()\n                self.conn = None\n\n    def open(self, conn_cls=None, throw=False):\n        conn = self.conn\n        conn_id = self._conn_id()\n        if conn:\n            try:\n                with conn as c:\n                    now = time.time()\n                    if (conn_id == self.conn_id and\n                            (now < self.last_op + 120 or\n                             self.timed(c.noop)[0] == 'OK')):\n                        # Make the timeout longer, so we don't drop things\n                        # on every hiccup and so downloads will be more\n                        # efficient (chunk size relates to timeout).\n                        self.timeout = self.TIMEOUT_LIVE\n                        if now >= self.last_op + 120:\n                            self.last_op = now\n                        return conn\n            except self.CONN_ERRORS + (AttributeError, ):\n                pass\n            with self._lock:\n                if self.conn == conn:\n                    self.conn = None\n            conn.quit()\n\n        my_config = self.my_config\n\n        # This facilitates testing, event should already exist in real life.\n        if self.event:\n            event = self.event\n        else:\n            event = Event(source=self, flags=Event.RUNNING, data={})\n\n        def logged_in_cb(conn, ev, capabilities):\n            with self._lock:\n                if self.conn is not None:\n                    raise IOError('Woah, we lost a race.')\n                self.capabilities = capabilities\n\n                if 'NAMESPACE' in capabilities:\n                    ok, data = self.timed_imap(conn.namespace)\n                    if ok:\n                        prv, oth, shr = data\n                        self.namespaces = {\n                            'private': prv if (prv != 'NIL') else [],\n                            'others': oth if (oth != 'NIL') else [],\n                            'shared': shr if (shr != 'NIL') else []\n                        }\n\n                if 'IDLE' in capabilities:\n                    self.conn = SharedImapConn(\n                        self.session, conn,\n                        idle_mailbox='INBOX',\n                        idle_callback=self._idle_callback)\n                else:\n                    self.conn = SharedImapConn(self.session, conn)\n\n            if self.event:\n                self._log_status(_('Connected to IMAP server %s'\n                                   ) % my_config.host)\n            if 'imap' in self.session.config.sys.debug:\n                self.session.ui.debug('CONNECTED %s' % self.conn)\n                self.session.ui.debug('CAPABILITIES %s' % self.capabilities)\n                self.session.ui.debug('NAMESPACES %s' % self.namespaces)\n\n            self.conn_id = conn_id\n            ev['live'] = True\n\n        conn = _connect_imap(self.session, self.my_config, event,\n                             conn_cls=conn_cls,\n                             timeout=self.timeout,\n                             throw=throw,\n                             logged_in_cb=logged_in_cb,\n                             source=self)\n        if conn:\n            self.logged_in_at = time.time()\n            return self.conn\n        else:\n            return WithaBool(False)\n\n    def _idle_callback(self, data):\n        if 'EXISTS' in data:\n            # Stop sleeping and check for mail\n            self.wake_up()\n            return True\n        else:\n            return False\n\n    def _check_keepalive(self):\n        alive_for = time.time() - (self.logged_in_at or time.time())\n        if (not self.my_config.keepalive) or alive_for > (12 * 3600):\n            if ('IDLE' not in self.capabilities or\n                    alive_for > self.my_config.interval):\n                self.close()\n\n    def open_mailbox(self, mbx_id, mfn):\n        try:\n            proto_me, path = mfn.split('/', 1)\n            if proto_me.startswith('src:%s' % self.my_config._key):\n                return SharedImapMailbox(self.session, self, mailbox_path=path)\n        except ValueError:\n            pass\n        return None\n\n    def _get_mbx_id_and_mfn(self, mbx_cfg):\n        mbx_id = FormatMbxId(mbx_cfg._key)\n        return mbx_id, self.session.config.sys.mailbox[mbx_id]\n\n    def _has_mailbox_changed(self, mbx_cfg, state):\n        shared_mbox = self.open_mailbox(*self._get_mbx_id_and_mfn(mbx_cfg))\n        uv = state['uv'] = shared_mbox.mailbox_info('UIDVALIDITY', ['0'])[0]\n        ex = state['ex'] = shared_mbox.mailbox_info('EXISTS', ['0'])[0]\n        uvex = '%s/%s' % (uv, ex)\n        if uvex == '0/0':\n            return True\n        return (uvex != self.event.data.get('mailbox_state',\n                                            {}).get(mbx_cfg._key))\n\n    def _mark_mailbox_rescanned(self, mbx, state):\n        uvex = '%s/%s' % (state['uv'], state['ex'])\n        if 'mailbox_state' in self.event.data:\n            self.event.data['mailbox_state'][mbx._key] = uvex\n        else:\n            self.event.data['mailbox_state'] = {mbx._key: uvex}\n\n    def _namespace_info(self, path):\n        for which, nslist in self.namespaces.iteritems():\n            for prefix, pathsep in nslist:\n                if path.startswith(prefix):\n                    return prefix, pathsep or '/'\n        # This is a hack for older servers that don't do NAMESPACE\n        if path.startswith('INBOX.'):\n            return 'INBOX', '.'\n        return '', '/'\n\n    def _default_policy(self, mbx_cfg):\n        if self._mailbox_path(self._path(mbx_cfg)\n                              ).lower() in BLACKLISTED_MAILBOXES:\n            return 'ignore'\n        else:\n            return 'inherit'\n\n    def _sorted_mailboxes(self):\n        # This allows changes to BLACKLISTED_MAILBOXES to have an effect\n        # even if peoples' configs say otherwise.\n        return [\n            m for m in BaseMailSource._sorted_mailboxes(self)\n            if m.name.lower() not in BLACKLISTED_MAILBOXES]\n\n    def _msg_key_order(self, key):\n        return [int(k, 36) for k in key.split('.')]\n\n    def _strip_file_extension(self, mbx_path):\n        return mbx_path  # Yes, a no-op :)\n\n    def _decode_path(self, path):\n        try:\n            return path.decode('imap4-utf-7')\n        except:\n            return path\n\n    def _mailbox_path(self, mbx_path):\n        # len('src:/') = 5\n        return str(mbx_path[(5 + len(self.my_config._key)):])\n\n    def _mailbox_path_split(self, mbx_path):\n        path = self._mailbox_path(mbx_path)\n        prefix, pathsep = self._namespace_info(path)\n        return [self._decode_path(p) for p in path.split(pathsep)]\n\n    def _mailbox_name(self, mbx_path):\n        path = self._mailbox_path(mbx_path)\n        prefix, pathsep = self._namespace_info(path)\n        return self._decode_path(path[len(prefix):])\n\n    def _fmt_path(self, path):\n        return 'src:%s/%s' % (self.my_config._key, path)\n\n    def _fix_empty_discovery_path_bug(self):\n        if self.my_config.discovery.policy not in ('unknown', 'ignore'):\n            if not self.my_config.discovery.paths:\n                self.my_config.discovery.paths.append('/')\n\n    def discover_mailboxes(self, paths=None):\n        config = self.session.config\n        ostate = self.on_event_discovery_starting()\n        self._fix_empty_discovery_path_bug()\n        try:\n            paths = copy.copy(paths or self.my_config.discovery.paths)\n            max_mailboxes = self.my_config.discovery.max_mailboxes\n            mailbox_count = len(self.my_config.mailbox)\n            existing = self._existing_mailboxes()\n            mailboxes = []\n\n            with self.open() as raw_conn:\n                for p in paths:\n                    mailboxes += self._walk_mailbox_path(raw_conn, str(p))\n\n            discovered = [mbx for mbx in mailboxes if mbx not in existing]\n            if discovered and (len(discovered) > max_mailboxes - mailbox_count):\n                discovered = discovered[:max(0, max_mailboxes - mailbox_count)]\n                if self.on_event_discovery_toomany():\n                    return self.discover_mailboxes(paths=paths)\n\n            self.set_event_discovery_state('adding')\n            for path in discovered:\n                idx = config.sys.mailbox.append(path)\n                mbx = self.take_over_mailbox(idx)\n\n            return len(discovered)\n        except:\n            if config.sys.debug:\n                self.session.ui.debug('%s' % traceback.format_exc())\n            raise\n        finally:\n            self.on_event_discovery_done(ostate)\n\n    def _cache_flags(self, path, flags=None):\n        path = self._fmt_path(path)\n        if flags is not None:\n            self.flag_cache[path] = flags\n        return self.flag_cache.get(path)\n\n    def _walk_mailbox_path(self, conn, prefix):\n        \"\"\"\n        Walks the IMAP path recursively and returns a list of all found\n        mailboxes.\n        \"\"\"\n        mboxes = []\n        subtrees = []\n        # We go well over the maximum here, so the calling code can detect\n        # detect that we want to go over the limits and can ask the user\n        # whether that's OK.\n        max_mailboxes = 5 + (2 * self.my_config.discovery.max_mailboxes)\n        if prefix == '/':\n            prefix = ''\n        try:\n            ok, data = self.timed_imap(conn.list, prefix, '%')\n            while ok and len(data) >= 3:\n                (flags, sep, path), data[:3] = data[:3], []\n                flags = [f.lower() for f in flags]\n                if '\\\\noselect' not in flags:\n                    if path.lower() not in BLACKLISTED_MAILBOXES:\n                        # We cache the flags for this mailbox, they may tell\n                        # use useful things about what kind of mailbox it is.\n                        self._cache_flags(path, flags)\n                        mboxes.append(self._fmt_path(path))\n                if '\\\\haschildren' in flags:\n                    subtrees.append('%s%s' % (path, sep))\n                if len(mboxes) > max_mailboxes:\n                    break\n            for path in subtrees:\n                if len(mboxes) <= max_mailboxes:\n                    mboxes.extend(self._walk_mailbox_path(conn, path))\n        except self.CONN_ERRORS:\n            pass\n        finally:\n            return mboxes\n\n    def quit(self, *args, **kwargs):\n        if self.conn:\n            self.conn.quit()\n        return BaseMailSource.quit(self, *args, **kwargs)\n\n\ndef TestImapSettings(session, settings, event,\n                     timeout=ImapMailSource.TIMEOUT_INITIAL):\n    conn = _connect_imap(session, settings, event, timeout=timeout)\n    if conn:\n        try:\n            conn.socket().shutdown(socket.SHUT_RDWR)\n            conn.file.close()\n        except (IOError, OSError, socket.error):\n            pass\n        return True\n    return False\n\n\n##[ Test code follows ]#######################################################\n\nclass _MockImap(object):\n    \"\"\"\n    Base mock that pretends to be an imaplib IMAP connection.\n\n    >>> imap = ImapMailSource(session, imap_config)\n    >>> imap.open(conn_cls=_MockImap)\n    <SharedImapConn(mock, started ...)>\n\n    >>> sorted(imap.capabilities)\n    ['IMAP4REV1', 'X-MAGIC-BEANS']\n    \"\"\"\n    DEFAULT_RESULTS = {\n        'append': ('OK', []),\n        'capability': ('OK', ['X-MAGIC-BEANS', 'IMAP4rev1']),\n        'list': ('OK', []),\n        'login': ('OK', ['\"Welcome, human\"']),\n        'noop': ('OK', []),\n        'select': ('OK', []),\n    }\n    RESULTS = {}\n\n    def __init__(self, *args, **kwargs):\n        self.host = 'mock'\n        def mkcmd(rval):\n            def cmd(*args, **kwargs):\n                return rval\n            return cmd\n        for cmd, rval in dict_merge(self.DEFAULT_RESULTS, self.RESULTS\n                                    ).iteritems():\n            self.__setattr__(cmd, mkcmd(rval))\n\n    def __getattr__(self, attr):\n        return self.__getattribute__(attr)\n\n\nclass _Mocks(object):\n    \"\"\"\n    A bunch of IMAP test classes for testing various configurations.\n\n    >>> ImapMailSource.Tester(_Mocks.NoDns, session, imap_config)\n    False\n\n    >>> ImapMailSource.Tester(_Mocks.BadLogin, session, imap_config)\n    False\n    \"\"\"\n    class NoDns(_MockImap):\n        def __init__(self, *args, **kwargs):\n            raise socket.gaierror('Oops')\n\n    class BadLogin(_MockImap):\n        RESULTS = {'login': ('BAD', ['\"Sorry dude\"'])}\n\n\nif __name__ == \"__main__\":\n    import doctest\n    import sys\n    import mailpile.config.defaults\n    import mailpile.config.manager\n    import mailpile.ui\n\n    rules = mailpile.config.defaults.CONFIG_RULES\n    config = mailpile.config.manager.ConfigManager(rules=rules)\n    config.sources.imap = {\n        'protocol': 'imap_ssl',\n        'host': 'imap.gmail.com',\n        'port': 993,\n        'username': 'nobody',\n        'password': 'nowhere'\n    }\n    session = mailpile.ui.Session(config)\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                              extraglobs={'session': session,\n                                          'imap_config': config.sources.imap})\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n\n    args = sys.argv[1:]\n    if args:\n        session.config.sys.debug = 'imap'\n\n        username, password = args.pop(0), args.pop(0)\n        config.sources.imap.username = username\n        config.sources.imap.password = password\n        imap = ImapMailSource(session, config.sources.imap)\n        with imap.open(throw=IMAP_IOError) as conn:\n            print('%s' % (conn.list(), ))\n        mbx = SharedImapMailbox(config, imap, mailbox_path='INBOX')\n        print('%s' % list(mbx.iterkeys()))\n        for key in args:\n            info, payload = mbx.get(key)\n            print('%s(%d bytes) = %s\\n%s' % (mbx.get_msg_ptr('0000', key),\n                                             mbx.get_msg_size(key),\n                                             info, payload))\n"
  },
  {
    "path": "mailpile/mail_source/imap_starttls.py",
    "content": "import imaplib\nimport ssl\n\nCommands = {\n    'STARTTLS': ('NONAUTH')\n}\n\nimaplib.Commands.update(Commands)\n\nclass IMAP4(imaplib.IMAP4, object):\n\n    # This is a bugfix for imaplib's default readline method. It is\n    # identical except it raises abort() instead of error() as the\n    # internal state will certainly be broken.\n    #\n    # Note: This would be the function to change if we want to do away\n    #       with the line-length limits altogether.\n    #\n    def readline(self):\n        \"\"\"Read line from remote.\"\"\"\n        line = self.file.readline(imaplib._MAXLINE + 1)\n        if len(line) > imaplib._MAXLINE:\n            raise self.abort(\"got more than %d bytes\" % imaplib._MAXLINE)\n        return line\n\n    def starttls(self, keyfile = None, certfile = None):\n        typ, data = self._simple_command('STARTTLS')\n        if typ != 'OK':\n            raise self.error('no STARTTLS')\n        self.sock = ssl.wrap_socket(self.sock, keyfile, certfile)\n        self.file = self.sock.makefile('rb')\n        return typ, data\n"
  },
  {
    "path": "mailpile/mail_source/imap_utf7.py",
    "content": "# -*- coding: utf-8- -*-\n\n# from: http://piao-tech.blogspot.no/2010/03/get-offlineimap-working-with-non-ascii.html#resources\n\nimport binascii\nimport codecs\n\n# encoding\n\ndef modified_base64 (s):\n  s = s.encode('utf-16be')\n  return binascii.b2a_base64(s).rstrip('\\n=').replace('/', ',')\n\ndef doB64(_in, r):\n  if _in:\n    r.append('&%s-' % modified_base64(''.join(_in)))\n    del _in[:]\n\ndef encoder(s):\n  r = []\n  _in = []\n  for c in s:\n    ordC = ord(c)\n    if 0x20 <= ordC <= 0x25 or 0x27 <= ordC <= 0x7e:\n      doB64(_in, r)\n      r.append (c)\n    elif c == '&':\n      doB64(_in, r)\n      r.append ('&-')\n    else:\n      _in.append(c)\n  doB64(_in, r)\n  return (str(''.join(r)), len(s))\n\n# decoding\ndef modified_unbase64(s):\n  b = binascii.a2b_base64(s.replace(',', '/') + '===')\n  return unicode (b, 'utf-16be')\n\ndef decoder (s):\n  r = []\n  decode = []\n  for c in s:\n    if c == '&' and not decode:\n      decode.append ('&')\n    elif c == '-' and decode:\n      if len(decode) == 1:\n        r.append('&')\n      else:\n        r.append(modified_unbase64(''.join(decode[1:])))\n      decode = []\n    elif decode:\n      decode.append(c)\n    else:\n      r.append(c)\n\n  if decode:\n    r.append(modified_unbase64(''.join(decode[1:])))\n  bin_str = ''.join(r)\n  return (bin_str, len(s))\n\nclass StreamReader (codecs.StreamReader):\n  def decode (self, s, errors='strict'):\n    return decoder(s)\n\nclass StreamWriter (codecs.StreamWriter):\n  def decode (self, s, errors='strict'):\n    return encoder(s)\n\ndef imap4_utf_7(name):\n  if name == 'imap4-utf-7':\n    return (encoder, decoder, StreamReader, StreamWriter)\n\ncodecs.register(imap4_utf_7)\n"
  },
  {
    "path": "mailpile/mail_source/local.py",
    "content": "import time\nimport os\n\nfrom mailpile.mail_source import BaseMailSource\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.vfs import FilePath\n\n\nclass LocalMailSource(BaseMailSource):\n    \"\"\"\n    This is a mail source that watches over one or more local mailboxes.\n    \"\"\"\n    # This is a helper for the events.\n    __classname__ = 'mailpile.mail_source.local.LocalMailSource'\n\n    def __init__(self, *args, **kwargs):\n        BaseMailSource.__init__(self, *args, **kwargs)\n        if not self.my_config.name:\n            self.my_config.name = _('Local mail')\n        self.recently_changed = []\n        self.my_config.protocol = 'local'  # We may be upgrading an old\n                                           # mbox or maildir source.\n        self.watching = -1\n\n    def _sleeping_is_ok(self, slept):\n        if slept > 5:\n            #\n            # If any of the most recently changed mailboxes has changed\n            # again, cut our sleeps short after 5 seconds. By basing this\n            # on recently changed mailboxes, we don't need to explicitly\n            # ask the user which mailbox(es) are being used as Inboxes.\n            #\n            # The number 10 should be \"big enough\", without us going\n            # nuts and scanning a gazillion mailboxes every second.\n            #\n            if len(self.recently_changed) > 10:\n                self.recently_changed = self.recently_changed[-10:]\n            for mbx in self.recently_changed:\n                if self._has_mailbox_changed(mbx, {}):\n                    return False\n        return True\n\n    def close(self):\n        pass\n\n    def open(self):\n        with self._lock:\n            mailboxes = self.my_config.mailbox.values()\n            if self.watching == len(mailboxes):\n                return True\n            else:\n                self.watching = len(mailboxes)\n\n            # Prepare the data section of our event, for keeping state.\n            for d in ('mailbox_state', ):\n                if d not in self.event.data:\n                    self.event.data[d] = {}\n\n        self._log_status(_('Watching %d mbox mailboxes') % self.watching)\n        return True\n\n    def _get_macmaildir_data(self, path):\n        ds = [d for d in os.listdir(path) if not d.startswith('.')\n              and os.path.isdir(os.path.join(path, d))]\n        return (len(ds) == 1) and os.path.join(path, ds[0], 'Data')\n\n    def _data_paths(self, mbx):\n        mbx_path = FilePath(self._path(mbx)).raw_fp\n        if os.path.exists(mbx_path):\n            yield mbx_path\n\n        if os.path.isdir(mbx_path):\n            # Maildir, WERVD\n            for s in ('cur', 'new', 'tmp', 'wervd.ver'):\n                sub_path = os.path.join(mbx_path, s)\n                if os.path.exists(sub_path):\n                    yield sub_path\n\n            # Mac Maildir\n            sub_path = self._get_macmaildir_data(mbx_path)\n            if sub_path:\n                yield sub_path\n\n    def _mailbox_sort_key(self, mbx):\n        # Sort mailboxes so the most recently modified get scanned first.\n        mt = 0\n        for p in self._data_paths(mbx):\n            try:\n                mt = max(mt, os.path.getmtime(p))\n            except (OSError, IOError):\n                pass\n        if mt:\n            return '%20.20d' % (0x10000000000 - long(mt))\n        else:\n            return BaseMailSource._mailbox_sort_key(self, mbx)\n\n    def _has_mailbox_changed(self, mbx, state):\n        mtszs = []\n        for p in self._data_paths(mbx):\n            try:\n                mt = long(os.path.getmtime(p))\n                sz = long(os.path.getsize(p))\n                mtszs.append('%s/%s' % (mt, sz))\n            except (OSError, IOError):\n                pass\n\n        if not mtszs:\n            # Try to rescan even if the above fails for some reason\n            mt = sz = (int(time.time()) // 7200)\n            mtszs = ['%s/%s' % (mt, sz)]\n\n        mtsz = state['mtsz'] = ','.join(mtszs)\n        if (mtsz != self.event.data.get('mailbox_state', {}).get(mbx._key)):\n            while mbx in self.recently_changed:\n                self.recently_changed.remove(mbx)\n            self.recently_changed.append(mbx)\n            return True\n        else:\n            return False\n\n    def _mark_mailbox_rescanned(self, mbx, state):\n        if 'mailbox_state' in self.event.data:\n            self.event.data['mailbox_state'][mbx._key] = state['mtsz']\n        else:\n            self.event.data['mailbox_state'] = {mbx._key: state['mtsz']}\n\n    def _is_mbox(self, fn):\n        try:\n            with open(fn, 'rb') as fd:\n                data = fd.read(2048)  # No point reading less...\n                if data.startswith('From '):\n                    # OK, this might be an mbox! Let's check if the first\n                    # few lines look like RFC2822 headers...\n                    headcount = 0\n                    for line in data.splitlines(True)[1:]:\n                        if (headcount > 2) and line in ('\\n', '\\r\\n'):\n                            return True\n                        if line[-1:] == '\\n' and line[:1] not in (' ', '\\t'):\n                            parts = line.split(':')\n                            if (len(parts) < 2 or\n                                    ' ' in parts[0] or '\\t' in parts[0]):\n                                return False\n                            headcount += 1\n                    return (headcount > 2)\n        except (IOError, OSError):\n            pass\n        return False\n\n    def _is_maildir(self, fn):\n        if not os.path.isdir(fn):\n            return False\n        for sub in ('cur', 'new', 'tmp'):\n            subdir = os.path.join(fn, sub)\n            if not os.path.exists(subdir) or not os.path.isdir(subdir):\n                return False\n        return True\n\n    def _is_macmaildir(self, path):\n        infoplist = os.path.join(path, 'Info.plist')\n        if not os.path.isdir(path) or not os.path.exists(infoplist):\n            return False\n        data = self._get_macmaildir_data(path)\n        return data and os.path.isdir(data)\n\n    def is_mailbox(self, fn):\n        fn = FilePath(fn).raw_fp\n        return (self._is_maildir(fn) or\n                self._is_macmaildir(fn) or\n                self._is_mbox(fn))\n"
  },
  {
    "path": "mailpile/mail_source/pop3.py",
    "content": "import os\nimport ssl\nimport traceback\n\nfrom mailpile.auth import IndirectPassword\nfrom mailpile.conn_brokers import Master as ConnBroker\nfrom mailpile.mail_source import BaseMailSource\nfrom mailpile.mailboxes import pop3\nfrom mailpile.mailutils import FormatMbxId, MBX_ID_LEN\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.util import *\n\n\n# We use this to enable \"recent mode\" on GMail accounts by default.\nGMAIL_TLDS = ('gmail.com', 'googlemail.com')\n\n\ndef _open_pop3_mailbox(session, event, host, port,\n                       username, password, auth_type,\n                       protocol, debug, throw=False):\n    cev = event.data['connection'] = {\n        'live': False,\n        'error': [False, _('Nothing is wrong')]\n    }\n    try:\n        # FIXME: Nothing actually adds gmail or gmail-full to the protocol\n        #        yet, so we're stuck in recent mode only for now.\n        if (username.lower().split('@')[-1] in GMAIL_TLDS\n                or 'gmail' in protocol):\n            if 'gmail-full' not in protocol:\n                username = 'recent:%s' % username\n\n        if 'ssl' in protocol:\n            need = [ConnBroker.OUTGOING_POP3S]\n        else:\n            need = [ConnBroker.OUTGOING_POP3]\n\n        with ConnBroker.context(need=need):\n            return pop3.MailpileMailbox(host,\n                                        port=port,\n                                        user=username,\n                                        password=password,\n                                        auth_type=auth_type,\n                                        use_ssl=('ssl' in protocol),\n                                        session=session,\n                                        debug=debug)\n    except AccessError:\n        cev['error'] = ['auth', _('Invalid username or password'),\n                        username, sha1b64(password)]\n    except (ssl.CertificateError, ssl.SSLError):\n        cev['error'] = ['tls', _('Failed to make a secure TLS connection'),\n                        '%s:%s' % (host, port)]\n    except (IOError, OSError):\n        cev['error'] = ['network', _('A network error occurred')]\n        event.data['traceback'] = traceback.format_exc()\n\n    if throw:\n        raise throw(cev['error'][1])\n\n    return None\n\n\nclass POP3_IOError(IOError):\n    pass\n\n\nclass Pop3MailSource(BaseMailSource):\n    \"\"\"\n    This is a mail source that watches over one or more POP3 mailboxes.\n    \"\"\"\n    # This is a helper for the events.\n    __classname__ = 'mailpile.mail_source.pop3.Pop3MailSource'\n\n    def __init__(self, *args, **kwargs):\n        BaseMailSource.__init__(self, *args, **kwargs)\n        self.watching = -1\n\n    def close(self):\n        mbx = self.my_config.mailbox.values()[0]\n        if mbx:\n            pop3 = self.session.config.open_mailbox(self.session,\n                                                    FormatMbxId(mbx._key),\n                                                    prefer_local=False,\n                                                    from_cache=True)\n            if pop3:\n                pop3.close() \n\n    def _sleep(self, *args, **kwargs):\n        self.close()\n        return BaseMailSource._sleep(self, *args, **kwargs)\n\n    def open(self):\n        with self._lock:\n            mailboxes = self.my_config.mailbox.values()\n            if self.watching == len(mailboxes):\n                return True\n            else:\n                self.watching = len(mailboxes)\n\n            for d in ('mailbox_state', ):\n                if d not in self.event.data:\n                    self.event.data[d] = {}\n            self.event.data['connection'] = {\n                'live': False,\n                'error': [False, _('Nothing is wrong')]\n            }\n\n        self._log_status(_('Watching %d POP3 mailboxes') % self.watching)\n        return True\n\n    def _has_mailbox_changed(self, mbx, state):\n        pop3 = self.session.config.open_mailbox(self.session,\n                                                FormatMbxId(mbx._key),\n                                                prefer_local=False)\n        state['stat'] = stat = '%s' % (pop3.stat(), )\n        return (self.event.data.get('mailbox_state', {}).get(mbx._key) != stat)\n\n    def _mark_mailbox_rescanned(self, mbx, state):\n        if 'mailbox_state' in self.event.data:\n            self.event.data['mailbox_state'][mbx._key] = state['stat']\n        else:\n            self.event.data['mailbox_state'] = {mbx._key: state['stat']}\n\n    def _fmt_path(self):\n        return 'src:%s' % (self.my_config._key,)\n\n    def open_mailbox(self, mbx_id, mfn):\n        my_cfg = self.my_config\n        if 'src:' in mfn[:5] and FormatMbxId(mbx_id) in my_cfg.mailbox:\n            debug = ('pop3' in self.session.config.sys.debug) and 99 or 0\n            password = IndirectPassword(self.session.config, my_cfg.password)\n            return _open_pop3_mailbox(self.session, self.event,\n                                      my_cfg.host, my_cfg.port,\n                                      my_cfg.username, password,\n                                      my_cfg.auth_type,\n                                      my_cfg.protocol, debug,\n                                      throw=POP3_IOError)\n        return None\n\n    def discover_mailboxes(self, paths=None):\n        config = self.session.config\n        existing = self._existing_mailboxes()\n        if self._fmt_path() not in existing:\n            idx = config.sys.mailbox.append(self._fmt_path())\n            self.take_over_mailbox(idx)\n            return 1\n        return 0\n\n    def is_mailbox(self, fn):\n        return False\n\n    def _mailbox_name(self, path):\n        return _(\"Inbox\")\n\n    def _create_tag(self, *args, **kwargs):\n        ptag = kwargs.get('parent')\n        try:\n            if ptag:\n                return self.session.config.get_tags(ptag)[0]._key\n        except (IndexError, KeyError):\n            pass\n        return BaseMailSource._create_tag(self, *args, **kwargs)\n\n\ndef TestPop3Settings(session, settings, event):\n    password = IndirectPassword(session.config, settings['password'])\n    conn = _open_pop3_mailbox(session, event,\n                              settings['host'],\n                              int(settings['port']),\n                              settings['username'],\n                              password,\n                              settings.get('auth_type', 'password'),\n                              settings['protocol'],\n                              True)\n    if conn:\n        conn.close()\n        return True\n    return False\n"
  },
  {
    "path": "mailpile/mailboxes/__init__.py",
    "content": "## Dear hackers!\n##\n## It would be great to have more mailbox classes.  They should be derived\n## from or implement the same interfaces as Python's native mailboxes, with\n## the additional constraint that they support pickling and unpickling using\n## cPickle.  The mailbox class is also responsible for generating and parsing\n## a \"pointer\" which should be a short as possible while still encoding the\n## info required to locate this message and this message only within the\n## larger mailbox.\n\nimport time\nfrom urllib import quote, unquote\n\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.index.mailboxes import MailboxIndex\nfrom mailpile.mailutils import MBX_ID_LEN\nfrom mailpile.util import MboxRLock\n\n\n__all__ = ['mbox', 'maildir', 'gmvault', 'macmail', 'pop3', 'wervd',\n           'MBX_ID_LEN',\n           'NoSuchMailboxError', 'IsMailbox', 'OpenMailbox']\n\nMAILBOX_CLASSES = []\n\n\nclass NoSuchMailboxError(OSError):\n    pass\n\n\ndef register(prio, cls):\n    global MAILBOX_CLASSES\n    MAILBOX_CLASSES.append((prio, cls))\n    MAILBOX_CLASSES.sort()\n\n\ndef IsMailbox(fn, config):\n    for pri, mbox_cls in MAILBOX_CLASSES:\n        try:\n            if mbox_cls.parse_path(config, fn):\n                return (True, mbox_cls)\n        except KeyboardInterrupt:\n            raise\n        except:\n            pass\n    return False\n\n\ndef OpenMailbox(fn, config, create=False):\n    for pri, mbox_cls in MAILBOX_CLASSES:\n        try:\n            return mbox_cls(\n                *mbox_cls.parse_path(config, fn, create=create, allow_empty=True))\n        except KeyboardInterrupt:\n            raise\n        except:\n            pass\n    raise ValueError('Not a mailbox: %s' % fn)\n\n\ndef UnorderedPicklable(parent, editable=False):\n    \"\"\"A factory for generating unordered, picklable mailbox classes.\"\"\"\n\n    class UnorderedPicklableMailbox(parent):\n        UNPICKLABLE = []\n\n        def __init__(self, *args, **kwargs):\n            parent.__init__(self, *args, **kwargs)\n            self.editable = editable\n            self.source_map = {}\n            self.is_local = False\n            self._last_updated = None\n            self._lock = MboxRLock()\n            self._index = None\n            self._save_to = None\n            self._encryption_key_func = lambda: None\n            self._decryption_key_func = lambda: None\n            self.__init2__(*args, **kwargs)\n\n\n        def __init2__(self, *args, **kwargs):\n            pass\n\n        def __enter__(self, *args, **kwargs):\n            self._lock.acquire()\n            return self\n\n        def __exit__(self, *args, **kwargs):\n            self._lock.release()\n\n        def __unicode__(self):\n            return unicode(str(self))\n\n        def describe_msg_by_ptr(self, msg_ptr):\n            try:\n                return self._describe_msg_by_ptr(msg_ptr)\n            except KeyError:\n                return _(\"message not found in mailbox\")\n\n        def _describe_msg_by_ptr(self, msg_ptr):\n            return unicode(msg_ptr)\n\n        def __setstate__(self, data):\n            self.__dict__.update(data)\n            self._lock = MboxRLock()\n            with self._lock:\n                self._index = None\n                self._save_to = None\n                self._encryption_key_func = lambda: None\n                self._decryption_key_func = lambda: None\n                if not hasattr(self, 'source_map'):\n                    self.source_map = {}\n                if (len(self.source_map) > 0 and\n                        not hasattr(self, 'is_local') or not self.is_local):\n                    self.is_local = True\n                self.update_toc()\n\n        def __getstate__(self):\n            odict = self.__dict__.copy()\n            # Pickle can't handle function objects.\n            for dk in ['_save_to', '_index', '_last_updated',\n                       '_encryption_key_func', '_decryption_key_func',\n                       '_file', '_lock', 'parsed'] + self.UNPICKLABLE:\n                if dk in odict:\n                    del odict[dk]\n            return odict\n\n        def save(self, session=None, to=None, pickler=None):\n            with self._lock:\n                if to and pickler:\n                    self._save_to = (pickler, to)\n                if self._save_to and len(self) > 0:\n                    pickler, fn = self._save_to\n                    if session:\n                        session.ui.mark(_('Saving %s state to %s')\n                                        % (self, fn))\n                    pickler(self, fn)\n\n        def add_from_source(self, source_id, metadata_kws, *args, **kwargs):\n            with self._lock:\n                key = self.add(*args, **kwargs)\n                self.set_metadata_keywords(key, metadata_kws)\n                self.source_map[source_id] = key\n            return key\n\n        def update_toc(self):\n            self._last_updated = time.time()\n            self._refresh()\n            self._last_updated = time.time()\n\n        def last_updated(self):\n            return self._last_updated\n\n        def get_msg_ptr(self, mboxid, toc_id):\n            return '%s%s' % (mboxid, quote(toc_id))\n\n        def get_file(self, *args, **kwargs):\n            with self._lock:\n                return parent.get_file(self, *args, **kwargs)\n\n        def get_file_by_ptr(self, msg_ptr):\n            return self.get_file(unquote(msg_ptr[MBX_ID_LEN:]))\n\n        def remove_by_ptr(self, msg_ptr):\n            self._last_updated = time.time()\n            return self.remove(unquote(msg_ptr[MBX_ID_LEN:]))\n\n        def get_msg_size(self, toc_id):\n            with self._lock:\n                fd = self.get_file(toc_id)\n                fd.seek(0, 2)\n                return fd.tell()\n\n        def get_bytes(self, toc_id, *args):\n            with self._lock:\n                return self.get_file(toc_id).read(*args)\n\n        def get_string(self, *args, **kwargs):\n            with self._lock:\n                return parent.get_string(self, *args, **kwargs)\n\n        def get_metadata_keywords(self, toc_id):\n            # Subclasses should translate whatever internal metadata they\n            # have into mailpile keywords describing message metadata\n            return []\n\n        def set_metadata_keywords(self, toc_id, kws):\n            pass\n\n        def get_index(self, config, mbx_mid=None):\n            with self._lock:\n                if self._index is None:\n                    self._index = MailboxIndex(config, self, mbx_mid=mbx_mid)\n            return self._index\n\n        def remove(self, *args, **kwargs):\n            with self._lock:\n                self._last_updated = time.time()\n                return parent.remove(self, *args, **kwargs)\n\n        def _get_fd(self, *args, **kwargs):\n            with self._lock:\n                return parent._get_fd(self, *args, **kwargs)\n\n        def _refresh(self, *args, **kwargs):\n            with self._lock:\n                return parent._refresh(self, *args, **kwargs)\n\n        def __setitem__(self, *args, **kwargs):\n            with self._lock:\n                self._last_updated = time.time()\n                return parent.__setitem__(self, *args, **kwargs)\n\n        def __getitem__(self, *args, **kwargs):\n            with self._lock:\n                return parent.__getitem__(self, *args, **kwargs)\n\n\n    return UnorderedPicklableMailbox\n"
  },
  {
    "path": "mailpile/mailboxes/gmvault.py",
    "content": "import mailbox\nimport os\nimport gzip\nimport rfc822\n\nimport mailpile.mailboxes\nimport mailpile.mailboxes.maildir as maildir\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\n\n\nclass MailpileMailbox(maildir.MailpileMailbox):\n    \"\"\"A Gmvault class that supports pickling and a few mailpile specifics.\"\"\"\n\n    @classmethod\n    def parse_path(cls, config, fn, create=False, allow_empty=False):\n        if (os.path.isdir(fn) and\n               os.path.isdirs(os.path.join(fn, 'db')) and\n               os.path.isdirs(os.path.join(fn, 'chats')) and\n               os.path.isdirs(os.path.join(fn, '.info'))):\n            return (fn, )\n        raise ValueError('Not a Gmvault: %s' % fn)\n\n    def __init__(self, dirname, factory=rfc822.Message, create=True):\n        maildir.MailpileMailbox.__init__(self, dirname, factory, create)\n        self._paths = {'db': os.path.join(self._path, 'db')}\n        self._toc_mtimes = {'db': 0}\n\n    def get_file(self, key):\n        \"\"\"Return a file-like representation or raise a KeyError.\"\"\"\n        fname = self._lookup(key)\n        if fname.endswith('.gz'):\n            f = gzip.open(os.path.join(self._path, fname), 'rb')\n        else:\n            f = open(os.path.join(self._path, fname), 'rb')\n        return mailbox._ProxyFile(f)\n\n    def _refresh(self):\n        \"\"\"Update table of contents mapping.\"\"\"\n        # Refresh toc\n        self._toc = {}\n        for path in self._paths:\n            for dirpath, dirnames, filenames in os.walk(self._paths[path]):\n                for filename in [f for f in filenames\n                                 if f.endswith(\".eml.gz\")\n                                 or f.endswith(\".eml\")]:\n                    self._toc[filename] = os.path.join(dirpath, filename)\n\n\nmailpile.mailboxes.register(50, MailpileMailbox)\n"
  },
  {
    "path": "mailpile/mailboxes/macmail.py",
    "content": "import mailbox\nimport sys\nimport os\nimport warnings\nimport rfc822\nimport time\nimport errno\n\nimport mailpile.mailboxes\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.mailboxes import UnorderedPicklable\n\n\nclass _MacMaildirPartialFile(mailbox._PartialFile):\n    def __init__(self, fd):\n        length = int(fd.readline().strip())\n        start = fd.tell()\n        stop = start+length\n        mailbox._PartialFile.__init__(self, fd, start=start, stop=stop)\n\n\nclass MacMaildirMessage(mailbox.Message):\n    def __init__(self, message=None):\n        if hasattr(message, \"read\"):\n            length = int(message.readline().strip())\n            message = message.read(length)\n\n        mailbox.Message.__init__(self, message)\n\n\nclass MacMaildir(mailbox.Mailbox):\n    def __init__(self, dirname, factory=rfc822.Message, create=True):\n        mailbox.Mailbox.__init__(self, dirname, factory, create)\n        if not os.path.exists(self._path):\n            if create:\n                raise NotImplemented(\"Why would we support creation of \"\n                                     \"silly mailboxes?\")\n            else:\n                raise mailbox.NoSuchMailboxError(self._path)\n\n        # What have we here?\n        ds = os.listdir(self._path)\n\n        # Okay, MacMaildirs have Info.plist files\n        if not 'Info.plist' in ds:\n            raise mailbox.FormatError(self._path)\n\n        # Now ignore all the files and dotfiles...\n        ds = [d for d in ds if not d.startswith('.')\n              and os.path.isdir(os.path.join(self._path, d))]\n\n        # There should be exactly one directory left, which is our \"ID\".\n        if len(ds) == 1:\n            self._id = ds[0]\n        else:\n            raise mailbox.FormatError(self._path)\n\n        # And finally, there's a Data folder (with .emlx files  in it)\n        self._mailroot = \"%s/%s/Data/\" % (self._path, self._id)\n        if not os.path.isdir(self._mailroot):\n            raise mailbox.FormatError(self._path)\n\n        self._toc = {}\n        self._last_read = 0\n\n    def remove(self, key):\n        \"\"\"Remove the message or raise error if nonexistent.\"\"\"\n        safe_remove(os.path.join(self._mailroot, self._lookup(key)))\n        try:\n            del self._toc[key]\n        except:\n            pass\n\n    def discard(self, key):\n        \"\"\"If the message exists, remove it.\"\"\"\n        try:\n            self.remove(key)\n        except KeyError:\n            pass\n        except OSError as e:\n            if e.errno != errno.ENOENT:\n                raise\n\n    def __setitem__(self, key, message):\n        \"\"\"Replace a message\"\"\"\n        raise NotImplemented(\"Mailpile is readonly, for now.\")\n\n    def iterkeys(self):\n        self._refresh()\n        for key in self._toc:\n            try:\n                self._lookup(key)\n            except KeyError:\n                continue\n            yield key\n\n    def has_key(self, key):\n        self._refresh()\n        return key in self._toc\n\n    def __len__(self):\n        self._refresh()\n        return len(self._toc)\n\n    def _refresh(self):\n        self._toc = {}\n        paths = [\"\"]\n\n        while not paths == []:\n            curpath = paths.pop(0)\n            fullpath = os.path.join(self._mailroot, curpath)\n            try:\n                for entry in os.listdir(fullpath):\n                    p = os.path.join(fullpath, entry)\n                    if os.path.isdir(p):\n                        paths.append(os.path.join(curpath, entry))\n                    elif entry[-5:] == \".emlx\":\n                        self._toc[entry[:-5]] = os.path.join(curpath, entry)\n            except (OSError, IOError):\n                pass  # Ignore difficulties reading individual folders\n\n    def _lookup(self, key):\n        try:\n            if os.path.exists(os.path.join(self._mailroot, self._toc[key])):\n                return self._toc[key]\n        except KeyError:\n            pass\n        self._refresh()\n        try:\n            return self._toc[key]\n        except KeyError:\n            raise KeyError(\"No message with key %s\" % key)\n\n    def get_message(self, key):\n        f = open(os.path.join(self._mailroot, self._lookup(key)), 'r')\n        msg = MacMaildirMessage(f)\n        f.close()\n        return msg\n\n    def get_file(self, key):\n        f = open(os.path.join(self._mailroot, self._lookup(key)), 'r')\n        return _MacMaildirPartialFile(f)\n\n\nclass MailpileMailbox(UnorderedPicklable(MacMaildir)):\n    \"\"\"A Mac Mail.app maildir class that supports pickling etc.\"\"\"\n    @classmethod\n    def parse_path(cls, config, fn, create=False, allow_empty=False):\n        if (os.path.isdir(fn)\n                and os.path.exists(os.path.join(fn, 'Info.plist'))):\n            return (fn, )\n        raise ValueError('Not a Mac Mail.app Maildir: %s' % fn)\n\n    def __unicode__(self):\n        return _(\"Mac Maildir %s\") % self._mailroot\n\n    def _describe_msg_by_ptr(self, msg_ptr):\n        return _(\"e-mail in file %s\") % self._lookup(msg_ptr[MBX_ID_LEN:])\n\n\nmailpile.mailboxes.register(50, MailpileMailbox)\n"
  },
  {
    "path": "mailpile/mailboxes/maildir.py",
    "content": "import mailbox\nimport os\nimport sys\n\nimport mailpile.mailboxes\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.mailboxes import UnorderedPicklable\n\n\nclass MailpileMailbox(UnorderedPicklable(mailbox.Maildir, editable=True)):\n    \"\"\"A Maildir class that supports pickling and a few mailpile specifics.\"\"\"\n    supported_platform = None\n\n    @classmethod\n    def parse_path(cls, config, fn, create=False, allow_empty=False):\n        if (((cls.supported_platform is None) or\n             (cls.supported_platform == sys.platform[:3].lower())) and\n                ((os.path.isdir(fn) and\n                  os.path.exists(os.path.join(fn, 'cur'))) or\n                 (create and not os.path.exists(fn)))):\n            return (fn, )\n        raise ValueError('Not a Maildir: %s' % fn)\n\n    def _refresh(self):\n        with self._lock:\n            mailbox.Maildir._refresh(self)\n            # Dotfiles are not mail. Ignore them.\n            for t in [k for k in self._toc.keys() if k.startswith('.')]:\n                del self._toc[t]\n\n    def __unicode__(self):\n        return _(\"Maildir at %s\") % self._path\n\n    def _describe_msg_by_ptr(self, msg_ptr):\n        return _(\"e-mail in file %s\") % self._lookup(msg_ptr[MBX_ID_LEN:])\n\n    def get_metadata_keywords(self, toc_id):\n        subdir, name = os.path.split(self._lookup(toc_id))\n        if self.colon in name:\n            flags = name.split(self.colon)[-1]\n            if flags[:2] == '2,':\n                return ['%s:maildir' % c for c in flags[2:]]\n        return []\n\n\nmailpile.mailboxes.register(25, MailpileMailbox)\n"
  },
  {
    "path": "mailpile/mailboxes/maildirwin.py",
    "content": "import mailpile.mailboxes\nimport mailpile.mailboxes.maildir as maildir\n\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\n\n\nclass MailpileMailbox(maildir.MailpileMailbox):\n    \"\"\"A Maildir class for Windows (using ! instead of : in filenames)\"\"\"\n    supported_platform = 'win'\n    colon = '!'\n\n\nmailpile.mailboxes.register(20, MailpileMailbox)\n"
  },
  {
    "path": "mailpile/mailboxes/mbox.py",
    "content": "from __future__ import print_function\nimport errno\nimport mailbox\nimport os\nimport re\nimport threading\nimport time\nimport traceback\n\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.index.mailboxes import MailboxIndex\nfrom mailpile.mailboxes import MBX_ID_LEN, NoSuchMailboxError\nfrom mailpile.util import *\n\n\nclass MboxIndex(MailboxIndex):\n    pass\n\n\nclass MailpileMailbox(mailbox.mbox):\n    \"\"\"A mbox class that supports pickling and a few mailpile specifics.\"\"\"\n    RE_STATUS = re.compile(\n        '^(X-)?Status:\\s*\\S+', flags=re.IGNORECASE|re.MULTILINE)\n\n    @classmethod\n    def parse_path(cls, config, fn, create=False, allow_empty=False):\n        try:\n            firstline = open(fn, 'r').readline()\n            if firstline.startswith('From '):\n                return (fn, )\n            if (allow_empty or create) and not firstline:\n                return (fn, )\n        except:\n            if create and os.path.exists(fn):\n                return (fn, )\n        raise ValueError('Not an mbox: %s' % fn)\n\n    def __init__(self, *args, **kwargs):\n        self._cs = {}\n        mailbox.mbox.__init__(self, *args, **kwargs)\n        self.editable = False\n        self.is_local = False\n        self._last_updated = 0\n        self._mtime = 0\n        self._index = None\n        self._save_to = None\n        self._encryption_key_func = lambda: None\n        self._decryption_key_func = lambda: None\n        self._lock = MboxRLock()\n\n    def __enter__(self, *args, **kwargs):\n        self._lock.acquire()\n        self.lock()\n        return self\n\n    def __exit__(self, *args, **kwargs):\n        self.unlock()\n        self._lock.release()\n\n    def __unicode__(self):\n        return _(\"Unix mbox at %s\") % self._path\n\n    def describe_msg_by_ptr(self, msg_ptr):\n        try:\n            parts, start, length = self._parse_ptr(msg_ptr)\n            return _(\"message at bytes %d..%d\") % (start, start + length)\n        except KeyError:\n            return _(\"message not found in mailbox\")\n\n    def _get_fd(self):\n        return open(self._path, 'rb+')\n\n    def __setstate__(self, dict):\n        self.__dict__.update(dict)\n        self._lock = MboxRLock()\n        self.is_local = False\n        with self._lock:\n            self._save_to = None\n            self._encryption_key_func = lambda: None\n            self._decryption_key_func = lambda: None\n            try:\n                if not os.path.exists(self._path):\n                    raise NoSuchMailboxError(self._path)\n                self._file = self._get_fd()\n            except IOError as e:\n                if e.errno == errno.ENOENT:\n                    raise NoSuchMailboxError(self._path)\n                elif e.errno == errno.EACCES:\n                    self._file = self._get_fd()\n                else:\n                    raise\n        self.update_toc()\n\n    def __getstate__(self):\n        odict = self.__dict__.copy()\n        # Pickle can't handle function objects.\n        for dk in ('_save_to', '_index', '_last_updated',\n                   '_encryption_key_func', '_decryption_key_func',\n                   '_file', '_lock', 'parsed'):\n            if dk in odict:\n                del odict[dk]\n        return odict\n\n    def last_updated(self):\n        return self._last_updated\n\n    def keys(self):\n        self.update_toc()\n        return mailbox.mbox.keys(self)\n\n    def toc_values(self):\n        self.update_toc()\n        return self._toc.values()\n\n    def update_toc(self):\n        fd = self._get_fd()\n        fd.seek(0, 2)\n        cur_length = fd.tell()\n        cur_mtime = os.path.getmtime(self._path)\n        try:\n            if (self._file_length == cur_length and\n                    self._mtime == cur_mtime):\n                return\n        except (NameError, AttributeError):\n            pass\n\n        with self._lock:\n            fd.seek(0)\n            self._next_key = 0\n            self._toc = {}\n            self._cs = {}\n            data = ''\n            start = None\n            len_nl = 1\n            while (cur_length > 0):\n                self._last_updated = time.time()\n                line_pos = fd.tell()\n                line = fd.readline()\n                if line.startswith('From '):\n                    if start is not None:\n                        len_nl = ('\\r' == line[-2]) and 2 or 1\n                        cs4k = self.get_msg_cs4k(0, 0, data[:-len_nl])\n                        self._toc[self._next_key] = (start, line_pos - len_nl)\n                        self._cs[cs4k] = self._next_key\n                        self._cs[self._next_key] = cs4k\n                        self._next_key += 1\n                    start = line_pos\n                    data = line\n                elif line == '':\n                    if (start is not None) and (start != line_pos):\n                        cs4k = self.get_msg_cs4k(0, 0, data[:-len_nl])\n                        self._toc[self._next_key] = (start, line_pos - len_nl)\n                        self._cs[cs4k] = self._next_key\n                        self._cs[self._next_key] = cs4k\n                        self._next_key += 1\n                    break\n                elif len(data) < (4096 + len_nl):\n                    data += line\n            self._file = fd\n            self._file_length = fd.tell()\n            self._mtime = cur_mtime\n        self.save(None)\n\n    def _generate_toc(self):\n        self.update_toc()\n\n    def __setitem__(self, *args, **kwargs):\n        with self._lock:\n            mailbox.mbox.__setitem__(self, *args, **kwargs)\n\n    def __delitem__(self, *args, **kwargs):\n        with self._lock:\n            mailbox.mbox.__delitem__(self, *args, **kwargs)\n\n    def save(self, session=None, to=None, pickler=None):\n        if to and pickler:\n            self._save_to = (pickler, to)\n        if self._save_to and len(self) > 0:\n            with self._lock:\n                pickler, fn = self._save_to\n                if session:\n                    session.ui.mark(_('Saving %s state to %s') % (self, fn))\n                pickler(self, fn)\n\n    def _locked_flush_without_tempfile(self):\n        \"\"\"Dangerous, but we need this for /var/mail/USER on many Linuxes\"\"\"\n        with open(self._path, 'rb+') as new_file:\n            new_toc = {}\n            for key in sorted(self._toc.keys()):\n                start, stop = self._toc[key]\n                new_start = new_file.tell()\n                while True:\n                    buf = self._file.read(min(4096, stop-self._file.tell()))\n                    if buf == '':\n                        break\n                    new_file.write(buf)\n                new_toc[key] = (new_start, new_file.tell())\n            new_file.truncate()\n        self._file.seek(0, 0)\n        self._toc = new_toc\n        self._pending = False\n        self._pending_sync = False\n\n    def flush(self, *args, **kwargs):\n        with self._lock:\n            self._last_updated = time.time()\n            try:\n                if kwargs.get('in_place', False):\n                    self._locked_flush_without_tempfile()\n                else:\n                    mailbox.mbox.flush(self, *args, **kwargs)\n            except OSError:\n                if '_create_temporary' in traceback.format_exc():\n                    self._locked_flush_without_tempfile()\n                else:\n                    raise\n            self._last_updated = time.time()\n\n    def clear(self, *args, **kwargs):\n        with self._lock:\n            mailbox.mbox.clear(self, *args, **kwargs)\n\n    def get_msg_size(self, toc_id):\n        try:\n            with self._lock:\n                # Note: This is 1 byte less than the TOC measures, because\n                #       the final newline is ommitted. The From line is\n                #       included though.\n                start, stop = self._toc[toc_id]\n                return (stop - start)\n        except (IndexError, KeyError, IndexError, TypeError):\n            return 0\n\n    def get_metadata_keywords(self, toc_id):\n        # In an mbox, all metadata is in the message headers.\n        return []\n\n    def set_metadata_keywords(self, *args, **kwargs):\n        pass\n\n    def get_index(self, config, mbx_mid=None):\n        with self._lock:\n            if self._index is None:\n                self._index = MboxIndex(config, self, mbx_mid=mbx_mid)\n        return self._index\n\n    def get_msg_cs(self, start, cs_size, max_length, chars=4, data=None):\n        \"\"\"Generate a checksum of a given length, ignoring Status headers.\"\"\"\n        if data is None:\n            if start is None:\n                raise IOError('No data found (start=None)')\n            with self._lock:\n                fd = self._file\n                fd.seek(start, 0)\n                data = fd.read(min(cs_size, max_length))\n                if data == '':\n                    raise IOError('No data found at %s:%s'\n                                  % (start, max_length))\n        elif len(data) >= cs_size:\n            data = data[:cs_size]\n        return b64w(sha1b64(\n            re.sub(self.RE_STATUS, 'Status: ?', data))[:chars])\n\n    def get_msg_cs4k(self, start, max_length, data=None):\n        \"\"\"A 48-bit (6*8) checksum of the first 4k of message data.\"\"\"\n        return self.get_msg_cs(start, 4096, max_length, chars=8, data=data)\n\n    def get_msg_cs80b(self, start, max_length, data=None):\n        \"\"\"A 24-bit (6*4) checksum of the first 80 bytes of message data.\"\"\"\n        return self.get_msg_cs(start, 80, max_length, data=data)\n\n    def get_msg_ptr(self, mboxid, toc_id, data=None):\n        with self._lock:\n            msg_start = self._toc[toc_id][0]\n            msg_size = self.get_msg_size(toc_id)\n            if (toc_id in self._cs) and (data is None):\n                msg_cs = self._cs[toc_id]\n            else:\n                msg_cs = self.get_msg_cs4k(msg_start, msg_size, data=data)\n            return '%s%s:%s:%s' % (\n                mboxid, b36(msg_start), b36(msg_size), msg_cs)\n\n    def _parse_ptr(self, msg_ptr):\n        parts = msg_ptr[MBX_ID_LEN:].split(':')\n        start = int(parts[0], 36)\n        length = int(parts[1], 36)\n        if len(parts) > 2:\n            if parts[2] in self._cs:\n                start, end = self._toc[self._cs[parts[2]]]\n                length = end - start\n        return parts, start, length\n\n    def _verify_ptr_checksums(self, msg_ptr, start, ignored_fd):\n        \"\"\"Check whether the msg_ptr checksums match the data at [start].\"\"\"\n        with self._lock:\n            parts, ignored_start, length = self._parse_ptr(msg_ptr)\n            cs80b = self.get_msg_cs80b(start, length)\n            if len(parts) > 2:\n                cs4k = self.get_msg_cs4k(start, length)\n                cs = parts[2]\n                if (cs4k != cs and cs80b != cs):\n                    return False\n        return True\n\n    def _possible_message_locations(self, msg_ptr, max_locations=15):\n        \"\"\"Yield possible locations for messages of a given size.\"\"\"\n        with self._lock:\n            parts, pstart, length = self._parse_ptr(msg_ptr)\n\n            # This is where it is SUPPOSED to be, always check that first.\n            starts = [pstart]\n\n            # Extend the list with other messages of the right size.\n            # We accept two lengths, because there were off-by-one errors\n            # in older versions of Mailpile. :-(\n            starts.extend(sorted([\n                b for b, e in self.toc_values()\n                if length in (e-b, e-b+1) and b != pstart]))\n\n        # Yield up to max_locations positions\n        for i, start in enumerate(starts[:max_locations]):\n            yield (start, length)\n\n    def _get_SSLP_by_ptr(self, msg_ptr, verifier=None, from_=False):\n        if verifier is None:\n            verifier = self._verify_ptr_checksums\n        tries = []\n        length = None\n        for from_start, length in self._possible_message_locations(msg_ptr):\n            # We duplicate the file descriptor here, in case other threads\n            # are accessing the same mailbox and moving it around, or in\n            # case we have multiple PartialFile objects in flight at once.\n            tries.append(str(from_start))\n            try:\n                start = from_start\n                stop = from_start + length\n                fd = self._get_fd()\n                if not from_:\n                    fd.seek(start)\n                    length -= len(fd.readline())\n                    start = fd.tell()\n                pf = mailbox._PartialFile(fd, start, stop)\n                if verifier(msg_ptr, from_start, pf):\n                    return (from_start, start, length, pf)\n            except IOError:\n                pass\n        err = '%s: %s %s@%s' % (\n            _('Message not found'), msg_ptr, length, '/'.join(tries))\n        raise IOError(err)\n\n    def update(self, *args, **kwargs):\n        with self._lock:\n            self._cs = {}  # FIXME\n            return mailbox.mbox.update(self, *args, **kwargs)\n\n    def discard(self, *args, **kwargs):\n        with self._lock:\n            self._cs = {}  # FIXME\n            return mailbox.mbox.discard(self, *args, **kwargs)\n\n    def remove(self, *args, **kwargs):\n        with self._lock:\n            self._cs = {}  # FIXME\n            return mailbox.mbox.remove(self, *args, **kwargs)\n\n    def get_file_by_ptr(self, msg_ptr, verifier=None, from_=False):\n        with self._lock:\n            from_start, start, length, pfile = self._get_SSLP_by_ptr(\n                msg_ptr, verifier=verifier, from_=from_)\n        return pfile\n\n    def remove_by_ptr(self, msg_ptr):\n        with self._lock:\n            from_start, start, length, pfile = self._get_SSLP_by_ptr(msg_ptr)\n            keys = [k for k in self._toc if self._toc[k][0] == from_start]\n            if keys:\n                return self.remove(keys[0])\n        raise KeyError('Not found: %s' % msg_ptr)\n\n    def get_bytes(self, toc_id, *args, **kwargs):\n        with self._lock:\n            return self.get_file(toc_id, *args, **kwargs).read()\n\n    def get_file(self, *args, **kwargs):\n        with self._lock:\n            return mailbox.mbox.get_file(self, *args, **kwargs)\n\n\nif __name__ == \"__main__\":\n    import tempfile, time, sys\n    verbose = ('-v' in sys.argv) or ('--verbose' in sys.argv)\n    wait = ('-w' in sys.argv) or ('--wait' in sys.argv)\n\n    MSG_TEMPLATE = \"\"\"\\\nFrom bre@mailpile.is  Mon Jan  1 08:14:00 2018\nReturn-Path: <bre@mailpile.is>\nSubject: %(subject)s\nMessage-ID: <%(msgid)s>\nContent-Length: %(length)s\n\n%(content)s\"\"\"\n\n    problems = tests = 0\n    with tempfile.NamedTemporaryFile() as tf:\n        lengths = []\n        for count in range(0, 35):\n             body = ''.join([\n                 'Hello world, this is a message!\\n'\n                 ] * ((27 * (100-count)) % 1230))\n             message = (MSG_TEMPLATE % {\n                 'subject': 'Test message #%d' % count,\n                 'msgid': '%d@example.com' % count,\n                 'length': len(body),\n                 'content': body})\n             lengths.append(len(message))\n             tf.write(message)\n             tf.write(\"\\n\")\n        tf.flush()\n        if verbose or wait:\n            print('Temporary mailbox in: %s' % tf.name)\n        if wait:\n            raw_input('Press ENTER to continue...')\n\n        pmbx = mailbox.mbox(tf.name)\n        mmbx = MailpileMailbox(tf.name)\n        ptrs = []\n        for i, key in enumerate(mmbx.keys()):\n             msg_ptr = mmbx.get_msg_ptr('0000', key)\n             o_size = lengths[i]\n             c_size = mmbx.get_msg_size(key)\n             f_size = len(mmbx.get_bytes(key, from_=True))\n             f2size = len(mmbx.get_file_by_ptr(msg_ptr, from_=True).read())\n             result = 'ok' if (o_size == c_size == f_size == f2size) else 'BAD'\n             if verbose or result != 'ok':\n                 print(\"%-3.3s [%s/%s/%s] %s ?= %s ?= %s ?= %s\" % (\n                     result, i, key, msg_ptr, o_size, c_size, f_size, f2size))\n             if result != 'ok':\n                 problems += 1\n             tests += 1\n             ptrs.append([msg_ptr, f2size])\n\n        # Remove some messages, bypassing MailpileMailbox\n        deletions = [0, 5, 10, 15, 34]\n        for d in reversed(sorted(deletions)):\n            del pmbx[d]\n        pmbx.flush()\n\n        # Remove a message using MailpileMailbox\n        try:\n            tests += 1\n            deletions.append(1)\n            mmbx.remove_by_ptr(ptrs[1][0])\n            mmbx.flush()\n        except KeyError:\n            problems += 1\n\n        for i, (msg_ptr, f2size) in enumerate(ptrs):\n            tests += 1\n            if i in deletions:\n                try:\n                    mmbx.get_file_by_ptr(msg_ptr, from_=True).read()\n                    problems += 1\n                    print('BAD Found deleted message %s' % msg_ptr)\n                except IOError:\n                    if verbose:\n                        print('ok  IOError on message %s' % msg_ptr)\n                continue\n            f3size = len(mmbx.get_file_by_ptr(msg_ptr, from_=True).read())\n            if (f2size != f3size):\n                problems += 1\n                print('BAD Message %s: wrong size in new location' % msg_ptr)\n            elif verbose:\n                print('ok  Message %s found in new location' % msg_ptr)\n\n        # This is formatted to look like doctest results...\n        print('TestResults(failed=%d, attempted=%d)' % (problems, tests))\n        if wait:\n            raw_input('Tests finished. Press ENTER to clean up...')\n\n    if problems:\n        sys.exit(1)\nelse:\n    import mailpile.mailboxes\n    mailpile.mailboxes.register(90, MailpileMailbox)\n"
  },
  {
    "path": "mailpile/mailboxes/pop3.py",
    "content": "from __future__ import print_function\ntry:\n    import cStringIO as StringIO\nexcept ImportError:\n    import StringIO\n\nimport poplib\nimport socket\nimport ssl\nimport time\nfrom mailbox import Mailbox, Message\n\nimport mailpile.mailboxes\nfrom mailpile.conn_brokers import Master as ConnBroker\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.mailboxes import UnorderedPicklable\nfrom mailpile.util import *\n\n\nclass wrappable_POP3_SSL(poplib.POP3_SSL):\n    \"\"\"\n    Override the default poplib.POP3_SSL init to use socket.create_connection\n    \"\"\"\n    def __init__(self, host,\n                 port=poplib.POP3_SSL_PORT, keyfile=None, certfile=None,\n                 timeout=socket._GLOBAL_DEFAULT_TIMEOUT):\n        self.host = host\n        self.port = port\n        self.keyfile = keyfile\n        self.certfile = certfile\n        self.buffer = \"\"\n        self.sock = socket.create_connection((host, port), timeout)\n        self.file = self.sock.makefile('rb')\n        self.sslobj = ssl.wrap_socket(self.sock, self.keyfile, self.certfile)\n        self._debugging = 0\n        self.welcome = self._getresp()\n\n\nclass UnsupportedProtocolError(Exception):\n    pass\n\n\nclass POP3Mailbox(Mailbox):\n    \"\"\"\n    Basic implementation of POP3 Mailbox.\n    \"\"\"\n    def __init__(self, host,\n                 user=None, password=None, auth_type='password',\n                 use_ssl=True, port=None, debug=False, conn_cls=None,\n                 session=None):\n        \"\"\"Initialize a Mailbox instance.\"\"\"\n        Mailbox.__init__(self, '/')\n        self.host = host\n        self.user = user\n        self.password = password\n        self.auth_type = auth_type\n        self.use_ssl = use_ssl\n        self.port = port\n        self.debug = debug\n        self.conn_cls = conn_cls\n        self.session = session\n\n        self._lock = MboxRLock()\n        self._pop3 = None\n        self._connect()\n\n    def lock(self):\n        pass\n\n    def unlock(self):\n        pass\n\n    def _connect(self):\n        with self._lock:\n            if self._pop3:\n                try:\n                    self._pop3.noop()\n                    return\n                except poplib.error_proto:\n                    self._pop3 = None\n\n            with ConnBroker.context(need=[ConnBroker.OUTGOING_POP3]):\n                if self.conn_cls:\n                    self._pop3 = self.conn_cls(self.host, self.port or 110,\n                                               timeout=120)\n                    self.secure = self.use_ssl\n                elif self.use_ssl:\n                    self._pop3 = wrappable_POP3_SSL(self.host, self.port or 995,\n                                                    timeout=120)\n                    self.secure = True\n                else:\n                    self._pop3 = poplib.POP3(self.host, self.port or 110,\n                                             timeout=120)\n                    self.secure = False\n\n            if hasattr(self._pop3, 'sock'):\n                self._pop3.sock.settimeout(120)\n            if self.debug:\n                self._pop3.set_debuglevel(self.debug)\n\n            self._keys = None\n            try:\n                if self.auth_type.lower() == 'oauth2':\n                    from mailpile.plugins.oauth import OAuth2\n                    token_info = OAuth2.GetFreshTokenInfo(self.session,\n                                                          self.user)\n                    if self.user and token_info and token_info.access_token:\n                        raise AccessError(\"FIXME: Do OAUTH2 Auth!\")\n                    else:\n                        raise AccessError()\n                else:\n                    self._pop3.user(self.user)\n                    self._pop3.pass_(self.password.encode('utf-8'))\n            except poplib.error_proto:\n                raise AccessError()\n\n    def _refresh(self):\n        with self._lock:\n            self._keys = None\n            self.iterkeys()\n\n    def __setitem__(self, key, message):\n        \"\"\"Replace the keyed message; raise KeyError if it doesn't exist.\"\"\"\n        raise NotImplementedError('Method must be implemented by subclass')\n\n    def _get(self, key, _bytes=None):\n        with self._lock:\n            if key not in self.iterkeys():\n                raise KeyError('Invalid key: %s' % key)\n\n            self._connect()\n            if _bytes is not None:\n                lines = max(10, _bytes//30)  # A wild guess!\n                ok, lines, octets = self._pop3.top(self._km[key], lines)\n            else:\n                ok, lines, octets = self._pop3.retr(self._km[key])\n            if not ok.startswith('+OK'):\n                raise KeyError('Invalid key: %s' % key)\n\n        # poplib is stupid in that it loses the linefeeds, so we need to\n        # do some guesswork to bring them back to what the server provided.\n        # If we don't do this jiggering, then sizes don't match up, which\n        # could cause allocation bugs down the line.\n\n        have_octets = sum(len(l) for l in lines)\n        if octets == have_octets + len(lines):\n            lines.append('')\n            data = '\\n'.join(lines)\n        elif octets == have_octets + 2*len(lines):\n            lines.append('')\n            data = '\\r\\n'.join(lines)\n        elif octets == have_octets + len(lines) - 1:\n            data = '\\n'.join(lines)\n        elif octets == have_octets + 2*len(lines) - 2:\n            data = '\\r\\n'.join(lines)\n        else:\n            raise ValueError('Length mismatch in message %s' % key)\n\n        if _bytes is not None:\n            return data[:_bytes]\n        else:\n            return data\n\n    def get_message(self, key):\n        \"\"\"Return a Message representation or raise a KeyError.\"\"\"\n        return Message(self._get(key))\n\n    def get_bytes(self, key, *args):\n        \"\"\"Return a byte string representation or raise a KeyError.\"\"\"\n        return self._get(key, *args)\n\n    def get_file(self, key):\n        \"\"\"Return a file-like representation or raise a KeyError.\"\"\"\n        return StringIO.StringIO(self._get(key))\n\n    def get_msg_size(self, key):\n        with self._lock:\n            self._connect()\n            if key not in self.iterkeys():\n                raise KeyError('Invalid key: %s' % key)\n            ok, info, octets = self._pop3.list(self._km[key]).split()\n            return int(octets)\n\n    def remove(self, key):\n        # FIXME: This is very inefficient if we are deleting multiple\n        #        messages at once.\n        with self._lock:\n            self._connect()\n            if key not in self.iterkeys():\n                raise KeyError('Invalid key: %s' % key)\n            ok = self._pop3.dele(self._km[key])\n            self._refresh()\n\n    def stat(self):\n        with self._lock:\n            self._connect()\n            return self._pop3.stat()\n\n    def iterkeys(self):\n        \"\"\"Return an iterator over keys.\"\"\"\n        # Note: POP3 *without UIDL* is useless.  We don't support it.\n        with self._lock:\n            if self._keys is None:\n                self._connect()\n                try:\n                    stat, key_list, octets = self._pop3.uidl()\n                except poplib.error_proto:\n                    raise UnsupportedProtocolError()\n                self._keys = [tuple(k.split(' ', 1)) for k in key_list]\n                self._km = dict([reversed(k) for k in self._keys])\n            return [k[1] for k in self._keys]\n\n    def __contains__(self, key):\n        \"\"\"Return True if the keyed message exists, False otherwise.\"\"\"\n        return key in self.iterkeys()\n\n    def __len__(self):\n        \"\"\"Return a count of messages in the mailbox.\"\"\"\n        return len(self.iterkeys())\n\n    def flush(self):\n        \"\"\"Write any pending changes to the disk.\"\"\"\n        self.close()\n\n    def close(self):\n        \"\"\"Flush and close the mailbox.\"\"\"\n        try:\n            if self._pop3:\n                self._pop3.quit()\n        finally:\n            self._pop3 = None\n            self._keys = None\n\n\nclass MailpileMailbox(UnorderedPicklable(POP3Mailbox)):\n    UNPICKLABLE = ['_pop3', '_debug']\n\n    @classmethod\n    def parse_path(cls, config, path, create=False, allow_empty=False):\n        path = path.split('/')\n        if path and path[0].lower() in ('pop:', 'pop3:',\n                                        'pop3_ssl:', 'pop3s:'):\n            proto = path[0][:-1].lower()\n            userpart, server = path[2].rsplit(\"@\", 1)\n            user, password = userpart.rsplit(\":\", 1)\n            if \":\" in server:\n                server, port = server.split(\":\", 1)\n            else:\n                port = 995 if ('s' in proto) else 110\n\n            # This is a hack for GMail\n            if 'recent' in path[3:]:\n                user = 'recent:' + user\n\n            if not config:\n                debug = False\n            elif 'pop3' in config.sys.debug:\n                debug = 99\n            elif 'rescan' in config.sys.debug:\n                debug = 1\n            else:\n                debug = False\n\n            # WARNING: Order must match POP3Mailbox.__init__(...)\n            return (server, user, password, 's' in proto, int(port), debug)\n        raise ValueError('Not a POP3 url: %s' % path)\n\n    def save(self, *args, **kwargs):\n        # Do not save state locally\n        pass\n\n\n##[ Test code follows ]#######################################################\n\nif __name__ == \"__main__\":\n    import doctest\n    import sys\n\n    class _MockPOP3(object):\n        \"\"\"\n        Base mock that pretends to be a poplib POP3 connection.\n\n        >>> pm = POP3Mailbox('localhost', user='bad', conn_cls=_MockPOP3)\n        Traceback (most recent call last):\n           ...\n        AccessError\n\n        >>> pm = POP3Mailbox('localhost', user='a', password='b',\n        ...                  conn_cls=_MockPOP3)\n        >>> pm.stat()\n        (2, 123456)\n\n        >>> pm.iterkeys()\n        ['evil', 'good']\n\n        >>> 'evil' in pm, 'bogon' in pm\n        (True, False)\n\n        >>> [msg['subject'] for msg in pm]\n        ['Msg 1', 'Msg 2']\n\n        >>> pm.get_msg_size('evil'), pm.get_msg_size('good')\n        (47, 51)\n\n        >>> pm.get_bytes('evil')\n        'From: test@mailpile.is\\\\nSubject: Msg 1\\\\n\\\\nOh, hi!\\\\n'\n\n        >>> pm.get_bytes('evil', 5)\n        'From:'\n\n        >>> pm['invalid-key']\n        Traceback (most recent call last):\n           ...\n        KeyError: ...\n        \"\"\"\n        TEST_MSG = ('From: test@mailpile.is\\r\\n'\n                    'Subject: Msg N\\r\\n'\n                    '\\r\\n'\n                    'Oh, hi!\\r\\n')\n        DEFAULT_RESULTS = {\n            'user': lambda s, u: '+OK' if (u == 'a') else '-ERR',\n            'pass_': lambda s, u: '+OK Logged in.' if (u == 'b') else '-ERR',\n            'stat': (2, 123456),\n            'noop': '+OK',\n            'list_': lambda s: ('+OK 2 messages:',\n                                ['1 %d' % len(s.TEST_MSG.replace('\\r', '')),\n                                 '2 %d' % len(s.TEST_MSG)], 0),\n            'uidl': ('+OK', ['1 evil', '2 good'], 0),\n            'retr': lambda s, m: ('+OK',\n                                  s.TEST_MSG.replace('N', m).splitlines(),\n                                  len(s.TEST_MSG)\n                                  if m[0] == '2' else\n                                  len(s.TEST_MSG.replace('\\r', ''))),\n            'top': lambda s, m, n: ('+OK',\n                                    s.TEST_MSG.splitlines()[:n],\n                                    len(''.join(s.TEST_MSG.splitlines(1)[:n]))),\n        }\n        RESULTS = {}\n\n        def __init__(self, *args, **kwargs):\n            def mkcmd(rval):\n                def r(rv):\n                    if isinstance(rv, (str, unicode)) and rv[0] != '+':\n                        raise poplib.error_proto(rv)\n                    return rv\n\n                def cmd(*args, **kwargs):\n                    if isinstance(rval, (str, unicode, list, tuple, dict)):\n                        return r(rval)\n                    else:\n                        return r(rval(self, *args, **kwargs))\n\n                return cmd\n            for cmd, rval in dict_merge(self.DEFAULT_RESULTS, self.RESULTS\n                                        ).iteritems():\n                self.__setattr__(cmd, mkcmd(rval))\n\n        def list(self, which=None):\n            msgs = self.list_()\n            if which:\n                return '+OK ' + msgs[1][1-int(which)]\n            return msgs\n\n        def __getattr__(self, attr):\n            return self.__getattribute__(attr)\n\n    class _MockPOP3_Without_UIDL(_MockPOP3):\n        \"\"\"\n        Mock that lacks the UIDL command.\n\n        >>> pm = POP3Mailbox('localhost', user='a', password='b',\n        ...                  conn_cls=_MockPOP3_Without_UIDL)\n        >>> pm.iterkeys()\n        Traceback (most recent call last):\n           ...\n        UnsupportedProtocolError\n        \"\"\"\n        RESULTS = {'uidl': '-ERR'}\n\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                              extraglobs={})\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n\n    if len(sys.argv) > 1:\n        mbx = MailpileMailbox(*MailpileMailbox.parse_path(None, sys.argv[1]))\n        print('Status is: %s' % (mbx.stat(), ))\n        print('Downloading mail and listing subjects, hit CTRL-C to quit')\n        for msg in mbx:\n            print(msg['subject'])\n            time.sleep(2)\n\nelse:\n    mailpile.mailboxes.register(10, MailpileMailbox)\n"
  },
  {
    "path": "mailpile/mailboxes/wervd.py",
    "content": "import email.generator\nimport email.message\nimport mailbox\nimport StringIO\nimport sys\n\nimport mailpile.mailboxes\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.mailboxes import UnorderedPicklable, MBX_ID_LEN\nfrom mailpile.crypto.streamer import *\nfrom mailpile.util import safe_remove\n\n\nclass MailpileMailbox(UnorderedPicklable(mailbox.Maildir, editable=True)):\n    \"\"\"A Maildir class that supports pickling and a few mailpile specifics.\"\"\"\n    supported_platform = None\n    colon = '!'  # Works on both Windows and Unix\n\n# FIXME: Copies were part of the original WERVD spec, to compensate for\n#        the additional fragility of encrypted data. This hasn't been\n#        implemented however, and while SSDs are expensive it is not\n#        obvious that doubling (or tripling...) the storage requirements\n#        for all e-mail is a cost folks are willing to pay for never\n#        losing a message to the occaisional bitflips. So this is all\n#        commented out at the moment. Revisit?\n#   MAX_COPIES = 5\n\n    @classmethod\n    def parse_path(cls, config, fn, create=False, allow_empty=False):\n        if (((cls.supported_platform is None) or\n             (cls.supported_platform == sys.platform[:3].lower())) and\n                ((os.path.isdir(fn) and\n                  os.path.exists(os.path.join(fn, 'cur')) and\n                  os.path.exists(os.path.join(fn, 'wervd.ver'))) or\n                 (create and not os.path.exists(fn)))):\n            return (fn, )\n        raise ValueError('Not a Mailpile Maildir: %s' % fn)\n\n    def __init2__(self, *args, **kwargs):\n        open(os.path.join(self._path, 'wervd.ver'), 'w+b').write('0')\n\n    def __unicode__(self):\n        return _(\"Mailpile mailbox at %s\") % self._path\n\n    def _describe_msg_by_ptr(self, msg_ptr):\n        return _(\"e-mail in file %s\") % self._lookup(msg_ptr[MBX_ID_LEN:])\n\n# FIXME: Copies\n#   def _copy_paths(self, where, key, copies):\n#       for cpn in range(1, copies):\n#           yield os.path.join(self._path, where, '%s.%s' % (key, cpn))\n\n    def remove(self, key):\n        with self._lock:\n            fn = os.path.join(self._path, self._lookup(key))\n            del self._toc[key]\n        safe_remove(fn)\n\n# FIXME: Copies\n#       # Also remove all the copies of this message!\n#       key = os.path.basename(fn)\n#       for where in ('cur', 'new', 'tmp'):\n#           for copy_fn in self._copy_paths(where, key, self.MAX_COPIES):\n#               if os.path.exists(copy_fn):\n#                   safe_remove(copy_fn)\n#               else:\n#                   break\n\n    def _refresh(self):\n        with self._lock:\n            mailbox.Maildir._refresh(self)\n            # WERVD mail names don't have dots in them\n            for t in [k for k in self._toc.keys() if '.' in k]:\n                del self._toc[t]\n        safe_remove()  # Try to remove any postponed removals\n\n    def _get_fd(self, key):\n        with self._lock:\n            fn = os.path.join(self._path, self._lookup(key))\n            mep_key = self._decryption_key_func()\n        fd = open(fn, 'rb')\n        if mep_key:\n            fd = DecryptingStreamer(fd, mep_key=mep_key,\n                                    name='WERVD(%s)' % fn)\n        return fd\n\n    def get_message(self, key):\n        \"\"\"Return a Message representation or raise a KeyError.\"\"\"\n        with self._lock:\n            with self._get_fd(key) as fd:\n                if self._factory:\n                    return self._factory(fd)\n                else:\n                    return mailbox.MaildirMessage(fd)\n\n    def get_string(self, key):\n        with self._lock:\n            with self._get_fd(key) as fd:\n                return fd.read()\n\n    def get_file(self, key):\n        with self._lock:\n            return StringIO.StringIO(self.get_string(key))\n\n    def get_metadata_keywords(self, toc_id):\n        subdir, name = os.path.split(self._lookup(toc_id))\n        if self.colon in name:\n            flags = name.split(self.colon)[-1]\n            if flags[:2] == '2,':\n                return ['%s:maildir' % c for c in flags[2:]]\n        return []\n\n    def set_metadata_keywords(self, toc_id, kws):\n        with self._lock:\n            old_fpath = self._lookup(toc_id)\n            new_fpath = old_fpath.rsplit(self.colon, 1)[0]\n\n            flags = ''.join(sorted([k[0] for k in kws]))\n            if flags:\n                new_fpath += '%s2,%s' % (self.colon, flags)\n                if new_fpath != old_fpath:\n                    os.rename(os.path.join(self._path, old_fpath),\n                              os.path.join(self._path, new_fpath))\n                    self._toc[toc_id] = new_fpath\n\n    def add(self, message):\n        \"\"\"Add message and return assigned key.\"\"\"\n        key = self._encryption_key_func()\n        es = None\n        try:\n            tmpdir = os.path.join(self._path, 'tmp')\n            if not os.path.exists(tmpdir):\n                os.mkdir(tmpdir, 0o700)\n            if key:\n                es = EncryptingStreamer(key,\n                                        dir=tmpdir, name='WERVD',\n                                        delimited=False)\n            else:\n                es = ChecksummingStreamer(dir=tmpdir, name='WERVD')\n            self._dump_message(message, es)\n            es.finish()\n\n            # We are using the MAC to detect file system corruption, not in a\n            # security context - so using as little as 40 bits should be fine.\n            saved = False\n            key = None\n            outer_mac = es.outer_mac_sha256()\n            for l in range(10, len(outer_mac)):\n                key = outer_mac[:l]\n                fn = os.path.join(self._path, 'new', key)\n                if not os.path.exists(fn):\n                    es.save(fn)\n                    saved = self._toc[key] = os.path.join('new', key)\n                    break\n            if not saved:\n                raise mailbox.ExternalClashError(_('Could not find a filename '\n                                                   'for the message.'))\n\n# FIXME: Copies\n#           for fn in self._copy_paths('new', key, copies):\n#               with mailbox._create_carefully(fn) as ofd:\n#                   es.save_copy(ofd)\n\n            return key\n        finally:\n            if es is not None:\n                es.close()\n\n    def _dump_message(self, message, target):\n        if isinstance(message, email.message.Message):\n            gen = email.generator.Generator(target, False, 0)\n            gen.flatten(message)\n        elif isinstance(message, str):\n            target.write(message)\n        else:\n            raise TypeError(_('Invalid message type: %s') % type(message))\n\n    def __setitem__(self, key, message):\n        raise IOError(_('Mailbox messages are immutable'))\n\n\nmailpile.mailboxes.register(15, MailpileMailbox)\n"
  },
  {
    "path": "mailpile/mailutils/__init__.py",
    "content": "# vim: set fileencoding=utf-8 :\n#\nMBX_ID_LEN = 4  # 4x36 == 1.6 million mailboxes\n\n\ndef FormatMbxId(n):\n    if not isinstance(n, (str, unicode)):\n        n = b36(n)\n    if len(n) > MBX_ID_LEN:\n        raise ValueError(_('%s is too large to be a mailbox ID') % n)\n    return ('0000' + n).lower()[-MBX_ID_LEN:]\n\n\nclass NotEditableError(ValueError):\n    pass\n\n\nclass NoFromAddressError(ValueError):\n    pass\n\n\nclass NoRecipientError(ValueError):\n    pass\n\n\nclass InsecureSmtpError(IOError):\n    def __init__(self, msg, details=None):\n        IOError.__init__(self, msg)\n        self.error_info = details or {}\n\n\nclass NoSuchMailboxError(OSError):\n    pass\n"
  },
  {
    "path": "mailpile/mailutils/addresses.py",
    "content": "# vim: set fileencoding=utf-8 :\n#\nfrom __future__ import print_function\nimport base64\nimport copy\nimport quopri\nimport re\n\nfrom mailpile.util import *\n\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.vcard import AddressInfo\n\n\nclass AddressHeaderParser(list):\n    \"\"\"\n    This is a class which tries very hard to interpret the From:, To:\n    and Cc: lines found in real-world e-mail and make sense of them.\n\n    The general strategy of this parser is to:\n       1. parse header data into tokens\n       2. group tokens together into address + name constructs.\n\n    And optionaly,\n       3. normalize each group to a standard format\n\n    In practice, we do this in multiple passes: first a strict pass where\n    we try to parse things semi-sensibly, followed by fuzzier heuristics.\n\n    Ideally, if folks format things correctly we should parse correctly.\n    But if that fails, there are are other passes where we try to cope\n    with various types of weirdness we've seen in the wild. The wild can\n    be pretty wild.\n\n    This parser is NOT (yet) fully RFC2822 compliant - in particular it\n    will get confused by nested comments (see FIXME in tests below).\n\n    The normalization will take pains to ensure that < and , are never\n    present inside a name/comment (even if legal), to make life easier\n    for lame parsers down the line.\n\n    Examples:\n\n    >>> ahp = AddressHeaderParser(AddressHeaderParser.TEST_HEADER_DATA)\n    >>> ai = ahp[1]\n    >>> ai.fn\n    u'Bjarni'\n    >>> ai.address\n    u'bre@klaki.net'\n    >>> ahpn = ahp.normalized_addresses()\n    >>> (ahpn == ahp.TEST_EXPECT_NORMALIZED_ADDRESSES) or ahpn\n    True\n\n    >>> AddressHeaderParser('Weird email@somewhere.com Header').normalized()\n    u'\"Weird Header\" <email@somewhere.com>'\n\n    >>> ai = AddressHeaderParser(unicode_data=ahp.TEST_UNICODE_DATA)\n    >>> ai[0].fn\n    u'Bjarni R\\\\xfanar'\n    >>> ai[0].fn == ahp.TEST_UNICODE_NAME\n    True\n    >>> ai[0].address\n    u'b@c.x'\n    \"\"\"\n\n    TEST_UNICODE_DATA = u'Bjarni R\\xfanar <b@c.x#61A015763D28D4>'\n    TEST_UNICODE_NAME = u'Bjarni R\\xfanar'\n    TEST_HEADER_DATA = \"\"\"\n        bre@klaki.net  ,\n        bre@klaki.net Bjarni ,\n        bre@klaki.net bre@klaki.net,\n        bre@klaki.net (bre@notmail.com),\n        \"<bre@notmail.com>\" <bre@klaki.net>,\n        =?utf-8?Q?=3Cbre@notmail.com=3E?= <bre@klaki.net>,\n        bre@klaki.net ((nested) bre@notmail.com comment),\n        (FIXME: (nested) bre@wrongmail.com parser breaker) bre@klaki.net,\n        undisclosed-recipients-gets-ignored:,\n        Bjarni [mailto:bre@klaki.net],\n        \"This is a key test\" <bre@klaki.net#61A015763D28D410A87B197328191D9B3B4199B4>,\n        bre@klaki.net (Bjarni Runar Einar's son);\n        Bjarni =?iso-8859-1?Q??=is bre @klaki.net,\n        Bjarni =?iso-8859-1?Q?Runar?=Einarsson<' bre'@ klaki.net>,\n        \"Einarsson, Bjarni\" <bre@klaki.net>,\n        =?iso-8859-1?Q?Lonia_l=F6gmannsstofa?= <lonia@example.com>,\n        \"Bjarni @ work\" <bre@pagekite.net>,\n    \"\"\"\n    TEST_EXPECT_NORMALIZED_ADDRESSES = [\n        '<bre@klaki.net>',\n        '\"Bjarni\" <bre@klaki.net>',\n        '\"bre@klaki.net\" <bre@klaki.net>',\n        '\"bre@notmail.com\" <bre@klaki.net>',\n        '=?utf-8?Q?=3Cbre@notmail.com=3E?= <bre@klaki.net>',\n        '=?utf-8?Q?=3Cbre@notmail.com=3E?= <bre@klaki.net>',\n        '\"(nested bre@notmail.com comment)\" <bre@klaki.net>',\n        '\"(FIXME: nested parser breaker) bre@klaki.net\" <bre@wrongmail.com>',\n        '\"Bjarni\" <bre@klaki.net>',\n        '\"This is a key test\" <bre@klaki.net>',\n        '\"Bjarni Runar Einar\\\\\\'s son\" <bre@klaki.net>',\n        '\"Bjarni is\" <bre@klaki.net>',\n        '\"Bjarni Runar Einarsson\" <bre@klaki.net>',\n        '=?utf-8?Q?Einarsson=2C_Bjarni?= <bre@klaki.net>',\n        '=?utf-8?Q?Lonia_l=C3=B6gmannsstofa?= <lonia@example.com>',\n        '\"Bjarni @ work\" <bre@pagekite.net>']\n\n    # Escaping and quoting\n    TXT_RE_QUOTE = '=\\\\?([^\\\\?\\\\s]+)\\\\?([QqBb])\\\\?([^\\\\?\\\\s]*)\\\\?='\n    TXT_RE_QUOTE_NG = TXT_RE_QUOTE.replace('(', '(?:')\n    RE_ESCAPES = re.compile('\\\\\\\\([\\\\\\\\\"\\'])')\n    RE_QUOTED = re.compile(TXT_RE_QUOTE)\n    RE_SHOULD_ESCAPE = re.compile('([\\\\\\\\\"\\'])')\n    RE_SHOULD_QUOTE = re.compile('[^a-zA-Z0-9()\\.:/_ \\'\"+@-]')\n\n    # This is how we normally break a header line into tokens\n    RE_TOKENIZER = re.compile('(<[^<>]*>'                    # <stuff>\n                              '|\\\\([^\\\\(\\\\)]*\\\\)'            # (stuff)\n                              '|\\\\[[^\\\\[\\\\]]*\\\\]'            # [stuff]\n                              '|\"(?:\\\\\\\\\\\\\\\\|\\\\\\\\\"|[^\"])*\"'  # \"stuff\"\n                              \"|'(?:\\\\\\\\\\\\\\\\|\\\\\\\\'|[^'])*'\"  # 'stuff'\n                              '|' + TXT_RE_QUOTE_NG +        # =?stuff?=\n                              '|,'                           # ,\n                              '|;'                           # ;\n                              '|\\\\s+'                        # white space\n                              '|[^\\\\s;,]+'                   # non-white space\n                              ')')\n\n    # Where to insert spaces to help the tokenizer parse bad data\n    RE_MUNGE_TOKENSPACERS = (re.compile('(\\S)(<)'), re.compile('(\\S)(=\\\\?)'))\n\n    # Characters to strip aware entirely when tokenizing munged data\n    RE_MUNGE_TOKENSTRIPPERS = (re.compile('[<>\"]'),)\n\n    # This is stuff we ignore (undisclosed-recipients, etc)\n    RE_IGNORED_GROUP_TOKENS = re.compile('(?i)undisclosed')\n\n    # Things we strip out to try and un-mangle e-mail addresses when\n    # working with bad data.\n    RE_MUNGE_STRIP = re.compile('(?i)(?:\\\\bmailto:|[\\\\s\"\\']|\\?$)')\n\n    # This a simple regular expression for detecting e-mail addresses.\n    RE_MAYBE_EMAIL = re.compile('^[^()<>@,;:\\\\\\\\\"\\\\[\\\\]\\\\s\\000-\\031]+'\n                                '@[a-zA-Z0-9_\\\\.-]+(?:#[A-Za-z0-9]+)?$')\n\n    # We try and interpret non-ascii data as a particular charset, in\n    # this order by default. Should be overridden whenever we have more\n    # useful info from the message itself.\n    DEFAULT_CHARSET_ORDER = ('iso-8859-1', 'utf-8')\n\n    def __init__(self,\n                 data=None, unicode_data=None, charset_order=None, **kwargs):\n        self.charset_order = charset_order or self.DEFAULT_CHARSET_ORDER\n        self._parse_args = kwargs\n        if data is None and unicode_data is None:\n            self._reset(**kwargs)\n        elif data is not None:\n            self.parse(data)\n        else:\n            self.charset_order = ['utf-8']\n            self.parse(unicode_data.encode('utf-8'))\n\n    def _reset(self, _raw_data=None, strict=False, _raise=False):\n        self._raw_data = _raw_data\n        self._tokens = []\n        self._groups = []\n        self[:] = []\n\n    def parse(self, data):\n        return self._parse(data, **self._parse_args)\n\n    def _parse(self, data, strict=False, _raise=False):\n        self._reset(_raw_data=data)\n\n        # 1st pass, strict\n        try:\n            self._tokens = self._tokenize(self._raw_data)\n            self._groups = self._group(self._tokens)\n            self[:] = self._find_addresses(self._groups,\n                                           _raise=(not strict))\n            return self\n        except ValueError:\n            if strict and _raise:\n                raise\n        if strict:\n            return self\n\n        # 2nd & 3rd passes; various types of sloppy\n        for _pass in ('2', '3'):\n            try:\n                self._tokens = self._tokenize(self._raw_data, munge=_pass)\n                self._groups = self._group(self._tokens, munge=_pass)\n                self[:] = self._find_addresses(self._groups,\n                                               munge=_pass,\n                                               _raise=_raise)\n                return self\n            except ValueError:\n                if _pass == '3' and _raise:\n                    raise\n        return self\n\n    def unquote(self, string, charset_order=None):\n        def uq(m):\n            cs, how, data = m.group(1), m.group(2), m.group(3)\n            if how in ('b', 'B'):\n                try:\n                    data = base64.b64decode(''.join(data.split())+'===')\n                except TypeError:\n                    print('FAILED TO B64DECODE: %s' % data)\n            else:\n                data = quopri.decodestring(data, header=True)\n            try:\n                return data.decode(cs)\n            except LookupError:\n                return data.decode('iso-8859-1')  # Always works\n\n        for cs in charset_order or self.charset_order:\n            try:\n                string = string.decode(cs)\n                break\n            except UnicodeDecodeError:\n                pass\n\n        return re.sub(self.RE_QUOTED, uq, string)\n\n    @classmethod\n    def unescape(self, string):\n        return re.sub(self.RE_ESCAPES, lambda m: m.group(1), string)\n\n    @classmethod\n    def escape(self, strng):\n        return re.sub(self.RE_SHOULD_ESCAPE, lambda m: '\\\\'+m.group(0), strng)\n\n    @classmethod\n    def quote(self, strng):\n        if re.search(self.RE_SHOULD_QUOTE, strng):\n            enc = quopri.encodestring(strng.encode('utf-8'), False,\n                                      header=True)\n            enc = enc.replace('<', '=3C').replace('>', '=3E')\n            enc = enc.replace(',', '=2C')\n            return '=?utf-8?Q?%s?=' % enc\n        else:\n            return '\"%s\"' % self.escape(strng)\n\n    def _tokenize(self, string, munge=False):\n        if munge:\n            for ts in self.RE_MUNGE_TOKENSPACERS:\n                string = re.sub(ts, '\\\\1 \\\\2', string)\n            if munge == 3:\n                for ts in self.RE_MUNGE_TOKENSTRIPPERS:\n                    string = re.sub(ts, '', string)\n        return re.findall(self.RE_TOKENIZER, string)\n\n    def _clean(self, token):\n        if token[:1] in ('\"', \"'\"):\n            if token[:1] == token[-1:]:\n                return self.unescape(token[1:-1])\n        elif token.startswith('[mailto:') and token[-1:] == ']':\n            # Just convert [mailto:...] crap into a <address>\n            return '<%s>' % token[8:-1]\n        elif (token[:1] == '[' and token[-1:] == ']'):\n            return token[1:-1]\n        return token\n\n    def _group(self, tokens, munge=False):\n        groups = [[]]\n        for token in tokens:\n            token = token.strip()\n            if token in (',', ';'):\n                # Those tokens SHOULD separate groups, but we don't like to\n                # create groups that have no e-mail addresses at all.\n                if groups[-1]:\n                    if [g for g in groups[-1] if '@' in g]:\n                        groups.append([])\n                        continue\n                    # However, this stuff is just begging to be ignored.\n                    elif [g for g in groups[-1]\n                          if re.match(self.RE_IGNORED_GROUP_TOKENS, g)]:\n                        groups[-1] = []\n                        continue\n            if token:\n                groups[-1].append(self.unquote(self._clean(token)))\n        if not groups[-1]:\n            groups.pop(-1)\n        return groups\n\n    def _find_addresses(self, groups, **fa_kwargs):\n        alist = [self._find_address(g, **fa_kwargs) for g in groups]\n        return [a for a in alist if a]\n\n    def _find_address(self, g, _raise=False, munge=False):\n        if g:\n            g = g[:]\n        else:\n            return []\n\n        def email_at(i):\n            for j in range(0, len(g)):\n                if g[j][:1] == '(' and g[j][-1:] == ')':\n                    g[j] = g[j][1:-1]\n            rest = ' '.join([g[j] for j in range(0, len(g))\n                             if (j != i) and g[j]\n                             ]).replace(' ,', ',').replace(' ;', ';')\n            email, keys = g[i], None\n            if '#' in email[email.index('@'):]:\n                email, key = email.rsplit('#', 1)\n                keys = [{'fingerprint': key}]\n            return AddressInfo(email, rest.strip(), keys=keys)\n\n        def munger(string):\n            if munge:\n                return re.sub(self.RE_MUNGE_STRIP, '', string)\n            else:\n                return string\n\n        # If munging, look for email @domain.com in two parts, rejoin\n        if munge:\n            for i in range(0, len(g)):\n                if i > 0 and i < len(g) and g[i][:1] == '@':\n                    g[i-1:i+1] = [g[i-1]+g[i]]\n                elif i < len(g)-1 and g[i][-1:] == '@':\n                    g[i:i+2] = [g[i]+g[i+1]]\n\n        # 1st, look for <email@domain.com>\n        #\n        # We search from the end, to make the algorithm stable in the case\n        # that the name part also starts with a < (is that allowed?).\n        #\n        for i in reversed(range(0, len(g))):\n            if g[i][:1] == '<' and g[i][-1:] == '>':\n                maybemail = munger(g[i][1:-1])\n                if re.match(self.RE_MAYBE_EMAIL, maybemail):\n                    g[i] = maybemail\n                    return email_at(i)\n\n        # 2nd, look for bare email@domain.com\n        for i in range(0, len(g)):\n            maybemail = munger(g[i])\n            if re.match(self.RE_MAYBE_EMAIL, maybemail):\n                g[i] = maybemail\n                return email_at(i)\n\n        if _raise:\n            raise ValueError('No email found in %s' % (g,))\n        else:\n            return None\n\n    def addresses_list(self, with_keys=False):\n        addresses = []\n        for addr in self:\n            m = addr.address\n            if with_keys and addr.keys:\n                m += \"#\" + addr.keys[0].get('fingerprint')\n            addresses.append(m)\n        return addresses\n\n    def normalized_addresses(self,\n                             addresses=None, quote=True, with_keys=False,\n                             force_name=False):\n        if addresses is None:\n            addresses = self\n        elif not addresses:\n            addresses = []\n        def fmt(ai):\n            email = ai.address\n            if with_keys and ai.keys:\n                fp = ai.keys[0].get('fingerprint')\n                epart = '<%s%s>' % (email, fp and ('#%s' % fp) or '')\n            else:\n                epart = '<%s>' % email\n            if ai.fn:\n                 return ' '.join([quote and self.quote(ai.fn) or ai.fn, epart])\n            elif force_name:\n                 return ' '.join([quote and self.quote(email) or email, epart])\n            else:\n                 return epart\n        return [fmt(ai) for ai in addresses]\n\n    def normalized(self, **kwargs):\n        return ', '.join(self.normalized_addresses(**kwargs))\n\n\nif __name__ == \"__main__\":\n    import doctest\n    import sys\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                              extraglobs={})\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/mailutils/emails.py",
    "content": "# vim: set fileencoding=utf-8 :\n#\n# FIXME: Refactor this monster into mailpile.mailutils.*\n#\nfrom __future__ import print_function\nimport base64\nimport copy\nimport email.header\nimport email.parser\nimport email.utils\nimport errno\nimport mailbox\nimport mimetypes\nimport os\nimport quopri\nimport random\nimport re\nimport StringIO\nimport threading\nimport traceback\nfrom email import encoders\nfrom email.mime.base import MIMEBase\nfrom email.mime.image import MIMEImage\nfrom email.mime.multipart import MIMEMultipart\nfrom email.mime.text import MIMEText\nfrom email.mime.application import MIMEApplication\nfrom mailpile.util import *\nfrom platform import system\nfrom urllib import quote, unquote\nfrom datetime import datetime, timedelta\n\nfrom mailpile.crypto.gpgi import GnuPG\nfrom mailpile.crypto.mime import UnwrapMimeCrypto, MessageAsString\nfrom mailpile.crypto.state import EncryptionInfo, SignatureInfo\nfrom mailpile.eventlog import GetThreadEvent\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.vcard import AddressInfo\nfrom mailpile.mailutils import *\nfrom mailpile.mailutils.addresses import AddressHeaderParser\nfrom mailpile.mailutils.generator import Generator\nfrom mailpile.mailutils.html import extract_text_from_html, clean_html\nfrom mailpile.mailutils.headerprint import HeaderPrints\nfrom mailpile.mailutils.safe import safe_decode_hdr\nfrom mailpile.mailutils.vcal import calendar_parse\n\nGLOBAL_CONTENT_ID_LOCK = MboxLock()\nGLOBAL_CONTENT_ID = random.randint(0, 0xfffffff)\n\ndef MakeContentID():\n    global GLOBAL_CONTENT_ID\n    with GLOBAL_CONTENT_ID_LOCK:\n        GLOBAL_CONTENT_ID += 1\n        GLOBAL_CONTENT_ID %= 0xfffffff\n        return '%x' % GLOBAL_CONTENT_ID\n\n\ndef MakeBoundary():\n    return '==%s==' % okay_random(30)\n\n\ndef MakeMessageID():\n    # We generate a message-ID which is almost entirely random; we\n    # include an element of the local time (give-or-take 36 hours)\n    # to further reduce the odds of any collision.\n    return '<%s%x@mailpile>' % (\n        okay_random(40), time.time() // (3600*48))\n\n\ndef MakeMessageDate(ts=None):\n    # Generate valid dates, but add some jitter to the seconds field\n    # so we're not trivially leaking our exact time. We also avoid\n    # leaking the time zone.\n    return email.utils.formatdate(\n        timeval=(ts or time.time()) + (random.randint(0, 60) - 30),\n        localtime=False)\n\n\nGLOBAL_PARSE_CACHE_LOCK = MboxLock()\nGLOBAL_PARSE_CACHE = []\n\ndef ClearParseCache(cache_id=None, pgpmime=False, full=False):\n    global GLOBAL_PARSE_CACHE\n    with GLOBAL_PARSE_CACHE_LOCK:\n        GPC = GLOBAL_PARSE_CACHE\n        for i in range(0, len(GPC)):\n            if (full or\n                    (pgpmime and GPC[i][1]) or\n                    (cache_id and GPC[i][0] == cache_id)):\n                GPC[i] = (None, None, None)\n\n\ndef ParseMessage(fd, cache_id=None, update_cache=False,\n                     pgpmime='all', config=None, event=None,\n                     allow_weak_crypto=False):\n    global GLOBAL_PARSE_CACHE\n    if not GnuPG:\n        pgpmime = False\n\n    if cache_id is not None and not update_cache:\n        with GLOBAL_PARSE_CACHE_LOCK:\n            for cid, pm, message in GLOBAL_PARSE_CACHE:\n                if cid == cache_id and pm == pgpmime:\n                    return message\n\n    if pgpmime:\n        message = ParseMessage(fd, cache_id=cache_id, pgpmime=False,\n                                   config=config)\n        if message is None:\n            return None\n        if cache_id is not None:\n            # Caching is enabled, let's not clobber the encrypted version\n            # of this message with a fancy decrypted one.\n            message = copy.deepcopy(message)\n        def MakeGnuPG(*args, **kwargs):\n            ev = event or GetThreadEvent()\n            if ev and 'event' not in kwargs:\n                kwargs['event'] = ev\n            return GnuPG(config, *args, **kwargs)\n\n        unwrap_attachments = ('all' in pgpmime or 'att' in pgpmime)\n        UnwrapMimeCrypto(message,\n            protocols={'openpgp': MakeGnuPG},\n            unwrap_attachments=unwrap_attachments,\n            require_MDC=(not allow_weak_crypto))\n\n    else:\n        try:\n            if not hasattr(fd, 'read'):  # Not a file, is it a function?\n                fd = fd()\n            safe_assert(hasattr(fd, 'read'))\n        except (TypeError, AssertionError):\n            return None\n\n        message = email.parser.Parser().parse(fd)\n        msi = message.signature_info = SignatureInfo(bubbly=False)\n        mei = message.encryption_info = EncryptionInfo(bubbly=False)\n        for part in message.walk():\n            part.signature_info = SignatureInfo(parent=msi)\n            part.encryption_info = EncryptionInfo(parent=mei)\n\n    if cache_id is not None:\n        with GLOBAL_PARSE_CACHE_LOCK:\n            # Keep 25 items, put new ones at the front\n            GLOBAL_PARSE_CACHE[24:] = []\n            GLOBAL_PARSE_CACHE[:0] = [(cache_id, pgpmime, message)]\n\n    return message\n\n\ndef GetTextPayload(part):\n    mimetype = part.get_content_type() or 'text/plain'\n    cte = part.get('content-transfer-encoding', '').lower()\n    if mimetype[:5] == 'text/' and cte == 'base64':\n        # Mailing lists like to mess with text/plain parts, and Majordomo\n        # in particular isn't aware of base64 encoding. Compensate!\n        payload = part.get_payload(None, False) or ''\n        parts = payload.split('\\n--')\n        try:\n            parts[0] = base64.b64decode(parts[0])\n        except TypeError:\n            pass\n        return '\\n--'.join(parts)\n    else:\n        return part.get_payload(None, True) or ''\n\n\ndef ExtractEmails(string, strip_keys=True):\n    emails = []\n    startcrap = re.compile('^[\\'\\\"<(]')\n    endcrap = re.compile('[\\'\\\">);]$')\n    string = string.replace('<', ' <').replace('(', ' (')\n    for w in [sw.strip() for sw in re.compile('[,\\s]+').split(string)]:\n        atpos = w.find('@')\n        if atpos >= 0:\n            while startcrap.search(w):\n                w = w[1:]\n            while endcrap.search(w):\n                w = w[:-1]\n            if w.startswith('mailto:'):\n                w = w[7:]\n                if '?' in w:\n                    w = w.split('?')[0]\n            if strip_keys and '#' in w[atpos:]:\n                w = w[:atpos] + w[atpos:].split('#', 1)[0]\n            # E-mail addresses are only allowed to contain ASCII\n            # characters, so we just strip everything else away.\n            emails.append(CleanText(w,\n                                    banned=CleanText.WHITESPACE,\n                                    replace='_').clean)\n    return emails\n\n\ndef ExtractEmailAndName(string):\n    email = (ExtractEmails(string) or [''])[0]\n    name = (string\n            .replace(email, '')\n            .replace('<>', '')\n            .replace('\"', '')\n            .replace('(', '')\n            .replace(')', '')).strip()\n    return email, (name or email)\n\n\ndef CleanHeaders(msg, copy_all=True, tombstones=False):\n    clean_headers = []\n    address_headers_lower = [h.lower() for h in Email.ADDRESS_HEADERS]\n    for key, value in msg.items():\n        lkey = key.lower()\n\n        # Remove headers we don't want to expose\n        if (lkey.startswith('x-mp-internal-') or\n                lkey in ('bcc', 'encryption', 'attach-pgp-pubkey')):\n            if tombstones:\n                clean_headers.append((key, None))\n\n        # Strip the #key part off any e-mail addresses:\n        elif lkey in address_headers_lower:\n            if '#' in value:\n                clean_headers.append((key, re.sub(\n                    r'(@[^<>\\s#]+)#[a-fxA-F0-9]+([>,\\s]|$)', r'\\1\\2', value)))\n            elif copy_all:\n                clean_headers.append((key, value))\n        elif copy_all:\n            clean_headers.append((key, value))\n\n    return clean_headers\n\n\ndef CleanMessage(config, msg):\n    replacements = CleanHeaders(msg, copy_all=False, tombstones=True)\n\n    for key, val in replacements:\n        del msg[key]\n    for key, val in replacements:\n        if val:\n            msg[key] = val\n\n    return msg\n\n\ndef PrepareMessage(config, msg,\n                   sender=None, rcpts=None, events=None, bounce=False):\n    msg = copy.deepcopy(msg)\n\n    # Short circuit if this message has already been prepared.\n    if ('x-mp-internal-sender' in msg and\n            'x-mp-internal-rcpts' in msg and\n            not bounce):\n        return (sender or msg['x-mp-internal-sender'],\n                rcpts or [r.strip()\n                          for r in msg['x-mp-internal-rcpts'].split(',')],\n                msg,\n                events)\n\n    crypto_policy = 'default'\n    crypto_format = 'default'\n\n    rcpts = rcpts or []\n    if bounce:\n        safe_assert(len(rcpts) > 0)\n\n    # Iterate through headers to figure out what we want to do...\n    need_rcpts = not rcpts\n    for hdr, val in msg.items():\n        lhdr = hdr.lower()\n        if lhdr == 'from':\n            sender = sender or val\n        elif lhdr == 'encryption':\n            crypto_policy = val\n        elif need_rcpts and lhdr in ('to', 'cc', 'bcc'):\n            rcpts += AddressHeaderParser(val).addresses_list(with_keys=True)\n\n    # Are we sane?\n    if not sender:\n        raise NoFromAddressError()\n    if not rcpts:\n        raise NoRecipientError()\n\n    # Are we encrypting? Signing?\n    crypto_policy = crypto_policy.lower()\n    if crypto_policy == 'default':\n        crypto_policy = config.prefs.crypto_policy.lower()\n\n    sender = AddressHeaderParser(sender)[0].address\n\n    # FIXME: Shouldn't this be using config.get_profile instead?\n    profile = config.vcards.get_vcard(sender)\n    if profile:\n        crypto_format = (profile.crypto_format or crypto_format).lower()\n    if crypto_format == 'default':\n        crypto_format = 'prefer_inline' if config.prefs.inline_pgp else ''\n\n    # Extract just the e-mail addresses from the RCPT list, make unique\n    rcpts, rr = [], rcpts\n    for r in rr:\n        for e in AddressHeaderParser(r).addresses_list(with_keys=True):\n            if e not in rcpts:\n                rcpts.append(e)\n\n    # Bouncing disables all transformations, including crypto.\n    if not bounce:\n        # This is the BCC hack that Brennan hates!\n        if config.prefs.always_bcc_self and sender not in rcpts:\n            rcpts += [sender]\n\n        # Add headers we require\n        while 'date' in msg:\n            del msg['date']\n        msg['Date'] = MakeMessageDate()\n\n        import mailpile.plugins\n        plugins = mailpile.plugins.PluginManager()\n\n        # Perform pluggable content transformations\n        sender, rcpts, msg, junk = plugins.outgoing_email_content_transform(\n            config, sender, rcpts, msg)\n\n        # Perform pluggable encryption transformations\n        sender, rcpts, msg, matched = plugins.outgoing_email_crypto_transform(\n            config, sender, rcpts, msg,\n            crypto_policy=crypto_policy,\n            crypto_format=crypto_format,\n            cleaner=lambda m: CleanMessage(config, m))\n\n        if crypto_policy and (crypto_policy != 'none') and not matched:\n            raise ValueError(_('Unknown crypto policy: %s') % crypto_policy)\n\n    rcpts = set([r.rsplit('#', 1)[0] for r in rcpts])\n    msg['x-mp-internal-readonly'] = str(int(time.time()))\n    msg['x-mp-internal-sender'] = sender\n    msg['x-mp-internal-rcpts'] = ', '.join(rcpts)\n    return (sender, rcpts, msg, events)\n\n\nclass Email(object):\n    \"\"\"This is a lazy-loading object representing a single email.\"\"\"\n\n    def __init__(self, idx, msg_idx_pos,\n                 msg_parsed=None, msg_parsed_pgpmime=(None, None),\n                 msg_info=None, ephemeral_mid=None):\n        self.index = idx\n        self.config = idx.config\n        self.msg_idx_pos = msg_idx_pos\n        self.ephemeral_mid = ephemeral_mid\n        self.reset_caches(msg_parsed=msg_parsed,\n                          msg_parsed_pgpmime=msg_parsed_pgpmime,\n                          msg_info=msg_info,\n                          clear_parse_cache=False)\n\n    def msg_mid(self):\n        return self.ephemeral_mid or b36(self.msg_idx_pos)\n\n    @classmethod\n    def encoded_hdr(self, msg, hdr, value=None):\n        hdr_value = value or (msg and msg.get(hdr)) or ''\n        try:\n            hdr_value.encode('us-ascii')\n        except (UnicodeEncodeError, UnicodeDecodeError):\n            if hdr.lower() in ('from', 'to', 'cc', 'bcc'):\n                addrs = []\n                for addr in [a.strip() for a in hdr_value.split(',')]:\n                    name, part = [], []\n                    words = addr.split()\n                    for w in words:\n                        if w[0] == '<' or '@' in w:\n                            part.append((w, 'us-ascii'))\n                        else:\n                            name.append(w)\n                    if name:\n                        name = ' '.join(name)\n                        try:\n                            part[0:0] = [(name.encode('us-ascii'), 'us-ascii')]\n                        except:\n                            part[0:0] = [(name, 'utf-8')]\n                        addrs.append(email.header.make_header(part).encode())\n                hdr_value = ', '.join(addrs)\n            else:\n                parts = [(hdr_value, 'utf-8')]\n                hdr_value = email.header.make_header(parts).encode()\n        return hdr_value\n\n    @classmethod\n    def Create(cls, idx, mbox_id, mbx,\n               msg_to=None, msg_cc=None, msg_bcc=None, msg_from=None,\n               msg_subject=None, msg_text='', msg_references=None,\n               msg_id=None, msg_atts=None, msg_headers=None,\n               save=True, ephemeral_mid='not-saved', append_sig=True,\n               use_default_from=True):\n        msg = MIMEMultipart(boundary=MakeBoundary())\n        msg.signature_info = msi = SignatureInfo(bubbly=False)\n        msg.encryption_info = mei = EncryptionInfo(bubbly=False)\n        msg_ts = int(time.time())\n\n        if msg_from:\n            from_email = AddressHeaderParser(unicode_data=msg_from)[0].address\n            from_profile = idx.config.get_profile(email=from_email)\n        elif use_default_from:\n            from_profile = idx.config.get_profile()\n            from_email = from_profile.get('email', None)\n            from_name = from_profile.get('name', None)\n            if from_email and from_name:\n                msg_from = '%s <%s>' % (from_name, from_email)\n        else:\n            from_email = from_profile = from_name = None\n\n        if msg_from:\n            msg['From'] = cls.encoded_hdr(None, 'from', value=msg_from)\n\n        msg['Date'] = MakeMessageDate(msg_ts)\n        msg['Message-Id'] = msg_id or MakeMessageID()\n        msg_subj = (msg_subject or '')\n        msg['Subject'] = cls.encoded_hdr(None, 'subject', value=msg_subj)\n\n        # Privacy trade-off: we want to help recipients do profiling and\n        # discard poorly forged messages that are not from from Mailpile.\n        # However, we don't want to leak too many details for privacy and\n        # security reasons. So no: version or platform info, just the word\n        # Mailpile. This will probably be obvious to a truly hostile\n        # adversary anyway from other details.\n        msg['User-Agent'] = 'Mailpile'\n\n        ahp = AddressHeaderParser()\n        norm = lambda a: ', '.join(sorted(list(set(ahp.normalized_addresses(\n            addresses=a, with_keys=True, force_name=True)))))\n        if msg_to:\n            msg['To'] = cls.encoded_hdr(None, 'to', value=norm(msg_to))\n        if msg_cc:\n            msg['Cc'] = cls.encoded_hdr(None, 'cc', value=norm(msg_cc))\n        if msg_bcc:\n            msg['Bcc'] = cls.encoded_hdr(None, 'bcc', value=norm(msg_bcc))\n        if msg_references:\n            msg['In-Reply-To'] = msg_references[-1]\n            msg['References'] = ', '.join(msg_references)\n\n        if msg_text:\n            try:\n                msg_text.encode('us-ascii')\n                charset = 'us-ascii'\n            except (UnicodeEncodeError, UnicodeDecodeError):\n                charset = 'utf-8'\n            tp = MIMEText(msg_text, _subtype='plain', _charset=charset)\n            tp.signature_info = SignatureInfo(parent=msi)\n            tp.encryption_info = EncryptionInfo(parent=mei)\n            msg.attach(tp)\n            del tp['MIME-Version']\n\n        for k, v in (msg_headers or []):\n            msg[k] = v\n\n        if msg_atts:\n            for att in msg_atts:\n                att = copy.deepcopy(att)\n                att.signature_info = SignatureInfo(parent=msi)\n                att.encryption_info = EncryptionInfo(parent=mei)\n# Disabled for now.\n#               if att.get('content-id') is None:\n#                   att.add_header('Content-Id', MakeContentID())\n                msg.attach(att)\n                del att['MIME-Version']\n\n        # Determine if we want to attach a PGP public key due to policy and\n        # timing...\n        if (idx.config.prefs.gpg_email_key and\n                from_profile and\n                'send_keys' in from_profile.get('crypto_format', 'none')):\n            from mailpile.plugins.crypto_policy import CryptoPolicy\n            addrs = ExtractEmails(norm(msg_to) + norm(msg_cc) + norm(msg_bcc))\n            if CryptoPolicy.ShouldAttachKey(idx.config, emails=addrs):\n                msg[\"Attach-PGP-Pubkey\"] = \"Yes\"\n\n        if save:\n            msg_key = mbx.add(MessageAsString(msg))\n            msg_to = msg_cc = []\n            msg_ptr = mbx.get_msg_ptr(mbox_id, msg_key)\n            msg_id = idx.get_msg_id(msg, msg_ptr)\n            msg_idx, msg_info = idx.add_new_msg(msg_ptr, msg_id, msg_ts,\n                                                msg_from, msg_to, msg_cc, 0,\n                                                msg_subj, '', [])\n            idx.set_conversation_ids(msg_info[idx.MSG_MID], msg,\n                                     subject_threading=False)\n            return cls(idx, msg_idx)\n        else:\n            msg_info = idx.edit_msg_info(idx.BOGUS_METADATA[:],\n                                         msg_mid=ephemeral_mid or '',\n                                         msg_id=msg['Message-ID'],\n                                         msg_ts=msg_ts,\n                                         msg_subject=msg_subj,\n                                         msg_from=msg_from,\n                                         msg_to=msg_to,\n                                         msg_cc=msg_cc)\n            return cls(idx, -1,\n                       msg_info=msg_info,\n                       msg_parsed=msg,\n                       msg_parsed_pgpmime=('basic', msg),\n                       ephemeral_mid=ephemeral_mid)\n\n    def is_editable(self, quick=False):\n        if self.ephemeral_mid:\n            return True\n        if not self.config.is_editable_message(self.get_msg_info()):\n            return False\n        if quick:\n            return True\n        return ('x-mp-internal-readonly' not in self.get_msg(pgpmime=False))\n\n    MIME_HEADERS = ('mime-version', 'content-type', 'content-disposition',\n                    'content-transfer-encoding')\n    UNEDITABLE_HEADERS = ('message-id', ) + MIME_HEADERS\n    MANDATORY_HEADERS = ('From', 'To', 'Cc', 'Bcc', 'Subject',\n                         'Encryption', 'Attach-PGP-Pubkey')\n    ADDRESS_HEADERS = ('From', 'To', 'Cc', 'Bcc', 'Reply-To')\n    HEADER_ORDER = {\n        'in-reply-to': -2,\n        'references': -1,\n        'date': 1,\n        'from': 2,\n        'subject': 3,\n        'to': 4,\n        'cc': 5,\n        'bcc': 6,\n        'encryption': 98,\n        'attach-pgp-pubkey': 99,\n    }\n\n    def _attachment_aid(self, att):\n        aid = att.get('aid')\n        if not aid:\n            cid = att.get('content-id')  # This comes from afar and might\n                                         # be malicious, so check it.\n            if (cid and\n                    cid == CleanText(cid, banned=(CleanText.WHITESPACE +\n                                                  CleanText.FS)).clean):\n                aid = cid\n            else:\n                aid = 'part-%s' % att['count']\n        return aid\n\n    def get_editing_strings(self, tree=None, build_tree=True):\n        if build_tree:\n            tree = self.get_message_tree(want=['editing_strings'], tree=tree)\n\n        strings = {\n            'from': '', 'to': '', 'cc': '', 'bcc': '', 'subject': '',\n            'encryption': '', 'attach-pgp-pubkey': '', 'attachments': {}\n        }\n        header_lines = []\n        body_lines = []\n\n        # We care about header order and such things...\n        hdrs = dict([(h.lower(), h) for h in tree['headers'].keys()\n                     if h.lower() not in self.UNEDITABLE_HEADERS])\n        for mandate in self.MANDATORY_HEADERS:\n            hdrs[mandate.lower()] = hdrs.get(mandate.lower(), mandate)\n        keys = hdrs.keys()\n        keys.sort(key=lambda k: (self.HEADER_ORDER.get(k.lower(), 99), k))\n        lowman = [m.lower() for m in self.MANDATORY_HEADERS]\n        lowadr = [m.lower() for m in self.ADDRESS_HEADERS]\n        for hdr in [hdrs[k] for k in keys]:\n            data = tree['headers'].get(hdr, '')\n            lhdr = hdr.lower()\n            if lhdr in lowadr and lhdr in lowman:\n                adata = tree.get('addresses', {}).get(lhdr, None)\n                if adata is None:\n                    adata = AddressHeaderParser(data)\n                strings[lhdr] = adata.normalized()\n            elif lhdr in lowman:\n                strings[lhdr] = unicode(data)\n            else:\n                header_lines.append(unicode('%s: %s' % (hdr, data)))\n\n        for att in tree['attachments']:\n            aid = self._attachment_aid(att)\n            strings['attachments'][aid] = (att['filename'] or '(unnamed)')\n\n        if not strings['encryption']:\n            strings['encryption'] = unicode(self.config.prefs.crypto_policy)\n\n        def _fixup(t):\n            try:\n                return unicode(t)\n            except UnicodeDecodeError:\n                return t.decode('utf-8')\n\n        strings['headers'] = '\\n'.join(header_lines).replace('\\r\\n', '\\n')\n        strings['body'] = unicode(''.join([_fixup(t['data'])\n                                           for t in tree['text_parts']])\n                                  ).replace('\\r\\n', '\\n')\n\n        return strings\n\n    def get_editing_string(self, tree=None,\n                                 estrings=None,\n                                 attachment_headers=True,\n                                 build_tree=True):\n        if estrings is None:\n            estrings = self.get_editing_strings(tree=tree,\n                                                build_tree=build_tree)\n\n        bits = [estrings['headers']] if estrings['headers'] else []\n        for mh in self.MANDATORY_HEADERS:\n            bits.append('%s: %s' % (mh, estrings[mh.lower()]))\n\n        if attachment_headers:\n            for aid in sorted(estrings['attachments'].keys()):\n                bits.append('Attachment-%s: %s'\n                            % (aid, estrings['attachments'][aid]))\n        bits.append('')\n        bits.append(estrings['body'])\n        return '\\n'.join(bits)\n\n    def _update_att_name(self, part, filename):\n        try:\n            del part['Content-Disposition']\n        except KeyError:\n            pass\n        part.add_header('Content-Disposition', 'attachment',\n                        filename=filename)\n        return part\n\n    def _make_attachment(self, fn, msg, filedata=None):\n        if filedata and fn in filedata:\n            data = filedata[fn]\n        else:\n            if isinstance(fn, unicode):\n                fn = fn.encode('utf-8')\n            data = open(fn, 'rb').read()\n        ctype, encoding = mimetypes.guess_type(fn)\n        maintype, subtype = (ctype or 'application/octet-stream').split('/', 1)\n        if maintype == 'image':\n            att = MIMEImage(data, _subtype=subtype)\n        else:\n            att = MIMEBase(maintype, subtype)\n            att.set_payload(data)\n            encoders.encode_base64(att)\n# Disabled for now.\n#       att.add_header('Content-Id', MakeContentID())\n\n        # FS paths are strings of bytes, should be represented as utf-8 for\n        # correct header encoding.\n        base_fn = os.path.basename(fn)\n        if not isinstance(base_fn, unicode):\n            base_fn = base_fn.decode('utf-8')\n\n        att.add_header('Content-Disposition', 'attachment',\n                       filename=self.encoded_hdr(None, 'file', base_fn))\n\n        att.signature_info = SignatureInfo(parent=msg.signature_info)\n        att.encryption_info = EncryptionInfo(parent=msg.encryption_info)\n        return att\n\n    def update_from_string(self, session, data, final=False):\n        if not self.is_editable():\n            raise NotEditableError(_('Message or mailbox is read-only.'))\n\n        oldmsg = self.get_msg()\n        if not data:\n            outmsg = oldmsg\n\n        else:\n            newmsg = email.parser.Parser().parsestr(data.encode('utf-8'))\n            outmsg = MIMEMultipart(boundary=MakeBoundary())\n            outmsg.signature_info = SignatureInfo(bubbly=False)\n            outmsg.encryption_info = EncryptionInfo(bubbly=False)\n\n            # Copy over editable headers from the input string, skipping blanks\n            for hdr in newmsg.keys():\n                if hdr.startswith('Attachment-') or hdr == 'Attachment':\n                    pass\n                else:\n                    encoded_hdr = self.encoded_hdr(newmsg, hdr)\n                    if len(encoded_hdr.strip()) > 0:\n                        if encoded_hdr == '!KEEP':\n                            if hdr in oldmsg:\n                                outmsg[hdr] = oldmsg[hdr]\n                        else:\n                            outmsg[hdr] = encoded_hdr\n\n            # Copy over the uneditable headers from the old message\n            for hdr in oldmsg.keys():\n                if ((hdr.lower() not in self.MIME_HEADERS)\n                        and (hdr.lower() in self.UNEDITABLE_HEADERS)):\n                    outmsg[hdr] = oldmsg[hdr]\n\n            # Copy the message text\n            new_body = newmsg.get_payload().decode('utf-8')\n            target_width = self.config.prefs.line_length\n            if target_width >= 40 and 'x-mp-internal-no-reflow' not in newmsg:\n                new_body = reflow_text(new_body, target_width=target_width)\n            try:\n                new_body.encode('us-ascii')\n                charset = 'us-ascii'\n            except (UnicodeEncodeError, UnicodeDecodeError):\n                charset = 'utf-8'\n\n            tp = MIMEText(new_body, _subtype='plain', _charset=charset)\n            tp.signature_info = SignatureInfo(parent=outmsg.signature_info)\n            tp.encryption_info = EncryptionInfo(parent=outmsg.encryption_info)\n            outmsg.attach(tp)\n            del tp['MIME-Version']\n\n            # FIXME: Use markdown and template to generate fancy HTML part?\n\n            # Copy the attachments we are keeping\n            attachments = [h for h in newmsg.keys()\n                           if h.lower().startswith('attachment')]\n            if attachments:\n                oldtree = self.get_message_tree(want=['attachments'])\n                for att in oldtree['attachments']:\n                    hdr = 'Attachment-%s' % self._attachment_aid(att)\n                    if hdr in attachments:\n                        outmsg.attach(self._update_att_name(att['part'],\n                                                            newmsg[hdr]))\n                        attachments.remove(hdr)\n\n            # Attach some new files?\n            for hdr in attachments:\n                try:\n                    att = self._make_attachment(newmsg[hdr], outmsg)\n                    outmsg.attach(att)\n                    del att['MIME-Version']\n                except:\n                    pass  # FIXME: Warn user that failed...\n\n        # Save result back to mailbox\n        if final:\n            sender, rcpts, outmsg, ev = PrepareMessage(self.config, outmsg)\n        return self.update_from_msg(session, outmsg)\n\n    def update_from_msg(self, session, newmsg):\n        if not self.is_editable():\n            raise NotEditableError(_('Message or mailbox is read-only.'))\n\n        if self.ephemeral_mid:\n            self.reset_caches(clear_parse_cache=False,\n                              msg_parsed=newmsg,\n                              msg_parsed_pgpmime=('basic', newmsg),\n                              msg_info=self.msg_info)\n\n        else:\n            mbx, ptr, fd = self.get_mbox_ptr_and_fd()\n            fd.close()  # Windows needs this\n\n            # OK, adding to the mailbox worked\n            newptr = ptr[:MBX_ID_LEN] + mbx.add(MessageAsString(newmsg))\n            self.update_parse_cache(newmsg)\n\n            # Remove the old message...\n            mbx.remove_by_ptr(ptr)\n\n            # FIXME: We should DELETE the old version from the index first.\n\n            # Update the in-memory-index\n            mi = self.get_msg_info()\n            mi[self.index.MSG_PTRS] = newptr\n            self.index.set_msg_at_idx_pos(self.msg_idx_pos, mi)\n            self.index.index_email(session, Email(self.index, self.msg_idx_pos))\n            self.reset_caches(clear_parse_cache=False)\n\n        return self\n\n    def reset_caches(self,\n                     msg_info=None,\n                     msg_parsed=None, msg_parsed_pgpmime=(None, None),\n                     clear_parse_cache=True):\n        self.msg_info = msg_info\n        self.msg_parsed = msg_parsed\n        self.msg_parsed_pgpmime = msg_parsed_pgpmime\n        if clear_parse_cache:\n            self.clear_from_parse_cache()\n\n    def update_parse_cache(self, newmsg):\n        cache_id = self.get_cache_id()\n        if cache_id:\n            with GLOBAL_PARSE_CACHE_LOCK:\n                GPC = GLOBAL_PARSE_CACHE\n                for i in range(0, len(GPC)):\n                    if GPC[i][0] == cache_id:\n                        GPC[i] = (cache_id, False, newmsg)\n\n    def clear_from_parse_cache(self):\n        cache_id = self.get_cache_id()\n        if cache_id:\n            ClearParseCache(cache_id=cache_id)\n\n    def delete_message(self, session, flush=True, keep=0):\n        mi = self.get_msg_info()\n        removed, failed, mailboxes = [], [], []\n        kept = keep\n        allow_deletion = session.config.prefs.allow_deletion\n        for msg_ptr, mbox, fd in self.index.enumerate_ptrs_mboxes_fds(mi):\n            try:\n                if mbox:\n                    try:\n                        if keep > 0:\n                            # Note: This will keep messages in the order of\n                            # preference implemented by enumerate_ptrs_...\n                            # FIXME: Allow more nuanced behaviour here.\n                            mbox.get_file_by_ptr(msg_ptr)\n                            keep -= 1\n                        elif allow_deletion:\n                            mbox.remove_by_ptr(msg_ptr)\n                        else:\n                            # FIXME: Allow deletion of local copies ONLY\n                            raise ValueError(\"Deletion is forbidden\")\n                    except (KeyError, IndexError):\n                        # Already gone!\n                        pass\n                    mailboxes.append(mbox)\n                    removed.append(msg_ptr)\n            except (IOError, OSError, ValueError, AttributeError) as e:\n                failed.append(msg_ptr)\n                print('FIXME: Could not delete %s: %s' % (msg_ptr, e))\n\n        if allow_deletion and not failed and not kept:\n            self.index.delete_msg_at_idx_pos(session, self.msg_idx_pos,\n                                             keep_msgid=False)\n        if flush:\n            for m in mailboxes:\n                m.flush()\n            return (not failed, [])\n        else:\n            return (not failed, mailboxes)\n\n    def get_msg_info(self, field=None, uncached=False):\n        if (uncached or not self.msg_info) and not self.ephemeral_mid:\n            self.msg_info = self.index.get_msg_at_idx_pos(self.msg_idx_pos)\n        if field is None:\n            return self.msg_info\n        else:\n            return self.msg_info[field]\n\n    def get_mbox_ptr_and_fd(self):\n        mi = self.get_msg_info()\n        for msg_ptr, mbox, fd in self.index.enumerate_ptrs_mboxes_fds(mi):\n            if fd is not None:\n                # FIXME: How do we know we have the right message?\n                return mbox, msg_ptr, FixupForWith(fd)\n        return None, None, None\n\n    def get_file(self):\n        return self.get_mbox_ptr_and_fd()[2]\n\n    def get_msg_size(self):\n        mbox, ptr, fd = self.get_mbox_ptr_and_fd()\n        with fd:\n            fd.seek(0, 2)\n            return fd.tell()\n\n    def get_metadata_kws(self):\n        # FIXME: Track these somehow...\n        return []\n\n    def get_cache_id(self):\n        if (self.msg_idx_pos >= 0) and not self.ephemeral_mid:\n            return '%s/%s' % (self.index, self.msg_idx_pos)\n        else:\n            return None\n\n    def _get_parsed_msg(self, pgpmime, update_cache=False):\n        weak_crypto_max_age = self.config.prefs.weak_crypto_max_age\n        allow_weak_crypto = False\n        if weak_crypto_max_age > 0:\n            ts = int(self.get_msg_info(self.index.MSG_DATE) or '0', 36)\n            allow_weak_crypto = (ts < weak_crypto_max_age)\n        return ParseMessage(self.get_file,\n            cache_id=self.get_cache_id(),\n            update_cache=update_cache,\n            pgpmime=pgpmime,\n            config=self.config,\n            allow_weak_crypto=allow_weak_crypto,\n            event=GetThreadEvent())\n\n    def _update_crypto_state(self):\n        if not (self.config.tags and\n                self.msg_idx_pos >= 0 and\n                self.msg_parsed_pgpmime[0] and\n                self.msg_parsed_pgpmime[1] and\n                not self.ephemeral_mid):\n            return\n\n        import mailpile.plugins.cryptostate as cs\n        kw = cs.meta_kw_extractor(self.index,\n                                  self.msg_mid(),\n                                  self.msg_parsed_pgpmime[1],\n                                  0, 0)  # msg_size, msg_ts\n\n        # We do NOT want to update tags if we are getting back\n        # a none/none state, as that can happen for the more\n        # complex nested crypto-in-text messages, which a more\n        # forceful parse of the message may have caught earlier.\n        no_sig = self.config.get_tag('mp_sig-none')\n        no_sig = no_sig and '%s:in' % no_sig._key\n        no_enc = self.config.get_tag('mp_enc-none')\n        no_enc = no_enc and '%s:in' % no_enc._key\n        if no_sig not in kw or no_enc not in kw:\n            msg_info = self.get_msg_info()\n            msg_tags = msg_info[self.index.MSG_TAGS].split(',')\n            msg_tags = sorted([t for t in msg_tags if t])\n\n            # Note: this has the side effect of cleaning junk off\n            #       the tag list, not just updating crypto state.\n            def tcheck(tag_id):\n                tag = self.config.get_tag(tag_id)\n                return (tag and tag.slug[:6] not in ('mp_enc', 'mp_sig'))\n            new_tags = sorted([t for t in msg_tags if tcheck(t)] +\n                              [ti.split(':', 1)[0] for ti in kw\n                               if ti.endswith(':in')])\n\n            if msg_tags != new_tags:\n                msg_info[self.index.MSG_TAGS] = ','.join(new_tags)\n                self.index.set_msg_at_idx_pos(self.msg_idx_pos, msg_info)\n\n    def get_msg(self, pgpmime='default', crypto_state_feedback=True):\n        if pgpmime:\n            if pgpmime == 'default':\n                pgpmime = 'basic' if self.is_editable() else 'all'\n\n            if self.msg_parsed_pgpmime[0] == pgpmime:\n                result = self.msg_parsed_pgpmime[1]\n            else:\n                result = self._get_parsed_msg(pgpmime)\n                self.msg_parsed_pgpmime = (pgpmime, result)\n\n                # Post-parse, we want to make sure that the crypto-state\n                # recorded on this message's metadata is up to date.\n                if crypto_state_feedback:\n                    self._update_crypto_state()\n        else:\n            if not self.msg_parsed:\n                self.msg_parsed = self._get_parsed_msg(pgpmime)\n            result = self.msg_parsed\n        if not result:\n            raise IndexError(_('Message not found'))\n        return result\n\n    def is_thread(self):\n        return ((self.get_msg_info(self.index.MSG_THREAD_MID)) or\n                (0 < len(self.get_msg_info(self.index.MSG_REPLIES))))\n\n    def get(self, field, default=''):\n        \"\"\"Get one (or all) indexed fields for this mail.\"\"\"\n        field = field.lower()\n        if field == 'subject':\n            return self.get_msg_info(self.index.MSG_SUBJECT)\n        elif field == 'from':\n            return self.get_msg_info(self.index.MSG_FROM)\n        else:\n            raw = ' '.join(self.get_msg(pgpmime=False).get_all(field, default))\n            return safe_decode_hdr(hdr=raw) or raw\n\n    def get_sender(self):\n        try:\n            ahp = AddressHeaderParser(unicode_data=self.get('from'))\n            return ahp[0].address\n        except IndexError:\n            return None\n\n    def get_headerprints(self):\n        return HeaderPrints(self.get_msg(pgpmime='basic'))\n\n    def get_msg_summary(self):\n        # We do this first to make sure self.msg_info is loaded\n        msg_mid = self.get_msg_info(self.index.MSG_MID)\n        return [\n            msg_mid,\n            self.get_msg_info(self.index.MSG_ID),\n            self.get_msg_info(self.index.MSG_FROM),\n            self.index.expand_to_list(self.msg_info),\n            self.get_msg_info(self.index.MSG_SUBJECT),\n            self.get_msg_info(self.index.MSG_BODY),\n            self.get_msg_info(self.index.MSG_DATE),\n            self.get_msg_info(self.index.MSG_TAGS).split(','),\n            self.is_editable(quick=True)\n        ]\n\n    def _find_attachments(self, att_id, negative=False):\n        msg = self.get_msg()\n        count = 0\n        for part in (msg.walk() if msg else []):\n            mimetype = (part.get_content_type() or 'text/plain').lower()\n            if mimetype.startswith('multipart/'):\n                continue\n\n            count += 1\n            content_id = part.get('content-id', '')\n            pfn = safe_decode_hdr(hdr=part.get_filename() or '')\n\n            if (('*' == att_id)\n                    or ('#%s' % count == att_id)\n                    or ('part-%s' % count == att_id)\n                    or (content_id == att_id)\n                    or (mimetype == att_id)\n                    or (pfn.lower().endswith('.%s' % att_id))\n                    or (pfn == att_id)):\n                if not negative:\n                    yield (count, content_id, pfn, mimetype, part)\n            elif negative:\n                yield (count, content_id, pfn, mimetype, part)\n\n    def add_attachments(self, session, filenames, filedata=None):\n        if not self.is_editable():\n            raise NotEditableError(_('Message or mailbox is read-only.'))\n        msg = self.get_msg()\n        for fn in filenames:\n            att = self._make_attachment(fn, msg, filedata=filedata)\n            msg.attach(att)\n            del att['MIME-Version']\n        return self.update_from_msg(session, msg)\n\n    def remove_attachments(self, session, *att_ids):\n        if not self.is_editable():\n            raise NotEditableError(_('Message or mailbox is read-only.'))\n\n        remove = []\n        for att_id in att_ids:\n            for count, cid, pfn, mt, part in self._find_attachments(att_id):\n                remove.append(self._attachment_aid({\n                    'msg_mid': self.msg_mid(),\n                    'count': count,\n                    'content-id': cid,\n                    'filename': pfn,\n                }))\n\n        es = self.get_editing_strings()\n        es['headers'] = None\n        for k in remove:\n            if k in es['attachments']:\n                del es['attachments'][k]\n\n        estring = self.get_editing_string(estrings=es)\n        return self.update_from_string(session, estring)\n\n    def extract_attachment(self, session, att_id, name_fmt=None, mode='get'):\n        extracted = 0\n        filename, attributes = '', {}\n        for (count, content_id, pfn, mimetype, part\n                ) in self._find_attachments(att_id):\n            payload = part.get_payload(None, True) or ''\n            attributes = {\n                'msg_mid': self.msg_mid(),\n                'count': count,\n                'length': len(payload),\n                'content-id': content_id,\n                'filename': pfn}\n            attributes['aid'] = self._attachment_aid(attributes)\n            if pfn:\n                if '.' in pfn:\n                    pfn, attributes['att_ext'] = pfn.rsplit('.', 1)\n                    attributes['att_ext'] = attributes['att_ext'].lower()\n                attributes['att_name'] = pfn\n            if mimetype:\n                attributes['mimetype'] = mimetype\n\n            filesize = len(payload)\n            if mode.startswith('inline'):\n                attributes['data'] = payload\n                session.ui.notify(_('Extracted attachment %s') % att_id)\n            elif mode.startswith('preview'):\n                attributes['thumb'] = True\n                attributes['mimetype'] = 'image/jpeg'\n                attributes['disposition'] = 'inline'\n                thumb = StringIO.StringIO()\n                if thumbnail(payload, thumb, height=250):\n                    attributes['length'] = thumb.tell()\n                    filename, fd = session.ui.open_for_data(\n                        name_fmt=name_fmt, attributes=attributes)\n                    thumb.seek(0)\n                    fd.write(thumb.read())\n                    fd.close()\n                    session.ui.notify(_('Wrote preview to: %s') % filename)\n                else:\n                    session.ui.notify(_('Failed to generate thumbnail'))\n                    raise UrlRedirectException('/static/img/image-default.png')\n            else:\n                WHITELIST = ('image/png',\n                             'image/gif',\n                             'image/jpeg',\n                             'image/tiff',\n                             'audio/mp3',\n                             'audio/ogg',\n                             'audio/x-wav',\n                             'audio/mpeg',\n                             'video/mpeg',\n                             'video/ogg',\n                             'application/pdf')\n                if mode.startswith('get') and mimetype in WHITELIST:\n                    # This allows the browser to (optionally) handle the\n                    # content, instead of always forcing a download dialog.\n                    attributes['disposition'] = 'inline'\n                filename, fd = session.ui.open_for_data(\n                    name_fmt=name_fmt, attributes=attributes)\n                fd.write(payload)\n                session.ui.notify(_('Wrote attachment to: %s') % filename)\n                fd.close()\n            extracted += 1\n\n        if 0 == extracted:\n            session.ui.notify(_('No attachments found for: %s') % att_id)\n            return None, None\n        else:\n            return filename, attributes\n\n    def get_message_tags(self):\n        tids = self.get_msg_info(self.index.MSG_TAGS).split(',')\n        return [self.config.get_tag(t) for t in tids]\n\n    def get_message_tree(self, want=None, tree=None, pgpmime='default'):\n        msg = self.get_msg(pgpmime=pgpmime)\n        want = list(want) if (want is not None) else None\n        tree = tree or {'_cleaned': []}\n        tree['id'] = self.get_msg_info(self.index.MSG_ID)\n\n        if want is not None:\n            if 'editing_strings' in want or 'editing_string' in want:\n                want.extend(['text_parts', 'headers', 'attachments',\n                             'addresses'])\n\n        for p in 'text_parts', 'html_parts', 'vcal_parts', 'attachments':\n            if want is None or p in want:\n                tree[p] = []\n\n        if (want is None or 'summary' in want) and 'summary' not in tree:\n            tree['summary'] = self.get_msg_summary()\n\n        if (want is None or 'tags' in want) and 'tags' not in tree:\n            tree['tags'] = self.get_msg_info(self.index.MSG_TAGS).split(',')\n\n        if (want is None or 'conversation' in want\n                ) and 'conversation' not in tree:\n            tree['conversation'] = {}\n            conv_id = self.get_msg_info(self.index.MSG_THREAD_MID)\n            if conv_id:\n                conv_id = conv_id.split('/')[0]\n                conv = Email(self.index, int(conv_id, 36))\n                tree['conversation'] = convs = [conv.get_msg_summary()]\n                for rid in conv.get_msg_info(self.index.MSG_REPLIES\n                                             ).split(','):\n                    if rid:\n                        convs.append(Email(self.index, int(rid, 36)\n                                           ).get_msg_summary())\n\n        if (want is None or 'headerprints' in want):\n            tree['headerprints'] = self.get_headerprints()\n\n        if (want is None or 'headers' in want) and 'headers' not in tree:\n            tree['headers'] = {}\n            for hdr in msg.keys():\n                tree['headers'][hdr] = safe_decode_hdr(msg, hdr)\n\n        if (want is None or 'headers_lc' in want\n                ) and 'headers_lc' not in tree:\n            tree['headers_lc'] = {}\n            for hdr in msg.keys():\n                tree['headers_lc'][hdr.lower()] = safe_decode_hdr(msg, hdr)\n\n        if (want is None or 'header_list' in want\n                ) and 'header_list' not in tree:\n            tree['header_list'] = [(k, safe_decode_hdr(msg, k, hdr=v))\n                                   for k, v in msg.items()]\n\n        if (want is None or 'addresses' in want\n                ) and 'addresses' not in tree:\n            address_headers_lower = [h.lower() for h in self.ADDRESS_HEADERS]\n            tree['addresses'] = {}\n            for hdr in msg.keys():\n                hdrl = hdr.lower()\n                if hdrl in address_headers_lower:\n                    tree['addresses'][hdrl] = AddressHeaderParser(msg[hdr])\n\n        # Note: count algorithm must match that used in extract_attachment\n        #       above\n        count = 0\n        broken_text_part = None\n        for part in msg.walk():\n            crypto = {\n                'signature': part.signature_info,\n                'encryption': part.encryption_info}\n\n            mimetype = (part.get_content_type() or 'text/plain').lower()\n            if (mimetype.startswith('multipart/')\n                    or mimetype == \"application/pgp-encrypted\"):\n                continue\n            try:\n                if (mimetype == \"application/octet-stream\"\n                        and part.cryptedcontainer is True):\n                    continue\n            except:\n                pass\n\n            count += 1\n            disposition = part.get('content-disposition', 'inline').lower()\n            if (disposition[:6] == 'inline'\n                    and mimetype.startswith('text/')):\n                payload, charset = self.decode_payload(part)\n                start = payload[:100].strip()\n\n                if mimetype == 'text/html':\n                    if want is None or 'html_parts' in want:\n                        tree['html_parts'].append({\n                            'charset': charset,\n                            'type': 'html',\n                            'data': clean_html(payload),\n                            'count': count,\n                            'mimetype': mimetype,\n                            'aid': 'part-%d' % count})\n\n                elif mimetype == \"text/calendar\":\n                    if want is None or 'vcal_parts' in want:\n                        tree[\"vcal_parts\"].extend(calendar_parse(payload))\n\n                elif want is None or 'text_parts' in want:\n                    for ht in ('<div', '<html', '<p>', '<p ', '<table', '<body'):\n                        if start.startswith(ht):\n                            broken_text_part = payload\n                            payload = extract_text_from_html(payload)\n                            break\n\n                    # Ignore white-space only text parts, they usually mean\n                    # the message is HTML only and we want the code below\n                    # to try and extract meaning from it.\n                    if (start or payload.strip()) != '':\n                        tree['text_parts'].extend(self.parse_text_part(\n                            payload, charset, crypto, mimetype, count))\n\n            elif want is None or 'attachments' in want:\n                filename_org = safe_decode_hdr(hdr=part.get_filename() or '')\n                filename = CleanText(filename_org,\n                                     banned=(CleanText.HTML +\n                                             CleanText.CRLF + '\\\\/'),\n                                     replace='_').clean\n                att = {\n                    'mimetype': mimetype,\n                    'count': count,\n                    'part': part,\n                    'length': len(part.get_payload(None, True) or ''),\n                    'content-id': part.get('content-id', ''),\n                    'filename': filename,\n                    'crypto': crypto}\n                att['aid'] = self._attachment_aid(att)\n                tree['attachments'].append(att)\n                if filename_org != filename:\n                    tree['_cleaned'].append('att: %s' % att['aid'])\n\n        if want is None or 'text_parts' in want:\n            if tree.get('html_parts') and not tree.get('text_parts'):\n                html_part = tree['html_parts'][0]\n                payload = extract_text_from_html(html_part['data'])\n                text_parts = self.parse_text_part(\n                    payload, html_part['charset'], crypto, None, None)\n                tree['text_parts'].extend(text_parts)\n            elif broken_text_part and not tree.get('text_parts'):\n                tree['text_parts'].extend(broken_text_part)\n\n        if self.is_editable():\n            if not want or 'editing_strings' in want:\n                tree['editing_strings'] = self.get_editing_strings(\n                    tree, build_tree=False)\n            if not want or 'editing_string' in want:\n                tree['editing_string'] = self.get_editing_string(\n                    tree, build_tree=False)\n\n        if want is None or 'crypto' in want:\n            if 'crypto' not in tree:\n                tree['crypto'] = {'encryption': msg.encryption_info,\n                                  'signature': msg.signature_info}\n            else:\n                tree['crypto']['encryption'] = msg.encryption_info\n                tree['crypto']['signature'] = msg.signature_info\n\n        msg.signature_info.mix_bubbles()\n        msg.encryption_info.mix_bubbles()\n        return tree\n\n    # FIXME: This should be configurable by the user, depending on where\n    #        he lives and what kind of e-mail he gets.\n    CHARSET_PRIORITY_LIST = ['utf-8', 'iso-8859-1']\n\n    def decode_text(self, payload, charset='utf-8', binary=True):\n        if charset:\n            charsets = [charset] + [c for c in self.CHARSET_PRIORITY_LIST\n                                    if charset.lower() != c]\n        else:\n            charsets = self.CHARSET_PRIORITY_LIST\n\n        for charset in charsets:\n            try:\n                payload = payload.decode(charset)\n                return payload, charset\n            except (UnicodeDecodeError, TypeError, LookupError):\n                pass\n\n        if binary:\n            return payload, '8bit'\n        else:\n            return _('[Binary data suppressed]\\n'), 'utf-8'\n\n    def decode_payload(self, part):\n        charset = part.get_content_charset() or None\n        return self.decode_text(GetTextPayload(part), charset=charset)\n\n    def parse_text_part(self, data, charset, crypto, mimetype, count):\n        psi = crypto['signature']\n        pei = crypto['encryption']\n        current = {\n            'type': 'bogus',\n            'charset': charset,\n            'crypto': {\n                'signature': SignatureInfo(parent=psi),\n                'encryption': EncryptionInfo(parent=pei)}}\n        parse = []\n        block = 'body'\n        clines = []\n        for count, line in enumerate(data.splitlines(True)):\n            block, ltype = self.parse_line_type(line, block, count)\n            if ltype != current['type']:\n\n                # This is not great, it's a hack to move the preamble\n                # before a quote section into the quote itself.\n                if ltype == 'quote' and clines and '@' in clines[-1]:\n                    current['data'] = ''.join(clines[:-1])\n                    clines = clines[-1:]\n                elif (ltype == 'quote' and len(clines) > 2\n                        and '@' in clines[-2] and '' == clines[-1].strip()):\n                    current['data'] = ''.join(clines[:-2])\n                    clines = clines[-2:]\n                else:\n                    clines = []\n\n                current = {\n                    'type': ltype,\n                    'data': ''.join(clines),\n                    'charset': charset,\n                    'crypto': {\n                        'signature': SignatureInfo(parent=psi),\n                        'encryption': EncryptionInfo(parent=pei)}}\n                parse.append(current)\n                if len(parse) == 1 and count and mimetype:\n                    current['aid'] = 'part-%d' % count\n                    current['mimetype'] = mimetype\n            current['data'] += line\n            clines.append(line)\n        return parse\n\n    BARE_QUOTE_STARTS = re.compile('(?i)^-+\\s*Original Message.*-+$')\n    GIT_DIFF_STARTS = re.compile('^diff --git a/.*b/')\n    GIT_DIFF_LINE = re.compile('^([ +@-]|index |$)')\n\n    def parse_line_type(self, line, block, line_count):\n        # FIXME: Detect forwarded messages, ...\n\n        if (block in ('body', 'quote', 'barequote')\n                and line in ('-- \\n', '-- \\r\\n', '- --\\n', '- --\\r\\n')):\n            return 'signature', 'signature'\n\n        if block == 'signature':\n            return block, block\n\n        if block == 'barequote':\n            return 'barequote', 'quote'\n\n        stripped = line.rstrip()\n\n        if stripped == GnuPG.ARMOR_BEGIN_SIGNED:\n            return 'pgpbeginsigned', 'pgpbeginsigned'\n        if block == 'pgpbeginsigned':\n            if line.startswith('Hash: ') or stripped == '':\n                return 'pgpbeginsigned', 'pgpbeginsigned'\n            else:\n                return 'pgpsignedtext', 'pgpsignedtext'\n        if block == 'pgpsignedtext':\n            if stripped == GnuPG.ARMOR_BEGIN_SIGNATURE:\n                return 'pgpsignature', 'pgpsignature'\n            else:\n                return 'pgpsignedtext', 'pgpsignedtext'\n        if block == 'pgpsignature':\n            if stripped == GnuPG.ARMOR_END_SIGNATURE:\n                return 'pgpend', 'pgpsignature'\n            else:\n                return 'pgpsignature', 'pgpsignature'\n\n        if (stripped == GnuPG.ARMOR_BEGIN_ENCRYPTED\n                # This is an EFail mitigation: do not decrypt content\n                # inlined somewhere well below a bunch of other stuff.\n                # The encrypted content must be high up enough that\n                # the user will plausibly see it when reading.\n                and line_count < 10 and block == 'body'):\n            return 'pgpbegin', 'pgpbegin'\n        if block == 'pgpbegin':\n            if ':' in line or stripped == '':\n                return 'pgpbegin', 'pgpbegin'\n            else:\n                return 'pgptext', 'pgptext'\n        if block == 'pgptext':\n            if stripped == GnuPG.ARMOR_END_ENCRYPTED:\n                return 'pgpend', 'pgpend'\n            else:\n                return 'pgptext', 'pgptext'\n\n        if self.BARE_QUOTE_STARTS.match(stripped):\n            return 'barequote', 'quote'\n\n        if block == 'quote':\n            if stripped == '':\n                return 'quote', 'quote'\n        if line.startswith('>'):\n            return 'quote', 'quote'\n\n        if self.GIT_DIFF_STARTS.match(stripped):\n            return 'gitdiff', 'quote'\n\n        if block == 'gitdiff':\n            if self.GIT_DIFF_LINE.match(stripped):\n                return 'gitdiff', 'quote'\n\n        return 'body', 'text'\n\n    WANT_MSG_TREE_PGP = ('text_parts', 'crypto')\n    PGP_OK = {\n        'pgpbeginsigned': 'pgpbeginverified',\n        'pgpsignedtext': 'pgpverifiedtext',\n        'pgpsignature': 'pgpverification',\n        'pgpbegin': 'pgpbeginverified',\n        'pgptext': 'pgpsecuretext',\n        'pgpend': 'pgpverification',\n    }\n\n    def evaluate_pgp(self, tree, check_sigs=True, decrypt=False,\n                                 crypto_state_feedback=True, event=None):\n        if 'text_parts' not in tree:\n            return tree\n\n        pgpdata = []\n        for part in tree['text_parts']:\n            if 'crypto' not in part:\n                part['crypto'] = {}\n\n            ei = si = None\n            if check_sigs:\n                if part['type'] == 'pgpbeginsigned':\n                    pgpdata = [part]\n                elif part['type'] == 'pgpsignedtext':\n                    pgpdata.append(part)\n                elif part['type'] == 'pgpsignature':\n                    pgpdata.append(part)\n                    try:\n                        gpg = GnuPG(self.config, event=event)\n                        message = ''.join([p['data'].encode(p['charset'])\n                                           for p in pgpdata])\n                        si = gpg.verify(message)\n                        pgpdata[0]['data'] = ''\n                        pgpdata[1]['crypto']['signature'] = si\n                        pgpdata[2]['data'] = ''\n\n                    except Exception as e:\n                        print(e)\n\n            if decrypt:\n                if part['type'] in ('pgpbegin', 'pgptext'):\n                    pgpdata.append(part)\n                elif part['type'] == 'pgpend':\n                    pgpdata.append(part)\n\n                    data = ''.join([p['data'] for p in pgpdata])\n                    gpg = GnuPG(self.config, event=event)\n                    si, ei, text = gpg.decrypt(data)\n\n                    # FIXME: If the data is binary, we should provide some\n                    #        sort of download link or maybe leave the PGP\n                    #        blob entirely intact, undecoded.\n                    text, charset = self.decode_text(text, binary=False)\n\n                    pgpdata[1]['crypto']['encryption'] = ei\n                    pgpdata[1]['crypto']['signature'] = si\n                    if ei[\"status\"] == \"decrypted\":\n                        pgpdata[0]['data'] = \"\"\n                        pgpdata[1]['data'] = text\n                        pgpdata[2]['data'] = \"\"\n\n            # Bubbling up!\n            if (si or ei) and 'crypto' not in tree:\n                tree['crypto'] = {'signature': SignatureInfo(bubbly=False),\n                                  'encryption': EncryptionInfo(bubbly=False)}\n            if si:\n                si.bubble_up(tree['crypto']['signature'])\n            if ei:\n                ei.bubble_up(tree['crypto']['encryption'])\n\n        # Cleanup, remove empty 'crypto': {} blocks.\n        for part in tree['text_parts']:\n            if not part['crypto']:\n                del part['crypto']\n\n        tree['crypto']['signature'].mix_bubbles()\n        tree['crypto']['encryption'].mix_bubbles()\n        if crypto_state_feedback:\n            self._update_crypto_state()\n\n        return tree\n\n    def _decode_gpg(self, message, decrypted):\n        header, body = message.replace('\\r\\n', '\\n').split('\\n\\n', 1)\n        for line in header.lower().split('\\n'):\n            if line.startswith('charset:'):\n                return decrypted.decode(line.split()[1])\n        return decrypted.decode('utf-8')\n\n\nif __name__ == \"__main__\":\n    import doctest\n    import sys\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                              extraglobs={})\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/mailutils/generator.py",
    "content": "# Copyright (C) 2001-2010 Python Software Foundation\n# Contact: email-sig@python.org\n#\n# Updated/forked January 2014 by Bjarni R. Einarsson <bre@mailpile.is>\n# to match the python 3.x email.generator CRLF control API (linesep=...).\n#\n# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2\n# --------------------------------------------\n#\n# 1. This LICENSE AGREEMENT is between the Python Software Foundation\n# (\"PSF\"), and the Individual or Organization (\"Licensee\") accessing and\n# otherwise using this software (\"Python\") in source or binary form and\n# its associated documentation.\n#\n# 2. Subject to the terms and conditions of this License Agreement, PSF\n# hereby grants Licensee a nonexclusive, royalty-free, world-wide\n# license to reproduce, analyze, test, perform and/or display publicly,\n# prepare derivative works, distribute, and otherwise use Python\n# alone or in any derivative version, provided, however, that PSF's\n# License Agreement and PSF's notice of copyright, i.e., \"Copyright (c)\n# 2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation; All Rights\n# Reserved\" are retained in Python alone or in any derivative version\n# prepared by Licensee.\n#\n# 3. In the event Licensee prepares a derivative work that is based on\n# or incorporates Python or any part thereof, and wants to make\n# the derivative work available to others as provided herein, then\n# Licensee hereby agrees to include in any such work a brief summary of\n# the changes made to Python.\n#\n# 4. PSF is making Python available to Licensee on an \"AS IS\"\n# basis.  PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR\n# IMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND\n# DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS\n# FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT\n# INFRINGE ANY THIRD PARTY RIGHTS.\n#\n# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON\n# FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS\n# A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,\n# OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.\n#\n# 6. This License Agreement will automatically terminate upon a material\n# breach of its terms and conditions.\n#\n# 7. Nothing in this License Agreement shall be deemed to create any\n# relationship of agency, partnership, or joint venture between PSF and\n# Licensee.  This License Agreement does not grant permission to use PSF\n# trademarks or trade name in a trademark sense to endorse or promote\n# products or services of Licensee, or any third party.\n#\n# 8. By copying, installing or otherwise using Python, Licensee\n# agrees to be bound by the terms and conditions of this License\n# Agreement.\n\n\n\"\"\"Classes to generate plain text from a message object tree.\"\"\"\nfrom __future__ import print_function\n\n__all__ = ['Generator', 'DecodedGenerator']\n\nimport re\nimport sys\nimport time\nimport random\nimport warnings\n\nfrom cStringIO import StringIO\nfrom email.header import Header\n\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\n\n\nUNDERSCORE = '_'\nNL = '\\n'\n\nfcre = re.compile(r'^From ', re.MULTILINE)\n\n\ndef _is8bitstring(s):\n    if isinstance(s, str):\n        try:\n            unicode(s, 'us-ascii')\n        except UnicodeError:\n            return True\n    return False\n\n\nclass Generator:\n    \"\"\"Generates output from a Message object tree.\n\n    This basic generator writes the message to the given file object as plain\n    text.\n    \"\"\"\n    #\n    # Public interface\n    #\n\n    def __init__(self, outfp,\n                 mangle_from_=True, maxheaderlen=78, linesep=None):\n        \"\"\"Create the generator for message flattening.\n\n        outfp is the output file-like object for writing the message to.  It\n        must have a write() method.\n\n        Optional mangle_from_ is a flag that, when True (the default), escapes\n        From_ lines in the body of the message by putting a `>' in front of\n        them.\n\n        Optional maxheaderlen specifies the longest length for a non-continued\n        header.  When a header line is longer (in characters, with tabs\n        expanded to 8 spaces) than maxheaderlen, the header will split as\n        defined in the Header class.  Set maxheaderlen to zero to disable\n        header wrapping.  The default is 78, as recommended (but not required)\n        by RFC 2822.\n        \"\"\"\n        self._fp = outfp\n        self._mangle_from_ = mangle_from_\n        self._maxheaderlen = maxheaderlen\n        self._NL = linesep or NL\n\n    def write(self, s):\n        # Just delegate to the file object\n        self._fp.write(s)\n\n    def flatten(self, msg, unixfrom=False, linesep=None):\n        \"\"\"Print the message object tree rooted at msg to the output file\n        specified when the Generator instance was created.\n\n        unixfrom is a flag that forces the printing of a Unix From_ delimiter\n        before the first object in the message tree.  If the original message\n        has no From_ delimiter, a `standard' one is crafted.  By default, this\n        is False to inhibit the printing of any From_ delimiter.\n\n        linesep specifies the characters used to indicate a new line in the\n        output. The default value is LF (not the standard CRLF).\n\n        Note that for subobjects, no From_ line is printed.\n        \"\"\"\n        if linesep:\n            self._NL = linesep\n        if unixfrom:\n            ufrom = msg.get_unixfrom()\n            if not ufrom:\n                ufrom = 'From nobody ' + time.ctime(time.time())\n            print(ufrom + self._NL, end='', file=self._fp)\n        self._write(msg)\n\n    def clone(self, fp):\n        \"\"\"Clone this generator with the exact same options.\"\"\"\n        return self.__class__(fp, self._mangle_from_, self._maxheaderlen)\n\n    #\n    # Protected interface - undocumented ;/\n    #\n\n    def _write(self, msg):\n        # We can't write the headers yet because of the following scenario:\n        # say a multipart message includes the boundary string somewhere in\n        # its body.  We'd have to calculate the new boundary /before/ we write\n        # the headers so that we can write the correct Content-Type:\n        # parameter.\n        #\n        # The way we do this, so as to make the _handle_*() methods simpler,\n        # is to cache any subpart writes into a StringIO.  The we write the\n        # headers and the StringIO contents.  That way, subpart handlers can\n        # Do The Right Thing, and can still modify the Content-Type: header if\n        # necessary.\n        oldfp = self._fp\n        try:\n            self._fp = sfp = StringIO()\n            self._dispatch(msg)\n        finally:\n            self._fp = oldfp\n        # Write the headers.  First we see if the message object wants to\n        # handle that itself.  If not, we'll do it generically.\n        meth = getattr(msg, '_write_headers', None)\n        if meth is None:\n            self._write_headers(msg)\n        else:\n            meth(self)\n        self._fp.write(sfp.getvalue())\n\n    def _dispatch(self, msg):\n        # Get the Content-Type: for the message, then try to dispatch to\n        # self._handle_<maintype>_<subtype>().  If there's no handler for the\n        # full MIME type, then dispatch to self._handle_<maintype>().  If\n        # that's missing too, then dispatch to self._writeBody().\n        main = msg.get_content_maintype()\n        sub = msg.get_content_subtype()\n        specific = UNDERSCORE.join((main, sub)).replace('-', '_')\n        meth = getattr(self, '_handle_' + specific, None)\n        if meth is None:\n            generic = main.replace('-', '_')\n            meth = getattr(self, '_handle_' + generic, None)\n            if meth is None:\n                meth = self._writeBody\n        meth(msg)\n\n    #\n    # Default handlers\n    #\n\n    def _write_headers(self, msg):\n        for h, v in msg.items():\n            print('%s:' % h, end=' ', file=self._fp)\n            if self._maxheaderlen == 0:\n                # Explicit no-wrapping\n                print(v + self._NL, end='', file=self._fp)\n            elif isinstance(v, Header):\n                # Header instances know what to do\n                hdr = v.encode().replace('\\n', self._NL)\n                print(hdr + self._NL, end='', file=self._fp)\n            elif _is8bitstring(v):\n                # If we have raw 8bit data in a byte string, we have no idea\n                # what the encoding is.  There is no safe way to split this\n                # string.  If it's ascii-subset, then we could do a normal\n                # ascii split, but if it's multibyte then we could break the\n                # string.  There's no way to know so the least harm seems to\n                # be to not split the string and risk it being too long.\n                print(v + self._NL, end='', file=self._fp)\n            else:\n                # Header's got lots of smarts, so use it.  Note that this is\n                # fundamentally broken though because we lose idempotency when\n                # the header string is continued with tabs.  It will now be\n                # continued with spaces.  This was reversedly broken before we\n                # fixed bug 1974.  Either way, we lose.\n                hdr = Header(v, maxlinelen=self._maxheaderlen, header_name=h\n                             ).encode().replace('\\n', self._NL)\n                print(hdr + self._NL, end='', file=self._fp)\n        # A blank line always separates headers from body\n        print(self._NL, end='', file=self._fp)\n\n    #\n    # Handlers for writing types and subtypes\n    #\n\n    def _handle_text(self, msg):\n        payload = msg.get_payload()\n        if payload is None:\n            return\n        if not isinstance(payload, basestring):\n            raise TypeError('string payload expected: %s' % type(payload))\n        if self._mangle_from_:\n            payload = fcre.sub('>From ', payload)\n        self._fp.write(payload)\n\n    # Default body handler\n    _writeBody = _handle_text\n\n    def _handle_multipart(self, msg):\n        # The trick here is to write out each part separately, merge them all\n        # together, and then make sure that the boundary we've chosen isn't\n        # present in the payload.\n        msgtexts = []\n        subparts = msg.get_payload()\n        if subparts is None:\n            subparts = []\n        elif isinstance(subparts, basestring):\n            # e.g. a non-strict parse of a message with no starting boundary.\n            self._fp.write(subparts)\n            return\n        elif not isinstance(subparts, list):\n            # Scalar payload\n            subparts = [subparts]\n        for part in subparts:\n            s = StringIO()\n            g = self.clone(s)\n            g.flatten(part, unixfrom=False, linesep=self._NL)\n            msgtexts.append(s.getvalue())\n        # BAW: What about boundaries that are wrapped in double-quotes?\n        boundary = msg.get_boundary()\n        if not boundary:\n            # Create a boundary that doesn't appear in any of the\n            # message texts.\n            alltext = self._NL.join(msgtexts)\n            boundary = _make_boundary(alltext)\n            msg.set_boundary(boundary)\n        # If there's a preamble, write it out, with a trailing CRLF\n        if msg.preamble is not None:\n            if self._mangle_from_:\n                preamble = fcre.sub('>From ', msg.preamble)\n            else:\n                preamble = msg.preamble\n            print(preamble + self._NL, end='', file=self._fp)\n        # dash-boundary transport-padding CRLF\n        print('--' + boundary + self._NL, end='', file=self._fp)\n        # body-part\n        if msgtexts:\n            self._fp.write(msgtexts.pop(0))\n        # *encapsulation\n        # --> delimiter transport-padding\n        # --> CRLF body-part\n        for body_part in msgtexts:\n            # delimiter transport-padding CRLF\n            print(self._NL + '--' + boundary + self._NL, end='', file=self._fp)\n            # body-part\n            self._fp.write(body_part)\n        # close-delimiter transport-padding\n        self._fp.write(self._NL + '--' + boundary + '--')\n        if msg.epilogue is not None:\n            print(self._NL, end='', file=self._fp)\n            if self._mangle_from_:\n                epilogue = fcre.sub('>From ', msg.epilogue)\n            else:\n                epilogue = msg.epilogue\n            self._fp.write(epilogue)\n\n    def _handle_multipart_signed(self, msg):\n        # The contents of signed parts has to stay unmodified in order to keep\n        # the signature intact per RFC1847 2.1, so we disable header wrapping.\n        # RDM: This isn't enough to completely preserve the part, but it helps.\n        # BRE: Disabled! We are using this to generate the stuff we sign, so\n        #      we actually want the logic UNCHANGED.\n        return self._handle_multipart(msg)\n        # Disabled the following...\n        old_maxheaderlen = self._maxheaderlen\n        try:\n            self._maxheaderlen = 0\n            self._handle_multipart(msg)\n        finally:\n            self._maxheaderlen = old_maxheaderlen\n\n    def _handle_message_delivery_status(self, msg):\n        # We can't just write the headers directly to self's file object\n        # because this will leave an extra newline between the last header\n        # block and the boundary.  Sigh.\n        blocks = []\n        for part in msg.get_payload():\n            s = StringIO()\n            g = self.clone(s)\n            g.flatten(part, unixfrom=False, linesep=self._NL)\n            text = s.getvalue()\n            lines = text.split(self._NL)\n            # Strip off the unnecessary trailing empty line\n            if lines and lines[-1] == '':\n                blocks.append(self._NL.join(lines[:-1]))\n            else:\n                blocks.append(text)\n        # Now join all the blocks with an empty line.  This has the lovely\n        # effect of separating each block with an empty line, but not adding\n        # an extra one after the last one.\n        self._fp.write(self._NL.join(blocks))\n\n    def _handle_message(self, msg):\n        s = StringIO()\n        g = self.clone(s)\n        # The payload of a message/rfc822 part should be a multipart sequence\n        # of length 1.  The zeroth element of the list should be the Message\n        # object for the subpart.  Extract that object, stringify it, and\n        # write it out.\n        # Except, it turns out, when it's a string instead, which happens when\n        # and only when HeaderParser is used on a message of mime type\n        # message/rfc822.  Such messages are generated by, for example,\n        # Groupwise when forwarding unadorned messages.  (Issue 7970.)  So\n        # in that case we just emit the string body.\n        payload = msg.get_payload()\n        if isinstance(payload, list):\n            g.flatten(msg.get_payload(0), unixfrom=False, linesep=self._NL)\n            payload = s.getvalue()\n        self._fp.write(payload)\n\n\n_FMT = '[Non-text (%(type)s) part of message omitted, filename %(filename)s]'\n\n\nclass DecodedGenerator(Generator):\n    \"\"\"Generates a text representation of a message.\n\n    Like the Generator base class, except that non-text parts are substituted\n    with a format string representing the part.\n    \"\"\"\n    def __init__(self, outfp,\n                 mangle_from_=True, maxheaderlen=78, fmt=None, linesep=None):\n        \"\"\"Like Generator.__init__() except that an additional optional\n        argument is allowed.\n\n        Walks through all subparts of a message.  If the subpart is of main\n        type `text', then it prints the decoded payload of the subpart.\n\n        Otherwise, fmt is a format string that is used instead of the message\n        payload.  fmt is expanded with the following keywords (in\n        %(keyword)s format):\n\n        type       : Full MIME type of the non-text part\n        maintype   : Main MIME type of the non-text part\n        subtype    : Sub-MIME type of the non-text part\n        filename   : Filename of the non-text part\n        description: Description associated with the non-text part\n        encoding   : Content transfer encoding of the non-text part\n\n        The default value for fmt is None, meaning\n\n        [Non-text (%(type)s) part of message omitted, filename %(filename)s]\n        \"\"\"\n        Generator.__init__(self, outfp, mangle_from_, maxheaderlen, linesep)\n        if fmt is None:\n            self._fmt = _FMT\n        else:\n            self._fmt = fmt\n\n    def _dispatch(self, msg):\n        for part in msg.walk():\n            maintype = part.get_content_maintype()\n            if maintype == 'text':\n                print(part.get_payload(decode=True) + self._NL, end='', file=self)\n            elif maintype == 'multipart':\n                # Just skip this\n                pass\n            else:\n                print(self._fmt % {\n                    'type': part.get_content_type(),\n                    'maintype': part.get_content_maintype(),\n                    'subtype': part.get_content_subtype(),\n                    'filename': part.get_filename('[no filename]'),\n                    'description': part.get('Content-Description',\n                                            '[no description]'),\n                    'encoding': part.get('Content-Transfer-Encoding',\n                                         '[no encoding]'),\n                    } + self._NL, end='', file=self)\n\n\n# Helper\n_width = len(repr(sys.maxsize-1))\n_fmt = '%%0%dd' % _width\n\n\ndef _make_boundary(text=None):\n    # Craft a random boundary.  If text is given, ensure that the chosen\n    # boundary doesn't appear in the text.\n    token = random.randrange(sys.maxsize)\n    boundary = ('=' * 15) + (_fmt % token) + '=='\n    if text is None:\n        return boundary\n    b = boundary\n    counter = 0\n    while True:\n        cre = re.compile('^--' + re.escape(b) + '(--)?$', re.MULTILINE)\n        if not cre.search(text):\n            break\n        b = boundary + '.' + str(counter)\n        counter += 1\n    return b\n"
  },
  {
    "path": "mailpile/mailutils/header.py",
    "content": "# vim: set fileencoding=utf-8 :\n\"\"\" Backport of Python > 3.3 email.header.decode_header()\n\nIt includes fixes that have not been ported to py2\nhttps://bugs.python.org/issue1079\n\n\"\"\"\nfrom __future__ import print_function\nimport binascii\nimport email.quoprimime\nimport email.base64mime\nimport re\nfrom email.errors import HeaderParseError\n\n\n# Match encoded-word strings in the form =?charset?q?Hello_World?=\necre = re.compile(r'''\n  =\\?                   # literal =?\n  (?P<charset>[^?]*?)   # non-greedy up to the next ? is the charset\n  \\?                    # literal ?\n  (?P<encoding>[qb])    # either a \"q\" or a \"b\", case insensitive\n  \\?                    # literal ?\n  (?P<encoded>.*?)      # non-greedy up to the next ?= is the encoded string\n  \\?=                   # literal ?=\n  ''', re.VERBOSE | re.IGNORECASE | re.MULTILINE)\n\n\ndef decode_header(header):\n    \"\"\" Decode a message header value without converting charset.\n    Returns a list of (string, charset) pairs containing each of the decoded\n    parts of the header.  Charset is None for non-encoded parts of the header,\n    otherwise a lower-case string containing the name of the character set\n    specified in the encoded string.\n    header may be a string that may or may not contain RFC2047 encoded words,\n    or it may be a Header object.\n    An email.errors.HeaderParseError may be raised when certain decoding error\n    occurs (e.g. a base64 decoding exception).\n    \"\"\"\n    # If it is a Header object, we can just return the encoded chunks.\n    if hasattr(header, '_chunks'):\n        return [(_charset._encode(string, str(charset)), str(charset))\n                for string, charset in header._chunks]\n    # If no encoding, just return the header with no charset.\n    if not ecre.search(header):\n        return [(header, None)]\n    # First step is to parse all the encoded parts into triplets of the form\n    # (encoded_string, encoding, charset).  For unencoded strings, the last\n    # two parts will be None.\n    words = []\n    for line in header.splitlines():\n        parts = ecre.split(line)\n        first = True\n        while parts:\n            unencoded = parts.pop(0)\n            if first:\n                unencoded = unencoded.lstrip()\n                first = False\n            if unencoded:\n                words.append((unencoded, None, None))\n            if parts:\n                charset = parts.pop(0).lower()\n                encoding = parts.pop(0).lower()\n                encoded = parts.pop(0)\n                words.append((encoded, encoding, charset))\n    # Now loop over words and remove words that consist of whitespace\n    # between two encoded strings.\n    droplist = []\n    for n, w in enumerate(words):\n        if n > 1 and w[1] and words[n-2][1] and words[n-1][0].isspace():\n            droplist.append(n-1)\n    for d in reversed(droplist):\n        del words[d]\n\n    # The next step is to decode each encoded word by applying the reverse\n    # base64 or quopri transformation.  decoded_words is now a list of the\n    # form (decoded_word, charset).\n    decoded_words = []\n    for encoded_string, encoding, charset in words:\n        if encoding is None:\n            # This is an unencoded word.\n            decoded_words.append((encoded_string, charset))\n        elif encoding == 'q':\n            word = email.quoprimime.header_decode(encoded_string)\n            decoded_words.append((word, charset))\n        elif encoding == 'b':\n            # Postel's law: add missing padding\n            paderr = len(encoded_string) % 4\n            if paderr:\n                encoded_string += '==='[:4 - paderr]\n            try:\n                word = email.base64mime.decode(encoded_string)\n            except binascii.Error:\n                raise HeaderParseError('Base64 decoding error')\n            else:\n                decoded_words.append((word, charset))\n        else:\n            raise AssertionError('Unexpected encoding: ' + encoding)\n    # Now convert all words to bytes and collapse consecutive runs of\n    # similarly encoded words.\n    collapsed = []\n    last_word = last_charset = None\n    for word, charset in decoded_words:\n        if isinstance(word, unicode):\n            word = bytes(word, 'raw-unicode-escape')\n        if last_word is None:\n            last_word = word\n            last_charset = charset\n        elif charset != last_charset:\n            collapsed.append((last_word, last_charset))\n            last_word = word\n            last_charset = charset\n        elif last_charset is None:\n            last_word += b' ' + word\n        else:\n            last_word += word\n    collapsed.append((last_word, last_charset))\n    return collapsed\n\n\nif __name__ == \"__main__\":\n    import doctest\n    import sys\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                              extraglobs={})\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/mailutils/headerprint.py",
    "content": "# vim: set fileencoding=utf-8 :\n#\nfrom __future__ import print_function\nimport re\nfrom mailpile.util import md5_hex\n\n\nMUA_ML_HEADERS = (# Mailing lists are sending MUAs in their own right\n                  'list-id', 'list-subscribe', 'list-unsubscribe')\n\nMUA_HP_HEADERS = ('date', 'from', 'to', 'reply-to',\n                  # We omit the Subject, because for some reason it seems\n                  # to jump around a lot. Same for CC.\n                  'message-id', 'return-path', 'precedence', 'organization',\n                  'mime-version', 'content-type',\n                  'user-agent', 'x-mailer',\n                  'x-mimeole', 'x-msmail-priority', 'x-priority',\n                  'x-originating-ip', 'x-message-info',\n                  'openpgp', 'x-openpgp',\n                  # Common services\n                  'x-github-recipient', 'feedback-id', 'x-facebook')\n\nMUA_ID_HEADERS = ('x-mailer', 'user-agent', 'x-mimeole')\n\nHP_MUA_ID_SPACE = re.compile(r'(\\s+)')\nHP_MUA_ID_IGNORE = re.compile(r'(\\[[a-fA-F0-9%:]+\\]|<\\S+@\\S+>'\n                              '|(mail|in)-[^\\.]+|\\d+)')\nHP_MUA_ID_SPLIT = re.compile(r'[\\s,/;=()]+')\nHP_RECVD_PARSE = re.compile(r'(by\\s+)'\n                             '[a-z0-9_\\.-]*?([a-z0-9_-]*?\\.?[a-z0-9_-]+\\s+.*'\n                             'with\\s+.*)\\s+id\\s+.*$',\n                            flags=(re.MULTILINE + re.DOTALL))\n\n\ndef HeaderPrintMTADetails(message):\n    \"\"\"Extract details about the sender's outgoing SMTP server.\"\"\"\n    details = []\n    # We want the first \"non-local\" received line. This can of course be\n    # trivially spoofed, but looking at this will still protect against\n    # all but the most targeted of spear phishing attacks.\n    for rcvd in reversed(message.get_all('received') or []):\n        if ('local' not in rcvd\n                and ' mapi id ' not in rcvd\n                and '127.0.0' not in rcvd\n                and '[::1]' not in rcvd):\n            parsed = HP_RECVD_PARSE.search(rcvd)\n            if parsed:\n                by = parsed.group(1) + parsed.group(2)\n                by = HP_MUA_ID_SPACE.sub(' ', HP_MUA_ID_IGNORE.sub('x', by))\n                details = ['Received ' + by]\n                break\n    for h in ('DKIM-Signature', 'X-Google-DKIM-Signature'):\n        for dkim in (message.get_all(h) or []):\n            attrs = [HP_MUA_ID_SPACE.sub('', a)\n                     for a in dkim.split(';') if a.strip()[:1] in 'vacd']\n            details.extend([h, '; '.join(sorted(attrs))])\n    return details\n\n\ndef HeaderPrintMUADetails(message, mta=None):\n    \"\"\"Summarize what the message tells us directly about the MUA.\"\"\"\n    details = []\n    for header in MUA_ID_HEADERS:\n        value = message.get(header)\n        if value:\n            # We want some details about the MUA, but also some stability.\n            # Thus the HP_MUA_ID_IGNORE regexp...\n            value = ' '.join([v for v in HP_MUA_ID_SPLIT.split(value.strip())\n                              if not HP_MUA_ID_IGNORE.search(v)])\n            details.extend([header, value.strip()])\n\n    if not details:\n        # FIXME: We could definitely make more educated guesses!\n        if mta and mta[0].startswith('Received by google.com'):\n            details.extend(['Guessed', 'GMail'])\n        elif ('x-ms-tnef-correlator' in message or\n                'x-ms-has-attach' in message):\n            details.extend(['Guessed', 'Exchange'])\n        elif '@mailpile' in message.get('message-id', ''):\n            details.extend(['Guessed', 'Mailpile'])\n\n    return details\n\n\ndef HeaderPrintGenericDetails(message, which=MUA_HP_HEADERS):\n    \"\"\"Extract message details which may help identify the MUA.\"\"\"\n    return [k for k, v in message.items() if k.lower() in which]\n\n\ndef HeaderPrints(message):\n    \"\"\"Generate fingerprints from message headers which identifies the MUA.\"\"\"\n    m = HeaderPrintMTADetails(message)\n    u = HeaderPrintMUADetails(message, mta=m)[:20]\n    g = HeaderPrintGenericDetails(message)[:50]\n    mua = (u[1] if u else None)\n    if mua and mua.startswith('Mozilla '):\n        mua = mua.split()[-1]\n    return {\n        # The sender-ID headerprints includes MTA info\n        'sender': md5_hex('\\n'.join(m+u+g)),\n        # Tool-chain headerprints ignore the MTA details\n        'tools': md5_hex('\\n'.join(u+g)),\n        # Our best guess about what the MUA actually is; may be None\n        'mua': mua}\n\n\nif __name__ == \"__main__\":\n    import doctest\n    import sys\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                              extraglobs={})\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/mailutils/html.py",
    "content": "# vim: set fileencoding=utf-8 :\n#\nfrom __future__ import print_function\nimport lxml.etree\nimport lxml.html\nimport lxml.html.clean\nimport re\n\n\nRE_HTML_BORING = re.compile(\n    '(\\s+|(<style[^>]*>\\s*)+.*?</style>)',\n    flags=re.DOTALL|re.IGNORECASE)\n\nRE_EXCESS_WHITESPACE = re.compile(\n    '\\n\\s*\\n\\s*',\n    flags=re.DOTALL)\n\nRE_HTML_NEWLINES = re.compile(\n    '(<br|</(tr|table))',\n    flags=re.IGNORECASE)\n\nRE_HTML_PARAGRAPHS = re.compile(\n    '(</?p|</?(title|div|html|body))',\n    flags=re.IGNORECASE)\n\nRE_HTML_LINKS = re.compile(\n    '<a\\s+[^>]*href=[\\'\"]?([^\\'\">]+)[^>]*>([^<]*)</a>',\n    flags=re.DOTALL|re.IGNORECASE)\n\nRE_HTML_IMGS = re.compile(\n    '<img\\s+[^>]*src=[\\'\"]?([^\\'\">]+)[^>]*>',\n    flags=re.DOTALL|re.IGNORECASE)\n\nRE_HTML_IMG_ALT = re.compile(\n    '<img\\s+[^>]*alt=[\\'\"]?([^\\'\">]+)[^>]*>',\n    flags=re.DOTALL|re.IGNORECASE)\n\nRE_XML_ENCODING = re.compile(\n    '(<\\?xml version=[^ ?>]*((?! +encoding=) [^ ?>]*)*)( +encoding=[^ ?>]*)',\n    flags=re.DOTALL|re.IGNORECASE)\n\n\n# FIXME: Decide if this is strict enough or too strict...?\nSHARED_HTML_CLEANER = lxml.html.clean.Cleaner(\n    page_structure=True,\n    meta=True,\n    links=True,\n    javascript=True,\n    scripts=True,\n    frames=True,\n    embedded=True,\n    safe_attrs_only=True)\n\n\ndef clean_html(html):\n    # Find and delete possibly conflicting xml encoding\n    # declaration to prevent lxml ValueError.\n    # e.g. <?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n    html = re.sub(RE_XML_ENCODING, r'\\1', html).strip()\n    return (SHARED_HTML_CLEANER.clean_html(html) if html else '')\n\n\ndef extract_text_from_html(html, url_callback=None):\n    try:\n        # We compensate for some of the limitations of lxml...\n        links, imgs = [], []\n        def delink(m):\n            url, txt = m.group(1), m.group(2).strip()\n            if url_callback is not None:\n                url_callback(url, txt)\n            if txt[:4] in ('http', 'www.'):\n                return txt\n            elif url.startswith('mailto:'):\n                if '@' in txt:\n                    return txt\n                else:\n                    return '%s (%s)' % (txt, url.split(':', 1)[1])\n            else:\n                links.append(' [%d] %s%s' % (len(links) + 1,\n                                             txt and (txt + ': ') or '',\n                                             url))\n                return '%s[%d]' % (txt, len(links))\n\n        def deimg(m):\n            tag, url = m.group(0), m.group(1)\n            if ' alt=' in tag:\n                return re.sub(RE_HTML_IMG_ALT, '\\1', tag).strip()\n            else:\n                imgs.append(' [%d] %s' % (len(imgs)+1, url))\n                return '[Image %d]' % len(imgs)\n\n        html = (\n            re.sub(RE_XML_ENCODING, r'\\1',\n                re.sub(RE_HTML_PARAGRAPHS, '\\n\\n\\\\1',\n                    re.sub(RE_HTML_NEWLINES, '\\n\\\\1',\n                        re.sub(RE_HTML_BORING, ' ',\n                            re.sub(RE_HTML_LINKS, delink,\n                                re.sub(RE_HTML_IMGS, deimg, html\n            ))))))).strip()\n\n        if html:\n            try:\n                html_text = lxml.html.fromstring(html).text_content()\n            except lxml.etree.Error:\n                html_text = _('(Invalid HTML suppressed)')\n        else:\n            html_text = ''\n\n        text = (html_text +\n                (links and '\\n\\nLinks:\\n' or '') + '\\n'.join(links) +\n                (imgs and '\\n\\nImages:\\n' or '') + '\\n'.join(imgs))\n\n        return re.sub(RE_EXCESS_WHITESPACE, '\\n\\n', text).strip()\n    except:\n        import traceback\n        traceback.print_exc()\n        return html\n\n\nif __name__ == \"__main__\":\n    import doctest\n    import sys\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                              extraglobs={})\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/mailutils/safe.py",
    "content": "from __future__ import print_function\nimport email\nimport email.errors\nimport email.message\nimport random\nimport re\nimport rfc822\nimport time\nfrom urllib import quote, unquote\n\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.mailutils.header import decode_header\nfrom mailpile.util import *\n\n\ndef safe_decode_hdr(msg=None, name=None, hdr=None, charset=None):\n    \"\"\"\n    This method stubbornly tries to decode header data and convert\n    to Pythonic unicode strings. The strings are guaranteed not to\n    contain tab, newline or carriage return characters.\n\n    If used with a message object, the header and the MIME charset\n    will be inferred from the message headers.\n    >>> msg = email.message.Message()\n    >>> msg['content-type'] = 'text/plain; charset=utf-8'\n    >>> msg['from'] = 'G\\\\xc3\\\\xadsli R \\\\xc3\\\\x93la <f@b.is>'\n    >>> safe_decode_hdr(msg, 'from')\n    u'G\\\\xedsli R \\\\xd3la <f@b.is>'\n\n    The =?...?= MIME header encoding is also recognized and processed.\n\n    >>> safe_decode_hdr(hdr='=?iso-8859-1?Q?G=EDsli_R_=D3la?=\\\\r\\\\n<f@b.is>')\n    u'G\\\\xedsli R \\\\xd3la <f@b.is>'\n\n    >>> safe_decode_hdr(hdr='\"=?utf-8?Q?G=EDsli_R?= =?iso-8859-1?Q?=D3la?=\"')\n    u'G\\\\xedsli R \\\\xd3la'\n\n    And finally, guesses are made with raw binary data. This process\n    could be improved, it currently only attempts utf-8 and iso-8859-1.\n\n    >>> safe_decode_hdr(hdr='\"G\\\\xedsli R \\\\xd3la\"\\\\r\\\\t<f@b.is>')\n    u'\"G\\\\xedsli R \\\\xd3la\"  <f@b.is>'\n\n    >>> safe_decode_hdr(hdr='\"G\\\\xc3\\\\xadsli R \\\\xc3\\\\x93la\"\\\\n <f@b.is>')\n    u'\"G\\\\xedsli R \\\\xd3la\"  <f@b.is>'\n\n    # See https://bugs.python.org/issue1079\n\n    # encoded word enclosed in parenthesis (comment syntax)\n    >>> safe_decode_hdr(hdr='rene@example.com (=?utf-8?Q?Ren=C3=A9?=)')\n    u'rene@example.com ( Ren\\\\xe9 )'\n\n    # no space after encoded word\n    >>> safe_decode_hdr(hdr='=?UTF-8?Q?Direction?=<dir@example.com>')\n    u'Direction <dir@example.com>'\n    \"\"\"\n    if hdr is None:\n        value = msg and msg[name] or ''\n        charset = charset or msg.get_content_charset() or 'utf-8'\n    else:\n        value = hdr\n        charset = charset or 'utf-8'\n\n    if not isinstance(value, unicode):\n        # Already a str! Oh shit, might be nasty binary data.\n        value = try_decode(value, charset, replace='?')\n\n    # At this point we know we have a unicode string. Next we try\n    # to very stubbornly decode and discover character sets.\n    if '=?' in value and '?=' in value:\n        try:\n            # decode_header wants an unquoted str (not unicode)\n            value = value.encode('utf-8').replace('\"', '')\n            # Decode!\n            pairs = decode_header(value)\n            value = ' '.join([try_decode(t, cs or charset)\n                              for t, cs in pairs])\n        except email.errors.HeaderParseError:\n            pass\n\n    # Finally, return the unicode data, with white-space normalized\n    return value.replace('\\r', ' ').replace('\\t', ' ').replace('\\n', ' ')\n\ndef safe_parse_date(date_hdr):\n    \"\"\"Parse a Date: or Received: header into a unix timestamp.\"\"\"\n    try:\n        if ';' in date_hdr:\n            date_hdr = date_hdr.split(';')[-1].strip()\n        msg_ts = long(rfc822.mktime_tz(rfc822.parsedate_tz(date_hdr)))\n        if (msg_ts > (time.time() + 24 * 3600)) or (msg_ts < 1):\n            return None\n        else:\n            return msg_ts\n    except (ValueError, TypeError, OverflowError):\n        return None\n\ndef safe_message_ts(msg, default=None, msg_mid=None, msg_id=None, session=None):\n    \"\"\"Extract a date, sanity checking against the Received: headers.\"\"\"\n    hdrs = [safe_decode_hdr(msg, 'date')] + (msg.get_all('received') or [])\n    dates = [safe_parse_date(date_hdr) for date_hdr in hdrs]\n    msg_ts = dates[0]\n    nz_dates = sorted([d for d in dates if d])\n\n    if nz_dates:\n        a_week = 7 * 24 * 3600\n\n        # Ideally, we compare with the date on the 2nd SMTP relay, as\n        # the first will often be the same host as composed the mail\n        # itself. If we don't have enough hops, just use the last one.\n        #\n        # We don't want to use a median or average, because if the\n        # message bounces around lots of relays or gets resent, we\n        # want to ignore the latter additions.\n        #\n        rcv_ts = nz_dates[min(len(nz_dates)-1, 2)]\n\n        # Now, if everything is normal, the msg_ts will be at nz_dates[0]\n        # and it won't be too far away from our reference date.\n        if (msg_ts == nz_dates[0]) and (abs(msg_ts - rcv_ts) < a_week):\n            # Note: Trivially true for len(nz_dates) in (1, 2)\n            return msg_ts\n\n        # Damn, dates are screwy!\n        #\n        # Maybe one of the SMTP servers has a wrong clock?  If the Date:\n        # header falls within the range of all detected dates (plus a\n        # week towards the past), still trust it.\n        elif ((msg_ts >= (nz_dates[0]-a_week))\n                and (msg_ts <= nz_dates[-1])):\n            return msg_ts\n\n        # OK, Date: is insane, use one of the early Received: lines\n        # instead.  We picked the 2nd one above, that should do.\n        else:\n            if session and msg_mid and msg_id:\n                session.ui.warning(_('=%s/%s using Received: instead of Date:'\n                                     ) % (msg_mid, msg_id))\n            return rcv_ts\n    else:\n        # If the above fails, we assume the messages in the mailbox are in\n        # chronological order and just add 1 second to the date of the last\n        # message if date parsing fails for some reason.\n        if session and msg_mid and msg_id:\n            session.ui.warning(_('=%s/%s has a bogus date'\n                                 ) % (msg_mid, msg_id))\n        return default\n\ndef safe_get_msg_id(msg):\n    raw_msg_id = safe_decode_hdr(msg, 'message-id')\n    if not raw_msg_id:\n        # Create a very long pseudo-msgid for messages without a\n        # Message-ID. This was a very badly behaved mailer, so if\n        # we create duplicates this way, we are probably only\n        # losing spam. Even then the Received line should save us.\n        raw_msg_id = ('\\t'.join([safe_decode_hdr(msg, 'date'),\n                                 safe_decode_hdr(msg, 'subject'),\n                                 safe_decode_hdr(msg, 'received'),\n                                 safe_decode_hdr(msg, 'from'),\n                                 safe_decode_hdr(msg, 'to')])\n                      # This is to avoid truncation in encode_msg_id:\n                      ).replace('<', '').strip()\n    return raw_msg_id\n\n\nif __name__ == '__main__':\n    import doctest\n    import sys\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                              extraglobs={})\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/mailutils/vcal.py",
    "content": "from __future__ import print_function\nimport time\nimport icalendar\nfrom datetime import datetime\n\ndef calendar_parse(payload):\n    c = icalendar.parser.Contentlines()\n    lines = c.from_ical(payload)\n\n    root = None\n    obj = None\n\n    for line in lines:\n        if line == \"\":\n            break\n        parts = line.parts()\n        if parts[0] == \"BEGIN\":\n            t = vmap[parts[2]]()\n            if not root: root = t\n            if obj:\n                obj.children.append(t)\n                t.parent = obj\n            obj = t\n\n            root.stack.append(parts[2])\n            continue\n        if parts[0] == \"END\":\n            obj = obj.parent\n            root.stack.pop()\n            continue\n\n        obj.add_part(*parts)\n\n    return root.to_json()\n\nclass VObject:\n    def __init__(self):\n        self.children = []\n        self.parent = None\n        self.stack = []\n        self.parts = []\n\n    def add_part(self, key, params, value):\n        self.parts.append([key, params, value])\n\n    def find_parts(self, key):\n        res = []\n        for p in self.parts:\n            if p[0] == key:\n                res.append(p)\n        return res\n\n    def find_one_part(self, key):\n        res = self.find_parts(key)\n        if len(res) == 0: return None\n        r = {\"value\": res[0][2], \"params\": res[0][1] }\n        return r\n\n    def find_one_part_value(self, key, value=None):\n        res = self.find_parts(key)\n        if len(res) == 0: return value\n        return res[0][2]\n\n    def get_datetime(self, key):\n        val = self.find_one_part_value(key)\n        try:\n            return datetime(*time.strptime(val, \"%Y%m%dT%H%M%SZ\")[:6])\n        except:\n            return datetime(*time.strptime(val, \"%Y%m%dT%H%M%S\")[:6])\n\n    def to_raw_json(self):\n        parts = {}\n        for p in self.parts:\n            if p[0] not in parts:\n                parts[p[0]] = []\n            parts[p[0]].append({\"value\": p[2], \"parameters\": p[1]})\n\n        children = [x.to_raw_json() for x in self.children]\n        return {\n            \"type\": self.__class__.__name__,\n            \"children\": children,\n            \"parts\": parts,\n        }\n\n    def to_json(self):\n        return to_raw_json()\n\nclass VTimeZone(VObject):\n    pass\n\nclass VTZStandard(VObject):\n    pass\n\nclass VTZDaylight(VObject):\n    pass\n\nclass VAlarm(VObject):\n    pass\n\nclass VEvent(VObject):\n    def __init__(self):\n        VObject.__init__(self)\n\n    def to_json(self):\n        summary = self.find_one_part_value(\"SUMMARY\", \"\")\n        description = self.find_one_part_value(\"DESCRIPTION\", \"\").replace(\"\\\\n\", \"\\n\").replace(\"\\n\\n\", \"\\n\")\n        dtstart = self.get_datetime(\"DTSTART\")\n        dtend = self.get_datetime(\"DTEND\")\n        location = self.find_one_part_value(\"LOCATION\", \"\")\n        attendees = [{\"cn\": x[1][\"cn\"], \"email\": x[2].split(\":\")[1]}\n                     for x in self.find_parts(\"ATTENDEE\")]\n        o = self.find_one_part(\"ORGANIZER\")\n        organizer = { \"cn\": o[\"params\"][\"CN\"], \"email\": o[\"value\"].split(\":\")[1]}\n        tzinfo = None\n\n        return {\n            \"summary\": summary,\n            \"description\": description,\n            \"dtstart\": dtstart,\n            \"dtend\": dtend,\n            \"location\": location,\n            \"timezone\": tzinfo,\n            \"organizer\": organizer,\n            \"attendees\": attendees,\n            \"alarms\": [],\n        }\n\nclass VCalendar(VObject):\n    def __init__(self):\n        VObject.__init__(self)\n\n    def print_events(self):\n        for e in self.children:\n            if isinstance(e, VEvent):\n                print(\"%s invited you to %s\" % (e.find_parts(\"ORGANIZER\")[0][1]['CN'],\n                 e.find_parts(\"SUMMARY\")[0][2]))\n                print(\"%s\" % e.find_parts(\"DTSTART\")[0][2])\n                print(\"%s\" % e.find_parts(\"LOCATION\")[0][2])\n\n    def to_json(self):\n        events = []\n        for e in self.children:\n            # We are assuming VEvents will only occur immediately under the\n            # VCalendar level. Haven't seen anything else in the wild.\n            if isinstance(e, VEvent):\n                events.append(e.to_json())\n\n        return events\n\n\nvmap = {\n    \"VALARM\": VAlarm,\n    \"VTIMEZONE\": VTimeZone,\n    \"VEVENT\": VEvent,\n    \"VCALENDAR\": VCalendar,\n    \"STANDARD\": VTZStandard,\n    \"DAYLIGHT\": VTZDaylight,\n}\n\nif __name__ == \"__main__\":\n    cal = calendar_parse(open(\"calitem.cal\").read())\n    # cal.print_tree()\n    print(\"------------------------------\")\n    cal.print_events()\n    print(\"------------------------------\")\n"
  },
  {
    "path": "mailpile/packing.py",
    "content": "from __future__ import print_function\nimport struct\nimport time\nimport zlib\n\nfrom mailpile.util import *\n\n\ndef PackIntSet(ints):\n    \"\"\"\n    Pack a set of ints to a compact string, unpackable by UnpackIntSet.\n\n    Short lists are binary packed directly, but long lists are converted\n    to a bitmask and then compressed using zlib.\n\n    >>> intset = set([1, 5, 9, 10000])\n    >>> intsetstr = PackIntSet(intset)\n    >>> type(intsetstr), len(intsetstr)\n    (<type 'str'>, 16)\n\n    >>> UnpackIntSet(intsetstr) == intset\n    True\n\n    >>> intset = set(list(range(1000, 50000) + [1, 2, 3]))\n    >>> intsetstr = PackIntSet(intset)\n    >>> intsetstr.startswith('\\xff\\xff\\xff\\xff')\n    True\n\n    >>> len(intsetstr)\n    37\n\n    >>> UnpackIntSet(intsetstr) == intset\n    True\n    \"\"\"\n    if len(ints) > 15:\n        return '\\xff\\xff\\xff\\xff' + zlib.compress(intlist_to_bitmask(ints))\n    else:\n        return struct.pack('<' + 'I' * len(ints), *ints)\n\n\ndef UnpackIntSet(data):\n    \"\"\"\n    Unpack a set of ints previously packed using PackIntSet.\n    \"\"\"\n    if len(data) > 13 and data[:4] == '\\xff\\xff\\xff\\xff':\n        return set(bitmask_to_intlist(zlib.decompress(data[4:])))\n    else:\n        return set(struct.unpack('<' + 'I' * (len(data)//4), data))\n\n\ndef PackLongList(longs):\n    \"\"\"\n    Pack a list of longs to a compact string, unpackable by UnpackLongList.\n\n    Short lists are binary packed directly:\n    >>> ll = [1, 5, 100000000000L]\n    >>> llstr = PackLongList(ll)\n\n    >>> UnpackLongList(llstr) == ll\n    True\n\n    >>> type(llstr), len(llstr)\n    (<type 'str'>, 24)\n\n    Longer lists are zlib compressed, which can result in significant space\n    savings for many types of data.\n    >>> ll += list(range(100, 1000))\n    >>> llstr = PackLongList(ll)\n    >>> llstr.startswith('\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff')\n    True\n\n    >>> UnpackLongList(llstr) == ll\n    True\n\n    >>> len(llstr)\n    1416\n    \"\"\"\n    packed = struct.pack('<' + 'q' * len(longs), *longs)\n    if (len(packed) > 8 * 15) or (longs[0] == 0xffffffffffffffff):\n        return ('\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff' + zlib.compress(packed))\n    else:\n        return packed\n\n\ndef UnpackLongList(data):\n    \"\"\"\n    Unpack a list of longs previously packed using PackLongList.\n    \"\"\"\n    if len(data) > 17 and data[:8] == '\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff':\n        data = zlib.decompress(data[8:])\n    return list(struct.unpack('<' + 'q' * (len(data)//8), data))\n\n\nclass StorageBackedData(object):\n    \"\"\"\n    This lovely hack exposes the full API of a Python set or list, but any\n    writes get flushed to a storage backend and the initial state is loaded\n    from the same.\n\n    It is NOT SAFE to ever have more than one of these for a given backend\n    as they will not stay in sync. Since most methods are proxies, using\n    set method on a backed list will fail and vice-versa.\n\n    This class must be subclassed and _pack and _unpack implemented.\n    \"\"\"\n    def __init__(self, storage, skey):\n        self._storage = storage\n        self._skey = skey\n        self.load()\n        self.last_save = time.time()\n        self.auto_save = True\n        self.interval = -1\n        self.dirty = False\n\n    def _pack(self, data): raise NotImplemented()\n    def _unpack(self, data): raise NotImplemented()\n\n    def load(self):\n        try:\n            self._obj = self._unpack(self._storage[self._skey])\n        except (KeyError, IndexError):\n            self._obj = self._unpack('')\n\n    def save(self, maybe=False):\n        if not maybe or self.dirty:\n            self._storage[self._skey] = self._pack(self._obj)\n            self.dirty = False\n\n    def _dirty_maybe_save(self):\n        self.dirty = True\n        if self.auto_save:\n            if (self.interval < 1 or\n                    self.last_save < time.time() - self.interval):\n                self.save()\n\n    def _r(self, method, *args, **kwargs):\n        return getattr(self._obj, method)(*args, **kwargs)\n\n    def _w(self, method, *args, **kwargs):\n        rv = getattr(self._obj, method)(*args, **kwargs)\n        self._dirty_maybe_save()\n        return rv\n\n    def _iw(self, method, *args, **kwargs):\n        self._obj = getattr(self._obj, method)(*args, **kwargs)\n        self._dirty_maybe_save()\n        return self\n \n    def __and__(s, *a, **kw): return s._r('__and__', *a, **kw)\n    def __cmp__(s, *a, **kw): return s._r('__cmp__', *a, **kw)\n    def __contains__(s, *a, **kw): return s._r('__contains__', *a, **kw)\n    def __eq__(s, *a, **kw): return s._r('__eq__', *a, **kw)\n    def __ge__(s, *a, **kw): return s._r('__ge__', *a, **kw)\n    def __getitem__(s, *a, **kw): return s._r('__getitem__', *a, **kw)\n    def __getslice__(s, *a, **kw): return s._r('__getslice__', *a, **kw)\n    def __gt__(s, *a, **kw): return s._r('__gt__', *a, **kw)\n    def __iter__(s, *a, **kw): return s._r('__iter__', *a, **kw)\n    def __le__(s, *a, **kw): return s._r('__le__', *a, **kw)\n    def __len__(s, *a, **kw): return s._r('__len__', *a, **kw)\n    def __lt__(s, *a, **kw): return s._r('__lt__', *a, **kw)\n    def __mul__(s, *a, **kw): return s._r('__mul__', *a, **kw)\n    def __ne__(s, *a, **kw): return s._r('__ne__', *a, **kw)\n    def __or__(s, *a, **kw): return s._r('__or__', *a, **kw)\n    def __rand__(s, *a, **kw): return s._r('__rand__', *a, **kw)\n    def __reduce__(s, *a, **kw): return s._r('__reduce__', *a, **kw)\n    def __repr__(s, *a, **kw): return s._r('__repr__', *a, **kw)\n    def __reversed__(s, *a, **kw): return s._r('__reversed__', *a, **kw)\n    def __rmul__(s, *a, **kw): return s._r('__rmul__', *a, **kw)\n    def __rsub__(s, *a, **kw): return s._r('__rsub__', *a, **kw)\n    def __rxor__(s, *a, **kw): return s._r('__rxor__', *a, **kw)\n    def __sizeof__(s, *a, **kw): return s._r('__sizeof__', *a, **kw)\n    def __sub__(s, *a, **kw): return s._r('__sub__', *a, **kw)\n    def __xor__(s, *a, **kw): return s._r('__xor__', *a, **kw)\n    def copy(s, *a, **kw): return s._r('copy', *a, **kw)\n    def count(s, *a, **kw): return s._r('count', *a, **kw)\n    def difference(s, *a, **kw): return s._r('difference', *a, **kw)\n    def index(s, *a, **kw): return s._r('index', *a, **kw)\n    def intersection(s, *a, **kw): return s._r('intersection', *a, **kw)\n    def isdisjoint(s, *a, **kw): return s._r('isdisjoint', *a, **kw)\n    def issubset(s, *a, **kw): return s._r('issubset', *a, **kw)\n    def issuperset(s, *a, **kw): return s._r('issuperset', *a, **kw)\n    def union(s, *a, **kw): return s._r('union', *a, **kw)\n\n    def symmetric_difference(s, *a, **kw):\n        return s._r('symmetric_difference', *a, **kw)\n\n    def __iadd__(s, *a, **kw): return s._iw('__iadd__', *a, **kw)\n    def __iand__(s, *a, **kw): return s._iw('__iand__', *a, **kw)\n    def __imul__(s, *a, **kw): return s._iw('__imul__', *a, **kw)\n    def __ior__(s, *a, **kw): return s._iw('__ior__', *a, **kw)\n    def __isub__(s, *a, **kw): return s._iw('__isub__', *a, **kw)\n    def __ixor__(s, *a, **kw): return s._iw('__ixor__', *a, **kw)\n\n    def __delitem__(s, *a, **kw): return s._w('__delitem__', *a, **kw)\n    def __delslice__(s, *a, **kw): return s._w('__delslice__', *a, **kw)\n    def __setitem__(s, *a, **kw): return s._w('__setitem__', *a, **kw)\n    def __setslice__(s, *a, **kw): return s._w('__setslice__', *a, **kw)\n    def add(s, *a, **kw): return s._w('add', *a, **kw)\n    def append(s, *a, **kw): return s._w('append', *a, **kw)\n    def clear(s, *a, **kw): return s._w('clear', *a, **kw)\n    def discard(s, *a, **kw): return s._w('discard', *a, **kw)\n    def extend(s, *a, **kw): return s._w('extend', *a, **kw)\n    def insert(s, *a, **kw): return s._w('insert', *a, **kw)\n    def pop(s, *a, **kw): return s._w('pop', *a, **kw)\n    def remove(s, *a, **kw): return s._w('remove', *a, **kw)\n    def reverse(s, *a, **kw): return s._w('reverse', *a, **kw)\n    def sort(s, *a, **kw): return s._w('sort', *a, **kw)\n    def update(s, *a, **kw): return s._w('update', *a, **kw)\n\n    def difference_update(s, *a, **kw):\n        return s._w('difference_update', *a, **kw)\n    def intersection_update(s, *a, **kw):\n        return s._w('intersection_update', *a, **kw)\n    def symmetric_difference_update(s, *a, **kw):\n        return s._w('symmetric_difference_update', *a, **kw)\n\n\nclass StorageBackedSet(StorageBackedData):\n    \"\"\"\n    This combines StorageBackedData with Pack/UnpackIntSet to pack\n    and save sets of ints.\n\n    >>> storage = {'sbs': '\\\\x01\\\\x00\\\\x00\\\\x00'}\n    >>> sbs = StorageBackedSet(storage, 'sbs')\n    >>> 1 in sbs\n    True\n\n    >>> sbs.add(2)\n    >>> sbs.save()\n    >>> UnpackIntSet(storage['sbs']) == set([1, 2])\n    True\n    \"\"\"\n    def _pack(self, data): return PackIntSet(data)\n    def _unpack(self, data): return UnpackIntSet(data)\n\n\nclass StorageBackedLongs(StorageBackedData):\n    \"\"\"\n    This combines StorageBackedData with Pack/UnpackLongList to pack\n    and save sets of ints.\n\n    >>> storage = {'sbl': '\\\\x01\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00'}\n    >>> sbl = StorageBackedLongs(storage, 'sbl')\n    >>> 1 in sbl\n    True\n\n    >>> sbl.append(2)\n    >>> sbl.save()\n    >>> UnpackLongList(storage['sbl']) == [1, 2]\n    True\n    \"\"\"\n    def _pack(self, data): return PackLongList(data)\n    def _unpack(self, data): return UnpackLongList(data)\n\n\nif __name__ == '__main__':\n    import doctest\n    import sys\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                              extraglobs={})\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/platforms.py",
    "content": "\"\"\"\nThis module tries to centralize most of the platform-specific code in use by\nMailpile. If you find yourself checking which platform the app runs on, adding\na function here instead is probably The Right Thing.\n\"\"\"\nimport copy\nimport os\nimport subprocess\nimport sys\n\n\n# This is a cache of discovered binaries and their paths.\nBINARIES = {}\n\n\n# These are the binaries we want, and the test we use to detect whether\n# they are available/working.\n#\nBINARIES_WANTED = {#   Test command            Required?\n    'GnuPG':         (['gpg', '--version'],        True),\n    'GnuPG_dirmngr': (['dirmngr', '--version'],    True),\n    'GnuPG_agent':   (['gpg-agent', '--version'],  True),\n    'OpenSSL':       (['openssl', 'version'],      True),\n    'Tor':           (['tor', '--version'],       False)}\n\n\ndef _assert_file_exists(path):\n    if not os.path.exists(path):\n        raise OSError('Not found: %s' % path)\n    return path\n\n\ndef DetectBinaries(\n        which=None, use_cache=True, preferred={}, skip=None, _raise=None):\n    import mailpile.util\n    import mailpile.safe_popen\n    import traceback\n\n    global BINARIES\n    if which and use_cache:\n        if which in BINARIES:\n            return BINARIES[which]\n        env_bin = os.getenv('MAILPILE_%s' % which.upper(), '')\n        if env_bin:\n            BINARIES[which] = env_bin\n            return env_bin\n\n    if skip is None:\n        skip = (os.getenv('MAILPILE_IGNORE_BINARIES', '')\n            .replace('/ga', '_agent')   # Backwards compatibility\n            .replace('/dm', '_dirmngr') # Backwards compatibility\n            .split())\n\n    def _run_bintest(bt):\n        p = mailpile.safe_popen.Popen(bt,\n                                      stdout=subprocess.PIPE,\n                                      stderr=subprocess.PIPE)\n        return p.communicate()\n\n    for binary, (bin_test, reqd) in BINARIES_WANTED.iteritems():\n        if binary in skip:\n            continue\n        if (which is None) or (binary == which):\n            if preferred.get(binary):\n                bin_test = copy.copy(bin_test)\n                bin_test[0] = preferred[binary]\n            else:\n                env_bin = os.getenv('MAILPILE_%s' % binary.upper(), '')\n                if env_bin:\n                    BINARIES[binary] = env_bin\n                    continue\n            try:\n                mailpile.util.RunTimed(5.0, _run_bintest, bin_test)\n                BINARIES[binary] = bin_test[0]\n                if (not os.path.dirname(BINARIES[binary])\n                        and not sys.platform.startswith('win')):\n                    try:\n                        path = subprocess.check_output(['which',\n                                                        BINARIES[binary]])\n                        if path:\n                            BINARIES[binary] = path.strip()\n                    except (OSError, subprocess.CalledProcessError):\n                        pass\n            except (OSError, subprocess.CalledProcessError, mailpile.util.TimedOut):\n                if binary in BINARIES:\n                    del BINARIES[binary]\n\n    if which:\n        if _raise not in (None, False):\n            if not BINARIES.get(which):\n                raise _raise('%s not found' % which)\n        return BINARIES.get(which)\n\n    elif _raise not in (None, False):\n        for binary, (bin_test, reqd) in BINARIES_WANTED.iteritems():\n            if binary in skip or not reqd:\n                continue\n            if not BINARIES.get(binary):\n                raise _raise('%s not found' % binary)\n\n    return BINARIES\n\n\ndef GetDefaultGnuPGCommand(_raise=OSError):\n    return DetectBinaries(which='GnuPG', _raise=_raise)\n\n\ndef GetDefaultOpenSSLCommand(_raise=OSError):\n    return DetectBinaries(which='OpenSSL', _raise=_raise)\n\n\ndef GetDefaultTorPath(_raise=OSError):\n    return DetectBinaries(which='Tor', _raise=_raise)\n\n\ndef InDesktopEnvironment():\n    \"\"\"\n    Returns True if we're running in a desktop environment of some sort.\n    \"\"\"\n    # FIXME: Detect if we are somehow in the background on Windows or OS X.\n    return (sys.platform[:3] in ('dar', 'win') or os.getenv('DISPLAY'))\n\n\ndef RenameCannotOverwrite():\n    \"\"\"\n    The os.rename() function will not overwrite existing files on Windows.\n    \"\"\"\n    return sys.platform.startswith('win')\n\n\ndef NeedExplicitPortCheck():\n    \"\"\"\n    Our HTTP worker doesn't detect port reuse on Windows, need explicit checks.\n    \"\"\"\n    return sys.platform.startswith('win')\n\n\ndef TerminalSupportsAnsiColors():\n    \"\"\"\n    Windows doesn't like ANSI colors. Also, we want a TTY.\n    \"\"\"\n    return (sys.stdout.isatty() and sys.platform[:3] != \"win\")\n\n\ndef WindowsPopenSemantics():\n    \"\"\"\n    The safe_popen module implements slightly different semantics on Windows.\n    \"\"\"\n    return sys.platform.startswith('win')\n\n\ndef GetAppDataDirectory():\n    if sys.platform.startswith('win'):\n        # Obey Windows conventions (more or less?)\n        return os.getenv('APPDATA', os.path.expanduser('~'))\n    elif sys.platform.startswith('darwin'):\n        # Obey Mac OS X conventions\n        return os.path.expanduser('~/Library/Application Support')\n    else:\n        # Assume other platforms are Unixy\n        return os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))\n\n\ndef RestrictReadAccess(path):\n    \"\"\"\n    Restrict access to a file or directory so only the user can read it.\n    \"\"\"\n    # FIXME: Windows code goes here!\n    if os.path.isdir(path):\n        os.chmod(path, 0o700)\n    else:\n        os.chmod(path, 0o600)\n\n\ndef RandomListeningPort(count=1, host='127.0.0.1'):\n    socks = []\n    ports = []\n    try:\n        import socket\n        for port in range(0, count):\n            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n            sock.bind((host, 0))\n            socks.append(sock)\n            ports.append(sock.getsockname()[1])\n        if count == 1:\n            return ports[0]\n        else:\n            return ports\n    finally:\n        for sock in socks:\n            sock.close()\n"
  },
  {
    "path": "mailpile/plugins/__init__.py",
    "content": "from __future__ import print_function\n# Plugins!\nimport imp\nimport inspect\nimport json\nimport os\nimport sys\nimport traceback\n\nimport mailpile.commands\nimport mailpile.config.defaults\nimport mailpile.vcard\nfrom mailpile.i18n import i18n_disabled\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.mailboxes import register as register_mailbox\nfrom mailpile.util import *\n\n\n##[ Plugin discovery ]########################################################\n\n\n# These are the plugins we ship/import by default\n__all__ = [\n    'core',\n    'eventlog', 'search', 'tags', 'contacts', 'compose', 'groups',\n    'dates', 'sizes', 'autotag', 'cryptostate', 'crypto_gnupg', 'gui',\n    'setup_magic', 'oauth', 'exporters', 'plugins', 'motd', 'backups',\n    'vcard_carddav', 'vcard_gnupg', 'vcard_gravatar', 'vcard_libravatar',\n    'vcard_mork', 'html_magic', 'migrate', 'smtp_server', 'crypto_policy',\n    'keylookup', 'webterminal', 'crypto_autocrypt'\n]\nPLUGINS = __all__\n\n\nclass EmailTransform(object):\n    \"\"\"Base class for e-mail transforms\"\"\"\n    def __init__(self, config):\n        self.config = config\n\n    def _get_sender_profile(self, sender, kwargs):\n        profile = kwargs.get('sender_profile')\n        if not profile:\n            profile = self.config.get_profile(sender)\n        return profile\n\n    def _get_first_part(self, msg, mimetype):\n        for part in msg.walk():\n             if not part.is_multipart():\n                 mimetype = (part.get_content_type() or 'text/plain').lower()\n                 if mimetype == 'text/plain':\n                     return part\n        return None\n\n    def TransformIncoming(self, *args, **kwargs):\n        return list(args[:]) + [False]\n\n    def TransformOutgoing(self, *args, **kwargs):\n        return list(args[:]) + [False, True]\n\n\nclass PluginError(Exception):\n    pass\n\n\nclass PluginManager(object):\n    \"\"\"\n    Manage importing and loading of plugins. Note that this class is\n    effectively a singleton, as it works entirely with globals within\n    the mailpile.plugins module.\n    \"\"\"\n    DEFAULT = __all__\n    BUILTIN = (DEFAULT + [\n        'autotag_sb'\n    ])\n\n    # These are plugins which we consider required\n    REQUIRED = [\n        'core',\n        'eventlog', 'search', 'tags', 'contacts', 'compose', 'groups',\n        'dates', 'sizes', 'cryptostate', 'setup_magic', 'oauth', 'html_magic',\n        'plugins', 'keylookup', 'motd', 'backups', 'gui'\n    ]\n    # Plugins we want, if they are discovered\n    WANTED = [\n        'autoajax', 'print', 'hints'\n    ]\n    # Plugins that have been renamed from past releases\n    RENAMED = {\n        'crypto_utils': 'crypto_gnupg'\n    }\n    DISCOVERED = {}\n    LOADED = []\n\n    def __init__(self, plugin_name=None, builtin=False, deprecated=False,\n                 config=None, session=None):\n        if builtin and isinstance(builtin, (str, unicode)):\n            builtin = os.path.basename(builtin)\n            for ignore in ('.py', '.pyo', '.pyc'):\n                if builtin.endswith(ignore):\n                    builtin = builtin[:-len(ignore)]\n            if builtin not in self.LOADED:\n                self.LOADED.append(builtin)\n\n        self.loading_plugin = plugin_name\n        self.loading_builtin = plugin_name and builtin\n        self.builtin = builtin\n        self.deprecated = deprecated\n        self.session = session\n        self.config = config\n        self.manifests = []\n\n    def _listdir(self, path):\n        try:\n            return [d for d in os.listdir(path) if not d.startswith('.')]\n        except OSError:\n            return []\n\n    def _uncomment(self, json_data):\n        return '\\n'.join([l for l in json_data.splitlines()\n                          if not l.strip().startswith('#')])\n\n    def discover(self, paths, update=False):\n        \"\"\"\n        Scan the plugin directories for plugins we could load.\n        This updates the global PluginManager state and returns the\n        PluginManager itself (for chaining).\n        \"\"\"\n        plugins = self.BUILTIN[:]\n        for pdir in paths:\n            for subdir in self._listdir(pdir):\n                pname = subdir.lower()\n                if pname in self.BUILTIN:\n                    print('Cannot overwrite built-in plugin: %s' % pname)\n                    continue\n                if pname in self.DISCOVERED and not update:\n                    # FIXME: this is lame\n                    # print 'Ignoring duplicate plugin: %s' % pname\n                    continue\n                plug_path = os.path.join(pdir, subdir)\n                manifest_filename = os.path.join(plug_path, 'manifest.json')\n                try:\n                    with open(manifest_filename) as mfd:\n                        manifest = json.loads(self._uncomment(mfd.read()))\n                        safe_assert(manifest.get('name') == subdir)\n                        # FIXME: Need more sanity checks\n                        self.DISCOVERED[pname] = (plug_path, manifest)\n                except (ValueError, AssertionError):\n                    print('Bad manifest: %s' % manifest_filename)\n                except (OSError, IOError):\n                    pass\n\n        return self\n\n    def available(self):\n        return self.BUILTIN[:] + self.DISCOVERED.keys()\n\n    def loadable(self):\n        return self.BUILTIN[:] + self.RENAMED.keys() + self.DISCOVERED.keys()\n\n    def loadable_early(self):\n        return [k for k, (n, m) in self.DISCOVERED.iteritems()\n                if not m.get('require_login', True)]\n\n    def _import(self, full_name, full_path):\n        # create parents as necessary\n        parents = full_name.split('.')[2:] # skip mailpile.plugins\n        module = \"mailpile.plugins\"\n        for parent in parents:\n            mp = '%s.%s' % (module, parent)\n            if mp not in sys.modules:\n                sys.modules[mp] = imp.new_module(mp)\n                sys.modules[module].__dict__[parent] = sys.modules[mp]\n            module = mp\n        safe_assert(module == full_name)\n\n        # load actual module\n        sys.modules[full_name].__file__ = full_path\n        with i18n_disabled:\n            with open(full_path, 'r') as mfd:\n                exec(mfd.read(), sys.modules[full_name].__dict__)\n\n    def _load(self, plugin_name, process_manifest=False, config=None):\n        full_name = 'mailpile.plugins.%s' % plugin_name\n        if full_name in sys.modules:\n            return self\n\n        self.loading_plugin = full_name\n        if plugin_name in self.BUILTIN:\n            # The builtins are just normal Python code. If they have a\n            # manifest, they'll invoke process_manifest themselves.\n            self.loading_builtin = True\n            module = __import__(full_name)\n\n        elif plugin_name in self.DISCOVERED:\n            dirname, manifest = self.DISCOVERED[plugin_name]\n            self.loading_builtin = False\n\n            # Load the Python requested by the manifest.json\n            files = manifest.get('code', {}).get('python', [])\n            try:\n                for filename in files:\n                    path = os.path.join(dirname, filename)\n                    if filename == '.':\n                        self._import(full_name, dirname)\n                        continue\n                    elif filename.endswith('.py'):\n                        subname = filename[:-3].replace('/', '.')\n                        # FIXME: Is this a good idea?\n                        if full_name.endswith('.'+subname):\n                            self._import(full_name, path)\n                            continue\n                    elif os.path.isdir(path):\n                        subname = filename.replace('/', '.')\n                    else:\n                        continue\n                    self._import('.'.join([full_name, subname]), path)\n            except KeyboardInterrupt:\n                raise\n            except:\n                traceback.print_exc(file=sys.stderr)\n                print('FIXME: Loading %s failed, tell user!' % full_name)\n                if full_name in sys.modules:\n                    del sys.modules[full_name]\n                return None\n\n            spec = (full_name, manifest, dirname)\n            self.manifests.append(spec)\n            if process_manifest:\n                self._process_manifest_pass_one(*spec)\n                self._process_manifest_pass_two(*spec)\n                self._process_startup_hooks(*spec)\n        else:\n            print('Unrecognized plugin: %s' % plugin_name)\n            return self\n\n        if plugin_name not in self.LOADED:\n            self.LOADED.append(plugin_name)\n        return self\n\n    def load(self, *args, **kwargs):\n        try:\n            return self._load(*args, **kwargs)\n        finally:\n            self.loading_plugin = None\n            self.loading_builtin = False\n\n    def process_shutdown_hooks(self):\n        for plugin_name in self.DISCOVERED.keys():\n            try:\n                package = 'mailpile.plugins.%s' % plugin_name\n                _, manifest = self.DISCOVERED[plugin_name]\n\n                if package in sys.modules:\n                    for method_name in self._mf_path(manifest,\n                                                     'lifecycle', 'shutdown'):\n                        method = self._get_method(package, method_name)\n                        method(self.config)\n            except:\n                # ignore exceptions here as mailpile is going to shut down\n                traceback.print_exc(file=sys.stderr)\n\n    def process_manifests(self):\n        failed = []\n        for process in (self._process_manifest_pass_one,\n                        self._process_manifest_pass_two,\n                        self._process_startup_hooks):\n            for spec in self.manifests:\n                try:\n                    if spec[0] not in failed:\n                        process(*spec)\n                except Exception as e:\n                    print('Failed to process manifest for %s: %s' % (spec[0], e))\n                    failed.append(spec[0])\n                    traceback.print_exc()\n        return self\n\n    def _mf_path(self, mf, *path):\n        for p in path:\n            mf = mf.get(p, {})\n        return mf\n\n    def _mf_iteritems(self, mf, *path):\n        return self._mf_path(mf, *path).iteritems()\n\n    def _get_method(self, full_name, method):\n        full_method_name = '.'.join([full_name, method])\n        package, method_name = full_method_name.rsplit('.', 1)\n\n        module = sys.modules[package]\n        return getattr(module, method_name)\n\n    def _get_class(self, full_name, class_name):\n        full_class_name = '.'.join([full_name, class_name])\n        mod_name, class_name = full_class_name.rsplit('.', 1)\n        module = __import__(mod_name, globals(), locals(), class_name)\n        return getattr(module, class_name)\n\n    def _process_manifest_pass_one(self, full_name,\n                                   manifest=None, plugin_path=None):\n        \"\"\"\n        Pass one of processing the manifest data. This updates the global\n        configuration and registers Python code with the URL map.\n        \"\"\"\n        if not manifest:\n            return\n\n        manifest_path = lambda *p: self._mf_path(manifest, *p)\n        manifest_iteritems = lambda *p: self._mf_iteritems(manifest, *p)\n\n        # Register config variables and sections\n        for section, rules in manifest_iteritems('config', 'sections'):\n            self.register_config_section(*(section.split('.') + [rules]))\n        for section, rules in manifest_iteritems('config', 'variables'):\n            self.register_config_variables(*(section.split('.') + [rules]))\n\n        # Register commands\n        for command in manifest_path('commands'):\n            cls = self._get_class(full_name, command['class'])\n\n            # FIXME: This is all a bit hacky, we probably just want to\n            #        kill the SYNOPSIS attribute entirely.\n            if 'input' in command:\n                name = url = '%s/%s' % (command['input'], command['name'])\n                cls.UI_CONTEXT = command['input']\n            else:\n                name = command.get('name', cls.SYNOPSIS[1])\n                url = command.get('url', cls.SYNOPSIS[2])\n            cls.SYNOPSIS = tuple([cls.SYNOPSIS[0], name, url,\n                                  cls.SYNOPSIS_ARGS or cls.SYNOPSIS[3]])\n\n            self.register_commands(cls)\n\n        # Register worker threads\n        for thr in manifest_path('threads'):\n            self.register_worker(self._get_class(full_name, thr))\n\n        # Register mailboxes\n        package = str(full_name)\n        for mailbox in manifest_path('mailboxes'):\n            cls = self._get_class(package, mailbox['class'])\n            priority = int(mailbox['priority'])\n            register_mailbox(priority, cls)\n\n    def _process_manifest_pass_two(self, full_name,\n                                   manifest=None, plugin_path=None):\n        \"\"\"\n        Pass two of processing the manifest data. This maps templates and\n        data to API commands and links registers classes and methods as\n        hooks here and there. As these things depend both on configuration\n        and the URL map, this happens as a second phase.\n        \"\"\"\n        if not manifest:\n            return\n\n        manifest_path = lambda *p: self._mf_path(manifest, *p)\n        manifest_iteritems = lambda *p: self._mf_iteritems(manifest, *p)\n\n        # Register javascript classes\n        for fn in manifest.get('code', {}).get('javascript', []):\n            class_name = fn.replace('/', '.').rsplit('.', 1)[0]\n            # FIXME: Is this a good idea?\n            if full_name.endswith('.'+class_name):\n                parent, class_name = full_name.rsplit('.', 1)\n            else:\n                parent = full_name\n            self.register_js(parent, class_name,\n                             os.path.join(plugin_path, fn))\n\n        # Register CSS files\n        for fn in manifest.get('code', {}).get('css', []):\n            file_name = fn.replace('/', '.').rsplit('.', 1)[0]\n            self.register_css(full_name, file_name,\n                              os.path.join(plugin_path, fn))\n\n        # Register web assets\n        if plugin_path:\n            from mailpile.urlmap import UrlMap\n            um = UrlMap(session=self.session, config=self.config)\n            for url, info in manifest_iteritems('routes'):\n                filename = os.path.join(plugin_path, info['file'])\n\n                # Short-cut for static content\n                if url.startswith('/static/'):\n                    self.register_web_asset(full_name, url[8:], filename,\n                        mimetype=info.get('mimetype', None))\n                    continue\n\n                # Finds the right command class and register asset in\n                # the right place for that particular command.\n                commands = []\n                if (not url.startswith('/api/')) and 'api' in info:\n                    url = '/api/%d%s' % (info['api'], url)\n                    if url[-1] == '/':\n                        url += 'as.html'\n                for method in ('GET', 'POST', 'PUT', 'UPDATE', 'DELETE'):\n                    try:\n                        commands = um.map(None, method, url, {}, {})\n                        break\n                    except UsageError:\n                        pass\n\n                output = [o.get_render_mode()\n                          for o in commands if hasattr(o, 'get_render_mode')]\n                output = output and output[-1] or 'html'\n                if commands:\n                    command = commands[-1]\n                    tpath = command.template_path(output.split('.')[-1],\n                                                  template=output)\n                    self.register_web_asset(full_name,\n                                            'html/' + tpath,\n                                            filename)\n                else:\n                    print('FIXME: Un-routable URL in manifest %s' % url)\n\n        # Register email content/crypto hooks\n        s = self\n        for which, reg in (\n            ('outgoing_content', s.register_outgoing_email_content_transform),\n            ('outgoing_crypto', s.register_outgoing_email_crypto_transform),\n            ('incoming_crypto', s.register_incoming_email_crypto_transform),\n            ('incoming_content', s.register_incoming_email_content_transform)\n        ):\n            for item in manifest_path('email_transforms', which):\n                name = '%3.3d_%s' % (int(item.get('priority', 999)), full_name)\n                reg(name, self._get_class(full_name, item['class']))\n\n        # Register search keyword extractors\n        s = self\n        for which, reg in (\n            ('meta', s.register_meta_kw_extractor),\n            ('text', s.register_text_kw_extractor),\n            ('data', s.register_data_kw_extractor)\n        ):\n            for item in manifest_path('keyword_extractors', which):\n                reg('%s.%s' % (full_name, item),\n                    self._get_class(full_name, item))\n\n        # Register contact/vcard hooks\n        for which, reg in (\n            ('importers', self.register_vcard_importers),\n            ('exporters', self.register_contact_exporters),\n            ('context', self.register_contact_context_providers)\n        ):\n            for item in manifest_path('contacts', which):\n                reg(self._get_class(full_name, item))\n\n        # Register periodic jobs\n        def reg_job(info, spd, register):\n            interval, cls = info['interval'], info['class']\n            callback = self._get_class(full_name, cls)\n            register('%s.%s/%s-%s' % (full_name, cls, spd, interval),\n                     interval, callback)\n        for info in manifest_path('periodic_jobs', 'fast'):\n            reg_job(info, 'fast', self.register_fast_periodic_job)\n        for info in manifest_path('periodic_jobs', 'slow'):\n            reg_job(info, 'slow', self.register_slow_periodic_job)\n\n        ucfull_name = full_name.capitalize()\n        for ui_type, elems in manifest.get('user_interface', {}).iteritems():\n            for hook in elems:\n                if 'javascript_setup' in hook:\n                    js = hook['javascript_setup']\n                    if not js.startswith('Mailpile.'):\n                       hook['javascript_setup'] = '%s.%s' % (ucfull_name, js)\n                if 'javascript_events' in hook:\n                    for event, call in hook['javascript_events'].iteritems():\n                        if not call.startswith('Mailpile.'):\n                            hook['javascript_events'][event] = '%s.%s' \\\n                                % (ucfull_name, call)\n                self.register_ui_element(ui_type, **hook)\n\n    def _process_startup_hooks(self, package,\n                               manifest=None, plugin_path=None):\n        if not manifest:\n            return\n\n        manifest_path = lambda *p: self._mf_path(manifest, *p)\n\n        for method_name in manifest_path('lifecycle', 'startup'):\n            method = self._get_method(package, method_name)\n            method(self.config)\n\n    def _compat_check(self, strict=True):\n        if ((strict and (not self.loading_plugin and not self.builtin)) or\n                self.deprecated):\n            stack = inspect.stack()\n            if str(stack[2][1]) == '<string>':\n                raise PluginError('Naughty plugin tried to directly access '\n                                  'mailpile.plugins!')\n\n            where = '->'.join(['%s:%s' % ('/'.join(stack[i][1].split('/')[-2:]),\n                                          stack[i][2])\n                              for i in reversed(range(2, len(stack)-1))])\n            print(('FIXME: Deprecated use of %s at %s (issue #547)'\n                   ) % (stack[1][3], where))\n\n    def _rhtf(self, kw_hash, term, function):\n        if term in kw_hash:\n            raise PluginError('Already registered: %s' % term)\n        kw_hash[term] = function\n\n\n    ##[ Pluggable configuration ]#############################################\n\n    def register_config_variables(self, *args):\n        self._compat_check()\n        args = list(args)\n        rules = args.pop(-1)\n        dest = mailpile.config.defaults.CONFIG_RULES\n        path = '/'.join(args)\n        for arg in args:\n            dest = dest[arg][-1]\n        for rname, rule in rules.iteritems():\n            if rname in dest:\n                raise PluginError('Variable already exist: %s/%s' % (path, rname))\n            else:\n                dest[rname] = rule\n\n    def register_config_section(self, *args):\n        self._compat_check()\n        args = list(args)\n        rules = args.pop(-1)\n        rname = args.pop(-1)\n        dest = mailpile.config.defaults.CONFIG_RULES\n        path = '/'.join(args)\n        for arg in args:\n            dest = dest[arg][-1]\n        if rname in dest:\n            raise PluginError('Section already exist: %s/%s' % (path, rname))\n        else:\n            dest[rname] = rules\n\n\n    ##[ Pluggable message transformations ]###################################\n\n    INCOMING_EMAIL_ENCRYPTION = {}\n    INCOMING_EMAIL_CONTENT = {}\n    OUTGOING_EMAIL_CONTENT = {}\n    OUTGOING_EMAIL_ENCRYPTION = {}\n\n    def _txf_in(self, transforms, config, msg, kwargs):\n        matched = 0\n        for name in sorted(transforms.keys()):\n            txf = transforms[name](config)\n            msg, match, cont = txf.TransformIncoming(msg, **kwargs)\n            if match:\n                matched += 1\n            if not cont:\n                break\n        return msg, matched\n\n    def _txf_out(self, transforms, cfg, s, r, msg, kwa):\n        matched = 0\n        for name in sorted(transforms.keys()):\n            txf = transforms[name](cfg)\n            s, r, msg, match, cont = txf.TransformOutgoing(s, r, msg, **kwa)\n            if match:\n                matched += 1\n            if not cont:\n                break\n        return s, r, msg, matched\n\n    def incoming_email_crypto_transform(self, cfg, msg, **kwa):\n        return self._txf_in(self.INCOMING_EMAIL_ENCRYPTION, cfg, msg, kwa)\n\n    def incoming_email_content_transform(self, config, msg, **kwa):\n        return self._txf_in(self.INCOMING_EMAIL_CONTENT, config, msg, kwa)\n\n    def outgoing_email_content_transform(self, cfg, s, r, m, **kwa):\n        return self._txf_out(self.OUTGOING_EMAIL_CONTENT, cfg, s, r, m, kwa)\n\n    def outgoing_email_crypto_transform(self, cfg, s, r, m, **kwa):\n        return self._txf_out(self.OUTGOING_EMAIL_ENCRYPTION, cfg, s, r, m, kwa)\n\n    def register_incoming_email_crypto_transform(self, name, transform):\n        return self._rhtf(self.INCOMING_EMAIL_ENCRYPTION, name, transform)\n\n    def register_incoming_email_content_transform(self, name, transform):\n        return self._rhtf(self.INCOMING_EMAIL_CONTENT, name, transform)\n\n    def register_outgoing_email_content_transform(self, name, transform):\n        return self._rhtf(self.OUTGOING_EMAIL_CONTENT, name, transform)\n\n    def register_outgoing_email_crypto_transform(self, name, transform):\n        return self._rhtf(self.OUTGOING_EMAIL_ENCRYPTION, name, transform)\n\n\n    ##[ Pluggable keyword extractors ]########################################\n\n    DATA_KW_EXTRACTORS = {}\n    TEXT_KW_EXTRACTORS = {}\n    META_KW_EXTRACTORS = {}\n\n    def register_data_kw_extractor(self, term, function):\n        self._compat_check()\n        return self._rhtf(self.DATA_KW_EXTRACTORS, term, function)\n\n    def register_text_kw_extractor(self, term, function):\n        self._compat_check()\n        return self._rhtf(self.TEXT_KW_EXTRACTORS, term, function)\n\n    def register_meta_kw_extractor(self, term, function):\n        self._compat_check()\n        return self._rhtf(self.META_KW_EXTRACTORS, term, function)\n\n    def get_data_kw_extractors(self):\n        self._compat_check(strict=False)\n        return self.DATA_KW_EXTRACTORS.values()\n\n    def get_text_kw_extractors(self):\n        self._compat_check(strict=False)\n        return self.TEXT_KW_EXTRACTORS.values()\n\n    def get_meta_kw_extractors(self):\n        self._compat_check(strict=False)\n        return self.META_KW_EXTRACTORS.values()\n\n\n    ##[ Pluggable search terms ]##############################################\n\n    SEARCH_TERMS = {}\n\n    def get_search_term(self, term, default=None):\n        self._compat_check(strict=False)\n        return self.SEARCH_TERMS.get(term, default)\n\n    def register_search_term(self, term, function):\n        self._compat_check()\n        if term in self.SEARCH_TERMS:\n            raise PluginError('Already registered: %s' % term)\n        self.SEARCH_TERMS[term] = function\n\n\n    ##[ Pluggable keyword filters ]###########################################\n\n    FILTER_HOOKS_PRE = {}\n    FILTER_HOOKS_POST = {}\n\n    def get_filter_hooks(self, hooks):\n        self._compat_check(strict=False)\n        return ([self.FILTER_HOOKS_PRE[k]\n                 for k in sorted(self.FILTER_HOOKS_PRE.keys())]\n                + hooks +\n                [self.FILTER_HOOKS_POST[k]\n                 for k in sorted(self.FILTER_HOOKS_POST.keys())])\n\n    def register_filter_hook_pre(self, name, hook):\n        self._compat_check()\n        self.FILTER_HOOKS_PRE[name] = hook\n\n    def register_filter_hook_post(self, name, hook):\n        self._compat_check()\n        self.FILTER_HOOKS_POST[name] = hook\n\n\n    ##[ Pluggable vcard functions ]###########################################\n\n    VCARD_IMPORTERS = {}\n    VCARD_EXPORTERS = {}\n    VCARD_CONTEXT_PROVIDERS = {}\n\n    def _reg_vcard_plugin(self, what, cfg_sect, plugin_classes, cls, dct):\n        for plugin_class in plugin_classes:\n            if not plugin_class.SHORT_NAME or not plugin_class.FORMAT_NAME:\n                raise PluginError(\"Please set SHORT_NAME \"\n                                  \"and FORMAT_* attributes!\")\n            if not issubclass(plugin_class, cls):\n                raise PluginError(\"%s must be a %s\" % (what, cls))\n            if plugin_class.SHORT_NAME in dct:\n                raise PluginError(\"%s for %s already registered\"\n                                  % (what, importer.FORMAT_NAME))\n\n            if plugin_class.CONFIG_RULES:\n                rules = {\n                    'guid': ['VCard source UID', str, ''],\n                    'description': ['VCard source description', str, '']\n                }\n                rules.update(plugin_class.CONFIG_RULES)\n                self.register_config_section(\n                    'prefs', 'vcard', cfg_sect, plugin_class.SHORT_NAME,\n                    [plugin_class.FORMAT_DESCRIPTION, rules, []])\n\n            dct[plugin_class.SHORT_NAME] = plugin_class\n\n    def register_vcard_importers(self, *importers):\n        self._compat_check()\n        self._reg_vcard_plugin('Importer', 'importers', importers,\n                               mailpile.vcard.VCardImporter,\n                               self.VCARD_IMPORTERS)\n\n    def register_contact_exporters(self, *exporters):\n        self._compat_check()\n        self._reg_vcard_plugin('Exporter', 'exporters', exporters,\n                               mailpile.vcard.VCardExporter,\n                               self.VCARD_EXPORTERS)\n\n    def register_contact_context_providers(self, *providers):\n        self._compat_check()\n        self._reg_vcard_plugin('Context provider', 'context', providers,\n                               mailpile.vcard.VCardContextProvider,\n                               self.VCARD_CONTEXT_PROVIDERS)\n\n\n    ##[ Pluggable cron jobs ]#################################################\n\n    FAST_PERIODIC_JOBS = {}\n    SLOW_PERIODIC_JOBS = {}\n\n    def register_fast_periodic_job(self, name, period, callback):\n        self._compat_check()\n        # FIXME: complain about duplicates?\n        self.FAST_PERIODIC_JOBS[name] = (period, callback)\n\n    def register_slow_periodic_job(self, name, period, callback):\n        self._compat_check()\n        # FIXME: complain about duplicates?\n        self.SLOW_PERIODIC_JOBS[name] = (period, callback)\n\n\n    ##[ Pluggable background worker threads ]################################\n\n    WORKERS = []\n\n    def register_worker(self, thread_obj):\n        self._compat_check()\n        safe_assert(hasattr(thread_obj, 'start'))\n        safe_assert(hasattr(thread_obj, 'quit'))\n        # FIXME: complain about duplicates?\n        self.WORKERS.append(thread_obj)\n\n\n    ##[ Pluggable commands ]##################################################\n\n    def register_commands(self, *args):\n        self._compat_check()\n        COMMANDS = mailpile.commands.COMMANDS\n        for cls in args:\n            if cls not in COMMANDS:\n                COMMANDS.append(cls)\n\n\n    ##[ Pluggable javascript, CSS template and static content ]###############\n\n    JS_CLASSES = {}\n    CSS_FILES = {}\n    WEB_ASSETS = {}\n\n    def register_js(self, plugin, classname, filename):\n        self.JS_CLASSES['%s.%s' % (plugin, classname)] = filename\n\n    def register_css(self, plugin, classname, filename):\n        self.CSS_FILES['%s.%s' % (plugin, classname)] = filename\n\n    def register_web_asset(self, plugin, path, filename, mimetype='text/html'):\n        if path in self.WEB_ASSETS:\n            raise PluginError(_('Already registered: %s') % path)\n        self.WEB_ASSETS[path] = (filename, mimetype, plugin)\n\n    def get_js_classes(self):\n        return self.JS_CLASSES\n\n    def get_css_files(self):\n        return self.CSS_FILES\n\n    def get_web_asset(self, path, default=None):\n        return tuple(self.WEB_ASSETS.get(path, [default, None])[0:2])\n\n\n    ##[ Pluggable UI elements ]###############################################\n\n    # These are the elements that exist at the moment\n    UI_ELEMENTS = {\n        'settings': [],\n        'activities': [],\n        'email_activities': [],  # Activities on e-mails\n        'thread_activities': [], # Activities on e-mails in a thread\n        'display_modes': [],\n        'display_refiners': [],\n        'selection_actions': []\n    }\n\n    def register_ui_element(self, ui_type,\n                            context=None, name=None,\n                            text=None, icon=None, description=None,\n                            url=None, javascript_setup=None,\n                            javascript_events=None, **kwargs):\n        name = name.replace('/', '_')\n        if name not in [e.get('name') for e in self.UI_ELEMENTS[ui_type]]:\n            # FIXME: Is context valid?\n            info = {\n                \"context\": context or [],\n                \"name\": name,\n                \"text\": text,\n                \"icon\": icon,\n                \"description\": description,\n                \"javascript_setup\": javascript_setup,\n                \"javascript_events\": javascript_events,\n                \"url\": url\n            }\n            for k, v in kwargs.iteritems():\n                info[k] = v\n            self.UI_ELEMENTS[ui_type].append(info)\n        else:\n            raise ValueError('Duplicate element: %s' % name)\n\n    def get_ui_elements(self, ui_type, context):\n        # FIXME: This is a bit inefficient.\n        #        The good thing is, it maintains a stable order.\n        return [elem for elem in self.UI_ELEMENTS[ui_type]\n                if context in elem['context']]\n\n\n##[ Backwards compatibility ]################################################\n\n_default_pm = PluginManager(builtin=False, deprecated=True)\n\nregister_config_variables = _default_pm.register_config_variables\nregister_config_section = _default_pm.register_config_section\nregister_data_kw_extractor = _default_pm.register_data_kw_extractor\nregister_text_kw_extractor = _default_pm.register_text_kw_extractor\nregister_meta_kw_extractor = _default_pm.register_meta_kw_extractor\nget_data_kw_extractors = _default_pm.get_data_kw_extractors\nget_text_kw_extractors = _default_pm.get_text_kw_extractors\nget_meta_kw_extractors = _default_pm.get_meta_kw_extractors\nget_search_term = _default_pm.get_search_term\nregister_search_term = _default_pm.register_search_term\nfilter_hooks = _default_pm.get_filter_hooks\nregister_filter_hook_pre = _default_pm.register_filter_hook_pre\nregister_filter_hook_post = _default_pm.register_filter_hook_post\nregister_vcard_importers = _default_pm.register_vcard_importers\nregister_contact_exporters = _default_pm.register_contact_exporters\nregister_contact_context_providers = _default_pm.register_contact_context_providers\nregister_fast_periodic_job = _default_pm.register_fast_periodic_job\nregister_slow_periodic_job = _default_pm.register_slow_periodic_job\nregister_worker = _default_pm.register_worker\nregister_commands = _default_pm.register_commands\n"
  },
  {
    "path": "mailpile/plugins/autotag.py",
    "content": "# This is the generic auto-tagging plugin.\n#\n# We feed the classifier the same terms as go into the search engine,\n# which should allow us to actually introspect a bit into the behavior\n# of the classifier.\n\nimport math\nimport time\nimport datetime\n\nimport mailpile.util\nfrom mailpile.commands import Command\nfrom mailpile.config.base import ConfigDict\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.mailutils.emails import Email\nfrom mailpile.plugins import PluginManager\nfrom mailpile.util import *\n\n\n_plugins = PluginManager(builtin=__file__)\n\n\n##[ Configuration ]###########################################################\n\nTAGGERS = {}\nTRAINERS = {}\n\nAUTO_TAG_DISABLED = (None, False, '', 'off', 'false', 'fancy', 'builtin')\n\nAUTO_TAG_CONFIG = {\n    'match_tag': ['Tag we are adding to automatically', str, ''],\n    'unsure_tag': ['If unsure, add to this tag', str, ''],\n    'exclude_tags': ['Tags on messages we should never match (ham)', str, []],\n    'ignore_kws': ['Ignore messages with these keywords', str, []],\n    'corpus_size': ['How many messages do we train on?', int, 1200],\n    'threshold': ['Size of the sure/unsure ranges', float, 0.1],\n    'tagger': ['Internal class name or |shell command', str, ''],\n    'trainer': ['Internal class name or |shell commant', str, '']}\n\n_plugins.register_config_section(\n    'prefs', 'autotag', [\"Auto-tagging\", AUTO_TAG_CONFIG , []])\n\n\ndef at_identify(at_config):\n    return md5_hex(at_config.match_tag,\n                   at_config.tagger,\n                   at_config.trainer)[:12]\n\n\ndef autotag_configs(config):\n    done = []\n    for at_config in config.prefs.autotag:\n        yield at_config\n        done.append(at_config.match_tag)\n\n    taggers = [k for k in TAGGERS.keys() if k != '_default']\n    if not taggers:\n        return\n\n    for tid, tag_info in config.tags.iteritems():\n        auto_tagging = (tag_info.auto_tag or '')\n        if (tid not in done and\n                auto_tagging.lower() not in AUTO_TAG_DISABLED):\n            at_config = ConfigDict(_rules=AUTO_TAG_CONFIG)\n            at_config.match_tag = tid\n            if auto_tagging not in taggers:\n                auto_tagging = taggers[0]\n            at_config.tagger = auto_tagging\n            at_config.trainer = auto_tagging\n            yield at_config\n\n\nclass AutoTagger(object):\n    def __init__(self, tagger, trainer):\n        self.tagger = tagger\n        self.trainer = trainer\n        self.trained = False\n\n    def reset(self, at_config):\n        \"\"\"Reset to an untrained state\"\"\"\n        self.trainer.reset(self, at_config)\n        self.trained = False\n\n    def learn(self, *args):\n        self.trained = True\n        return self.trainer.learn(self, *args)\n\n    def should_tag(self, *args):\n        return self.tagger.should_tag(self, *args)\n\n\ndef SaveAutoTagger(config, at_config):\n    aid = at_identify(at_config)\n    at = config.autotag.get(aid)\n    if at and at.trained:\n        config.save_pickle(at, 'pickled-autotag.%s' % aid)\n\n\ndef LoadAutoTagger(config, at_config):\n    if not config.real_hasattr('autotag'):\n        config.real_setattr('autotag', {})\n    aid = at_identify(at_config)\n    at = config.autotag.get(aid)\n    if aid not in config.autotag:\n        cfn = 'pickled-autotag.%s' % aid\n        try:\n            config.autotag[aid] = config.load_pickle(cfn)\n        except (IOError, EOFError):\n            tagger = at_config.tagger\n            trainer = at_config.trainer\n            config.autotag[aid] = AutoTagger(\n                TAGGERS.get(tagger, TAGGERS['_default'])(tagger),\n                TRAINERS.get(trainer, TRAINERS['_default'])(trainer))\n            SaveAutoTagger(config, at_config)\n    return config.autotag[aid]\n\n\n# FIXME: This is dumb\nimport mailpile.config.manager\nmailpile.config.manager.ConfigManager.load_auto_tagger = LoadAutoTagger\nmailpile.config.manager.ConfigManager.save_auto_tagger = SaveAutoTagger\n\n\n##[ Internal classes ]########################################################\n\nclass AutoTagCommand(object):\n    def __init__(self, command):\n        self.command = command\n\n\nclass Tagger(AutoTagCommand):\n    def should_tag(self, atagger, at_config, msg, keywords):\n        \"\"\"Returns (result, evidence), result =True, False or None\"\"\"\n        return (False, None)\n\n\nclass Trainer(AutoTagCommand):\n    def learn(self, atagger, at_config, msg, keywords, should_tag):\n        \"\"\"Learn that this message should (or should not) be tagged\"\"\"\n        pass\n\n    def reset(self, atagger, at_config):\n        \"\"\"Reset to an untrained state (called by AutoTagger.reset)\"\"\"\n        pass\n\n\nTAGGERS['_default'] = Tagger\nTRAINERS['_default'] = Trainer\n\n\n##[ Commands ]################################################################\n\n\nclass AutoTagCommand(Command):\n    ORDER = ('Tagging', 9)\n\n    def _get_keywords(self, e):\n        idx = self._idx()\n        if not hasattr(self, 'rcache'):\n            self.rcache = {}\n        mid = e.msg_mid()\n        if mid not in self.rcache:\n            kws, snippet = idx.read_message(\n                self.session,\n                mid,\n                e.get_msg_info(field=idx.MSG_ID),\n                e.get_msg(),\n                e.get_msg_size(),\n                int(e.get_msg_info(field=idx.MSG_DATE), 36))\n            self.rcache[mid] = kws\n        return self.rcache[mid]\n\n\nclass Retrain(AutoTagCommand):\n    SYNOPSIS = (None, 'autotag/retrain', None, '[<tags>]')\n\n    def command(self):\n        return self._retrain(tags=self.args)\n\n    def _retrain(self, tags=None):\n        \"Retrain autotaggers\"\n        session, config, idx = self.session, self.session.config, self._idx()\n        tags = tags or [asb.match_tag for asb in autotag_configs(config)]\n        tids = [config.get_tag(t)._key for t in tags if t]\n\n        session.ui.mark(_('Retraining SpamBayes autotaggers'))\n        if not config.real_hasattr('autotag'):\n            config.real_setattr('autotag', {})\n\n        # Find all the interesting messages! We don't look in the trash,\n        # but we do look at interesting spam.\n        #\n        # Note: By specifically stating that we DON'T want trash, we\n        #       disable the search engine's default result suppression\n        #       and guarantee these results don't corrupt the somewhat\n        #       lame/broken result cache.\n        #\n        no_trash = ['-in:%s' % t._key for t in config.get_tags(type='trash')]\n        interest = {}\n        for ttype in ('replied', 'read', 'tagged'):\n            interest[ttype] = set()\n            for tag in config.get_tags(type=ttype):\n                interest[ttype] |= idx.search(session,\n                                              ['in:%s' % tag.slug] + no_trash\n                                              ).as_set()\n            session.ui.notify(_('Have %d interesting %s messages'\n                                ) % (len(interest[ttype]), ttype))\n\n        retrained, unreadable = [], []\n        count_all = 0\n        for at_config in autotag_configs(config):\n            at_tag = config.get_tag(at_config.match_tag)\n            if at_tag and at_tag._key in tids:\n                session.ui.mark('Retraining: %s' % at_tag.name)\n\n                yn = [(set(), set(), 'in:%s' % at_tag.slug, True),\n                      (set(), set(), '-in:%s' % at_tag.slug, False)]\n\n                # Get the current message sets: tagged and untagged messages\n                # excluding trash.\n                for tset, mset, srch, which in yn:\n                    mset |= idx.search(session, [srch] + no_trash).as_set()\n\n                # If we have any exclude_tags, they are particularly\n                # interesting, so we'll look at them first.\n                interesting = []\n                for etagid in at_config.exclude_tags:\n                    etag = config.get_tag(etagid)\n                    if etag._key not in interest:\n                        srch = ['in:%s' % etag._key] + no_trash\n                        interest[etag._key] = idx.search(session, srch\n                                                         ).as_set()\n                    interesting.append(etag._key)\n                interesting.extend(['replied', 'read', 'tagged', None])\n\n                # Go through the interest types in order of preference and\n                # while we still lack training data, add to the training set.\n                for ttype in interesting:\n                    for tset, mset, srch, which in yn:\n                        # False positives are really annoying, and generally\n                        # speaking any autotagged subset should be a small\n                        # part of the Universe. So we divide the corpus\n                        # budget 33% True, 67% False.\n                        full_size = int(at_config.corpus_size *\n                                        (0.33 if which else 0.67))\n                        want = min(full_size // len(interesting),\n                                   max(0, full_size - len(tset)))\n                        # Make sure we always fully utilize our budget\n                        if full_size > len(tset) and not ttype:\n                            want = full_size - len(tset)\n\n                        if want:\n                            if ttype:\n                                adding = sorted(list(mset & interest[ttype]))\n                            else:\n                                adding = sorted(list(mset))\n                            adding = set(list(reversed(adding))[:want])\n                            tset |= adding\n                            mset -= adding\n\n                # Load classifier, reset\n                atagger = config.load_auto_tagger(at_config)\n                atagger.reset(at_config)\n                for tset, mset, srch, which in yn:\n                    count = 0\n                    # We go through the list of message in order, to avoid\n                    # thrashing caches too badly.\n                    for msg_idx in sorted(list(tset)):\n                        try:\n                            e = Email(idx, msg_idx)\n                            count += 1\n                            count_all += 1\n                            session.ui.mark(\n                                _('Reading %s (%d/%d, %s=%s)'\n                                  ) % (e.msg_mid(), count, len(tset),\n                                       at_tag.name, which))\n                            atagger.learn(at_config,\n                                          e.get_msg(),\n                                          self._get_keywords(e),\n                                          which)\n                            play_nice_with_threads()\n                            if mailpile.util.QUITTING:\n                                return self._error('Aborted')\n                        except (IndexError, TypeError, ValueError,\n                                OSError, IOError):\n                            if 'autotag' in session.config.sys.debug:\n                                import traceback\n                                traceback.print_exc()\n                            unreadable.append(msg_idx)\n                            session.ui.warning(\n                                _('Failed to process message at =%s'\n                                  ) % (b36(msg_idx)))\n\n                # We got this far without crashing, so save the result.\n                config.save_auto_tagger(at_config)\n                retrained.append(at_tag.name)\n\n        message = _('Retrained SpamBayes auto-tagging for %s'\n                    ) % ', '.join(retrained)\n        session.ui.mark(message)\n        return self._success(message, result={\n            'retrained': retrained,\n            'unreadable': unreadable,\n            'read_messages': count_all\n        })\n\n    @classmethod\n    def interval_retrain(cls, session):\n        \"\"\"\n        Retrains autotaggers\n\n        Classmethod used for periodic automatic retraining\n        \"\"\"\n        result = cls(session)._retrain()\n        if result:\n            return True\n        else:\n            return False\n\n\n_plugins.register_config_variables('prefs', {\n    'autotag_retrain_interval': [\n        _('Periodically retrain autotagger (seconds)'), int, 24*60*60]})\n\n_plugins.register_slow_periodic_job(\n    'retrain_autotag',\n    'prefs.autotag_retrain_interval',\n    Retrain.interval_retrain)\n\n\nclass Classify(AutoTagCommand):\n    SYNOPSIS = (None, 'autotag/classify', None, '<msgs>')\n    ORDER = ('Tagging', 9)\n\n    def _classify(self, emails):\n        session, config, idx = self.session, self.session.config, self._idx()\n        results = {}\n        unknown = []\n        for e in emails:\n            kws = self._get_keywords(e)\n            result = results[e.msg_mid()] = {}\n            for at_config in autotag_configs(config):\n                if not at_config.match_tag:\n                    continue\n                at_tag = config.get_tag(at_config.match_tag)\n                if not at_tag and at_config.match_tag not in unknown:\n                    session.ui.error(_('Unknown tag: %s'\n                                       ) % at_config.match_tag)\n                    unknown.append(at_config.match_tag)\n                    continue\n\n                atagger = config.load_auto_tagger(at_config)\n                if atagger.trained:\n                    result[at_tag._key] = result.get(at_tag._key, [])\n                    result[at_tag._key].append(atagger.should_tag(\n                        at_config, e.get_msg(), kws\n                    ))\n        return results\n\n    def command(self):\n        session, config, idx = self.session, self.session.config, self._idx()\n        emails = [Email(idx, mid) for mid in self._choose_messages(self.args)]\n        return self._success(_('Classified %d messages') % len(emails),\n                             self._classify(emails))\n\n\nclass AutoTag(Classify):\n    SYNOPSIS = (None, 'autotag', None, '<msgs>')\n    ORDER = ('Tagging', 9)\n\n    def command(self):\n        session, config, idx = self.session, self.session.config, self._idx()\n        emails = [Email(idx, mid) for mid in self._choose_messages(self.args)]\n        scores = self._classify(emails)\n        tag = {}\n        for mid in scores:\n            for at_config in autotag_configs(config):\n                at_tag = config.get_tag(at_config.match_tag)\n                if not at_tag:\n                    continue\n\n                wants = scores[mid].get(at_tag._key, [(False, )])\n                want = bool([True for w in wants if w[0]])\n\n                if want is True:\n                    if at_config.match_tag not in tag:\n                        tag[at_config.match_tag] = [mid]\n                    else:\n                        tag[at_config.match_tag].append(mid)\n\n                elif at_config.unsure_tag and want is None:\n                    if at_config.unsure_tag not in tag:\n                        tag[at_config.unsure_tag] = [mid]\n                    else:\n                        tag[at_config.unsure_tag].append(mid)\n\n        for tid in tag:\n            idx.add_tag(session, tid, msg_idxs=[int(i, 36) for i in tag[tid]])\n\n        return self._success(_('Auto-tagged %d messages') % len(emails), tag)\n\n\n_plugins.register_commands(Retrain, Classify, AutoTag)\n\n\n##[ Keywords ]################################################################\n\ndef filter_hook(session, msg_mid, msg, keywords, **kwargs):\n    \"\"\"Classify this message.\"\"\"\n    if not kwargs.get('incoming', False):\n        return keywords\n\n    config = session.config\n    for at_config in autotag_configs(config):\n        try:\n            at_tag = config.get_tag(at_config.match_tag)\n            atagger = config.load_auto_tagger(at_config)\n            if not atagger.trained:\n                continue\n            want, info = atagger.should_tag(at_config, msg, keywords)\n            if want is True:\n                if 'autotag' in config.sys.debug:\n                    session.ui.debug(('Autotagging %s with %s (w=%s, i=%s)'\n                                      ) % (msg_mid, at_tag.name, want, info))\n                keywords.add('%s:in' % at_tag._key)\n            elif at_config.unsure_tag and want is None:\n                unsure_tag = config.get_tag(at_config.unsure_tag)\n                if 'autotag' in config.sys.debug:\n                    session.ui.debug(('Autotagging %s with %s (w=%s, i=%s)'\n                                      ) % (msg_mid, unsure_tag.name,\n                                           want, info))\n                keywords.add('%s:in' % unsure_tag._key)\n        except (KeyError, AttributeError, ValueError):\n            pass\n\n    return keywords\n\n\n# We add a filter pre-hook with a high (late) priority.  Late priority to\n# maximize the amount of data we are feeding to the classifier, but a\n# pre-hook so normal filter rules will override the autotagging.\n_plugins.register_filter_hook_pre('90-autotag', filter_hook)\n"
  },
  {
    "path": "mailpile/plugins/autotag_sb.py",
    "content": "# Add SpamBayes as an option to the autotagger. We like SpamBayes.\n#\n# We feed the classifier the same terms as go into the search engine,\n# which should allow us to actually introspect a bit into the behavior\n# of the classifier.\n\nfrom mailpile.spambayes import Classifier\n\nimport mailpile.plugins.autotag\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\n\n\ndef _classifier(autotagger):\n    if not hasattr(autotagger, 'spambayes'):\n        autotagger.spambayes = Classifier()\n    return autotagger.spambayes\n\n\nclass SpamBayesTagger(mailpile.plugins.autotag.Trainer):\n    def should_tag(self, atagger, at_config, msg, keywords):\n        score, evidence = _classifier(atagger).chi2_spamprob(keywords,\n                                                             evidence=True)\n        if score >= 1 - at_config.threshold:\n            want = True\n        elif score > at_config.threshold:\n            want = None\n        else:\n            want = False\n        return (want, score)\n\n\nclass SpamBayesTrainer(mailpile.plugins.autotag.Trainer):\n    def learn(self, atagger, at_config, msg, keywords, should_tag):\n        _classifier(atagger).learn(keywords, should_tag)\n\n    def reset(self, atagger, at_config):\n        atagger.spambayes = Classifier()\n\n\nmailpile.plugins.autotag.TAGGERS['spambayes'] = SpamBayesTagger\nmailpile.plugins.autotag.TRAINERS['spambayes'] = SpamBayesTrainer\n"
  },
  {
    "path": "mailpile/plugins/backups.py",
    "content": "from __future__ import print_function\nimport cStringIO\nimport datetime\nimport gzip\nimport json\nimport os\nimport sys\nimport time\nimport traceback\nimport urllib\nimport zipfile\n\nfrom mailpile.auth import VerifyAndStorePassphrase\nfrom mailpile.config.defaults import APPVER\nfrom mailpile.commands import Command\nfrom mailpile.crypto.streamer import EncryptingStreamer, DecryptingStreamer\nfrom mailpile.plugins import PluginManager\nfrom mailpile.plugins.core import Quit\nfrom mailpile.i18n import ActivateTranslation\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.ui import SuppressHtmlOutput\nfrom mailpile.util import *\nfrom mailpile.vfs import FilePath, vfs\n\n\n_ = lambda t: t\n_plugins = PluginManager(builtin=__file__)\n\n\ndef _gzip(filename, data):\n    gzip_data = cStringIO.StringIO()\n    gzip_obj = gzip.GzipFile(filename, 'w', 9, gzip_data, 0)\n    gzip_obj.write(data)\n    gzip_obj.close()\n    return gzip_data.getvalue()\n\n\ndef _gunzip(data):\n    with gzip.GzipFile('', 'rb', 0, cStringIO.StringIO(data)) as gzf:\n        return gzf.read()\n\n\ndef _decrypt(data, config):\n    with DecryptingStreamer(cStringIO.StringIO(data),\n                            mep_key=config.get_master_key()) as fd:\n        data = fd.read()\n        fd.verify(_raise=IOError)\n    return data\n\n\nclass MakeBackup(Command):\n    \"\"\"Generate an encrypted backup of Stuff\"\"\"\n    SYNOPSIS = (None, 'backup', 'backup', '[download]')\n    ORDER = ('Internals', 6)\n    RAISES = (SuppressHtmlOutput,)\n    CONFIG_REQUIRED = True\n    IS_USER_ACTIVITY = False\n\n    @classmethod\n    def SummarizeTags(cls, config):\n        # First, decide which tags to include.\n        # Not all tags are interesting! Most, but not all.\n        keep = {}\n        suppress = {}\n        for tid, tag in config.tags.iteritems():\n            if tag.type in ('tag', 'group', 'attribute', 'inbox', 'drafts',\n                            'sent', 'spam', 'read', 'tagged', 'fwded',\n                            'replied', 'search', 'profile'):\n                if tid in config.index.TAGS:\n                    keep[tid] = tag\n            elif tag.type == 'trash':\n                suppress[tid] = tag\n\n        msg_idx_set = set()\n        for tid in keep:\n            msg_idx_set |= config.index.TAGS[tid]\n        for tid in suppress:\n            msg_idx_set -= config.index.TAGS.get(tid, set([]))\n\n        msg_id_list = [''] * len(config.index.INDEX)\n        for msgid, msg_idx in config.index.MSGIDS.iteritems():\n            if msg_idx in msg_idx_set:\n                msg_id_list[msg_idx] = msgid\n\n        return {\n            'tags': dict((tid, list(config.index.TAGS[tid]))\n                         for tid in keep),\n            'msgids': msg_id_list}\n\n    @classmethod\n    def MakeBackupArchive(cls, config, gnupg, what=None):\n        backup_date = datetime.date.today().strftime('%Y-%m-%d')\n        if what:\n            backup_fn = 'Mailpile_Backup_%s_%s.zip' % (\n                backup_date, ','.join(what))\n        else:\n            backup_fn = 'Mailpile_Backup_%s.zip' % (backup_date,)\n\n        # Prep archive!\n        backup_data = cStringIO.StringIO()\n        backup_zip = zipfile.ZipFile(backup_data, 'w', zipfile.ZIP_DEFLATED)\n        backup_zip.writestr('README.txt', (('\\n'.join([\n            _(\"This is a backup of Mailpile v%(ver)s keys and configuration.\"),\n            '',\n            '   * ' + _(\"This backup was generated on: %(date)s.\"),\n            '   * ' + _(\"The contents of this file should be encrypted.\"),\n            '   * ' + _(\"The entire ZIP file must be uploaded during \"\n                        \"restoration.\"),\n            '',\n            '-- ',\n            '{\"backup_date\": \"%(date)s\",',\n            ' \"backup_version\": 1.0,',\n            ' \"mailpile_version\": \"%(ver)s\"}'\n            ])) % {'ver': APPVER, 'date': backup_date}).strip())\n        backup_contents = []\n\n        def _add_file(realfile, zipname):\n            backup_zip.write(realfile, zipname)\n            backup_contents.append(zipname)\n\n        # The .ZIP is unencrypted, so generated contents needs protecting\n        def _encrypt_and_add_data(filename, data):\n            tempfile = os.path.join(config.tempfile_dir(), filename)\n            with EncryptingStreamer(config.get_master_key(),\n                                    dir=config.tempfile_dir()) as fd:\n                fd.write(data)\n                fd.save(tempfile)\n            _add_file(tempfile, filename)\n            safe_remove(tempfile)\n\n        # What has been requested?\n        if what and what[0] == 'full':\n            what += ['config', 'profiles', 'keys', 'gnupg', 'vcards', 'tags']\n\n        # Critical: Copy the configuration and master keys\n        if not what or 'config' in what:\n            for fn in (config.conf_pub, config.conf_key, config.conffile):\n                _add_file(fn, os.path.basename(fn))\n\n        # Critical: Copy the profile VCard data\n        if not what or 'profiles' in what:\n            for profile in config.vcards.find_vcards([], kinds=['profile']):\n                target = os.path.basename(profile.filename)\n                _add_file(profile.filename, os.path.join('vcards', target))\n\n        # Critical: Copy all the private GnuPG keys!\n        if not what or 'keys' in what:\n            _encrypt_and_add_data('gnupg-privkeys.asc.gze',\n                _gzip('gnupg-privkeys.asc', gnupg.export_privkeys()))\n\n        # Recommended: Copy all the public GnuPG keys!\n        if not what or 'gnupg' in what:\n            _encrypt_and_add_data('gnupg-pubkeys.asc.gze',\n                _gzip('gnupg-pubkeys.asc', gnupg.export_pubkeys()))\n\n        # Recommended: Copy the \"interesting\" VCards.\n        if not what or 'vcards' in what:\n            for vcard in config.vcards.find_vcards([],\n                    kinds=['individual', 'group']):\n                if ((what and 'full' in what)\n                        or vcard.recent_history()\n                        or vcard.crypto_policy\n                        or vcard.html_policy\n                        or vcard.pgp_key_shared\n                        or vcard.pgp_key):\n                    target = os.path.basename(vcard.filename)\n                    _add_file(vcard.filename, os.path.join('vcards', target))\n\n        # Optional: Backup the tag structure. This is useful if we lose the\n        # metadata index, but have the original e-mails. This is DISABLED BY\n        # DEFAULT because it is expensive and that may not be a real use case.\n        if what and 'tags' in what:\n            _encrypt_and_add_data('tags.json.gze',\n                _gzip('tags.json', json.dumps(cls.SummarizeTags(config))))\n\n        # Finalize archive\n        backup_zip.close()\n        backup_data = backup_data.getvalue()\n\n        return backup_fn, backup_contents, backup_data\n\n    def command(self):\n        session, config = self.session, self.session.config\n        html_variables = session.ui.html_variables\n\n        if not (html_variables and\n                session.ui.valid_csrf_token(self.data.get('csrf', [''])[0])):\n            raise AccessError('Invalid CSRF token')\n\n        backup_fn, backup_contents, backup_data = self.MakeBackupArchive(\n            config, self._gnupg(),\n            what=[a for a in self.args if a not in ('download',)])\n\n        if 'download' in self.args:\n            encoded_fn = urllib.quote(backup_fn.encode('utf-8'))\n            request = html_variables['http_request']\n            request.send_http_response(200, 'OK')\n            request.send_standard_headers(mimetype='application/zip',\n                                          header_list=[\n                ('Content-Length', len(backup_data)),\n                ('Content-Disposition',\n                    'attachment; filename*=UTF-8\\'\\'%s' % (encoded_fn,))])\n            request.wfile.write(backup_data)\n            raise SuppressHtmlOutput()\n\n        return self._success('Generated backup', result={\n            'filename': backup_fn,\n            'contents': backup_contents,\n            'data_b64': backup_data.encode('base64')})\n\n\nAVAILABLE_BACKUPS = {}\n\nclass RestoreBackup(Command):\n    \"\"\"Bootstraup setup from a backup archive\"\"\"\n    SYNOPSIS = (None, 'backup/restore', 'backup/restore', '[/path/to.zip]')\n    ORDER = ('Internals', 6)\n    RAISES = (UrlRedirectException,)\n    CONFIG_REQUIRED = False\n    HTTP_AUTH_REQUIRED = 'maybe'\n    HTTP_CALLABLE = ('GET', 'POST')\n    HTTP_QUERY_VARS = {\n        'lang': 'Language to use in UI'}\n    HTTP_POST_VARS = {\n        'restore': 'date of backup to restore',\n        'password': 'Mailpile master password',\n        'keychain': 'GnuPG keychain policy: shared*, mailpile, none',\n        'os_settings': 'OS settings policy: keep, backup*',\n        'file-data': 'file data'}\n\n    def _restore_PGP_keys(self, config, backup_zip, policy):\n        if policy not in ('shared', 'mailpile'):\n            return\n\n        if policy == 'mailpile':\n            config.sys.gpg_home = config.workdir\n        else:\n            config.sys.gpg_home = ''\n\n        for keyfile in ('gnupg-pubkeys.asc.gze', 'gnupg-privkeys.asc.gze'):\n            gze = backup_zip.read(keyfile)\n            print('DATA: %s' % gze)\n            self._gnupg().import_keys(_gunzip(_decrypt(gze, config)))\n\n\n    def _adjust_paths(self, config):\n        # Go through sys.mailboxes, sources.*.mailbox:\n        #   - if the path is outside Workdir, does not exist, clear entry\n        #   - if the path is inside Workdir, does not exist, create it\n        #   - if the path is src:, source does not exist, clear entry\n        def path_ok(mbx_path):\n            if 'src:' in mbx_path.raw_fp[:5]:\n                return True\n            elif vfs.mailbox_type(mbx_path, config):\n                return True\n            elif unicode(mbx_path).startswith('/Mailpile$/'):\n                config.create_local_mailstore(\n                    self.session, name=mbx_path.raw_fp)\n                return True\n            else:\n                return False\n\n        for i, mbx_path in config.sys.mailbox.iteritems():\n            mbx_path = FilePath(mbx_path)\n            if not path_ok(mbx_path):\n                config.sys.mailbox[i] = '/dev/null'\n\n        for i, p, src in config.get_mailboxes(with_mail_source=True,\n                                              mail_source_locals=True):\n            mbx_path = FilePath(p)\n            if src.mailbox[i].local and not path_ok(mbx_path):\n                src.mailbox[i].local = '!CREATE'\n\n    def command(self):\n        global AVAILABLE_BACKUPS\n        session, config = self.session, self.session.config\n        message, results = '', {}\n\n        if config.prefs.gpg_recipient or os.path.exists(config.conf_key):\n            raise UrlRedirectException('/' + (config.sys.http_path or ''))\n\n        if 'lang' in self.data:\n            ActivateTranslation(session, config, self.data['lang'][0])\n\n        password = ''\n        if self.args and '_method' not in self.data:\n            try:\n                if self.args[0] in AVAILABLE_BACKUPS:\n                    backup_data = AVAILABLE_BACKUPS[self.args[0]]\n                    self.data['restore'] = [self.args[0]]\n                    password = session.ui.get_password(_(\"Your password: \"))\n                else:\n                    with open(self.args[0], 'r') as fd:\n                        backup_data = fd.read()\n            except (IOError, OSError):\n                return self._error('Failed to read: %s' % self.args[0])\n        elif self.data.get('_method') == 'POST':\n            if 'restore' in self.data:\n                backup_data = AVAILABLE_BACKUPS[self.data['restore'][0]]\n                password = self.data.get('password', [''])[0]\n            else:\n                backup_data = self.data.get('file-data', [None])[0]\n        else:\n            backup_data = None\n\n        if backup_data is not None:\n            try:\n                if isinstance(backup_data, str):\n                    backup_data = cStringIO.StringIO(backup_data)\n                backup_zip = zipfile.ZipFile(backup_data, 'r')\n\n                # Load and validate metadata (from README.txt)\n                results['metadata'] = metadata = json.loads(\n                    backup_zip.read('README.txt').split('-- ')[1])\n                results['metadata']['contents'] = backup_zip.namelist()\n                backup_date = metadata['backup_date']\n                if metadata['backup_version'] != 1.0:\n                    raise ValueError('Unrecognized backup version')\n\n                # If we get this far, the backup looks good. Restore?\n                if (password and\n                        backup_date == self.data.get('restore', [''])[0]):\n                    # This should be safe: we are in the setup phase where\n                    # almost no background stuff is running, so it should be\n                    # fine to just overwrite files and reload.\n                    config.stop_workers()\n                    backup_zip.extractall(config.workdir)\n                    VerifyAndStorePassphrase(config, password)\n\n                    os_gpg_home = config.sys.gpg_home\n                    os_gpg_binary = config.sys.gpg_binary\n                    os_http_port = config.sys.http_port\n                    os_minfree_mb = config.sys.minfree_mb\n                    try:\n                        config.load(session)\n                    except IOError:\n                        pass\n\n                    B = ['backup']\n                    if 'keep' == self.data.get('os_settings', B)[0]:\n                        config.sys.gpg_home = os_gpg_home\n                        config.sys.gpg_binary = os_gpg_binary\n                        config.sys.http_port = os_http_port\n                        config.sys.minfree_mb = os_minfree_mb\n\n                    self._restore_PGP_keys(config, backup_zip,\n                        self.data.get('keychain', ['shared'])[0])\n\n                    self._adjust_paths(config)\n\n                    config.prepare_workers(session, daemons=True)\n                    message = _('Backup restored')\n                    results['restored'] = True\n                    AVAILABLE_BACKUPS = {}\n                else:\n                    message = _('Backup validated, restoration is possible')\n                    AVAILABLE_BACKUPS[backup_date] = backup_data\n\n            except (ValueError, KeyError, zipfile.BadZipfile, IOError):\n                traceback.print_exc()\n                return self._error('Incomplete, invalid or corrupt backup')\n        else:\n            message = _('Restore from backup')\n\n        results['available'] = AVAILABLE_BACKUPS.keys()\n        return self._success(message, result=results)\n\n\n_plugins.register_commands(MakeBackup, RestoreBackup)\n"
  },
  {
    "path": "mailpile/plugins/compose.py",
    "content": "import datetime\nimport email.utils\nimport os\nimport os.path\nimport re\nimport traceback\n\nimport mailpile.security as security\nfrom mailpile.commands import Command\nfrom mailpile.crypto.state import *\nfrom mailpile.crypto.mime import EncryptionFailureError, SignatureFailureError\nfrom mailpile.eventlog import Event\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.mailutils import NoFromAddressError, NotEditableError\nfrom mailpile.mailutils.addresses import AddressHeaderParser\nfrom mailpile.mailutils.emails import ExtractEmailAndName, Email\nfrom mailpile.mailutils.emails import PrepareMessage, MakeMessageID\nfrom mailpile.search import MailIndex\nfrom mailpile.smtp_client import SendMail\nfrom mailpile.urlmap import UrlMap\nfrom mailpile.util import *\nfrom mailpile.vcard import AddressInfo\n\nfrom mailpile.plugins.search import Search, SearchResults, View\n\n\nGLOBAL_EDITING_LOCK = MboxRLock()\n\n_plugins = PluginManager(builtin=__file__)\n\n\nclass EditableSearchResults(SearchResults):\n    def __init__(self, session, idx, new, sent, **kwargs):\n        SearchResults.__init__(self, session, idx, **kwargs)\n        self.new_messages = new\n        self.sent_messages = sent\n        if new:\n            self['created'] = [m.msg_mid() for m in new]\n        if sent:\n            self['sent'] = [m.msg_mid() for m in new]\n            self['summary'] = _('Sent: %s') % self['summary']\n\n\ndef AddComposeMethods(cls):\n    class newcls(cls):\n        COMMAND_CACHE_TTL = 0\n        COMMAND_SECURITY = security.CC_COMPOSE_EMAIL\n\n        def _create_contacts(self, emails):\n            try:\n                from mailpile.plugins.contacts import AddContact\n                AddContact(self.session,\n                           arg=['=%s' % e.msg_mid() for e in emails]\n                           ).run(recipients=True, quietly=True, internal=True)\n            except (TypeError, ValueError, IndexError):\n                self._ignore_exception()\n\n        def _tag_emails(self, emails, tag):\n            try:\n                idx = self._idx()\n                idx.add_tag(self.session,\n                            self.session.config.get_tag_id(tag),\n                            msg_idxs=[e.msg_idx_pos for e in emails],\n                            conversation=False)\n            except (TypeError, ValueError, IndexError):\n                self._ignore_exception()\n\n        def _untag_emails(self, emails, tag):\n            try:\n                idx = self._idx()\n                idx.remove_tag(self.session,\n                               self.session.config.get_tag_id(tag),\n                               msg_idxs=[e.msg_idx_pos for e in emails],\n                               conversation=False)\n            except (TypeError, ValueError, IndexError):\n                self._ignore_exception()\n\n        def _tagger(self, emails, untag, **kwargs):\n            tag = self.session.config.get_tags(**kwargs)\n            if tag and untag:\n                return self._untag_emails(emails, tag[0]._key)\n            elif tag:\n                return self._tag_emails(emails, tag[0]._key)\n\n        def _tag_blank(self, emails, untag=False):\n            return self._tagger(emails, untag, type='blank')\n\n        def _tag_drafts(self, emails, untag=False):\n            return self._tagger(emails, untag, type='drafts')\n\n        def _tag_outbox(self, emails, untag=False):\n            return self._tagger(emails, untag, type='outbox')\n\n        def _tag_sent(self, emails, untag=False):\n            return self._tagger(emails, untag, type='sent')\n\n        def _track_action(self, action_type, refs):\n            session, idx = self.session, self._idx()\n            for tag in session.config.get_tags(type=action_type):\n                idx.add_tag(session, tag._key,\n                            msg_idxs=[m.msg_idx_pos for m in refs])\n\n        def _actualize_ephemeral(self, ephemeral_mid):\n            idx = self._idx()\n\n            if isinstance(ephemeral_mid, int):\n                # Not actually ephemeral, just return a normal Email\n                return Email(idx, ephemeral_mid)\n\n            msgid, mid = ephemeral_mid.rsplit('-', 1)\n            etype, etarg, msgid = msgid.split('-', 2)\n            if etarg not in ('all', 'att'):\n                msgid = etarg + '-' + msgid\n            msgid = '<%s>' % msgid.replace('_', '@')\n            etype = etype.lower()\n\n            enc_msgid = idx._encode_msg_id(msgid)\n            msg_idx = idx.MSGIDS.get(enc_msgid)\n            if msg_idx is not None:\n                # Already actualized, just return a normal Email\n                return Email(idx, msg_idx)\n\n            if etype == 'forward':\n                refs = [Email(idx, int(mid, 36))]\n                e = Forward.CreateForward(idx, self.session, refs, msgid,\n                                          with_atts=(etarg == 'att'))[0]\n                self._track_action('fwded', refs)\n\n            elif etype == 'reply':\n                refs = [Email(idx, int(mid, 36))]\n                e = Reply.CreateReply(idx, self.session, refs, msgid,\n                                      reply_all=(etarg == 'all'))[0]\n                self._track_action('replied', refs)\n\n            else:\n                e = Compose.CreateMessage(idx, self.session, msgid)[0]\n\n            self._tag_blank([e])\n            self.session.ui.debug('Actualized: %s' % e.msg_mid())\n\n            return Email(idx, e.msg_idx_pos)\n\n    return newcls\n\n\nclass CompositionCommand(AddComposeMethods(Search)):\n    HTTP_QUERY_VARS = {}\n    HTTP_POST_VARS = {}\n    UPDATE_STRING_DATA = {\n        'mid': 'metadata-ID',\n        'subject': '..',\n        'from': '..',\n        'to': '..',\n        'cc': '..',\n        'bcc': '..',\n        'body': '..',\n        'encryption': '..',\n        'attachment': '..',\n        'attach-pgp-pubkey': '..',\n    }\n\n    UPDATE_HEADERS = ('Subject', 'From', 'To', 'Cc', 'Bcc', 'Encryption',\n                      'Attach-PGP-Pubkey')\n\n    def _new_msgid(self):\n        msgid = (MakeMessageID()\n                 .replace('.', '-')   # Dots may bother JS/CSS\n                 .replace('_', '-'))  # We use _ to encode the @ later on\n        return msgid\n\n    def _get_email_updates(self, idx, create=False, noneok=False, emails=None):\n        # Split the argument list into files and message IDs\n        files = [f[1:].strip() for f in self.args if f.startswith('<')]\n        args = [a for a in self.args if not a.startswith('<')]\n\n        # Message IDs can come from post data\n        for mid in self.data.get('mid', []):\n            args.append('=%s' % mid)\n        emails = emails or [self._actualize_ephemeral(mid) for mid in\n                            self._choose_messages(args, allow_ephemeral=True)]\n\n        update_header_set = (set(self.data.keys()) &\n                             set([k.lower() for k in self.UPDATE_HEADERS]))\n        updates, fofs = [], 0\n        for e in (emails or (create and [None]) or []):\n            # If we don't have a file, check for posted data\n            if len(files) not in (0, 1, len(emails)):\n                return (self._error(_('Cannot update from multiple files')),\n                        None)\n            elif len(files) == 1:\n                updates.append((e, self._read_file_or_data(files[0])))\n            elif files and (len(files) == len(emails)):\n                updates.append((e, self._read_file_or_data(files[fofs])))\n            elif update_header_set:\n                # No file name, construct an update string from the POST data.\n                etree = e and e.get_message_tree() or {}\n                defaults = etree.get('editing_strings', {})\n\n                up = []\n                for hdr in self.UPDATE_HEADERS:\n                    if hdr.lower() in self.data:\n                        data = ', '.join(self.data[hdr.lower()])\n                    else:\n                        data = defaults.get(hdr.lower(), '')\n                    up.append('%s: %s' % (hdr, data))\n\n                # This preserves in-reply-to, references and any other\n                # headers we're not usually keen on editing.\n                if defaults.get('headers'):\n                    up.append(defaults['headers'])\n\n                # This weird thing converts attachment=1234:bla.txt into a\n                # dict of 1234=>bla.txt values, attachment=1234 to 1234=>None.\n                # .. or just keeps all attachments if nothing is specified.\n                att_keep = (dict([(ai.split(':', 1) if (':' in ai)\n                                   else (ai, None))\n                                  for ai in self.data.get('attachment', [])])\n                            if 'attachment' in self.data\n                            else defaults.get('attachments', {}))\n                for att_id, att_fn in defaults.get('attachments',\n                                                   {}).iteritems():\n                    if att_id in att_keep:\n                        fn = att_keep[att_id] or att_fn\n                        up.append('Attachment-%s: %s' % (att_id, fn))\n\n                updates.append((e, '\\n'.join(\n                    up +\n                    ['', '\\n'.join(self.data.get('body',\n                                                 defaults.get('body', '')))]\n                )))\n            elif noneok:\n                updates.append((e, None))\n            elif 'compose' in self.session.config.sys.debug:\n                sys.stderr.write('Doing nothing with %s' % update_header_set)\n            fofs += 1\n\n        if 'compose' in self.session.config.sys.debug:\n            for e, up in updates:\n                sys.stderr.write(('compose/update: Update %s with:\\n%s\\n--\\n'\n                                  ) % ((e and e.msg_mid() or '(new'), up))\n            if not updates:\n                sys.stderr.write('compose/update: No updates!\\n')\n\n        return updates\n\n    def _return_search_results(self, message, emails,\n                               expand=None, new=[], sent=[], ephemeral=False,\n                               error=None):\n        session, idx = self.session, self._idx()\n        if not ephemeral:\n            session.results = [e.msg_idx_pos for e in emails]\n        else:\n            session.results = ephemeral\n        session.displayed = EditableSearchResults(session, idx,\n                                                  new, sent,\n                                                  results=session.results,\n                                                  num=len(emails),\n                                                  emails=expand)\n        if error:\n            return self._error(message,\n                               result=session.displayed,\n                               info=error)\n        else:\n            return self._success(message, result=session.displayed)\n\n    def _edit_messages(self, *args, **kwargs):\n        try:\n            return self._real_edit_messages(*args, **kwargs)\n        except NotEditableError:\n            return self._error(_('Message is not editable'))\n\n    def _real_edit_messages(self, emails, new=True, tag=True, ephemeral=False):\n        session, idx = self.session, self._idx()\n        if (not ephemeral and\n                (session.ui.edit_messages(session, emails) or not new)):\n            if tag:\n                self._tag_blank(emails, untag=True)\n                self._tag_drafts(emails)\n            self.message = _('%d message(s) edited') % len(emails)\n        else:\n            self.message = _('%d message(s) created') % len(emails)\n        self._background_save(index=True)\n        session.ui.mark(self.message)\n        return self._return_search_results(self.message, emails,\n                                           expand=emails,\n                                           new=(new and emails),\n                                           ephemeral=ephemeral)\n\n\nclass Draft(AddComposeMethods(View)):\n    \"\"\"Edit an existing draft\"\"\"\n    SYNOPSIS = ('E', 'edit', 'message/draft', '[<messages>]')\n    ORDER = ('Composing', 0)\n    HTTP_QUERY_VARS = {\n        'mid': 'metadata-ID'\n    }\n\n    def _side_effects(self, emails):\n        session, idx = self.session, self._idx()\n        with GLOBAL_EDITING_LOCK:\n            if not emails:\n                session.ui.mark(_('No messages!'))\n            elif session.ui.edit_messages(session, emails):\n                self._tag_blank(emails, untag=True)\n                self._tag_drafts(emails)\n                self._background_save(index=True)\n                self.message = _('%d message(s) edited') % len(emails)\n            else:\n                self.message = _('%d message(s) unchanged') % len(emails)\n        session.ui.mark(self.message)\n        return None\n\n\nclass Compose(CompositionCommand):\n    \"\"\"Create a new blank e-mail for editing\"\"\"\n    SYNOPSIS = ('C', 'compose', 'message/compose', \"[ephemeral]\")\n    ORDER = ('Composing', 0)\n    HTTP_CALLABLE = ('POST', 'GET')\n    HTTP_QUERY_VARS = dict_merge(CompositionCommand.UPDATE_STRING_DATA, {\n        'cid': 'canned response metadata-ID',\n    })\n\n    @classmethod\n    def _get_canned(cls, idx, cid):\n        try:\n            return Email(idx, int(cid, 36)\n                         ).get_editing_strings().get('body', '')\n        except (ValueError, IndexError, TypeError, OSError, IOError):\n            traceback.print_exc()  # FIXME, ugly\n            return ''\n\n    @classmethod\n    def CreateMessage(cls, idx, session, msgid, cid=None, ephemeral=False):\n        if not ephemeral:\n            local_id, lmbox = session.config.open_local_mailbox(session)\n        else:\n            local_id, lmbox = -1, None\n            ephemeral = ['new-E-%s-mail' % msgid[1:-1].replace('@', '_')]\n        profiles = session.config.vcards.find_vcards([], kinds=['profile'])\n        return (Email.Create(idx, local_id, lmbox,\n                             save=(not ephemeral),\n                             msg_text=(cid and cls._get_canned(idx, cid)\n                                       or ''),\n                             msg_id=msgid,\n                             ephemeral_mid=ephemeral and ephemeral[0],\n                             use_default_from=(len(profiles) == 1)),\n                ephemeral)\n\n    def command(self):\n        if 'mid' in self.data:\n            return self._error('Please use update for editing messages')\n\n        session, idx = self.session, self._idx()\n        cid = self.data.get('cid', [None])[0]\n\n        ephemeral = (self.args and \"ephemeral\" in self.args)\n        if self.data.get('_method', 'POST') != 'POST':\n            ephemeral = True\n\n        email, ephemeral = self.CreateMessage(idx, session, self._new_msgid(),\n                                              cid=cid,\n                                              ephemeral=ephemeral)\n        if not ephemeral:\n            self._tag_blank([email])\n\n        email_updates = self._get_email_updates(idx,\n                                                emails=[email],\n                                                create=True)\n        update_string = email_updates and email_updates[0][1]\n        if update_string:\n            email.update_from_string(session, update_string)\n\n        return self._edit_messages([email],\n                                   ephemeral=ephemeral,\n                                   new=(ephemeral or not update_string))\n\n\nclass RelativeCompose(Compose):\n    _ATT_MIMETYPES = ('application/pgp-signature', )\n    _TEXT_PARTTYPES = ('text', 'quote', 'pgpsignedtext', 'pgpsecuretext',\n                       'pgpverifiedtext')\n\n    _FW_REGEXP = re.compile(r'^(fwd|fw):.*', re.IGNORECASE)\n    _RE_REGEXP = re.compile(r'^(rep|re):.*', re.IGNORECASE)\n\n    @staticmethod\n    def prefix_subject(subject, prefix, prefix_regex):\n        \"\"\"Avoids stacking several consecutive Fw: Re: Re: Re:\"\"\"\n        if subject is None:\n            return prefix\n        elif prefix_regex.match(subject):\n            return subject\n        else:\n            return '%s %s' % (prefix, subject)\n\n\nclass Reply(RelativeCompose):\n    \"\"\"Create reply(-all) drafts to one or more messages\"\"\"\n    SYNOPSIS = ('r', 'reply', 'message/reply', '[all|ephemeral] <messages>')\n    ORDER = ('Composing', 3)\n    HTTP_QUERY_VARS = {\n        'mid': 'metadata-ID',\n        'cid': 'canned response metadata-ID',\n        'reply_all': 'reply to all',\n        'ephemeral': 'ephemerality',\n    }\n    HTTP_POST_VARS = {}\n\n    @classmethod\n    def _add_gpg_key(cls, idx, session, addr):\n        fe, fn = ExtractEmailAndName(addr)\n        vcard = session.config.vcards.get_vcard(fe)\n        if vcard:\n            keys = vcard.get_all('KEY')\n            if keys:\n                mime, fp = keys[0].value.split('data:')[1].split(',', 1)\n                return \"%s <%s#%s>\" % (fn, fe, fp)\n        return \"%s <%s>\" % (fn, fe)\n\n    @classmethod\n    def _create_from_to_cc(cls, idx, session, trees):\n        config = session.config\n        ahp = AddressHeaderParser()\n        ref_from, ref_to, ref_cc = [], [], []\n        result = {'from': '', 'to': [], 'cc': []}\n\n        def merge_contact(ai):\n            vcard = config.vcards.get_vcard(ai.address)\n            if vcard:\n                ai.merge_vcard(vcard)\n            return ai\n\n        # Parse the headers, so we know what we're working with. We prune\n        # some of the duplicates at this stage.\n        for addrs in [t['addresses'] for t in trees]:\n            alist = []\n            for dst, addresses in (\n                    (ref_from, addrs.get('reply-to') or addrs.get('from', [])),\n                    (ref_to, addrs.get('to', [])),\n                    (ref_cc, addrs.get('cc', []))):\n                alist += [d.address for d in dst]\n                dst.extend([a for a in addresses if a.address not in alist])\n\n        # 1st, choose a from address.\n        from_ai = config.vcards.choose_from_address(\n            config, ref_from, ref_to, ref_cc)  # Note: order matters!\n        if from_ai:\n            result['from'] = ahp.normalized(addresses=[from_ai],\n                                            force_name=True)\n\n        def addresses(addrs, exclude=[]):\n            alist = [from_ai.address] if (from_ai) else []\n            alist += [a.address for a in exclude]\n            return [merge_contact(a) for a in addrs\n                    if a.address not in alist\n                    and not a.address.startswith('noreply@')\n                    and '@noreply' not in a.address]\n\n        # If only replying to messages sent from chosen from, then this is\n        # a follow-up or clarification, so just use the same headers.\n        if (from_ai and\n               len([e for e in ref_from\n                    if e and e.address == from_ai.address]) == len(ref_from)):\n            if ref_to:\n                result['to'] = addresses(ref_to)\n            if ref_cc:\n                result['cc'] = addresses(ref_cc)\n\n        # Else, if replying to other people:\n        #   - Construct To from the From lines, excluding own from\n        #   - Construct Cc from the To and CC lines, except new To/From\n        else:\n            result['to'] = addresses(ref_from)\n            result['cc'] = addresses(ref_to + ref_cc, exclude=ref_from)\n\n        return result\n\n    @classmethod\n    def CreateReply(cls, idx, session, refs, msgid,\n                    reply_all=False, cid=None, ephemeral=False):\n        trees = [m.evaluate_pgp(m.get_message_tree(), decrypt=True)\n                 for m in refs]\n\n        headers = cls._create_from_to_cc(idx, session, trees)\n        if not reply_all and 'cc' in headers:\n            del headers['cc']\n\n        ref_ids = [t['headers_lc'].get('message-id') for t in trees]\n        ref_subjs = [(t['summary'][4] or t['headers_lc'].get('subject'))\n                      for t in trees]\n        msg_bodies = []\n        for t in trees:\n            # FIXME: Templates/settings for how we quote replies?\n            quoted = ''.join([p['data'] for p in t['text_parts']\n                              if p['type'] in cls._TEXT_PARTTYPES\n                              and p['data']])\n            if quoted:\n                target_width = session.config.prefs.line_length\n                if target_width > 40:\n                    quoted = reflow_text(quoted, target_width=target_width-2)\n                text = ((_('%s wrote:') % t['headers_lc']['from']) + '\\n' +\n                        quoted)\n                msg_bodies.append('\\n\\n' + text.replace('\\n', '\\n> '))\n\n        if not ephemeral:\n            local_id, lmbox = session.config.open_local_mailbox(session)\n        else:\n            local_id, lmbox = -1, None\n            fmt = 'reply-all-%s-%s' if reply_all else 'reply-%s-%s'\n            ephemeral = [fmt % (msgid[1:-1].replace('@', '_'),\n                                refs[0].msg_mid())]\n\n        if 'cc' in headers:\n            fmt = _('Composing a reply from %(from)s to %(to)s, cc %(cc)s')\n        else:\n            fmt = _('Composing a reply from %(from)s to %(to)s')\n        session.ui.debug(fmt % headers)\n\n        extra_headers = []\n        for tree in trees:\n            try:\n                if 'decrypted' in tree['crypto']['encryption']['status']:\n                    extra_headers.append(('x-mp-internal-should-encrypt', 'Y'))\n                    extra_headers.append(('Encryption', 'openpgp-sign-encrypt'))\n                    break\n            except KeyError:\n                pass\n\n        if cid:\n            # FIXME: Instead, we should use placeholders in the template\n            #        and insert the quoted bits in the right place (or\n            #        nowhere if the template doesn't want them).\n            msg_bodies[:0] = [cls._get_canned(idx, cid)]\n\n        email = Email.Create(idx, local_id, lmbox,\n                             msg_text='\\n\\n'.join(msg_bodies),\n                             msg_subject=cls.prefix_subject(\n                                 ref_subjs[-1], 'Re:', cls._RE_REGEXP),\n                             msg_from=headers.get('from', None),\n                             msg_to=headers.get('to', []),\n                             msg_cc=headers.get('cc', []),\n                             msg_references=[i for i in ref_ids if i],\n                             msg_headers=extra_headers,\n                             msg_id=msgid,\n                             save=(not ephemeral),\n                             ephemeral_mid=ephemeral and ephemeral[0])\n\n\n        return (email, ephemeral)\n\n    def command(self):\n        session, config, idx = self.session, self.session.config, self._idx()\n        reply_all = False\n        ephemeral = False\n        args = list(self.args)\n        if not args:\n            args = [\"=%s\" % x for x in self.data.get('mid', [])]\n            ephemeral = truthy((self.data.get('ephemeral') or [False])[0])\n            reply_all = truthy((self.data.get('reply_all') or [False])[0])\n        else:\n            while args:\n                if args[0].lower() == 'all':\n                    reply_all = args.pop(0) or True\n                elif args[0].lower() == 'ephemeral':\n                    ephemeral = args.pop(0) or True\n                else:\n                    break\n\n        # Make sure GET does not change backend state, allow on CLI.\n        if self.data.get('_method', 'POST') != 'POST':\n            ephemeral = True\n\n        refs = [Email(idx, i) for i in self._choose_messages(args)]\n        if refs:\n            try:\n                cid = self.data.get('cid', [None])[0]\n                email, ephemeral = self.CreateReply(idx, session, refs,\n                                                    self._new_msgid(),\n                                                    reply_all=reply_all,\n                                                    cid=cid,\n                                                    ephemeral=ephemeral)\n            except NoFromAddressError:\n                return self._error(_('You must configure a '\n                                     'From address first.'))\n\n            if not ephemeral:\n                self._track_action('replied', refs)\n                self._tag_blank([email])\n\n            return self._edit_messages([email], ephemeral=ephemeral)\n        else:\n            return self._error(_('No message found'))\n\n\nclass Forward(RelativeCompose):\n    \"\"\"Create forwarding drafts of one or more messages\"\"\"\n    SYNOPSIS = ('f', 'forward', 'message/forward', '[att|ephemeral] <messages>')\n    ORDER = ('Composing', 4)\n    HTTP_QUERY_VARS = {\n        'mid': 'metadata-ID',\n        'cid': 'canned response metadata-ID',\n        'ephemeral': 'ephemerality',\n        'atts': 'forward attachments'\n    }\n    HTTP_POST_VARS = {}\n\n    @classmethod\n    def CreateForward(cls, idx, session, refs, msgid,\n                      with_atts=False, cid=None, ephemeral=False):\n        trees = [m.evaluate_pgp(m.get_message_tree(), decrypt=True)\n                 for m in refs]\n        ref_subjs = [t['headers_lc']['subject'] for t in trees]\n        msg_bodies = []\n        msg_atts = []\n        for t in trees:\n            # FIXME: Templates/settings for how we quote forwards?\n            text = '-------- Original Message --------\\n'\n            for h in ('Date', 'Subject', 'From', 'To'):\n                v = t['headers_lc'].get(h.lower(), None)\n                if v:\n                    text += '%s: %s\\n' % (h, v)\n            text += '\\n'\n            text += ''.join([p['data'] for p in t['text_parts']\n                             if p['type'] in cls._TEXT_PARTTYPES])\n            msg_bodies.append(text)\n            if with_atts:\n                for att in t['attachments']:\n                    if att['mimetype'] not in cls._ATT_MIMETYPES:\n                        msg_atts.append(att['part'])\n\n        if not ephemeral:\n            local_id, lmbox = session.config.open_local_mailbox(session)\n        else:\n            local_id, lmbox = -1, None\n            fmt = 'forward-att-%s-%s' if msg_atts else 'forward-%s-%s'\n            ephemeral = [fmt % (msgid[1:-1].replace('@', '_'),\n                                refs[0].msg_mid())]\n\n        if cid:\n            # FIXME: Instead, we should use placeholders in the template\n            #        and insert the quoted bits in the right place (or\n            #        nowhere if the template doesn't want them).\n            msg_bodies[:0] = [cls._get_canned(idx, cid)]\n\n        email = Email.Create(idx, local_id, lmbox,\n                             msg_text='\\n\\n'.join(msg_bodies),\n                             msg_subject=cls.prefix_subject(\n                                 ref_subjs[-1], 'Fwd:', cls._FW_REGEXP),\n                             msg_id=msgid,\n                             msg_atts=msg_atts,\n                             save=(not ephemeral),\n                             ephemeral_mid=ephemeral and ephemeral[0])\n\n        return email, ephemeral\n\n    def command(self):\n        session, config, idx = self.session, self.session.config, self._idx()\n\n        with_atts = False\n        ephemeral = False\n        args = list(self.args)\n        if not args:\n            args = [\"=%s\" % x for x in self.data.get('mid', [])]\n            ephemeral = truthy((self.data.get('ephemeral') or [False])[0])\n            with_atts = truthy((self.data.get('atts') or [False])[0])\n        else:\n            while args:\n                if args[0].lower() == 'att':\n                    with_atts = args.pop(0) or True\n                elif args[0].lower() == 'ephemeral':\n                    ephemeral = args.pop(0) or True\n                else:\n                    break\n\n        # Make sure GET does not change backend state\n        if self.data.get('_method', 'POST') != 'POST':\n            ephemeral = True\n\n        if ephemeral and with_atts:\n            raise UsageError(_('Sorry, ephemeral messages cannot have '\n                               'attachments at this time.'))\n\n        refs = [Email(idx, i) for i in self._choose_messages(args)]\n        if refs:\n            cid = self.data.get('cid', [None])[0]\n            email, ephemeral = self.CreateForward(idx, session, refs,\n                                                  self._new_msgid(),\n                                                  with_atts=with_atts,\n                                                  cid=cid,\n                                                  ephemeral=ephemeral)\n\n            if not ephemeral:\n                self._track_action('fwded', refs)\n                self._tag_blank([email])\n\n            return self._edit_messages([email], ephemeral=ephemeral)\n        else:\n            return self._error(_('No message found'))\n\n\nclass Attach(CompositionCommand):\n    \"\"\"Attach a file to a message\"\"\"\n    SYNOPSIS = ('a', 'attach', 'message/attach', '<messages> [<path/to/file>]')\n    ORDER = ('Composing', 2)\n    WITH_CONTEXT = (GLOBAL_EDITING_LOCK, )\n    HTTP_CALLABLE = ('POST', 'UPDATE')\n    HTTP_QUERY_VARS = {}\n    HTTP_POST_VARS = {\n        'mid': 'metadata-ID',\n        'name': '(ignored)',\n        'file-data': 'file data'\n    }\n\n    def command(self, emails=None):\n        session, idx = self.session, self._idx()\n        args = list(self.args)\n\n        files = []\n        filedata = {}\n        if 'file-data' in self.data:\n            count = 0\n            for fd in self.data['file-data']:\n                fn = (hasattr(fd, 'filename')\n                      and fd.filename or 'attach-%d.dat' % count)\n                filedata[fn] = fd\n                files.append(fn)\n                count += 1\n        else:\n            if args:\n                fb = security.forbid_command(self,\n                                             security.CC_ACCESS_FILESYSTEM)\n                if fb:\n                    return self._error(fb)\n            while os.path.exists(args[-1]):\n                files.append(args.pop(-1))\n\n        if not files:\n            return self._error(_('No files found'))\n\n        if not emails:\n            args.extend(['=%s' % mid for mid in self.data.get('mid', [])])\n            emails = [self._actualize_ephemeral(i) for i in\n                      self._choose_messages(args, allow_ephemeral=True)]\n        if not emails:\n            return self._error(_('No messages selected'))\n\n        updated = []\n        errors = []\n        def err(msg):\n            errors.append(msg)\n            session.ui.error(msg)\n        for email in emails:\n            subject = email.get_msg_info(MailIndex.MSG_SUBJECT)\n            try:\n                email.add_attachments(session, files, filedata=filedata)\n                updated.append(email)\n            except KeyboardInterrupt:\n                raise\n            except NotEditableError:\n                err(_('Read-only message: %s') % subject)\n            except:\n                err(_('Error attaching to %s') % subject)\n                self._ignore_exception()\n\n        file_list = ', '.join(f.decode('utf-8') for f in files)\n        if errors:\n            self.message = _('Attached %s to %d messages, failed %d'\n                             ) % (file_list, len(updated), len(errors))\n        else:\n            self.message = _('Attached %s to %d messages'\n                             ) % (file_list, len(updated))\n\n        if updated:\n            self._background_save(index=True)\n\n        session.ui.notify(self.message)\n        return self._return_search_results(self.message, updated,\n                                           expand=updated, error=errors)\n\n\nclass UnAttach(CompositionCommand):\n    \"\"\"Remove an attachment from a message\"\"\"\n    SYNOPSIS = (None, 'unattach', 'message/unattach', '<mid> <atts>')\n    ORDER = ('Composing', 2)\n    WITH_CONTEXT = (GLOBAL_EDITING_LOCK, )\n    HTTP_CALLABLE = ('POST', 'UPDATE')\n    HTTP_QUERY_VARS = {}\n    HTTP_POST_VARS = {\n        'mid': 'metadata-ID',\n        'att': 'Attachment IDs or filename'\n    }\n\n    def command(self, emails=None):\n        session, idx = self.session, self._idx()\n        args = list(self.args)\n        atts = []\n\n        if '--' in args:\n            atts = args[args.index('--') + 1:]\n            args = args[:args.index('--')]\n        elif args:\n            atts = [args.pop(-1)]\n        atts.extend(self.data.get('att', []))\n\n        if not emails:\n            args.extend(['=%s' % mid for mid in self.data.get('mid', [])])\n            emails = [self._actualize_ephemeral(i) for i in\n                      self._choose_messages(args, allow_ephemeral=True)]\n        if not emails:\n            return self._error(_('No messages selected'))\n\n        updated = []\n        errors = []\n        def err(msg):\n            errors.append(msg)\n            session.ui.error(msg)\n\n        for email in emails:\n            subject = email.get_msg_info(MailIndex.MSG_SUBJECT)\n            try:\n                email.remove_attachments(session, *atts)\n                updated.append(email)\n            except KeyboardInterrupt:\n                raise\n            except NotEditableError:\n                err(_('Read-only message: %s') % subject)\n            except:\n                err(_('Error removing from %s') % subject)\n                self._ignore_exception()\n\n        if errors:\n            self.message = _('Removed %s from %d messages, failed %d'\n                             ) % (', '.join(atts), len(updated), len(errors))\n        else:\n            self.message = _('Removed %s from %d messages'\n                             ) % (', '.join(atts), len(updated))\n\n        if updated:\n            self._background_save(index=True)\n\n        session.ui.notify(self.message)\n        return self._return_search_results(self.message, updated,\n                                           expand=updated, error=errors)\n\n\n\nclass Sendit(CompositionCommand):\n    \"\"\"Mail/bounce a message (to someone)\"\"\"\n    SYNOPSIS = (None, 'bounce', 'message/send', '<messages> [<emails>]')\n    ORDER = ('Composing', 5)\n    HTTP_CALLABLE = ('POST', )\n    HTTP_QUERY_VARS = {}\n    HTTP_POST_VARS = {\n        'mid': 'metadata-ID',\n        'to': 'recipients',\n        'from': 'sender e-mail'\n    }\n\n    # We set our events' source class explicitly, so subclasses don't\n    # accidentally create orphaned mail tracking events.\n    EVENT_SOURCE = 'mailpile.plugins.compose.Sendit'\n\n    def command(self, emails=None):\n        session, config, idx = self.session, self.session.config, self._idx()\n        args = list(self.args)\n\n        bounce_to = []\n        while args and '@' in args[-1]:\n            bounce_to.append(args.pop(-1))\n        for rcpt in (self.data.get('to', []) +\n                     self.data.get('cc', []) +\n                     self.data.get('bcc', [])):\n            bounce_to.extend(AddressHeaderParser(rcpt).addresses_list())\n\n        sender = self.data.get('from', [None])[0]\n        if not sender and bounce_to:\n            sender = idx.config.get_profile().get('email', None)\n\n        if not emails:\n            args.extend(['=%s' % mid for mid in self.data.get('mid', [])])\n            emails = [self._actualize_ephemeral(i) for i in\n                      self._choose_messages(args, allow_ephemeral=True)]\n\n        # First make sure the draft tags are all gone, so other edits either\n        # fail or complete while we wait for the lock.\n        with GLOBAL_EDITING_LOCK:\n            self._tag_drafts(emails, untag=True)\n            self._tag_blank(emails, untag=True)\n\n        # Process one at a time so we don't eat too much memory\n        sent = []\n        missing_keys = []\n        locked_keys = []\n        for email in emails:\n            events = []\n            try:\n                msg_mid = email.get_msg_info(idx.MSG_MID)\n\n                # This is a unique sending-ID. This goes in the public (meant\n                # for debugging help) section of the event-log, so we take\n                # care to not reveal details about the message or recipients.\n                msg_sid = sha1b64(email.get_msg_info(idx.MSG_ID),\n                                  *sorted(bounce_to))[:8]\n\n                # We load up any incomplete events for sending this message\n                # to this set of recipients. If nothing is in flight, create\n                # a new event for tracking this operation.\n                events = list(config.event_log.incomplete(\n                    source=self.EVENT_SOURCE,\n                    data_mid=msg_mid,\n                    data_sid=msg_sid))\n                if not events:\n                    events.append(config.event_log.log(\n                        source=self.EVENT_SOURCE,\n                        flags=Event.RUNNING,\n                        message=_('Sending message'),\n                        data={'mid': msg_mid, 'sid': msg_sid}))\n\n                SendMail(session, msg_mid,\n                         [PrepareMessage(config,\n                                         email.get_msg(pgpmime=False),\n                                         sender=sender,\n                                         rcpts=(bounce_to or None),\n                                         bounce=(True if bounce_to else False),\n                                         events=events)])\n                for ev in events:\n                    ev.flags = Event.COMPLETE\n                    config.event_log.log_event(ev)\n                sent.append(email)\n\n            # Encryption related failures are fatal, don't retry\n            except (KeyLookupError,\n                    EncryptionFailureError,\n                    SignatureFailureError) as exc:\n                message = unicode(exc)\n                session.ui.warning(message)\n                if hasattr(exc, 'missing_keys'):\n                    missing_keys.extend(exc.missing)\n                if hasattr(exc, 'from_key'):\n                    # FIXME: We assume signature failures happen because\n                    # the key is locked. Are there any other reasons?\n                    locked_keys.append(exc.from_key)\n                for ev in events:\n                    ev.flags = Event.COMPLETE\n                    ev.message = message\n                    config.event_log.log_event(ev)\n                self._ignore_exception()\n\n            # FIXME: Also fatal, when the SMTP server REJECTS the mail\n            except:\n                # We want to try that again!\n                to = email.get_msg(pgpmime=False).get('x-mp-internal-rcpts',\n                                                      '').split(',')[0]\n                if to:\n                    message = _('Could not send mail to %s') % to\n                else:\n                    message = _('Could not send mail')\n                for ev in events:\n                    ev.flags = Event.INCOMPLETE\n                    ev.message = message\n                    config.event_log.log_event(ev)\n                session.ui.error(message)\n                self._ignore_exception()\n\n        if 'compose' in config.sys.debug:\n            sys.stderr.write(('compose/Sendit: Send %s to %s (sent: %s)\\n'\n                              ) % (len(emails),\n                                   bounce_to or '(header folks)', sent))\n\n        if missing_keys:\n            self.error_info['missing_keys'] = missing_keys\n        if locked_keys:\n            self.error_info['locked_keys'] = locked_keys\n        if sent:\n            self._tag_sent(sent)\n            self._tag_outbox(sent, untag=True)\n            for email in sent:\n                email.reset_caches()\n                idx.index_email(self.session, email)\n\n            self._background_save(index=True)\n            return self._return_search_results(\n                _('Sent %d messages') % len(sent), sent, sent=sent)\n        else:\n            return self._error(_('Nothing was sent'))\n\n\nclass Update(CompositionCommand):\n    \"\"\"Update message from a file or HTTP upload.\"\"\"\n    SYNOPSIS = (None, 'update', 'message/update', '<messages> <<filename>')\n    ORDER = ('Composing', 1)\n    WITH_CONTEXT = (GLOBAL_EDITING_LOCK, )\n    HTTP_CALLABLE = ('POST', 'UPDATE')\n    HTTP_POST_VARS = dict_merge(CompositionCommand.UPDATE_STRING_DATA,\n                                Attach.HTTP_POST_VARS)\n\n    def command(self, create=True, outbox=False):\n        session, config, idx = self.session, self.session.config, self._idx()\n        email_updates = self._get_email_updates(idx,\n                                                create=create,\n                                                noneok=outbox)\n\n        if not email_updates:\n            return self._error(_('Nothing to do!'))\n        try:\n            if (self.data.get('file-data') or [''])[0]:\n                if not Attach(session, data=self.data).command(emails=emails):\n                    return self._error(_('Failed to attach files'))\n\n            for email, update_string in email_updates:\n                if not email:\n                    return self._error(_('Cannot find message'))\n                    break\n                email.update_from_string(session, update_string, final=outbox)\n\n            emails = [e for e, u in email_updates]\n            message = _('%d message(s) updated') % len(email_updates)\n\n            self._tag_blank(emails, untag=True)\n            self._tag_drafts(emails, untag=outbox)\n            self._tag_outbox(emails, untag=(not outbox))\n\n            if outbox:\n                self._create_contacts(emails)\n                self._background_save(index=True)\n                return self._return_search_results(message, emails,\n                                                   sent=emails)\n            else:\n                return self._edit_messages(emails, new=False, tag=False)\n        except KeyLookupError as kle:\n            return self._error(_('Missing encryption keys'),\n                               info={'missing_keys': kle.missing})\n        except EncryptionFailureError as efe:\n            # This should never happen, should have been prevented at key\n            # lookup!\n            return self._error(_('Could not encrypt message'),\n                               info={'to_keys': efe.to_keys})\n        except SignatureFailureError as sfe:\n            # FIXME: We assume signature failures happen because\n            # the key is locked. Are there any other reasons?\n            return self._error(_('Could not sign message'),\n                               info={'locked_keys': [sfe.from_key]})\n\n\nclass UpdateAndSendit(Update):\n    \"\"\"Update message from an HTTP upload and move to outbox.\"\"\"\n    SYNOPSIS = ('m', 'mail', 'message/update/send', None)\n\n    def command(self, create=True, outbox=True):\n        return Update.command(self, create=create, outbox=outbox)\n\n\nclass UnThread(CompositionCommand):\n    \"\"\"Remove a message from a thread.\"\"\"\n    SYNOPSIS = (None, 'unthread', 'message/unthread', None)\n    HTTP_CALLABLE = ('GET', 'POST')\n    HTTP_QUERY_VARS = {\n        'mid': 'message-id'}\n    HTTP_POST_VARS = {\n        'subject': 'Update the metadata subject as well'}\n\n    def command(self):\n        session, config, idx = self.session, self.session.config, self._idx()\n        args = list(self.args)\n\n        # On the CLI, anything after -- is the new metadata subject.\n        if '--' in args:\n            subject = ' '.join(args[(args.index('--')+1):])\n            args = args[:args.index('--')]\n        else:\n            subject = self.data.get('subject', [None])[0]\n\n        # Message IDs can come from post data\n        for mid in self.data.get('mid', []):\n            args.append('=%s' % mid)\n        emails = [self._actualize_ephemeral(i) for i in\n                  self._choose_messages(args, allow_ephemeral=True)]\n\n        if emails:\n            if self.data.get('_method', 'POST') == 'POST':\n                for email in emails:\n                    idx.unthread_message(email.msg_mid(), new_subject=subject)\n                self._background_save(index=True)\n                return self._return_search_results(\n                    _('Unthreaded %d messages') % len(emails), emails)\n            else:\n                return self._return_search_results(\n                    _('Unthread %d messages') % len(emails), emails)\n        else:\n            return self._error(_('Nothing to do!'))\n\n\nclass EmptyOutbox(Sendit):\n    \"\"\"Try to empty the outbox.\"\"\"\n    SYNOPSIS = (None, 'sendmail', None, None)\n    IS_USER_ACTIVITY = False\n\n    @classmethod\n    def sendmail(cls, session):\n        cls(session).run()\n\n    def command(self):\n        cfg, idx = self.session.config, self.session.config.index\n        if not idx:\n            return self._error(_('The index is not ready yet'))\n\n        # Collect a list of messages from the outbox\n        messages = []\n        for tag in cfg.get_tags(type='outbox'):\n            search = ['in:%s' % tag._key]\n            for msg_idx_pos in idx.search(self.session, search,\n                                          order='flat-index').as_set():\n                messages.append('=%s' % b36(msg_idx_pos))\n\n        # Messages no longer in the outbox get their events canceled...\n        if cfg.event_log:\n            events = cfg.event_log.incomplete(source='.plugins.compose.Sendit')\n            for ev in events:\n                if ('mid' in ev.data and\n                        ('=%s' % ev.data['mid']) not in messages):\n                    ev.flags = ev.COMPLETE\n                    ev.message = _('Sending cancelled.')\n                    cfg.event_log.log_event(ev)\n\n        # Send all the mail!\n        if messages:\n            self.args = tuple(set(messages))\n            return Sendit.command(self)\n        else:\n            return self._success(_('The outbox is empty'))\n\n\n_plugins.register_config_variables('prefs', {\n    'empty_outbox_interval': [_('Delay between attempts to send mail'),\n                              int, 90]\n})\n_plugins.register_slow_periodic_job('sendmail',\n                                    'prefs.empty_outbox_interval',\n                                    EmptyOutbox.sendmail)\n_plugins.register_commands(Compose, Reply, Forward,           # Create\n                           Draft, Update, Attach, UnAttach,   # Manipulate\n                           UnThread,                          # ...\n                           Sendit, UpdateAndSendit,           # Send\n                           EmptyOutbox)                       # ...\n"
  },
  {
    "path": "mailpile/plugins/contacts.py",
    "content": "import os\nimport random\nimport time\nfrom email import encoders\n\nimport mailpile.config.defaults\nimport mailpile.security as security\nfrom mailpile.crypto.gpgi import GnuPG\nfrom mailpile.crypto.gpgi import GnuPGBaseKeyGenerator, GnuPGKeyGenerator\nfrom mailpile.crypto.autocrypt import generate_autocrypt_setup_code\nfrom mailpile.plugins import EmailTransform, PluginManager\nfrom mailpile.commands import Command, Action\nfrom mailpile.eventlog import Event\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.mailutils.addresses import AddressHeaderParser\nfrom mailpile.mailutils.emails import Email, ExtractEmails, ExtractEmailAndName\nfrom mailpile.security import SecurePassphraseStorage\nfrom mailpile.vcard import VCardLine, VCardStore, MailpileVCard, AddressInfo, GLOBAL_VCARD_LOCK\nfrom mailpile.util import *\n\n\n_plugins = PluginManager(builtin=__file__)\n\n\n##[ VCards ]########################################\n\nclass VCardCommand(Command):\n    VCARD = \"vcard\"\n    IS_USER_ACTIVITY = True\n    WITH_CONTEXT = (GLOBAL_VCARD_LOCK,)\n\n    class CommandResult(Command.CommandResult):\n        IGNORE = ('line_id', 'pid', 'x-rank')\n\n        def as_text(self):\n            try:\n                return self._as_text()\n            except (KeyError, ValueError, IndexError, TypeError):\n                return ''\n\n        def _as_text(self):\n            if isinstance(self.result, dict):\n                co = self.command_obj\n                if co.VCARD in self.result:\n                    return self._vcards_as_text([self.result[co.VCARD]])\n                if co.VCARD + 's' in self.result:\n                    return self._vcards_as_text(self.result[co.VCARD + 's'])\n            return Command.CommandResult.as_text(self)\n\n        def _vcards_as_text(self, result):\n            lines = []\n            b64re = re.compile('base64,.*$')\n            for card in result:\n                if isinstance(card, list):\n                    for line in card:\n                        key = line.name\n                        data = re.sub(b64re, _('(BASE64 ENCODED DATA)'),\n                                      unicode(line[key]))\n                        attrs = ', '.join([('%s=%s' % (k, v))\n                                           for k, v in line.attrs\n                                           if k not in ('pid',)])\n                        if attrs:\n                            attrs = ' (%s)' % attrs\n                        lines.append('%3.3s %-5.5s %s: %s%s'\n                                     % (line.line_id,\n                                        line.get('pid', ''),\n                                        key, data, attrs))\n                    lines.append('')\n                else:\n                    emails = [k['email'] for k in card['email']]\n                    photos = [k['photo'] for k in card.get('photo', [])]\n                    lines.append('%s %-32.32s %s'\n                                 % (photos and ':)' or '  ',\n                                    card['fn'] + (' (%s)' % card['note']\n                                                  if card.get('note') else ''),\n                                    ', '.join(emails)))\n                    for key in [k['key'].split(',')[-1]\n                                for k in card.get('key', [])]:\n                        lines.append('   %-32.32s key:%s' % ('', key))\n            return '\\n'.join(lines)\n\n    def _form_defaults(self):\n        return {'form': self.HTTP_POST_VARS}\n\n    def _make_new_vcard(self, handle, name, note, kind):\n        l = [VCardLine(name='fn', value=name),\n             VCardLine(name='kind', value=kind)]\n        if note:\n            l.append(VCardLine(name='note', value=note))\n        if kind in VCardStore.KINDS_PEOPLE:\n            return MailpileVCard(VCardLine(name='email',\n                                           value=handle, type='pref'), *l,\n                                 config=self.session.config)\n        else:\n            return MailpileVCard(VCardLine(name='nickname', value=handle), *l,\n                                 config=self.session.config)\n\n    def _valid_vcard_handle(self, vc_handle):\n        return (vc_handle and '@' in vc_handle[1:])\n\n    def _pre_delete_vcard(self, vcard):\n        pass\n\n    def _vcard_list(self, vcards, mode='mpCard', info=None, simplify=False):\n        info = info or {}\n        if mode == 'lines':\n            data = [x.as_lines() for x in vcards if x]\n        else:\n            data = [x.as_mpCard() for x in vcards if x]\n\n        # Generate some helpful indexes for finding stuff\n        by_email = {}\n        by_rid = {}\n        for count, vc in enumerate(vcards):\n            by_rid[vc.random_uid] = count\n            by_email[vc.email] = count\n        for count, vc in enumerate(vcards):\n            for vcl in vc.get_all('EMAIL'):\n                if vcl.value not in by_email:\n                    by_email[vcl.value] = count\n\n        # Simplify lists when there is only one element?\n        if simplify and len(data) == 1:\n            data = data[0]\n            whatsit = self.VCARD\n        else:\n            whatsit = self.VCARD + 's'\n\n        info.update({\n            whatsit: data,\n            \"emails\": by_email,\n            \"rids\": by_rid,\n            \"count\": len(vcards)\n        })\n        return info\n\n\nclass VCard(VCardCommand):\n    \"\"\"Display a single vcard\"\"\"\n    SYNOPSIS = (None, 'vcards/view', None, '<nickname>')\n    ORDER = ('Internals', 6)\n    KIND = ''\n\n    def command(self, save=True):\n        self._idx()  # Make sure VCards are all loaded\n        session, config = self.session, self.session.config\n        vcards = []\n        for email in self.args:\n            vcard = config.vcards.get_vcard(email)\n            if vcard:\n                vcards.append(vcard)\n            else:\n                session.ui.warning('No such %s: %s' % (self.VCARD, email))\n        return self._success(_('Found %d results') % len(vcards),\n                             result=self._vcard_list(vcards, simplify=True))\n\n\nclass AddVCard(VCardCommand):\n    \"\"\"Add one or more vcards\"\"\"\n    SYNOPSIS = (None, 'vcards/add', None, '[all] <msgs> OR <email> = <name>')\n    ORDER = ('Internals', 6)\n    KIND = ''\n    HTTP_CALLABLE = ('POST', 'PUT', 'GET')\n    HTTP_POST_VARS = {\n        'email': 'E-mail address',\n        'name': 'Contact name',\n        'note': 'Note about contact',\n        'mid': 'Message ID'\n    }\n    COMMAND_SECURITY = security.CC_CHANGE_CONTACTS\n\n    IGNORED_EMAILS_AND_DOMAINS = (\n        'reply.airbnb.com',\n        'notifications@github.com'\n    )\n\n    def _add_from_messages(self, args, add_recipients):\n        pairs, idx = [], self._idx()\n        for email in [Email(idx, i) for i in self._choose_messages(args)]:\n            msg_info = email.get_msg_info()\n            pairs.append(ExtractEmailAndName(msg_info[idx.MSG_FROM]))\n            if add_recipients:\n                people = (idx.expand_to_list(msg_info) +\n                          idx.expand_to_list(msg_info, field=idx.MSG_CC))\n                for e in people:\n                    pair = ExtractEmailAndName(e)\n                    domain = pair[0].split('@')[-1]\n                    if (pair[0] not in self.IGNORED_EMAILS_AND_DOMAINS and\n                            domain not in self.IGNORED_EMAILS_AND_DOMAINS and\n                            'noreply' not in pair[0]):\n                        pairs.append(pair)\n        return [(p1, p2, '') for p1, p2 in pairs]\n\n    def _sanity_check(self, kind, vcard):\n        pass\n\n    def _before_vcard_create(self, kind, triplets):\n        return {}\n\n    def _after_vcard_create(self, kind, vcard, state):\n        pass\n\n    def command(self, recipients=False, quietly=False, internal=False):\n        idx = self._idx()  # Make sure VCards are all loaded\n        session, config = self.session, self.session.config\n        args = list(self.args)\n\n        if self.data.get('_method', 'not-http').upper() == 'GET':\n            return self._success(_('Add contacts here!'),\n                                 self._form_defaults())\n\n        if (len(args) > 2\n                and args[1] == '='\n                and self._valid_vcard_handle(args[0])):\n            handle = args[0]\n            name = []\n            note = []\n            inname = True\n            for v in args[2:]:\n                if v.startswith('('):\n                    inname = False\n                    v = v[1:]\n                if v.endswith(')'):\n                    v = v[:-1]\n                if inname:\n                    name.append(v)\n                else:\n                    note.append(v)\n\n            triplets = [(args[0], ' '.join(name), ' '.join(note))]\n\n        elif self.data:\n            if self.data.get('email'):\n                emails = self.data[\"email\"]\n                names = self.data[\"name\"][:]\n                names.extend(['' for i in range(len(names), len(emails))])\n                notes = self.data.get(\"note\", [])[:]\n                notes.extend(['' for i in range(len(notes), len(emails))])\n                triplets = zip(emails, names, notes)\n            elif self.data.get('mid'):\n                mids = self.data.get('mid')\n                triplets = self._add_from_messages(\n                    ['=%s' % mid.replace('=', '') for mid in mids])\n        else:\n            if args and args[0] == 'all':\n                recipients = args.pop(0) and True\n            triplets = self._add_from_messages(args, recipients)\n\n        if triplets:\n            vcards = []\n            kind = self.KIND if not internal else 'internal'\n\n            self._sanity_check(kind, triplets)\n            state = self._before_vcard_create(kind, triplets)\n\n            for handle, name, note in triplets:\n                vcard = config.vcards.get(handle.lower())\n                if vcard:\n                    if not quietly:\n                        session.ui.warning('Already exists: %s' % handle)\n                    if kind != 'profile' and vcard.kind != 'internal':\n                        continue\n\n                if vcard and vcard.kind == 'internal':\n                    config.vcards.deindex_vcard(vcard)\n                    vcard.email = handle.lower()\n                    vcard.name = name\n                    vcard.note = note\n                    vcard.kind = kind\n                else:\n                    vcard = self._make_new_vcard(handle.lower(), name, note,\n                                                 kind)\n                self._after_vcard_create(kind, vcard, state)\n                config.vcards.add_vcards(vcard)\n                vcards.append(vcard)\n            if state.get('save_config', False):\n                self._background_save(config=True)\n        else:\n            return self._error('Nothing to do!')\n        return self._success(_('Added %d contacts') % len(vcards),\n            result=self._vcard_list(vcards, simplify=True))\n\n\nclass RemoveVCard(VCardCommand):\n    \"\"\"Delete vcards\"\"\"\n    SYNOPSIS = (None, 'vcards/remove', None, '<email|x-mailpile-rid>')\n    ORDER = ('Internals', 6)\n    KIND = ''\n    HTTP_CALLABLE = ('POST', 'DELETE')\n    HTTP_POST_VARS = {\n        'email': 'delete by e-mail',\n        'rid': 'delete by x-mailpile-rid'\n    }\n    COMMAND_SECURITY = security.CC_CHANGE_CONTACTS\n\n    def command(self):\n        idx = self._idx()  # Make sure VCards are all loaded\n        session, config = self.session, self.session.config\n        removed = []\n        for handle in (list(self.args) +\n                       self.data.get('email', []) +\n                       self.data.get('rid', [])):\n            vcard = config.vcards.get_vcard(handle)\n            if vcard:\n                self._pre_delete_vcard(vcard)\n                config.vcards.del_vcards(vcard)\n                removed.append(handle)\n            else:\n                session.ui.error(_('No such contact: %s') % handle)\n        if removed:\n            return self._success(_('Removed contacts: %s')\n                                 % ', '.join(removed))\n        else:\n            return self._error(_('No contacts found'))\n\n\nclass VCardAddLines(VCardCommand):\n    \"\"\"Add a lines to a VCard\"\"\"\n    SYNOPSIS = (None,\n                'vcards/addlines', 'vcards/addlines',\n                '<email> <[[<NR>]=]line> ...')\n    ORDER = ('Internals', 6)\n    KIND = ''\n    HTTP_CALLABLE = ('POST', 'UPDATE')\n    HTTP_POST_VARS = {\n        'email': 'update by e-mail',\n        'rid': 'update by x-mailpile-rid',\n        'name': 'Line name',\n        'value': 'Line value',\n        'replace': 'int=replace line by number',\n        'replace_all': 'Boolean: replace all lines, or not',\n        'client': 'Source of this change'\n    }\n    COMMAND_SECURITY = security.CC_CHANGE_CONTACTS\n    DEFAULT_REPLACE_ALL = False\n\n    def _get_vcard(self, handle):\n        return self.session.config.vcards.get_vcard(handle)\n\n    def command(self):\n        idx = self._idx()  # Make sure VCards are all loaded\n        session, config = self.session, self.session.config\n\n        if self.args:\n            handle, lines = self.args[0], self.args[1:]\n        else:\n            handle = self.data.get('rid', self.data.get('email', [None]))[0]\n            if not handle:\n                raise ValueError('Must set rid or email to choose VCard')\n            name, value, replace, replace_all = (self.data.get(n, [None])[0]\n                for n in ('name', 'value', 'replace', 'replace_all'))\n            if not name or not value or ':' in name or '=' in name:\n                raise ValueError('Must send a line name and line data')\n            value = '%s:%s' % (name, value)\n            if replace:\n                value = '%d=%s' % (replace, value)\n            elif truthy(replace_all, default=self.DEFAULT_REPLACE_ALL):\n                value = '=' + value\n            lines = [value]\n\n        vcard = self._get_vcard(handle)\n        if not vcard:\n            return self._error('%s not found: %s' % (self.VCARD, handle))\n        config.vcards.deindex_vcard(vcard)\n        client = self.data.get('client', [vcard.USER_CLIENT])[0]\n        try:\n            for l in lines:\n                lname = l.split(':', 1)[0].lower()\n                if lname[0] == '=':\n                    l = l[1:].strip()\n                    lname = lname[1:]\n                    removing = [ex._line_id for ex in vcard.get_all(lname)]\n                elif lname in MailpileVCard.MPCARD_SINGLETONS:\n                    removing = [ex._line_id for ex in vcard.get_all(lname)]\n                else:\n                    removing = []\n\n                if '=' in l[:5]:\n                    ln, l = l.split('=', 1)\n                    vcard.set_line(int(ln.strip()), VCardLine(l.strip()),\n                                   client=client)\n\n                else:\n                    if removing:\n                        vcard.remove(*removing)\n                    vcard.add(VCardLine(l), client=client)\n\n            vcard.save()\n            return self._success(_(\"Added %d lines\") % len(lines),\n                result=self._vcard_list([vcard], simplify=True, info={\n                    'updated': handle,\n                    'added': len(lines)\n                }))\n        except KeyboardInterrupt:\n            raise\n        except:\n            config.vcards.index_vcard(vcard)\n            self._ignore_exception()\n            return self._error(_('Error adding lines to %s') % handle)\n        finally:\n            config.vcards.index_vcard(vcard)\n\n\nclass VCardSet(VCardAddLines):\n    \"\"\"Add a lines to a VCard, ensuring VCard exists\"\"\"\n    SYNOPSIS = (None, 'vcards/set', 'vcards/set', '<email> <[[<NR>]=]line> ...')\n    HTTP_POST_VARS = dict_merge(VCardAddLines.HTTP_POST_VARS, {\n        'fn': 'Name on card'\n    })\n    DEFAULT_REPLACE_ALL = True\n\n    def _get_vcard(self, handle):\n        vcard = self.session.config.vcards.get_vcard(handle)\n        if not vcard:\n            vcard = self._make_new_vcard(handle,\n                                         self.data.get('fn', [handle])[0],\n                                         None,\n                                         self.KIND or 'individual')\n            self.session.config.vcards.add_vcards(vcard)\n        return vcard\n\n\nclass VCardRemoveLines(VCardCommand):\n    \"\"\"Remove lines from a VCard\"\"\"\n    SYNOPSIS = (None, 'vcards/rmlines', 'vcards/rmlines', '<email> <line IDs>')\n    ORDER = ('Internals', 6)\n    KIND = ''\n    HTTP_CALLABLE = ('POST', 'UPDATE')\n    COMMAND_SECURITY = security.CC_CHANGE_CONTACTS\n    HTTP_POST_VARS = {\n        'email': 'update by e-mail',\n        'rid': 'update by x-mailpile-rid',\n        'name': 'Line names',\n        'line_id': 'Line IDs'}\n\n    def command(self):\n        idx = self._idx()  # Make sure VCards are all loaded\n        session, config = self.session, self.session.config\n\n        if self.args:\n            handle, names, line_ids = self.args[0], [], []\n            for arg in self.args[1:]:\n                try:\n                    line_ids.append('%d' % int(arg))\n                except ValueError:\n                    names.append(arg)\n        else:\n            handle = self.data.get('rid', self.data.get('email', [None]))[0]\n            if not handle:\n                raise ValueError('Must set rid or email to choose VCard')\n            names = self.data.get('name', [])\n            line_ids = self.data.get('line_id', [])\n            if not (names or line_ids):\n                raise ValueError('Must send a line name or line ID')\n\n        vcard = config.vcards.get_vcard(handle)\n        if not vcard:\n            return self._error('%s not found: %s' % (self.VCARD, handle))\n        config.vcards.deindex_vcard(vcard)\n        removed = 0\n        try:\n            for lname in names:\n                line_ids.extend(ex._line_id for ex in vcard.get_all(lname))\n            removed += vcard.remove(*[int(li) for li in line_ids])\n            vcard.save()\n            return self._success(_(\"Removed %d lines\") % removed,\n                result=self._vcard_list([vcard], simplify=True, info={\n                    'updated': handle,\n                    'removed': removed\n                }))\n        except KeyboardInterrupt:\n            raise\n        except:\n            config.vcards.index_vcard(vcard)\n            self._ignore_exception()\n            return self._error(_('Error removing lines from %s') % handle)\n        finally:\n            config.vcards.index_vcard(vcard)\n\n\nclass ListVCards(VCardCommand):\n    \"\"\"Find vcards\"\"\"\n    SYNOPSIS = (None, 'vcards', None, '[--lines] [<terms>]')\n    ORDER = ('Internals', 6)\n    KIND = ''\n    HTTP_QUERY_VARS = {\n        'q': 'search terms',\n        'format': 'lines or mpCard (default)',\n        'count': 'how many to display (default=40)',\n        'offset': 'skip how many in the display (default=0)',\n    }\n    HTTP_CALLABLE = ('GET')\n\n    def _augment_list_info(self, info):\n        return info\n\n    def command(self):\n        session, config = self.session, self.session.config\n        kinds = self.KIND and [self.KIND] or None\n        args = list(self.args)\n\n        if 'format' in self.data:\n            fmt = self.data['format'][0]\n        elif args and args[0] == '--lines':\n            args.pop(0)\n            fmt = 'lines'\n        else:\n            fmt = 'mpCard'\n\n        if 'q' in self.data:\n            terms = self.data['q']\n        else:\n            terms = args\n\n        if 'count' in self.data:\n            count = int(self.data['count'][0])\n        else:\n            count = 120\n\n        if 'offset' in self.data:\n            offset = int(self.data['offset'][0])\n        else:\n            offset = 0\n\n        # If we're loading, stall a bit but then report current state\n        loading, loaded = config.vcards.loading, config.vcards.loaded\n        if loading:\n            time.sleep(2)\n            loading, loaded = config.vcards.loading, config.vcards.loaded\n\n        vcards = config.vcards.find_vcards(terms, kinds=kinds)\n        total = len(vcards)\n        vcards = vcards[offset:offset + count]\n        info = self._augment_list_info({\n                   'terms': args,\n                   'offset': offset,\n                   'count': min(count, total),\n                   'total': total,\n                   'start': offset,\n                   'end': offset + min(count, total - offset),\n                   'loading': loading,\n                   'loaded': loaded})\n        return self._success(\n            _(\"Listed %d/%d results\") % (min(total, count), total),\n            result=self._vcard_list(vcards, mode=fmt, info=info))\n\n\ndef ContactVCard(parent):\n    \"\"\"A factory for generating contact commands\"\"\"\n    synopsis = [(t and t.replace('vcard', 'contact') or t)\n                for t in parent.SYNOPSIS]\n    synopsis[2] = synopsis[1]\n\n    class ContactVCardCommand(parent):\n        SYNOPSIS = tuple(synopsis)\n        KIND = 'individual'\n        ORDER = ('Tagging', 3)\n        VCARD = \"contact\"\n\n    return ContactVCardCommand\n\n\nclass Contact(ContactVCard(VCard)):\n    \"\"\"View contacts\"\"\"\n    SYNOPSIS = (None, 'contacts/view', 'contacts/view', '[<email>]')\n\n    def command(self, save=True):\n        contact = VCard.command(self, save)\n        # Tee-hee, monkeypatching results.\n        contact[\"sent_messages\"] = 0\n        contact[\"received_messages\"] = 0\n        contact[\"last_contact_from\"] = 10000000000000\n        contact[\"last_contact_to\"] = 10000000000000\n\n        for email in contact[\"contact\"][\"email\"]:\n            s = Action(self.session, \"search\",\n                       [\"in:Sent\", \"to:%s\" % (email[\"email\"])]).as_dict()\n            contact[\"sent_messages\"] += s[\"result\"][\"stats\"][\"total\"]\n            for mid in s[\"result\"][\"thread_ids\"]:\n                msg = s[\"result\"][\"data\"][\"metadata\"][mid]\n                if msg[\"timestamp\"] < contact[\"last_contact_to\"]:\n                    contact[\"last_contact_to\"] = msg[\"timestamp\"]\n                    contact[\"last_contact_to_msg_url\"] = msg[\"urls\"][\"thread\"]\n\n            s = Action(self.session, \"search\",\n                       [\"from:%s\" % (email[\"email\"])]).as_dict()\n            contact[\"received_messages\"] += s[\"result\"][\"stats\"][\"total\"]\n            for mid in s[\"result\"][\"thread_ids\"]:\n                msg = s[\"result\"][\"data\"][\"metadata\"][mid]\n                if msg[\"timestamp\"] < contact[\"last_contact_from\"]:\n                    contact[\"last_contact_from\"] = msg[\"timestamp\"]\n                    contact[\"last_contact_from_msg_url\"\n                            ] = msg[\"urls\"][\"thread\"]\n\n        if contact[\"last_contact_to\"] == 10000000000000:\n            contact[\"last_contact_to\"] = False\n            contact[\"last_contact_to_msg_url\"] = \"\"\n\n        if contact[\"last_contact_from\"] == 10000000000000:\n            contact[\"last_contact_from\"] = False\n            contact[\"last_contact_from_msg_url\"] = \"\"\n\n        return contact\n\n\nclass AddContact(ContactVCard(AddVCard)):\n    \"\"\"Add contacts\"\"\"\n\n\nclass RemoveContact(ContactVCard(RemoveVCard)):\n    \"\"\"Remove a contact\"\"\"\n\n\nclass ListContacts(ContactVCard(ListVCards)):\n    SYNOPSIS = (None, 'contacts', 'contacts', '[--lines] [<terms>]')\n    \"\"\"Find contacts\"\"\"\n\n\nclass ContactSet(ContactVCard(VCardSet)):\n    \"\"\"Set contact lines, ensuring contact exists\"\"\"\n\n\nclass ContactImport(Command):\n    \"\"\"Import contacts\"\"\"\n    SYNOPSIS = (None, 'contacts/import', 'contacts/import', '[<parameters>]')\n    ORDER = ('Internals', 6)\n    HTTP_CALLABLE = ('GET', )\n    COMMAND_SECURITY = security.CC_CHANGE_CONTACTS\n\n    def command(self, format, terms=None, **kwargs):\n        idx = self._idx()  # Make sure VCards are all loaded\n        session, config = self.session, self.session.config\n\n        if not format in PluginManager.CONTACT_IMPORTERS.keys():\n            session.ui.error(\"No such import format\")\n            return False\n\n        importer = PluginManager.CONTACT_IMPORTERS[format]\n\n        if not all([x in kwargs.keys() for x in importer.required_parameters]):\n            session.ui.error(\n                _(\"Required parameter missing. Required parameters \"\n                  \"are: %s\") % \", \".join(importer.required_parameters))\n            return False\n\n        allparams = importer.required_parameters + importer.optional_parameters\n\n        if not all([x in allparams for x in kwargs.keys()]):\n            session.ui.error(\n                _(\"Unknown parameter passed to importer. \"\n                  \"Provided %s; but known parameters are: %s\"\n                  ) % (\", \".join(kwargs), \", \".join(allparams)))\n            return False\n\n        imp = importer(kwargs)\n        if terms:\n            contacts = imp.filter_contacts(terms)\n        else:\n            contacts = imp.get_contacts()\n\n        for importedcontact in contacts:\n            # Check if contact exists. If yes, then update. Else create.\n            pass\n\n\nclass ContactImporters(Command):\n    \"\"\"Return a list of contact importers\"\"\"\n    SYNOPSIS = (None, 'contacts/importers', 'contacts/importers', '')\n    ORDER = ('Internals', 6)\n    HTTP_CALLABLE = ('GET', )\n\n    def command(self):\n        res = []\n        for iname, importer in CONTACT_IMPORTERS.iteritems():\n            r = {}\n            r[\"short_name\"] = iname\n            r[\"format_name\"] = importer.format_name\n            r[\"format_description\"] = importer.format_description\n            r[\"optional_parameters\"] = importer.optional_parameters\n            r[\"required_parameters\"] = importer.required_parameters\n            res.append(r)\n\n        return res\n\n\nclass AddressSearch(VCardCommand):\n    \"\"\"Find addresses (in contacts or mail index)\"\"\"\n    SYNOPSIS = (None, 'search/address', 'search/address', '[<terms>]')\n    ORDER = ('Searching', 6)\n    HTTP_QUERY_VARS = {\n        'q': 'search terms',\n        'count': 'number of results',\n        'offset': 'offset results',\n        'ms': 'deadline in ms'\n    }\n\n    def _boost_rank(self, boost, term, *matches):\n        boost = 0.0\n        for match in matches:\n            match = match.lower()\n            if term in match:\n                if match.startswith(term):\n                    boost += boost * boost * (float(len(term)) / len(match))\n                else:\n                    boost += boost * (float(len(term)) / len(match))\n        return int(boost)\n\n    def _vcard_addresses(self, cfg, terms, ignored_count, deadline):\n        addresses = {}\n        for vcard in cfg.vcards.find_vcards(terms,\n                                            kinds=VCardStore.KINDS_PEOPLE):\n            fn = vcard.get('fn')\n            for email_vcl in vcard.get_all('email'):\n                info = addresses.get(email_vcl.value) or {}\n                info.update(AddressInfo(email_vcl.value, fn.value,\n                                        vcard=vcard))\n                info['rank'] = min(15, info.get('rank', 15))\n                addresses[email_vcl.value] = info\n                for term in terms:\n                    info['rank'] += self._boost_rank(5, term, fn.value,\n                                                     email_vcl.value)\n            if len(addresses) and time.time() > deadline:\n                break\n\n        return addresses.values()\n\n    def _index_addresses(self, cfg, terms, vcard_addresses, count, deadline):\n        existing = dict([(k['address'].lower(), k) for k in vcard_addresses])\n        index = self._idx()\n\n        # Figure out which tags are invisible so we can skip messages marked\n        # with those tags.\n        invisible = set([t._key for t in cfg.get_tags(flag_hides=True)])\n        matches = {}\n        addresses = []\n\n        # 1st, search the social graph for matches, give low priority.\n        for frm in index.EMAILS:\n            frm_lower = frm.lower()\n            match = True\n            for term in terms:\n                if term not in frm_lower:\n                    match = False\n                    break\n            if match:\n                matches[frm] = matches.get(frm, 0) + 3\n                if len(matches) > (count * 10):\n                    break\n            elif len(matches) and time.time() > deadline:\n                break\n\n        # 2nd, go through at most the last 5000 messages in the index and\n        # search for matching senders or recipients, give medium priority.\n        # Note: This is more CPU intensive, so we do this last.\n        if len(matches) < (count * 5):\n            for msg_idx in xrange(max(0, len(index.INDEX)-5000),\n                                  len(index.INDEX)):\n                msg_info = index.get_msg_at_idx_pos(msg_idx)\n                tags = set(msg_info[index.MSG_TAGS].split(','))\n                match = not (tags & invisible)\n                if match:\n                    frm = msg_info[index.MSG_FROM]\n                    search = (frm + ' ' + msg_info[index.MSG_SUBJECT]).lower()\n                    for term in terms:\n                        if term not in search:\n                            match = False\n                            break\n                    if match:\n                        matches[frm] = matches.get(frm, 0) + 1\n                        if len(matches) > (count * 5):\n                            break\n                    if len(matches) and time.time() > deadline:\n                        break\n\n        # Assign info & scores!\n        for frm in matches:\n            email, fn = ExtractEmailAndName(frm)\n            boost = min(10, matches[frm])\n            for term in terms:\n                boost += self._boost_rank(4, term, fn, email)\n\n            if not email or '@' not in email:\n                # FIXME: This may not be the right thing for alternate\n                #        message transports.\n                pass\n            elif email.lower() in existing:\n                existing[email.lower()]['rank'] += boost\n            else:\n                info = AddressInfo(email, fn)\n                info['rank'] = info.get('rank', 0) + boost\n                existing[email.lower()] = info\n                addresses.append(info)\n\n        return addresses\n\n    def command(self):\n        session, config = self.session, self.session.config\n\n        count = int(self.data.get('count', 10))\n        offset = int(self.data.get('offset', 0))\n        deadline = time.time() + float(self.data.get('ms', 150)) / 1000.0\n        terms = []\n        for q in self.data.get('q', []):\n            terms.extend(q.lower().split())\n        for a in self.args:\n            terms.extend(a.lower().split())\n\n        self.session.ui.mark('Searching VCards')\n        vcard_addrs = self._vcard_addresses(config, terms, count, deadline)\n\n        self.session.ui.mark('Searching Metadata')\n        index_addrs = self._index_addresses(config, terms, vcard_addrs,\n                                            count, deadline)\n\n        self.session.ui.mark('Sorting')\n        addresses = vcard_addrs + index_addrs\n        addresses.sort(key=lambda k: -k['rank'])\n        total = len(addresses)\n        return self._success(_('Searched for addresses'), result={\n            'addresses': addresses[offset:min(offset+count, total)],\n            'displayed': min(count, total),\n            'total': total,\n            'offset': offset,\n            'count': count,\n            'start': offset,\n            'end': offset+count,\n        })\n\n\ndef ProfileVCard(parent):\n    \"\"\"A factory for generating profile commands\"\"\"\n    synopsis = [(t and t.replace('vcard', 'profile') or t)\n                for t in parent.SYNOPSIS]\n    synopsis[2] = synopsis[1]\n\n    class ProfileVCardCommand(parent):\n        SYNOPSIS = tuple(synopsis)\n        KIND = 'profile'\n        ORDER = ('Tagging', 3)\n        VCARD = \"profile\"\n\n        DEFAULT_KEYTYPE = 'RSA3072'\n\n        def _default_signature(self):\n            return _('Sent using Mailpile, Free Software from www.mailpile.is')\n\n        def _augment_list_info(self, info):\n            info['default_sig'] = self._default_signature()\n            return info\n\n        def _yn(self, val, default='no'):\n            return truthy(self.data.get(val, [default])[0])\n\n        def _sendmail_command(self):\n            # FIXME - figure out where sendmail is for reals\n            return mailpile.config.defaults.DEFAULT_SENDMAIL\n\n        def _sanity_check(self, kind, triplets):\n            route_id = self.data.get('route_id', [None])[0]\n            if (route_id or [k for k in self.data.keys() if k[:5] in\n                             ('route', 'smtp-', 'sourc', 'secur', 'local')]):\n                if len(triplets) > 1 or kind != 'profile':\n                    raise ValueError('Can only configure detailed settings '\n                                     'for one profile at a time')\n\n            # FIXME: Check more important invariants and raise\n\n        def _configure_sending_route(self, vcard, route_id):\n            # Sending route\n            route = self.session.config.routes.get(route_id)\n            protocol = self.data.get('route-protocol', ['none'])[0]\n            if protocol == 'none':\n                try:\n                    del self.session.config.routes[route_id]\n                except (KeyError, IndexError):\n                    pass\n                vcard.route = ''\n                return\n            elif protocol == 'local':\n                route.password = route.username = route.host = ''\n                route.name = _(\"Local mail\")\n                route.command = self.data.get('route-command', [None]\n                                              )[0] or self._sendmail_command()\n            elif protocol in ('smtp', 'smtptls', 'smtpssl'):\n                route.command = ''\n                route.name = vcard.email\n                for var in ('route-username', 'route-auth_type',\n                            'route-host', 'route-port'):\n                    rvar = var.split('-', 1)[1]\n                    route[rvar] = self.data.get(var, [''])[0]\n                if not self.data.get('route-username', [''])[0]:\n                    route['auth_type'] = ''\n                if 'route-password' in self.data:\n                    route['password'] = self.data['route-password'][0]\n            else:\n                raise ValueError(_('Unhandled outgoing mail protocol: %s'\n                                   ) % protocol)\n            route.protocol = protocol\n            vcard.route = route_id\n\n        def _get_mail_spool(self):\n            path = os.getenv('MAIL') or None\n            user = os.getenv('USER')\n            if user and not path:\n                if os.path.exists('/var/spool/mail'):\n                    path = os.path.normpath('/var/spool/mail/%s' % user)\n                if os.path.exists('/var/mail'):\n                    path = os.path.normpath('/var/mail/%s' % user)\n            return path\n\n        def _configure_mail_sources(self, vcard):\n            config = self.session.config\n            sources = [r[7:].rsplit('-', 1)[0] for r in self.data.keys()\n                       if r.startswith('source-') and r.endswith('-protocol')]\n            for src_id in sources:\n                prefix = 'source-%s-' % src_id\n                protocol = self.data.get(prefix + 'protocol', ['none'])[0]\n                def configure_source(source):\n                    source.host = ''\n                    source.username = ''\n                    source.enabled = self._yn(prefix + 'enabled')\n                    source.discovery.create_tag = True\n                    source.discovery.process_new = True\n                    if src_id not in vcard.sources():\n                        vcard.add_source(source._key)\n                    return source\n                def make_new_source():\n                    # This little dance makes sure source is actually a\n                    # config section, not just an anonymous dict.\n                    if src_id not in config.sources:\n                        config.sources[src_id] = {}\n                    source = config.sources[src_id]\n                    source.profile = vcard.random_uid\n                    source.discovery.apply_tags = [vcard.tag]\n                    return configure_source(source)\n\n                if protocol == 'none':\n                    pass\n\n                elif protocol == 'local':\n                    source = configure_source(vcard.get_source_by_proto(\n                        'local', create=src_id))\n\n                elif protocol == 'spool':\n                    path = self._get_mail_spool()\n                    if not path:\n                        raise ValueError(_('Mail spool not found'))\n\n                    if path in config.sys.mailbox.values():\n                        raise ValueError(_('Already configured: %s') % path)\n                    else:\n                        mailbox_idx = config.sys.mailbox.append(path)\n\n                    source = configure_source(vcard.get_source_by_proto(\n                        'local', create=src_id))\n                    src_id = source._key\n\n                    # We need to communicate with the source below,\n                    # so we save config to trigger instanciation.\n                    self._background_save(config=True, wait=True)\n\n                    inbox = [t._key for t in config.get_tags(type='inbox')]\n                    local_copy = self._yn(prefix + 'copy-local')\n                    if self._yn(prefix + 'delete-source'):\n                        policy = 'move'\n                    else:\n                        policy = 'read'\n\n                    src_obj = config.mail_sources[src_id]\n                    src_obj.take_over_mailbox(mailbox_idx,\n                                              policy=policy,\n                                              create_local=local_copy,\n                                              apply_tags=inbox,\n                                              save=False)\n\n                elif protocol in ('imap', 'imap_ssl', 'imap_tls',\n                                  'pop3', 'pop3_ssl'):\n                    source = make_new_source()\n\n                    # Discovery policy\n                    disco = source.discovery\n                    if self._yn(prefix + 'index-all-mail'):\n                        if self._yn(prefix + 'leave-on-server'):\n                            disco.policy = 'sync'\n                        else:\n                            disco.policy = 'move'\n                        disco.local_copy = True\n                        disco.paths = ['/']\n                    else:\n                        disco.policy = 'ignore'\n                        disco.local_copy = False\n                        disco.paths = []\n                    disco.guess_tags = True\n\n                    # Connection settings\n                    for rvar in ('protocol', 'auth_type', 'username',\n                                 'host', 'port'):\n                        source[rvar] = self.data.get(prefix + rvar, [''])[0]\n                    if (prefix + 'password') in self.data:\n                        source['password'] = self.data[prefix + 'password'][0]\n                    if (self._yn(prefix + 'force-starttls')\n                            and source.protocol == 'imap'):\n                        source.protocol = 'imap_tls'\n                    username = source.username\n                    if '@' not in username:\n                        username += '@%s' % source.host\n                    source.name = username\n\n                    # We need to communicate with the source below,\n                    # so we save config to trigger instanciation.\n                    self._background_save(config=True, wait=True)\n                    src_obj = config.mail_sources[src_id]\n\n                else:\n                    raise ValueError(_('Unhandled incoming mail protocol: %s'\n                                       ) % protocol)\n\n        def _new_key_created(self, event, vcard_rid, passphrase):\n            config = self.session.config\n            fingerprint = self._key_generator.generated_key\n            if fingerprint:\n                with GLOBAL_VCARD_LOCK:\n                    vcard = vcard_rid and config.vcards.get_vcard(vcard_rid)\n                    if vcard:\n                        vcard.pgp_key = fingerprint\n                        vcard.save()\n                        event.message = _('The PGP key for %s is ready for use.'\n                                          ) % vcard.email\n                    else:\n                        event.message = _('PGP key generation is complete')\n\n                # Record the passphrase!\n                config.secrets[fingerprint] = {\n                    'password': passphrase,\n                    'policy': 'protect'}\n\n                # FIXME: Toggle something that indicates we need a backup ASAP.\n                self._background_save(config=True)\n            else:\n                event.message = _('PGP key generation failed!')\n                event.data['keygen_failed'] = True\n\n            event.flags = event.COMPLETE\n            event.data['keygen_finished'] = int(time.time())\n            config.event_log.log_event(event)\n\n        def _create_new_key(self, vcard, keytype_arg):\n            passphrase = generate_autocrypt_setup_code()\n            random_uid = vcard.random_uid\n\n            if keytype_arg[:3].upper() == 'RSA':\n                keytype = GnuPGBaseKeyGenerator.KEYTYPE_RSA\n                bits = int(keytype_arg[3:])\n            elif keytype_arg.upper() in ('ECC', 'ED25519', 'CURVE25519'):\n                keytype = GnuPGBaseKeyGenerator.KEYTYPE_CURVE25519\n                bits = None\n            else:\n                raise ValueError('Unknown keytype: %s' % keytype_arg)\n\n            key_args = {\n                'keytype': keytype,\n                'bits': bits,\n                'name': vcard.fn,\n                'email': vcard.email,\n                'passphrase': passphrase,\n                'comment': ''}\n            event = Event(source=self,\n                          flags=Event.INCOMPLETE,\n                          data={'keygen_started': int(time.time()),\n                                'profile_id': random_uid},\n                          private_data=key_args)\n            self._key_generator = GnuPGKeyGenerator(\n               # FIXME: Passphrase handling is a problem here\n               GnuPG(self.session.config, event=event),\n               event=event,\n               variables=dict_merge(GnuPGBaseKeyGenerator.VARIABLES, key_args),\n               on_complete=(random_uid,\n                            lambda: self._new_key_created(event, random_uid,\n                                                          passphrase)))\n            self._key_generator.start()\n            self.session.config.event_log.log_event(event)\n\n        def _configure_security(self, vcard):\n            openpgp_key = self.data.get('security-pgp-key', [''])[0]\n            if openpgp_key:\n                if openpgp_key.startswith('!CREATE'):\n                    key_type = openpgp_key[8:] or self.DEFAULT_KEYTYPE\n                    self._create_new_key(vcard, key_type)\n                else:\n                    vcard.pgp_key = openpgp_key\n                    # FIXME: Schedule a background sync job which edits\n                    #        the key to add this Account as a UID, if it\n\n            else:\n                vcard.remove_all('key')\n\n            # Set the following even if we don't have a key, so they don't\n            # get lost if the user edits settings while a key is being\n            # generated - or if they just deselect a key temporarily.\n\n            # Encryption policy rules\n            outg_auto = self._yn('security-best-effort-crypto')\n            outg_ac11 = self._yn('security-autocrypt-crypto')\n            outg_sig  = self._yn('security-always-sign')\n            outg_enc  = self._yn('security-always-encrypt')\n            if outg_ac11:\n                vcard.crypto_policy = 'autocrypt'\n            elif outg_enc and outg_sig:\n                vcard.crypto_policy = 'openpgp-sign-encrypt'\n            elif outg_sig:\n                vcard.crypto_policy = 'openpgp-sign'\n            elif outg_enc:\n                vcard.crypto_policy = 'openpgp-encrypt'\n            elif outg_auto:\n                vcard.crypto_policy = 'best-effort'\n            else:\n                vcard.crypto_policy = 'none'\n\n            # Crypto formatting rules\n            pgp_autocrypt    = outg_ac11 or self._yn('security-use-autocrypt')\n            pgp_publish      = self._yn('security-publish-to-keyserver')\n            pgp_keys         = self._yn('security-attach-keys')\n            pgp_inline       = self._yn('security-prefer-inline')\n            pgp_pgpmime      = self._yn('security-prefer-pgpmime')\n            pgp_obscure_meta = self._yn('security-obscure-metadata')\n            pgp_hdr_enc      = self._yn('security-openpgp-header-encrypt')\n            pgp_hdr_sig      = self._yn('security-openpgp-header-sign')\n            pgp_hdr_none     = self._yn('security-openpgp-header-none')\n            pgp_hdr_both     = pgp_hdr_enc and pgp_hdr_sig\n            if pgp_hdr_both:\n                pgp_hdr_enc = pgp_hdr_sig = False\n            if pgp_pgpmime and pgp_inline:\n                pgp_pgpmime = pgp_inline = False\n            vcard.crypto_format = ''.join([\n                'openpgp_header:SE' if (pgp_hdr_both)     else '',\n                'openpgp_header:S'  if (pgp_hdr_sig)      else '',\n                'openpgp_header:E'  if (pgp_hdr_enc)      else '',\n                'openpgp_header:N'  if (pgp_hdr_none)     else '',\n                '+autocrypt'        if (pgp_autocrypt)    else '',\n                '+send_keys'        if (pgp_keys)         else '',\n                '+prefer_inline'    if (pgp_inline)       else '',\n                '+pgpmime'          if (pgp_pgpmime)      else '',\n                '+obscure_meta'     if (pgp_obscure_meta) else '',\n                '+publish'          if (pgp_publish)      else ''\n            ])\n\n    return ProfileVCardCommand\n\n\nclass Profile(ProfileVCard(VCard)):\n    \"\"\"View profile\"\"\"\n\n\nclass AddProfile(ProfileVCard(AddVCard)):\n    \"\"\"Add profiles (Accounts)\"\"\"\n    HTTP_POST_VARS = dict_merge(AddVCard.HTTP_POST_VARS, {\n        'route_id': 'Route ID for sending mail',\n\n        'signature': '.signature',\n        'route-*': 'Route settings',\n        'source-*': 'Source settings',\n        'security-*': 'Security settings'\n    })\n\n    def _form_defaults(self):\n        new_src_id = randomish_uid();\n        return dict_merge(AddVCard._form_defaults(self), {\n            'new_src_id': new_src_id,\n            'signature': self._default_signature(),\n            'route-protocol': 'none',\n            'route-auth_type': 'password',\n            'source-NEW-protocol': 'none',\n            'source-NEW-auth_type': 'password',\n            'source-NEW-leave-on-server': True,\n            'source-NEW-index-all-mail': True,\n            'source-NEW-force-starttls': False,\n            'source-NEW-copy-local': True,\n            'source-NEW-delete-source': False,\n            'security-best-effort-crypto': True,\n            'security-autocrypt-crypto': False,\n            'security-always-sign': False,\n            'security-always-encrypt': False,\n            'security-use-autocrypt': True,\n            'security-attach-keys': False,\n            'security-prefer-inline': False,\n            'security-prefer-pgpmime': False,\n            'security-obscure-metadata': False,\n            'security-openpgp-header-encrypt': False,\n            'security-openpgp-header-sign': True,\n            'security-openpgp-header-none': False,\n            'security-publish-to-keyserver': False\n        });\n\n    def _before_vcard_create(self, kind, triplets, vcard=None):\n        route_id = self.data.get('route_id',\n                                 [vcard and vcard.route or None])[0]\n        if route_id:\n            if route_id not in self.session.config.routes:\n                raise ValueError('Not a valid route ID: %s' % route_id)\n        elif self.data.get('route-protocol', ['none'])[0] != 'none':\n            route_id = self.session.config.routes.append({})\n\n        return {\n            'save_config': True,\n            'route_id': route_id\n        }\n\n    def _update_vcard_from_post(self, vcard, state=None):\n        if not state:\n            # When editing, this doesn't run first, so we invoke it now.\n            state = self._before_vcard_create(vcard.kind, [], vcard=vcard)\n\n        vcard.signature = self.data.get('signature', [''])[0]\n        vcard.email = self.data.get('email', [None])[0] or vcard.email\n        vcard.fn = self.data.get('name', [None])[0] or vcard.fn\n\n        if not vcard.tag:\n            with self.session.config._lock:\n                tags = self.session.config.tags\n                vcard.tag = tags.append({\n                    'name': vcard.email,\n                    'slug': '%8.8x' % time.time(),\n                    'type': 'profile',\n                    'icon': 'icon-user',\n                    'flag_msg_only': True,\n                    'label': False,\n                    'display': 'invisible'\n                })\n                from mailpile.plugins.tags import Slugify\n                tags[vcard.tag].slug = Slugify(\n                    'account-%s' % vcard.email, tags=self.session.config.tags)\n\n        route_id = state.get('route_id', None)\n        if route_id is not None:\n            self._configure_sending_route(vcard, route_id)\n\n        self._configure_mail_sources(vcard)\n        self._configure_security(vcard)\n\n    def _after_vcard_create(self, kind, vcard, state):\n        self._update_vcard_from_post(vcard, state=state)\n\n\nclass EditProfile(AddProfile):\n    \"\"\"Edit a profile\"\"\"\n    SYNOPSIS = (None, None, 'profiles/edit', None)\n    HTTP_QUERY_VARS = dict_merge(AddProfile.HTTP_QUERY_VARS, {\n        'rid': 'update by x-mailpile-rid'})\n\n    def _vcard_to_post_vars(self, vcard):\n        cp = vcard.crypto_policy or ''\n        cf = vcard.crypto_format or ''\n        vc_sig = vcard.signature\n        default_sig = self._default_signature()\n        pvars = {\n            'rid': vcard.random_uid,\n            'name': vcard.fn,\n            'email': vcard.email,\n            'signature': default_sig if (vc_sig is None) else vc_sig,\n            'password': '',\n            'route-protocol': 'none',\n            'route-auth_type': 'password',\n            'source-NEW-protocol': 'none',\n            'source-NEW-auth_type': 'password',\n            'security-pgp-key': vcard.pgp_key or '',\n            'security-best-effort-crypto': ('best-effort' in cp),\n            'security-autocrypt-crypto': ('autocrypt' in cp),\n            'security-use-autocrypt': ('autocrypt' in cf or 'autocrypt' in cp),\n            'security-always-sign': ('sign' in cp),\n            'security-always-encrypt': ('encrypt' in cp),\n            'security-attach-keys': ('send_keys' in cf),\n            'security-prefer-inline': ('prefer_inline' in cf),\n            'security-prefer-pgpmime': ('pgpmime' in cf),\n            'security-obscure-metadata': ('obscure_meta' in cf),\n            'security-openpgp-header-encrypt': ('openpgp_header:E' in cf or\n                                                'openpgp_header:SE' in cf),\n            'security-openpgp-header-sign': ('openpgp_header:S' in cf or\n                                             'openpgp_header:ES' in cf),\n            'security-openpgp-header-none': ('openpgp_header:N' in cf),\n            'security-publish-to-keyserver': ('publish' in cf)\n        }\n        route = self.session.config.routes.get(vcard.route or 'ha ha ha')\n        if route:\n            pvars.update({\n                'route-protocol': route.protocol,\n                'route-host': route.host,\n                'route-port': route.port,\n                'route-username': route.username,\n                'route-password': route.password,\n                'route-auth_type': route.auth_type,\n                'route-command': route.command\n            })\n        pvars['sources'] = vcard.sources()\n        for sid in pvars['sources']:\n            prefix = 'source-%s-' % sid\n            source = self.session.config.sources.get(sid)\n            disco = source.discovery\n            info = {}\n            for rvar in ('protocol', 'auth_type', 'host', 'port',\n                         'username', 'password'):\n                info[prefix + rvar] = source[rvar]\n            dp = disco.policy\n            if not info[prefix + 'auth_type']:\n                info[prefix + 'auth_type'] = 'password'\n            info[prefix + 'leave-on-server'] = (dp not in 'move')\n            info[prefix + 'index-all-mail'] = (dp in ('move', 'sync', 'read')\n                                               and disco.local_copy)\n            info[prefix + 'enabled'] = source.enabled\n            if source.protocol == 'imap_tls':\n                info[prefix + 'protocol'] = 'imap'\n                info[prefix + 'force-starttls'] = True\n            else:\n                info[prefix + 'force-starttls'] = False\n\n            pvars.update(info)\n        return pvars\n\n    def command(self):\n        idx = self._idx()  # Make sure VCards are all loaded\n        session, config = self.session, self.session.config\n\n        # OK, fetch the VCard.\n        safe_assert('rid' in self.data and len(self.data['rid']) == 1)\n        vcard = config.vcards.get_vcard(self.data['rid'][0])\n        safe_assert(vcard)\n\n        if self.data.get('_method') == 'POST':\n            self._update_vcard_from_post(vcard)\n            self._background_save(config=True)\n            vcard.save()\n            return self._success(_('Account Updated!'),\n                                 self._vcard_to_post_vars(vcard))\n        else:\n            return self._success(_('Edit Account'), dict_merge(\n                 self._form_defaults(), self._vcard_to_post_vars(vcard)))\n\n\nclass RemoveProfile(ProfileVCard(RemoveVCard)):\n    \"\"\"Remove a profile\"\"\"\n    HTTP_CALLABLE = ('GET', 'POST')\n    HTTP_QUERY_VARS = dict_merge(RemoveVCard.HTTP_QUERY_VARS, {\n        'rid': 'x-mailpile-rid of profile to remove'})\n    HTTP_POST_VARS = {\n        'rid': 'x-mailpile-rid of profile to remove',\n        'trash-email': 'If yes, move linked e-mail to Trash',\n        'delete-keys': 'If yes, delete linked PGP keys',\n        'delete-tags': 'If yes, remove linked Tags'}\n\n    def _trash_email(self, vcard):\n        trashes = self.session.config.get_tags(type='trash', default=[])\n        if vcard.tag and trashes:\n            idx = self.session.config.index\n            idx.add_tag(self.session, trashes[0]._key,\n                        msg_idxs=idx.TAGS.get(vcard.tag, set()),\n                        allow_message_id_clearing=True,\n                        conversation=False)\n\n    def _delete_keys(self, vcard):\n        if vcard.pgp_key:\n            found = 0\n            for vc in self.session.config.vcards.find_vcards([], kinds=['profile']):\n                if vc.pgp_key == vcard.pgp_key:\n                    found += 1\n            if found == 1:\n                self._gnupg().delete_key(vcard.pgp_key)\n\n    def _cleanup_tags(self, vcard, delete_tags=False):\n        if vcard.tag:\n            if delete_tags:\n                from mailpile.plugins.tags import DeleteTag\n                DeleteTag(self.session, arg=[vcard.tag]).run()\n            else:\n                self.session.config.tags[vcard.tag].type = 'attribute'\n                self.session.config.tags[vcard.tag].display = 'invisible'\n                self.session.config.tags[vcard.tag].label = True\n\n    def _unique_usernames(self, vcard):\n        config, usernames = self.session.config, set()\n\n        if (vcard.route not in ('', None)) and config.routes[vcard.route].username:\n            usernames.add(config.routes[vcard.route].username)\n        for source_id in vcard.sources():\n            if config.sources[source_id].username:\n                usernames.add(config.sources[source_id].username)\n\n        for msid, source in config.sources.iteritems():\n            if (source.username in usernames) and (source.profile != vcard.random_uid):\n                usernames.remove(source.username)\n        for mrid, route in config.routes.iteritems():\n            if (route.username in usernames) and (mrid != vcard.route):\n                usernames.remove(source.username)\n\n        return usernames\n\n    def _delete_credentials(self, vcard):\n        # Check every stored credential; if it is in use by any route or source that\n        # doesn't belong to this VCard, leave intact. Otherwise, delete.\n        usernames = self._unique_usernames(vcard)\n        oauth_tks = self.session.config.oauth.tokens\n        for username in (set(oauth_tks.keys()) & usernames):\n            del oauth_tks[username]\n        secrets = self.session.config.secrets\n        for username in (set(secrets.keys()) & usernames):\n            del secrets[username]\n\n    def _delete_routes(self, vcard):\n        if vcard.route not in (None, ''):\n            found = 0\n            for vc in self.session.config.vcards.find_vcards([], kinds=['profile']):\n                if vc.route == vcard.route:\n                    found += 1\n            if found == 1:\n                self.session.config.routes[vcard.route] = {}\n\n    def _delete_sources(self, vcard, delete_tags=False):\n        config, sources = self.session.config, self.session.config.sources\n        for source_id in vcard.sources():\n            src = sources[source_id]\n            tids = set()\n\n            # Tell the worker to shut down, if any\n            if source_id in config.mail_sources:\n                config.mail_sources[source_id].quit()\n                config.mail_sources[source_id].event.flags = Event.COMPLETE\n\n            # Keep the reference to our local mailboxes in sys.mailbox\n            for mbx_id, mbx_info in src.mailbox.iteritems():\n                if mbx_info.primary_tag:\n                    tids.add(mbx_info.primary_tag)\n                if mbx_info.local and (mbx_info.path[:1] == '@'):\n                    config.sys.mailbox[mbx_info.path[1:]] = mbx_info.local\n\n            # Reconfigure all the tags\n            from mailpile.plugins.tags import DeleteTag\n            if src.discovery.parent_tag:\n                tids.add(src.discovery.parent_tag)\n            for tid in tids:\n                if ((tid in config.tags)\n                        and config.tags[tid].type in ('mailbox', 'profile')):\n                    config.tags[tid].type = 'tag'\n                    if delete_tags:\n                        DeleteTag(self.session, arg=[tid]).run()\n\n            # Nuke it!\n            sources[source_id] = {\n                'name': 'Deleted source',\n                'enabled': False}\n\n    def _trash_email_is_safe(self, vcard):\n        if vcard:\n            for src_id in vcard.sources():\n                if self.session.config.sources[src_id].protocol == 'local':\n                    return False\n            return True\n        return False\n\n    def command(self, *args, **kwargs):\n        session, config = self.session, self.session.config\n\n        if 'rid' in self.data:\n            vcard = config.vcards.get_vcard(self.data['rid'][0])\n        else:\n            vcard = None\n\n        if vcard and self.data.get('_method', 'not-http').upper() != 'GET':\n            if self.data.get('trash-email', [''])[0].lower() == 'yes':\n                self._trash_email(vcard)\n\n            if self.data.get('delete-keys', [''])[0].lower() == 'yes':\n                self._delete_keys(vcard)\n\n            delete_tags = (self.data.get('delete-tags', [''])[0].lower() == 'yes')\n            self._delete_credentials(vcard)\n            self._delete_routes(vcard)\n            self._delete_sources(vcard, delete_tags=delete_tags)\n            self._cleanup_tags(vcard, delete_tags=delete_tags)\n            self._background_save(config=True, index=True)\n\n            return RemoveVCard.command(self, *args, **kwargs)\n\n        return self._success(_(\"Remove account\"), result=dict_merge(\n            self._form_defaults(), {\n                'rid': vcard.random_uid if vcard else None,\n                'trash_email_is_safe': self._trash_email_is_safe(vcard),\n                'profile': (self._vcard_list([vcard])['profiles'][0]\n                            if vcard else None)}))\n\n\nclass ListProfiles(ProfileVCard(ListVCards)):\n    \"\"\"Find profiles\"\"\"\n    SYNOPSIS = (None, 'profiles', 'profiles', '[--lines] [<terms>]')\n\n\nclass ProfileSet(ProfileVCard(VCardSet)):\n    \"\"\"Set contact lines, ensuring contact exists\"\"\"\n\n\nclass ChooseFromAddress(Command):\n    \"\"\"Display a single vcard\"\"\"\n    SYNOPSIS = (None, 'profiles/choose_from', 'profiles/choose_from',\n                '<MIDs or addresses>')\n    ORDER = ('Internals', 6)\n    HTTP_CALLABLE = ('GET',)\n    HTTP_QUERY_VARS = {\n        'mid': 'Message ID',\n        'email': 'E-mail address',\n        'no_from': 'Ignore From: lines'\n    }\n\n    def command(self):\n        idx, vcards = self._idx(), self.session.config.vcards\n\n        emails = [e for e in self.args if '@' in e]\n        emails.extend(self.data.get('email', []))\n\n        messages = self._choose_messages(\n            [m for m in self.args if '@' not in m] +\n            ['=%s' % mid for mid in self.data.get('mid', [])]\n        )\n        for msg_idx_pos in messages:\n            try:\n                msg_info = idx.get_msg_at_idx_pos(msg_idx_pos)\n                msg_emails = (idx.expand_to_list(msg_info, field=idx.MSG_TO) +\n                              idx.expand_to_list(msg_info, field=idx.MSG_CC))\n                emails.extend(msg_emails)\n                if 'no_from' not in self.data:\n                    emails.append(msg_info[idx.MSG_FROM])\n            except ValueError:\n                pass\n\n        addrs = [ai for ee in emails\n                 for ai in AddressHeaderParser(unicode_data=ee)]\n        return self._success(_('Choosing from address'), result={\n            'emails': addrs,\n            'from': vcards.choose_from_address(self.session.config, addrs)\n        })\n\n\nclass ContentTxf(EmailTransform):\n    def TransformOutgoing(self, sender, rcpts, msg, **kwargs):\n        txf_matched, txf_continue = False, True\n\n        profile = self._get_sender_profile(sender, kwargs)\n        sig = profile.get('signature')\n        if sig:\n            part = self._get_first_part(msg, 'text/plain')\n            if part is not None:\n                msg_text = (part.get_payload(decode=True) or '\\n\\n'\n                            ).replace('\\r', '').decode('utf-8')\n                if '\\n-- \\n' not in msg_text:\n                    msg_text = msg_text.strip() + '\\n\\n-- \\n' + sig\n                    try:\n                        msg_text.encode('us-ascii')\n                        need_utf8 = False\n                    except (UnicodeEncodeError, UnicodeDecodeError):\n                        msg_text = msg_text.encode('utf-8')\n                        need_utf8 = True\n\n                    part.set_payload(msg_text)\n                    if need_utf8:\n                        part.set_charset('utf-8')\n                        while 'content-transfer-encoding' in part:\n                            del part['content-transfer-encoding']\n                        encoders.encode_base64(part)\n\n                    txf_matched = True\n\n        return sender, rcpts, msg, txf_matched, txf_continue\n\n\n_plugins.register_commands(VCard, AddVCard, RemoveVCard, ListVCards,\n                           VCardAddLines, VCardSet, VCardRemoveLines)\n_plugins.register_commands(Contact, AddContact, RemoveContact, ListContacts,\n                           ContactSet, AddressSearch)\n_plugins.register_commands(Profile, AddProfile, EditProfile,\n                           RemoveProfile, ListProfiles, ProfileSet,\n                           ChooseFromAddress)\n_plugins.register_commands(ContactImport, ContactImporters)\n\n_plugins.register_outgoing_email_content_transform('100_sender_vc', ContentTxf)\n"
  },
  {
    "path": "mailpile/plugins/core.py",
    "content": "# These are the Mailpile core commands, the public \"API\" we expose for\n# searching, tagging and editing e-mail.\n#\n# FIXME: This should probably be broken into smaller modules\n#\nimport datetime\nimport json\nimport os\nimport random\nimport re\nimport socket\nimport subprocess\nimport sys\nimport traceback\nimport thread\nimport threading\nimport time\nimport webbrowser\n\nimport mailpile.util\nimport mailpile.postinglist\nimport mailpile.security as security\nimport mailpile.platforms\nfrom mailpile.commands import *\nfrom mailpile.config.validators import WebRootCheck\nfrom mailpile.crypto.gpgi import GnuPG\nfrom mailpile.eventlog import Event\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.mailboxes import IsMailbox\nfrom mailpile.mailutils.emails import ClearParseCache, Email\nfrom mailpile.postinglist import GlobalPostingList\nfrom mailpile.plugins import PluginManager\nfrom mailpile.safe_popen import MakePopenUnsafe, MakePopenSafe\nfrom mailpile.search import MailIndex\nfrom mailpile.util import *\nfrom mailpile.vcard import AddressInfo\nfrom mailpile.vfs import vfs, FilePath\n\n_plugins = PluginManager(builtin=__file__)\n\n\nclass Load(Command):\n    \"\"\"Load or reload the metadata index\"\"\"\n    SYNOPSIS = (None, 'load', None, None)\n    ORDER = ('Internals', 1)\n    CONFIG_REQUIRED = False\n    IS_INTERACTIVE = True\n\n    def command(self, reset=True, wait=True, wait_all=False, quiet=False):\n        try:\n            if self._idx(reset=reset,\n                         wait=wait,\n                         wait_all=wait_all,\n                         quiet=quiet):\n                return self._success(_('Loaded metadata index'))\n            else:\n                return self._error(_('Failed to load metadata index'))\n        except IOError:\n            return self._error(_('Failed to decrypt configuration, '\n                                 'please log in!'))\n\n\nclass Rescan(Command):\n    \"\"\"Add new messages to index\"\"\"\n    SYNOPSIS = (None, 'rescan', 'rescan',\n                '[full|vcards|vcards:<src>|sources|mailboxes|both|mailbox:<id>|<msgs>]')\n    ORDER = ('Internals', 2)\n    LOG_PROGRESS = True\n\n    HTTP_CALLABLE = ('POST',)\n    HTTP_POST_VARS = {\n        'which': '[full|vcards|vcards:<src>|both|mailboxes|sources|<msgs>]'\n    }\n\n    def _progress(self, progress):\n         self.session.ui.mark(progress)\n         self.event.data[\"progress\"].append((int(time.time()), progress))\n\n    def command(self, slowly=False, cron=False):\n        session, config, idx = self.session, self.session.config, self._idx()\n        self.event.data[\"progress\"] = []\n        args = list(self.args)\n        if 'which' in self.data:\n            args.extend(self.data['which'])\n\n        # Abort if we are out of disk space\n        full_path = config.need_more_disk_space()\n        if full_path:\n            return self._error(_('Insufficient free space in %s') % full_path)\n\n        # Pretend we're idle, to make rescan go fast fast.\n        if not slowly:\n            mailpile.util.LAST_USER_ACTIVITY = 0\n\n        # Cron always runs the rescan command, no matter what else\n        if cron:\n            self._run_rescan_command(session)\n\n        a0lower = (args and args[0] or '').lower()\n        if a0lower.startswith('vcards'):\n            return self._success(_('Rescanned vCards'),\n                                 result=self._rescan_vcards(session, args[0]))\n        elif (a0lower in ('both', 'mailboxes', 'sources', 'editable')\n                or a0lower.startswith('mailbox:')):\n            return self._success(_('Rescanned mailboxes'),\n                                 result=self._rescan_mailboxes(session,\n                                                               which=a0lower))\n        elif a0lower == 'full':\n            config.flush_mbox_cache(session, wait=True)\n            args.pop(0)\n\n        # Clear the cache first, in case the user is flailing about\n        ClearParseCache(full=True)\n\n        msg_idxs = self._choose_messages(args)\n        if msg_idxs:\n            for msg_idx_pos in msg_idxs:\n                e = Email(idx, msg_idx_pos)\n                try:\n                    self._progress('Re-indexing %s' % e.msg_mid())\n                    idx.index_email(self.session, e)\n                except KeyboardInterrupt:\n                    self._progress('Interrupted')\n                    raise\n                except:\n                    self._ignore_exception()\n                    session.ui.warning(_('Failed to reindex: %s'\n                                         ) % e.msg_mid())\n\n            self.event.data[\"messages\"] = len(msg_idxs)\n            self.session.config.event_log.log_event(self.event)\n            self._background_save(index=True)\n\n            return self._success(_('Indexed %d messages') % len(msg_idxs),\n                                 result={'messages': len(msg_idxs)})\n\n        else:\n            deadline = (int(time.time() + 0.75 * config.prefs.rescan_interval)\n                        if cron else None)\n            if 'rescan' in config._running:\n                return self._success(_('Rescan already in progress'))\n            config._running['rescan'] = True\n            try:\n                results = {}\n                results.update(self._rescan_vcards(session, 'vcards'))\n                results.update(self._rescan_mailboxes(session,\n                    deadline=deadline,\n                    which=('mailboxes' if cron else 'both'),\n                    force=(not cron and not slowly)))\n\n                self.event.data.update(results)\n                self.session.config.event_log.log_event(self.event)\n                if 'aborted' in results:\n                    self._progress('Aborted')\n                    raise KeyboardInterrupt()\n                return self._success(_('Rescanned vCards and mailboxes'),\n                                     result=results)\n            except KeyboardInterrupt:\n                self._progress('Interrupted')\n                return self._error(_('User aborted'), info=results)\n            finally:\n                self._progress(\"Rescan complete\")\n                del config._running['rescan']\n\n    def _rescan_vcards(self, session, which):\n        from mailpile.plugins import PluginManager\n        config = session.config\n        imported = 0\n        importer_cfgs = config.prefs.vcard.importers\n        which_spec = which.split(':')\n        importers = []\n        try:\n            self._progress(_('Rescanning: %s') % 'vcards')\n            for importer in PluginManager.VCARD_IMPORTERS.values():\n                if (len(which_spec) > 1 and\n                        which_spec[1] != importer.SHORT_NAME):\n                    continue\n                importers.append(importer.SHORT_NAME)\n                for cfg in importer_cfgs.get(importer.SHORT_NAME, []):\n                    if cfg:\n                        imp = importer(session, cfg)\n                        self._progress(_('Importing VCards from: %s') % imp)\n                        imported += imp.import_vcards(session, config.vcards)\n                    if mailpile.util.QUITTING:\n                        return {\n                            'vcards': imported,\n                            'vcard_sources': importers,\n                            'aborted': True}\n        except KeyboardInterrupt:\n            return {\n                'vcards': imported,\n                'vcard_sources': importers,\n                'aborted': True}\n        return {\n            'vcards': imported,\n            'vcard_sources': importers}\n\n    def _run_rescan_command(self, session, timeout=120):\n        pre_command = session.config.prefs.rescan_command\n        if pre_command and not mailpile.util.QUITTING:\n            self._progress(_('Running: %s') % pre_command)\n            if not ('|' in pre_command or\n                    '&' in pre_command or\n                    ';' in pre_command):\n                pre_command = pre_command.split()\n            cmd = subprocess.Popen(pre_command,\n                                   stdout=subprocess.PIPE,\n                                   stderr=subprocess.PIPE,\n                                   shell=not isinstance(pre_command, list))\n            countdown = [timeout]\n            def eat(fmt, fd):\n                for line in fd:\n                    session.ui.notify(fmt % line.strip())\n                    countdown[0] = timeout\n            for t in [\n                threading.Thread(target=eat, args=['E: %s', cmd.stderr]),\n                threading.Thread(target=eat, args=['O: %s', cmd.stdout])\n            ]:\n                t.daemon = True\n                t.start()\n            try:\n                while countdown[0] > 0:\n                    countdown[0] -= 1\n                    if cmd.poll() is not None:\n                        rv = cmd.wait()\n                        if rv != 0:\n                            session.ui.notify(_('Rescan command returned %d')\n                                              % rv)\n                        return\n                    elif mailpile.util.QUITTING:\n                        return\n                    time.sleep(1)\n            finally:\n                if cmd.poll() is None:\n                    session.ui.notify(_('Aborting rescan command'))\n                    cmd.terminate()\n                    time.sleep(0.2)\n                    if cmd.poll() is None:\n                        cmd.kill()\n# NOTE: For some reason we were using the un-safe Popen before, not sure\n#       if that matters. Leaving this commented out for now for reference.\n#\n#            try:\n#                MakePopenUnsafe()\n#                subprocess.check_call(pre_command, shell=True)\n#            finally:\n#                MakePopenSafe()\n\n    def _rescan_mailboxes(self, session, which='mailboxes', force=True, deadline=None):\n        import mailpile.mail_source\n        config = session.config\n        idx = self._idx()\n        msg_count = 0\n        mbox_count = 0\n        rv = True\n        try:\n            self._progress(_('Rescanning: %s') % which)\n\n            self._run_rescan_command(session)\n            if which.startswith('mailbox:'):\n                only = which.split(':')[1]\n                which = 'mailboxes'\n            else:\n                only = None\n\n            msg_count = 1\n            if which in ('both', 'mailboxes', 'editable'):\n                if only or which == 'editable':\n                    mailboxes = config.get_mailboxes()\n                else:\n                    # This combination of arguments will ignore mailboxes linked to\n                    # active mail sources, but include the local caches of sources\n                    # that have been disabled.\n                    mailboxes = config.get_mailboxes(with_mail_source=False,\n                                                     mail_source_locals=True)\n\n                for fid, fpath, sc in mailboxes:\n                    if mailpile.util.QUITTING:\n                        break\n                    if fpath == '/dev/null':\n                        continue\n                    if only and (only != fpath) and (only != fid):\n                        continue\n                    try:\n                        self._progress(_('Rescanning: %s %s') % (fid, fpath))\n                        rescan_args = {\n                            'event': self.event,\n                            'deadline': deadline,\n                            'force': force}\n                        if which == 'editable':\n                            count = idx.scan_mailbox(session, fid, fpath,\n                                                     config.open_mailbox,\n                                                     process_new=False,\n                                                     editable=True,\n                                                     **rescan_args)\n                        else:\n                            count = idx.scan_mailbox(session, fid, fpath,\n                                                     config.open_mailbox,\n                                                     **rescan_args)\n                    except ValueError:\n                        self._ignore_exception()\n                        count = -1\n                    if count < 0:\n                        session.ui.warning(_('Failed to rescan: %s') %\n                                           FilePath(fpath).display())\n                    elif count > 0:\n                        msg_count += count\n                        mbox_count += 1\n                    session.ui.mark('\\n')\n\n            if which in ('both', 'sources'):\n                ocount = msg_count - 1\n                while ocount != msg_count:\n                    ocount = msg_count\n                    src_ids = config.sources.keys()\n                    src_ids.sort(key=lambda k: random.randint(0, 100))\n                    for src_id in src_ids:\n                        try:\n                            src = config.get_mail_source(src_id, start=True)\n                            if mailpile.util.QUITTING:\n                                ocount = msg_count\n                                break\n                            self._progress(_('Rescanning: %s') % (src, ))\n                            (messages, mailboxes) = src.rescan_now(session)\n                        except ValueError:\n                            messages = mailboxes = 0\n                        if messages > 0:\n                            msg_count += messages\n                        mbox_count += mailboxes\n                        session.ui.mark('\\n')\n                    if not session.ui.interactive:\n                        break\n        except (KeyboardInterrupt, subprocess.CalledProcessError) as e:\n            return {\n                'aborted': True,\n                'messages': msg_count,\n                'mailboxes': mbox_count}\n        finally:\n            if msg_count:\n                session.ui.mark('\\n')\n                if msg_count < 500:\n                    self._background_save(index=True)\n                else:\n                    self._background_save(index_full=True)\n            else:\n                self._progress(_('Nothing changed'))\n        return {\n            'messages': msg_count,\n            'mailboxes': mbox_count}\n\n\nclass Optimize(Command):\n    \"\"\"Optimize the keyword search index\"\"\"\n    SYNOPSIS = (None, 'optimize', None, '[harder]')\n    ORDER = ('Internals', 3)\n\n    def command(self, slowly=False):\n        try:\n            if not slowly:\n                mailpile.util.LAST_USER_ACTIVITY = 0\n            self._idx().save(self.session)\n            GlobalPostingList.Optimize(self.session, self._idx(),\n                                       force=('harder' in self.args))\n            return self._success(_('Optimized search engine'))\n        except KeyboardInterrupt:\n            return self._error(_('Aborted'))\n\n\nclass DeleteMessages(Command):\n    \"\"\"Delete one or more messages.\"\"\"\n    SYNOPSIS = (None, 'delete', 'message/delete', '[--keep] <messages>')\n    ORDER = ('Searching', 99)\n    IS_USER_ACTIVITY = True\n\n    def command(self, slowly=False):\n        idx = self._idx()\n\n        args = list(self.args)\n        keep = 0\n        while '--keep' in args:\n            args.remove('--keep')\n            keep += 1\n\n        # We group messages by mailbox and delete in batches. This should\n        # avoid loading all the mailboxes into RAM at once, which is a big\n        # deal on larger setups.\n        targets = [Email(idx, mi) for mi in self._choose_messages(args)]\n        msg_ptr_pairs = [\n            (e, e.index.unique_mbox_ids(e.get_msg_info())) for e in targets]\n\n        if 'deletion' in self.session.config.sys.debug:\n            self.session.ui.debug('Targets: %s' % msg_ptr_pairs)\n\n        # Message are sorted so the ones present in the most mailboxes\n        # are listed first.\n        msg_ptr_pairs.sort(key=lambda mpp: (-len(mpp[1]), mpp[0]))\n\n        deleted, failed = [], []\n        while msg_ptr_pairs:\n            # Pick the largest set of mailboxes we have yet to delete from\n            mid_set = msg_ptr_pairs[0][1]\n\n            # Pick all the messages contained in this set of mailboxes\n            messages = [e for e, mids in msg_ptr_pairs\n                        if ((mid_set | mids) == mid_set)]\n\n            # Go delete them!\n            mailboxes = []\n            for e in messages:\n                msg_idx = e.msg_idx_pos\n                del_ok, mboxes = e.delete_message(self.session,\n                                                  flush=False, keep=keep)\n                mailboxes.extend(mboxes)\n                if del_ok:\n                    deleted.append(msg_idx)\n                else:\n                    failed.append(msg_idx)\n\n            # This will actually delete from mboxes, etc.\n            for m in set(mailboxes):\n                with m:\n                    m.flush()\n\n            # OK, these are done, reduce our target list\n            msg_ptr_pairs = [(e, mids) for e, mids in msg_ptr_pairs\n                             if ((mid_set | mids) != mid_set)]\n\n        # FIXME: Trigger a background rescan of affected mailboxes, as\n        #        the flush() above may have broken our pointers.\n\n        result = {'deleted': deleted}\n        if failed:\n            result['failed'] = failed\n            return self._error(_('Could not delete all messages'),\n                               result=result)\n        return self._success(_('Deleted %d messages') % len(deleted),\n                             result=result)\n\n\nclass BrowseOrLaunch(Command):\n    \"\"\"Launch browser and exit, if already running\"\"\"\n    SYNOPSIS = (None, 'browse_or_launch', None, None)\n    ORDER = ('Internals', 5)\n    CONFIG_REQUIRED = False\n    RAISES = (KeyboardInterrupt,)\n\n    @classmethod\n    def Browse(cls, sspec):\n        http_url = ('http://%s:%s%s/' % sspec\n                    ).replace('//0.0.0.0:', '//localhost:')\n        try:\n            MakePopenUnsafe()\n            webbrowser.open(http_url)\n            return http_url\n        except:\n            pass\n        finally:\n            MakePopenSafe()\n        return False\n\n    def command(self):\n        config = self.session.config\n\n        if config.http_worker:\n            sspec = config.http_worker.sspec\n        else:\n            sspec = (config.sys.http_host, config.sys.http_port,\n                     config.sys.http_path or '')\n\n        try:\n            socket.create_connection(sspec[:2])\n            self.Browse(sspec)\n            os._exit(127)\n        except IOError:\n            pass\n\n        return self._success(_('Launching Mailpile'), result=True)\n\n\nclass RunWWW(Command):\n    \"\"\"Just run the web server\"\"\"\n    SYNOPSIS = (None, 'www', None, '[<host:port/path>]')\n    ORDER = ('Internals', 5)\n    CONFIG_REQUIRED = False\n\n    def command(self):\n        config = self.session.config\n        ospec = (config.sys.http_host, config.sys.http_port,\n                 config.sys.http_path)\n\n        if self.args:\n            host, portpath = self.args[0].split('://')[-1].split(':', 1)\n            port, path = (portpath+'/').split('/', 1)\n            port = int(port)\n            sspec = (host, port, WebRootCheck(path))\n        else:\n            sspec = ospec\n\n        if self.session.config.http_worker:\n            self.session.config.http_worker.quit(join=True)\n            self.session.config.http_worker = None\n\n        self.session.config.prepare_workers(self.session,\n                                            httpd_spec=tuple(sspec),\n                                            daemons=True)\n        if config.http_worker:\n            sspec = config.http_worker.httpd.sspec\n            http_url = 'http://%s:%s%s/' % sspec\n            if sspec != ospec:\n                (config.sys.http_host, config.sys.http_port,\n                 config.sys.http_path) = sspec\n                self._background_save(config=True)\n                return self._success(_('Moved the web server to %s'\n                                       ) % http_url)\n            else:\n                return self._success(_('Started the web server on %s'\n                                       ) % http_url)\n        else:\n            return self._error(_('Failed to start the web server'))\n\n\nclass Cleanup(Command):\n    \"\"\"Perform cleanup actions (runs before shutdown)\"\"\"\n    SYNOPSIS = (None, 'cleanup', None, \"\")\n    ORDER = ('Internals', 5)\n    CONFIG_REQUIRED = False\n    SPLIT_ARG = False\n    TASKS = []\n\n    @classmethod\n    def AddTask(cls, task, last=False, first=False):\n        safe_assert(not (first and last))\n        if (first or last) and not cls.TASKS:\n            cls.TASKS = [lambda: True]\n        if first:\n            cls.TASKS.insert(0, task)\n        elif last:\n            cls.TASKS.append(task)\n        else:\n            cls.TASKS.insert(len(cls.TASKS) - 1, task)\n\n    def command(self):\n        while self.TASKS:\n            try:\n                self.TASKS.pop(0)()\n            except:\n                traceback.print_exc()\n                pass\n        return self._success(_('Performed shutdown tasks'))\n\n\nclass WritePID(Command):\n    \"\"\"Write the PID to a file\"\"\"\n    SYNOPSIS = (None, 'pidfile', None, \"</path/to/pidfile>\")\n    ORDER = ('Internals', 5)\n    CONFIG_REQUIRED = False\n    SPLIT_ARG = False\n\n    def command(self):\n        filename = self.args[0]\n        with vfs.open(filename, 'w') as fd:\n            fd.write('%d' % os.getpid())\n            Cleanup.AddTask(lambda: os.unlink(filename), last=True)\n        return self._success(_('Wrote PID to %s') % self.args)\n\n\nclass RenderPage(Command):\n    \"\"\"Does nothing, for use by semi-static jinja2 pages\"\"\"\n    SYNOPSIS = (None, None, 'page', None)\n    ORDER = ('Internals', 6)\n    CONFIG_REQUIRED = False\n    SPLIT_ARG = False\n    HTTP_STRICT_VARS = False\n    IS_USER_ACTIVITY = True\n\n    def template_path(self, ttype, template_id=None, **kwargs):\n        if not template_id:\n            template_id = '%s/%s' % (self.SYNOPSIS[2],\n                                     self.args and self.args[0] or '')\n        return Command.template_path(self, ttype, template_id=template_id,\n                                     **kwargs)\n\n    def command(self):\n        return self._success(_('Rendered the page'), result={\n            'path': (self.args and self.args[0] or ''),\n            'data': self.data\n        })\n\n\nclass ProgramStatus(Command):\n    \"\"\"Display list of running threads, locks and outstanding events.\"\"\"\n    SYNOPSIS = (None, 'ps', 'ps', None)\n    ORDER = ('Internals', 5)\n    CONFIG_REQUIRED = False\n    IS_USER_ACTIVITY = False\n    LOG_NOTHING = True\n\n    class CommandResult(Command.CommandResult):\n        def as_text(self):\n            now = time.time()\n\n            sessions = self.result.get('sessions')\n            if sessions:\n                sessions = '\\n'.join(sorted(['  %s/%s = %s (%ds)'\n                                             % (us['sessionid'],\n                                                us['userdata'],\n                                                us['userinfo'],\n                                                now - us['timestamp'])\n                                             for us in sessions]))\n            else:\n                sessions = '  ' + _('Nothing Found')\n\n            ievents = self.result.get('ievents')\n            cevents = self.result.get('cevents')\n            if cevents:\n                cevents = '\\n'.join(['  %s' % (e.as_text(compact=True),)\n                                     for e in cevents])\n            else:\n                cevents = '  ' + _('Nothing Found')\n\n            ievents = self.result.get('ievents')\n            if ievents:\n                ievents = '\\n'.join([' %s' % (e.as_text(compact=True),)\n                                     for e in ievents])\n            else:\n                ievents = '  ' + _('Nothing Found')\n\n            threads = self.result.get('threads')\n            if threads:\n                threads = '\\n'.join(sorted([('  ' + str(t)) for t in threads]))\n            else:\n                threads = _('Nothing Found')\n\n            locks = self.result.get('locks')\n            if locks:\n                locks = '\\n'.join(sorted([('  %s.%s is %slocked'\n                                           ) % (l[0], l[1],\n                                                '' if l[2] else 'un')\n                                          for l in locks]))\n            else:\n                locks = _('Nothing Found')\n\n            return ('Recent events:\\n%s\\n\\n'\n                    'Events in progress:\\n%s\\n\\n'\n                    'Live sessions:\\n%s\\n\\n'\n                    'Postinglist timers:\\n%s\\n\\n'\n                    'Threads: (bg delay %.3fs, live=%s, httpd=%s)\\n%s\\n\\n'\n                    'Locks:\\n%s'\n                    ) % (cevents, ievents, sessions,\n                         self.result['pl_timers'],\n                         self.result['delay'],\n                         self.result['live'],\n                         self.result['httpd'],\n                         threads, locks)\n\n    def command(self, args=None):\n        import mailpile.auth\n        import mailpile.mail_source\n        import mailpile.plugins.compose\n        import mailpile.plugins.contacts\n\n        config = self.session.config\n\n        try:\n            idx = config.index\n            locks = [\n                ('config.index', '_lock', idx._lock._is_owned()),\n                ('config.index', '_save_lock', idx._save_lock._is_owned())\n            ]\n        except AttributeError:\n            locks = []\n        if config.vcards:\n            locks.extend([\n                ('config.vcards', '_lock', config.vcards._lock._is_owned()),\n            ])\n        locks.extend([\n            ('config', '_lock', config._lock._is_owned()),\n            ('mailpile.plugins.compose',\n                 'GLOBAL_EDITING_LOCK',\n                 mailpile.plugins.compose.GLOBAL_EDITING_LOCK._is_owned()),\n            ('mailpile.plugins.contacts',\n                 'GLOBAL_VCARD_LOCK',\n                 mailpile.plugins.contacts.GLOBAL_VCARD_LOCK._is_owned()),\n            ('mailpile.postinglist',\n                 'PLC_CACHE_LOCK',\n                 mailpile.postinglist.PLC_CACHE_LOCK.locked()),\n            ('mailpile.postinglist',\n                 'GLOBAL_POSTING_LOCK',\n                 mailpile.postinglist.GLOBAL_POSTING_LOCK._is_owned()),\n            ('mailpile.postinglist',\n                 'GLOBAL_OPTIMIZE_LOCK',\n                 mailpile.postinglist.GLOBAL_OPTIMIZE_LOCK.locked()),\n            ('mailpile.postinglist',\n                 'GLOBAL_GPL_LOCK',\n                 mailpile.postinglist.GLOBAL_GPL_LOCK._is_owned())])\n\n        threads = threading.enumerate()\n        for thread in threads:\n            try:\n                if hasattr(thread, 'lock'):\n                    locks.append([thread, 'lock', thread.lock])\n                if hasattr(thread, '_lock'):\n                    locks.append([thread, '_lock', thread._lock])\n                if locks and hasattr(locks[-1][-1], 'locked'):\n                    locks[-1][-1] = locks[-1][-1].locked()\n                elif locks and hasattr(locks[-1][-1], '_is_owned'):\n                    locks[-1][-1] = locks[-1][-1]._is_owned()\n            except AttributeError:\n                pass\n\n        import mailpile.auth\n        import mailpile.httpd\n        result = {\n            'sessions': [{'sessionid': k,\n                          'timestamp': v.ts,\n                          'userdata': v.data,\n                          'userinfo': v.auth} for k, v in\n                         mailpile.auth.SESSION_CACHE.iteritems()],\n            'pl_timers': mailpile.postinglist.TIMERS,\n            'delay': play_nice_with_threads(sleep=False),\n            'live': mailpile.util.LIVE_USER_ACTIVITIES,\n            'httpd': mailpile.httpd.LIVE_HTTP_REQUESTS,\n            'threads': threads,\n            'locks': sorted(locks)\n        }\n        if config.event_log:\n            result.update({\n                'cevents': list(config.event_log.events(flag='c'))[-10:],\n                'ievents': config.event_log.incomplete(),\n            })\n\n        return self._success(_(\"Listed events, threads, and locks\"),\n                             result=result)\n\n\nclass CronStatus(Command):\n    \"\"\"Manually edit or display the background job schedule\"\"\"\n    SYNOPSIS = (None, 'cron', None,\n                \"[<job> <--trigger|--interval <n>|--postpone <hours>>]\")\n    ORDER = ('Internals', 4)\n    IS_USER_ACTIVITY = False\n\n    class CommandResult(Command.CommandResult):\n        def as_text(self):\n            def _t(dt):\n                return '%4.4d-%2.2d-%2.2d %2.2d:%2.2d' % (\n                    dt.year, dt.month, dt.day, dt.hour, dt.minute)\n\n            fmt = ' %-23s %8s %-16s %-16s %s'\n            lines = [\n                'Background CRON last ran at %s.' % _t(\n                    datetime.datetime.fromtimestamp(self.result['last_run'])),\n                'Current schedule:',\n                '',\n                fmt % ('JOB', 'INTERVAL', 'LAST RUN', 'NEXT RUN', 'STATUS')]\n\n            for job_name, interval, func, last, status in self.result['jobs']:\n                lines.append(fmt % (\n                    job_name,\n                    interval,\n                    (_t(datetime.datetime.fromtimestamp(last))\n                     if (status != 'new') else ''),\n                    _t(datetime.datetime.fromtimestamp(last + interval)),\n                    status))\n\n            return '\\n'.join(lines)\n\n    def command(self, args=None):\n        config = self.session.config\n        args = args if (args is not None) else list(self.args)\n        now = int(time.time())\n\n        if args:\n            job = args.pop(0)\n        while args:\n            op = args.pop(0).lower().replace('-', '')\n            if op == 'interval':\n                interval = int(args.pop(0))\n                config.cron_worker.schedule[job][1] = interval\n            elif op == 'trigger':\n                interval = config.cron_worker.schedule[job][1]\n                config.cron_worker.schedule[job][3] = now - interval\n            elif op == 'postpone':\n                hours = float(args.pop(0))\n                config.cron_worker.schedule[job][3] += int(hours * 3600)\n            else:\n                raise NotImplementedError('Unknown op: %s' % op)\n\n        return self._success(\n            _(\"Displayed CRON schedule\"),\n            result={\n                'last_run': config.cron_worker.last_run,\n                'jobs': config.cron_worker.schedule.values()})\n\n\nclass HealthCheck(Command):\n    \"\"\"Check and report app health\"\"\"\n    SYNOPSIS = (None, 'health', None, \"\")\n    ORDER = ('Internals', 4)\n    CONFIG_REQUIRED = False\n    IS_USER_ACTIVITY = False\n\n    # We cache our health event, so it can be updated by the class methods.\n    health_event = None\n\n    def _create_event(self):\n        if HealthCheck.health_event is not None:\n            self.event = HealthCheck.health_event\n        else:\n            Command._create_event(self)\n            self.event.data['starttime'] = int(time.time())\n            self.event.data['problems'] = {}\n            self.event.data['healthy'] = True\n            HealthCheck.health_event = self.event\n\n        # Cancel any obsolete HealthCheck events we find\n        if self.session.config.event_log:\n            for ev in self.session.config.event_log.events():\n                if (ev.source == self.event.source and\n                        ev.event_id != self.event.event_id):\n                    ev.flags = ev.COMPLETE\n                    self.session.config.event_log.log_event(ev)\n\n    @classmethod\n    def _mem_check(cls, session, config):\n        if config.detected_memory_corruption:\n            return _('Memory corruption detected') + '!'\n        return False\n\n    @classmethod\n    def _disk_check(cls, session, config):\n        if config.need_more_disk_space():\n            return _('Insufficient free disk space') + '.'\n        return False\n\n    @classmethod\n    def _readonly_check(cls, session, config):\n        from mailpile.security import _lockdown_basic\n        if _lockdown_basic(config):\n            return _('Your Mailpile is read-only!')\n        return False\n\n    @classmethod\n    def check(cls, session, config):\n        # Check all the things! The order here matters, more critical things\n        # should be reported last as they will determine the final message.\n        if not cls.health_event:\n            return False\n\n        messages = []\n        problems = cls.health_event.data['problems']\n\n        was_healthy = cls.health_event.data['healthy']\n        old_problems = ' '.join(sorted(problems.keys()))\n\n        now_healthy = True\n        for crit, name, check in ((True, 'disk', cls._disk_check),\n                                  (True, 'memcheck', cls._mem_check),\n                                  (True, 'readonly', cls._readonly_check)):\n             message = check(session, config)\n             if message:\n                 problems[name] = message\n                 messages.append(message)\n                 if crit:\n                     now_healthy = False\n             elif name in problems:\n                 del problems[name]\n\n        cls.health_event.data['healthy'] = now_healthy\n        if messages:\n            cls.health_event.message = ' '.join(messages[-2:])\n            cls.health_event.flags = cls.health_event.RUNNING\n        else:\n            cls.health_event.message = _('We are healthy!')\n            cls.health_event.flags = cls.health_event.COMPLETE\n\n        # Only record changes to the event log\n        new_problems = ' '.join(sorted(problems.keys()))\n        if old_problems != new_problems and config.event_log:\n            config.event_log.log_event(cls.health_event)\n\n        return True\n\n    def command(self, args=None):\n        self.check(self.session, self.session.config)\n        return self._success(self.event.message, result=self.event)\n\n\nclass GpgCommand(Command):\n    \"\"\"Interact with GPG directly\"\"\"\n    SYNOPSIS = (None, 'gpg', None, \"<GPG arguments ...>\")\n    ORDER = ('Internals', 4)\n    IS_USER_ACTIVITY = True\n\n    class CommandResult(Command.CommandResult):\n        def as_text(self):\n            if self.result:\n                return '%s\\n\\n%s' % (\n                    (self.result['stdout'] or _('(no output)')).strip(),\n                    self.message)\n            return '%s' % self.message\n\n    def command(self, args=None):\n        args = list((args is None) and self.args or args or [])\n        gnupg = self._gnupg()\n        gnupg_args = gnupg.common_args(interactive=True)\n        binary = gnupg.gpgbinary\n        rv = '(unknown)'\n        def _shellquote(s):\n            return \"'\" + s.replace(\"'\", \"'\\\\''\") + \"'\"\n        with self.session.ui.term:\n            try:\n                self.session.ui.block()\n                if (self.session.ui.interactive and\n                        self.session.ui.render_mode == 'text'):\n                    rv = os.system(' '.join(_shellquote(a)\n                                            for a in (gnupg_args + args)))\n                    stdout = None\n                else:\n                    sp = subprocess.Popen(\n                        gnupg_args + ['--batch', '--no-tty'] + args,\n                        stdout=subprocess.PIPE, stderr=subprocess.PIPE,\n                        stdin=subprocess.PIPE)\n                    (stdout, stderr) = sp.communicate(input='')\n                    rv = sp.wait()\n                from mailpile.plugins.vcard_gnupg import PGPKeysImportAsVCards\n                PGPKeysImportAsVCards(self.session).run()\n            except:\n                self.session.ui.unblock()\n\n        return self._success(_(\"That was fun!\") + ' ' +\n                             _(\"%s returned: %s\") % (binary, rv),\n                             result={'binary': binary,\n                                     'stdout': stdout,\n                                     'returned': rv})\n\n\nclass ListDir(Command):\n    \"\"\"Display working directory listing\"\"\"\n    SYNOPSIS = (None, 'ls', 'browse', \"[-a] [-d] [</path/*.foo> ...]\")\n    ORDER = ('Internals', 5)\n    CONFIG_REQUIRED = False\n    IS_USER_ACTIVITY = True\n    COMMAND_SECURITY = security.CC_BROWSE_FILESYSTEM\n\n    class CommandResult(Command.CommandResult):\n        def as_text(self):\n            if self.result and self.result['entries']:\n                lines = []\n                for i in self.result['entries']:\n                    sz = i.get('bytes')\n                    dn = i['display_name']\n                    dp = '' if (i['display_path'].endswith(dn)\n                                ) else i['display_path']\n                    dn += '/' if i.get('flag_directory') else ''\n                    lines.append(('%12.12s %s%-20s %s'\n                                  ) % ('' if (sz is None) else sz,\n                                       '>' if i.get('flag_mailsource') else\n                                       '*' if i.get('flag_mailbox') else ' ',\n                                       dn, dp))\n                return '\\n'.join(lines)\n            else:\n                return _('Nothing Found')\n\n    def command(self, args=None):\n        args = list((args is None) and self.args or args or [])\n        flags = [f for f in args if f[:1] == '-']\n        args = [a for a in args if a[:1] != '-']\n\n        if '_method' in self.data:\n            args = ['/' + '/'.join(args)]\n\n        if not args:\n            args = ['.']\n\n        def lsf(f):\n            info = {'path': f}\n            try:\n                info = vfs.getinfo(f, self.session.config)\n                info['icon'] = ''\n                for k in info.get('flags', []):\n                    info['flag_%s' % unicode(k).lower().replace('.', '_')\n                         ] = True\n            except (OSError, IOError, UnicodeDecodeError):\n                info['flag_error'] = True\n            return info\n        def ls(p):\n            return [lsf(vfs.path_join(p, f)) for f in vfs.listdir(p)\n                    if '-a' in flags or f.raw_fp[:1] != '.']\n\n        file_list = []\n        errors = 0\n        for path in args:\n            if (security.forbid_command(self, security.CC_ACCESS_FILESYSTEM)\n                    and (path != '/')\n                    and (not path.endswith('$'))\n                    and ('$/' not in path)):\n                continue\n            try:\n                path = os.path.expanduser(path.encode('utf-8'))\n                if vfs.isdir(path) and '*' not in path:\n                    file_list.extend(ls(path))\n                else:\n                    for p in vfs.glob(path):\n                        if vfs.isdir(p) and '-d' not in flags:\n                            file_list.extend(ls(p))\n                        else:\n                            file_list.append(lsf(p))\n            except (socket.error, socket.gaierror) as e:\n                return self._error(_('Network error: %s') % e)\n            except (OSError, IOError, UnicodeDecodeError) as e:\n                errors += 1\n\n        if errors and not file_list:\n            traceback.print_exc()\n            return self._error(_('Failed to list: %s') % e)\n\n        id_src_map = self.session.config.find_mboxids_and_sources_by_path(\n            *[unicode(f['path']) for f in file_list])\n        for info in file_list:\n            path = unicode(info['path'])\n            mid_src = id_src_map.get(path)\n            if mid_src:\n                mid, src = mid_src\n                if src:\n                    info['source'] = src._key\n                if src and src.mailbox[mid] and src.mailbox[mid].primary_tag:\n                    tid = src.mailbox[mid].primary_tag\n                    if tid in self.session.config.tags:\n                        info['tag'] = self.session.config.tags[tid].slug\n                        info['icon'] = self.session.config.tags[tid].icon\n            elif info.get('flag_mailsource'):\n                if path.startswith('/src:'):\n                    info['source'] = path[5:]\n\n        file_list.sort(key=lambda i: i['display_path'].lower())\n        return self._success(_('Listed %d files or directories'\n                               ) % len(file_list),\n                             result={\n            'path': args[0] if (len(args) == 1) else args,\n            'name': vfs.display_name(args[0], self.session.config),\n            'entries': file_list\n        })\n\n\nclass ChangeDir(ListDir):\n    \"\"\"Change working directory\"\"\"\n    SYNOPSIS = (None, 'cd', None, \"<.../new/path/...>\")\n    ORDER = ('Internals', 5)\n    CONFIG_REQUIRED = False\n    IS_USER_ACTIVITY = True\n    COMMAND_SECURITY = security.CC_ACCESS_FILESYSTEM\n\n    def command(self, args=None):\n        try:\n            args = list((args is None) and self.args or args or [])\n            os.chdir(FilePath.unalias(\n                        os.path.expanduser(args.pop(0).encode('utf-8'))))\n            return ListDir.command(self, args=['.'])\n        except (OSError, IOError, UnicodeEncodeError) as e:\n            return self._error(_('Failed to change directories: %s') % e)\n\n\nclass CatFile(Command):\n    \"\"\"Dump the contents of a file, decrypting if necessary\"\"\"\n    SYNOPSIS = (None, 'cat', None, \"</path/to/file> [>/path/to/output]\")\n    ORDER = ('Internals', 5)\n    CONFIG_REQUIRED = False\n    IS_USER_ACTIVITY = True\n    COMMAND_SECURITY = security.CC_ACCESS_FILESYSTEM\n\n    class CommandResult(Command.CommandResult):\n        def as_text(self):\n            if isinstance(self.result, list):\n                return ''.join(self.result)\n            else:\n                return ''\n\n    def command(self, args=None):\n        lines = []\n        files = list(args or self.args)\n        target = tfd = None\n        if files and files[-1] and files[-1][:1] == '>':\n            target = files.pop(-1)[1:]\n            if vfs.exists(target):\n                return self._error(_('That file already exists: %s'\n                                     ) % target)\n            tfd = vfs.open(target, 'wb')\n            cb = lambda ll: [tfd.write(l) for l in ll]\n        else:\n            cb = lambda ll: lines.extend((l.decode('utf-8') for l in ll))\n\n        for fn in files:\n            with vfs.open(fn, 'r') as fd:\n                def errors(where):\n                    self.session.ui.error('Decrypt failed at %d' % where)\n                decrypt_and_parse_lines(fd, cb, self.session.config,\n                                        newlines=True, decode=None,\n                                        gpgi=self._gnupg(),\n                                        _raise=False, error_cb=errors)\n\n        if tfd:\n            tfd.close()\n            return self._success(_('Dumped to %s: %s'\n                                   ) % (target, ', '.join(files)))\n        else:\n            return self._success(_('Dumped: %s') % ', '.join(files),\n                                   result=lines)\n\n\n##[ Configuration commands ]###################################################\n\n\nclass ListLanguages(Command):\n    \"\"\"List available languages\"\"\"\n    SYNOPSIS = (None, 'languages', 'settings/languages', '')\n    ORDER = ('Config', 1)\n    CONFIG_REQUIRED = False\n    IS_USER_ACTIVITY = False\n    HTTP_CALLABLE = ('GET', )\n\n    def command(self):\n        from mailpile.i18n import ListTranslations\n        langs = ListTranslations(self.session.config)\n        return self._success(_('Listed available translations'),\n                             result=sorted([(l, langs[l]) for l in langs]))\n\n\nclass ConfigSet(Command):\n    \"\"\"Change a setting\"\"\"\n    SYNOPSIS = ('S', 'set', 'settings/set',\n                '[--force] <section.variable> <value>')\n    ORDER = ('Config', 1)\n    CONFIG_REQUIRED = False\n    IS_USER_ACTIVITY = False\n\n    SPLIT_ARG = False\n\n    HTTP_CALLABLE = ('POST', 'UPDATE')\n    HTTP_STRICT_VARS = False\n    HTTP_POST_VARS = {\n        '_section': 'common section, create if needed',\n        'section.variable': 'value|json-string'\n    }\n\n    def command(self):\n        from mailpile.httpd import BLOCK_HTTPD_LOCK, Idle_HTTPD\n\n        config = self.session.config\n        args = list(self.args)\n        arg = ' '.join(args)\n        ops = []\n        on_cli = (self.data.get('_method', 'CLI') == 'CLI')\n        force = False\n\n        if arg.startswith('--force '):\n            if not on_cli:\n                raise ValueError('The --force flag only works on the CLI')\n            force = True\n            arg = arg[8:]\n\n        if not force:\n            fb = security.forbid_command(self, security.CC_CHANGE_CONFIG)\n            if fb:\n                return self._error(fb)\n\n        if not config.loaded_config:\n            self.session.ui.warning(_('WARNING: Any changes will '\n                                      'be overwritten on login'))\n\n        section = self.data.get('_section', [''])[0]\n        if section:\n            # Make sure section exists\n            ops.append((section, '!CREATE_SECTION'))\n\n        for var in self.data.keys():\n            if (var in ('_section', '_method', 'context', 'csrf')\n                   or var.startswith('ui_')):\n                continue\n            sep = '/' if ('/' in (section+var)) else '.'\n            svar = (section+sep+var) if section else var\n            parts = svar.split(sep)\n            if parts[0] in config.rules:\n                if svar.endswith('[]'):\n                    ops.append((svar[:-2], json.dumps(self.data[var])))\n                else:\n                    ops.append((svar, self.data[var][0]))\n            else:\n                raise ValueError(_('Invalid section or variable: %s') % var)\n\n        if args:\n            if '=' in arg:\n                # Backwards compatiblity with the old 'var = value' syntax.\n                var, value = [s.strip() for s in arg.split('=', 1)]\n                var = var.replace(': ', '.').replace(':', '.').replace(' ', '')\n            else:\n                var, value = arg.split(' ', 1)\n            ops.append((var, value))\n\n        # Access controls...\n        if not force:\n            for path, value in ops:\n                fb = security.forbid_config_change(config, path)\n                if fb:\n                    return self._error(fb)\n                elif path == 'master_key' and config.get_master_key():\n                    return self._error(_('I refuse to change the master key!'))\n\n        # We don't have transactions really, but making sure the HTTPD\n        # is idle (aside from this request) will definitely help.\n        with BLOCK_HTTPD_LOCK, Idle_HTTPD():\n            updated = {}\n            for path, value in ops:\n                if not force:\n                    if path == 'master_key' and config.get_master_key():\n                        raise ValueError('Need --force to change master key.')\n                    if path == 'sys.http_no_auth':\n                        raise ValueError('Need --force to change auth policy.')\n\n                value = value.strip()\n                if value == '{None}':\n                    value = None\n                elif value == '{Blank}':\n                    value = ''\n                elif value == '{False}':\n                    value = False\n                elif value == '{True}':\n                    value = True\n                elif value[:1] in ('{', '[') and value[-1:] in ( ']', '}'):\n                    value = json.loads(value)\n                try:\n                    try:\n                        cfg, var = config.walk(path.strip(), parent=1)\n                        if value == '!CREATE_SECTION':\n                            if var not in cfg:\n                                cfg[var] = {}\n                        else:\n                            cfg[var] = value\n                            updated[path] = value\n                    except IndexError:\n                        cfg, v1, v2 = config.walk(path.strip(), parent=2)\n                        cfg[v1] = {v2: value}\n                except TypeError:\n                    raise ValueError('Could not set variable: %s' % path)\n\n        if config.loaded_config:\n            self._background_save(config=True)\n\n        return self._success(_('Updated your settings'), result=updated)\n\n\nclass ConfigAdd(Command):\n    \"\"\"Add a new value to a list (or ordered dict) setting\"\"\"\n    SYNOPSIS = (None, 'append', 'settings/add', '<section.variable> <value>')\n    ORDER = ('Config', 1)\n    SPLIT_ARG = False\n    HTTP_CALLABLE = ('POST', 'UPDATE')\n    HTTP_STRICT_VARS = False\n    HTTP_POST_VARS = {\n        'section.variable': 'value|json-string',\n    }\n    IS_USER_ACTIVITY = True\n    COMMAND_SECURITY = security.CC_CHANGE_CONFIG\n\n    def command(self):\n        from mailpile.httpd import BLOCK_HTTPD_LOCK, Idle_HTTPD\n        config = self.session.config\n        args = list(self.args)\n        ops = []\n\n        for var in self.data.keys():\n            parts = ('.' in var) and var.split('.') or var.split('/')\n            if parts[0] in config.rules:\n                ops.append((var, self.data[var][0]))\n\n        if args:\n            arg = ' '.join(args)\n            if '=' in arg:\n                # Backwards compatible with the old 'var = value' syntax.\n                var, value = [s.strip() for s in arg.split('=', 1)]\n                var = var.replace(': ', '.').replace(':', '.').replace(' ', '')\n            else:\n                var, value = arg.split(' ', 1)\n            ops.append((var, value))\n\n        # Access controls...\n        for path, value in ops:\n            fb = security.forbid_config_change(config, path)\n            if fb:\n                return self._error(fb)\n            elif path == 'master_key' and config.get_master_key():\n                return self._error(_('I refuse to change the master key!'))\n\n        # We don't have transactions really, but making sure the HTTPD\n        # is idle (aside from this request) will definitely help.\n        with BLOCK_HTTPD_LOCK, Idle_HTTPD():\n            updated = {}\n            for path, value in ops:\n                value = value.strip()\n                if value.startswith('{') or value.startswith('['):\n                    value = json.loads(value)\n                cfg, var = config.walk(path.strip(), parent=1)\n                cfg[var].append(value)\n                updated[path] = value\n\n        if updated:\n            self._background_save(config=True)\n\n        return self._success(_('Updated your settings'), result=updated)\n\n\nclass ConfigUnset(Command):\n    \"\"\"Reset one or more settings to their defaults\"\"\"\n    SYNOPSIS = ('U', 'unset', 'settings/unset', '<var>')\n    ORDER = ('Config', 2)\n    HTTP_CALLABLE = ('POST', )\n    HTTP_POST_VARS = {\n        'var': 'section.variables'\n    }\n    IS_USER_ACTIVITY = True\n    COMMAND_SECURITY = security.CC_CHANGE_CONFIG\n\n    def command(self):\n        from mailpile.httpd import BLOCK_HTTPD_LOCK, Idle_HTTPD\n        session, config = self.session, self.session.config\n\n        def unset(cfg, key):\n            if isinstance(cfg[key], dict):\n                if '_any' in cfg[key].rules:\n                    for skey in cfg[key].keys():\n                        del cfg[key][skey]\n                else:\n                    for skey in cfg[key].keys():\n                        unset(cfg[key], skey)\n            elif isinstance(cfg[key], list):\n                cfg[key] = []\n            else:\n                del cfg[key]\n\n        # Access controls...\n        vlist = list(self.args) + (self.data.get('var', None) or [])\n        # Access controls...\n        for v in vlist:\n            fb = security.forbid_config_change(config, v)\n            if fb:\n                return self._error(fb)\n            elif v == 'master_key' and config.get_master_key():\n                return self._error(_('I refuse to change the master key!'))\n\n        # We don't have transactions really, but making sure the HTTPD\n        # is idle (aside from this request) will definitely help.\n        with BLOCK_HTTPD_LOCK, Idle_HTTPD():\n            updated = []\n            for v in vlist:\n                cfg, vn = config.walk(v, parent=True)\n                unset(cfg, vn)\n                updated.append(v)\n\n        if updated:\n            self._background_save(config=True)\n\n        return self._success(_('Reset to default values'), result=updated)\n\n\nclass ConfigPrint(Command):\n    \"\"\"Print one or more settings\"\"\"\n    SYNOPSIS = ('P', 'print', 'settings', '[-short|-secrets|-flat] <var>')\n    ORDER = ('Config', 3)\n    CONFIG_REQUIRED = False\n    IS_USER_ACTIVITY = False\n\n    HTTP_CALLABLE = ('GET', 'POST')\n    HTTP_QUERY_VARS = {\n        'var': 'section.variable',\n        'short': 'Set True to omit unchanged values (defaults)',\n        'secrets': 'Set True to show passwords and other secrets'\n    }\n    HTTP_POST_VARS = {\n        'user': 'Authenticate as user',\n        'pass': 'Authenticate with password'\n    }\n\n    def _maybe_all(self, list_all, data, key_types, recurse, sanitize):\n        if isinstance(data, (dict, list)) and list_all:\n            rv = {}\n            for key in data.all_keys():\n                if [t for t in data.key_types(key) if t not in key_types]:\n                    # Silently omit things that are considered sensitive\n                    continue\n                rv[key] = data[key]\n                if hasattr(rv[key], 'all_keys'):\n                    if recurse:\n                        rv[key] = self._maybe_all(True, rv[key], key_types,\n                                                  recurse, sanitize)\n                    else:\n                        if 'name' in rv[key]:\n                            rv[key] = '{ ..(%s).. }' % rv[key]['name']\n                        elif 'description' in rv[key]:\n                            rv[key] = '{ ..(%s).. }' % rv[key]['description']\n                        elif 'host' in rv[key]:\n                            rv[key] = '{ ..(%s).. }' % rv[key]['host']\n                        else:\n                            rv[key] = '{ ... }'\n                elif (sanitize\n                        and key.lower()[:4] in ('pass', 'secr', 'obfu')):\n                    rv[key] = '(SUPPRESSED)'\n            return rv\n        return data\n\n    def command(self):\n        session, config = self.session, self.session.config\n        result = {}\n        invalid = []\n\n        args = list(self.args)\n        recurse = not self.data.get('flat', ['-flat' in args])[0]\n        list_all = not self.data.get('short', ['-short' in args])[0]\n        sanitize = not self.data.get('secrets', ['-secrets' in args])[0]\n\n        if security.forbid_command(self, security.CC_LIST_PRIVATE_DATA):\n            sanitize = True\n\n        # FIXME: Shouldn't we suppress critical variables as well?\n        key_types = ['public', 'critical']\n        access_denied = False\n\n        if self.data.get('_method') == 'POST':\n            if 'pass' in self.data:\n                from mailpile.auth import CheckPassword\n                password = self.data['pass'][0]\n                auth_user = CheckPassword(config,\n                                          self.data.get('user', [None])[0],\n                                          password)\n                if auth_user == 'DEFAULT':\n                    key_types += ['key']\n                result['_auth_user'] = auth_user\n                result['_auth_pass'] = password\n\n        for key in (args + self.data.get('var', [])):\n            if key in ('-short', '-flat', '-secrets'):\n                continue\n            try:\n                data = config.walk(key, key_types=key_types)\n                result[key] = self._maybe_all(list_all, data, key_types,\n                                              recurse, sanitize)\n            except AccessError:\n                access_denied = True\n                invalid.append(key)\n            except KeyError:\n                invalid.append(key)\n\n        if invalid:\n            return self._error(_('Invalid keys'),\n                               result=result, info={\n                                   'keys': invalid,\n                                   'key_types': key_types,\n                                   'access_denied': access_denied\n                               })\n        else:\n            return self._success(_('Displayed settings'), result=result)\n\n\nclass ConfigureMailboxes(Command):\n    \"\"\"\n    Add one or more mailboxes.\n\n    If not account is specified, the mailbox is only assigned an ID for use\n    in the metadata index.\n\n    If an account is specified, the mailbox will be assigned to that account\n    and configured for automatic indexing.\n    \"\"\"\n    SYNOPSIS = ('A', 'add', 'settings/mailbox',\n                '[+<tag>] [--<option>] [account@email] <path/to/mailbox>')\n    ORDER = ('Config', 4)\n    IS_USER_ACTIVITY = True\n    HTTP_CALLABLE = ('GET', 'POST', 'UPDATE')\n    HTTP_QUERY_VARS = {\n        'path': 'Path to mailbox',\n        'profile': 'Profile/account ID or e-mail',\n        'recurse': 'y/n: search subdirectories?',\n        'apply_tags': 'Mailbox tags',\n        'guess_tags': 'Guess mailbox tags',\n        'auto_index': 'Account e-mail or ID',\n        'local_copy': 'Make local copy of mail'\n    }\n    COMMAND_SECURITY = security.CC_CHANGE_CONFIG\n\n    MAX_PATHS = 50000\n\n    def _truthy(self, var, default='n'):\n        return truthy(self.data.get(var, [default])[0])\n\n    def command(self):\n        from mailpile.httpd import BLOCK_HTTPD_LOCK, Idle_HTTPD\n\n        session, config = self.session, self.session.config\n        paths = list(self.args)\n\n        # Which tags do we want to apply?\n        apply_tags = self.data.get('apply_tags', [])\n        atis = [i for i, p in enumerate(paths)\n                if p.startswith('+')]\n        for ati in atis:\n            at = paths.pop(ati)[1:].split(',')\n            apply_tags += [config.get_tag_id(a) for a in at]\n\n        # Parse arguments from the web\n        paths += self.data.get('path', [])\n        account_id = self.data.get('profile', [None])[0]\n        recurse = self._truthy('recurse', default='n')\n\n        if self.data.get('_method', 'CLI') == 'POST':\n            auto_index = self._truthy('auto_index', default='n')\n            local_copy = self._truthy('local_copy', default='n')\n            guess_tags = self._truthy('guess_tags', default='n')\n        else:\n            auto_index = True\n            local_copy = None\n            guess_tags = None\n\n        # Recursion or other options requested on CLI?\n        if self.data.get('_method', 'CLI') == 'CLI':\n            while paths and '--recurse' in paths:\n                recurse = paths.pop(paths.index('--recurse'))\n            while paths and '--local_copy' in paths:\n                local_copy = paths.pop(paths.index('--local_copy'))\n            while paths and '--guess_tags' in paths:\n                guess_tags = paths.pop(paths.index('--guess_tags'))\n            while paths and '--no_guess_tags' in paths:\n                guess_tags = not paths.pop(paths.index('--guess_tags'))\n            while paths and '--no_auto_index' in paths:\n                auto_index = not paths.pop(paths.index('--no_auto_index'))\n\n        # Are we linking these mailboxes to a particular account?\n        if (not account_id and\n                paths and '@' in paths[0] and paths[0][:1] != '/'):\n            account_id = paths.pop(0)\n        account = account_id and config.vcards.get_vcard(account_id)\n        if account_id and (not account or account.kind != 'profile'):\n            return self._error(_('Account not found: %s') % account_id)\n\n        # Turn raw paths into FilePath objects\n        paths = [FilePath(p) for p in paths]\n        # Strip leading slashes of src: paths\n        paths = [FilePath(p.raw_fp[1:]) if p.raw_fp.startswith('/src:') else p\n                 for p in paths]\n        opaths = paths[:]\n\n        # Get a list of existing mailboxes...\n        existing = {}\n        existing.update(dict((FilePath(p).encoded(), (FilePath(p), _id, src))\n                             for _id, p, src in\n                             config.get_mailboxes(mail_source_locals=False)))\n        existing.update(dict((FilePath(p).encoded(), (FilePath(p), _id, src))\n                             for _id, p, src in\n                             config.get_mailboxes(mail_source_locals=True)))\n\n        # Figure out which mailboxes the user is asking us to add...\n        adding = []\n        configure = []\n        has_source = False\n        try:\n            while paths:\n                fn = paths.pop(0)\n                fn_display = fn.display()\n                einfo = existing.get(fn.encoded())\n                if fn.raw_fp.startswith(\"src:\"):\n                    if einfo:\n                        configure.append((fn, einfo))\n                    else:\n                        adding.append(fn)\n                    has_source = True\n                elif einfo:\n                    if einfo[2]:\n                        has_source = True\n                    configure.append((fn, einfo))\n                    if not account:\n                        session.ui.warning('Already in the pile: %s'\n                                           % fn_display)\n                else:\n                    if IsMailbox(fn.raw_fp, config):\n                        adding.append(fn)\n                        added = True\n                        blacklist = ('.', '..', 'new', 'cur', 'tmp')\n                    else:\n                        added = False\n                        blacklist = ('.', '..')\n\n                    if recurse and vfs.exists(fn) and vfs.isdir(fn):\n                        session.ui.mark('Scanning %s for mailboxes' % fn_display)\n                        try:\n                            for f in [f for f in vfs.listdir(fn)\n                                      if not f.raw_fp in blacklist]:\n                                paths.append(vfs.path_join(fn, f))\n                                if len(paths) > self.MAX_PATHS:\n                                    return self._error(_('Too many files'))\n                        except OSError:\n                            if fn in opaths:\n                                return self._error(_('Failed to read: %s'\n                                                     ) % fn_display)\n                    elif fn in opaths and not added:\n                        return self._error(_('Not a mailbox: %s') % fn_display)\n\n        except KeyboardInterrupt:\n            return self._error(_('User aborted'))\n\n        if local_copy is None:\n            local_copy = has_source  # No source; probably already local\n\n        if self.data.get('_method', 'CLI') == 'GET':\n            if not apply_tags and len(opaths) == 1:\n                apply_tags = list(config.guess_tags(opaths[0].raw_fp))\n            return self._success(_('Add and configure mailboxes'), result={\n                'paths': opaths,\n                'profile': account_id,\n                'apply_tags': apply_tags,\n                'auto_index': auto_index,\n                'local_copy': local_copy,\n                'recurse': recurse,\n                'has_source': has_source,\n                'adding': adding,\n                'configure': configure\n            })\n\n        added = {}\n        configured = {}\n        # We don't have transactions really, but making sure the HTTPD\n        # is idle (aside from this request) will definitely help.\n        with BLOCK_HTTPD_LOCK, Idle_HTTPD():\n            for arg in adding:\n                mbox_id = config.sys.mailbox.append(arg)\n                added[mbox_id] = arg\n                if account or arg.raw_fp.startswith('src:'):\n                    configure.append((arg, (arg, mbox_id, None)))\n\n            def _get_source(path, einfo):\n                if einfo and einfo[2]:\n                    return einfo[2]\n                raw_path = path.raw_fp\n                if raw_path.split(':')[0] == 'src':\n                    src_id = raw_path.split(':')[1].split('/')[0]\n                    return config.sources[src_id]\n                return account.get_source_by_proto('local', create=True)\n\n            for path, einfo in configure:\n                mbox_id = einfo[1]\n                source_cfg = _get_source(path, einfo)\n                source_obj = config.get_mail_source(source_cfg._key)\n                policy = 'read' if auto_index else 'ignore'\n                source_obj.take_over_mailbox(mbox_id,\n                                             policy=policy,\n                                             create_local=local_copy,\n                                             guess_tags=guess_tags,\n                                             apply_tags=apply_tags,\n                                             save=False)\n                configured[mbox_id] = path\n\n        if added or configured:\n            self._background_save(config=True)\n            return self._success(_('Configured %d mailboxes'\n                                   ) % max(len(added), len(configured)),\n                                 result={'added': added,\n                                         'configured': configured})\n        else:\n            return self._success(_('Nothing was added'))\n\n\n###############################################################################\n\nclass Output(Command):\n    \"\"\"Choose format for command results.\"\"\"\n    SYNOPSIS = (None, 'output', None, '[json|text|html|<template>.html|...]')\n    ORDER = ('Internals', 7)\n    CONFIG_REQUIRED = False\n    HTTP_STRICT_VARS = False\n    HTTP_AUTH_REQUIRED = False\n    IS_USER_ACTIVITY = False\n    LOG_NOTHING = True\n\n    def etag_data(self):\n        return self.get_render_mode()\n\n    def max_age(self):\n        return 364 * 24 * 3600  # A long time!\n\n    def get_render_mode(self):\n        return self.args and self.args[0] or self.session.ui.render_mode\n\n    def command(self):\n        m = self.session.ui.render_mode = self.get_render_mode()\n        return self._success(_('Set output mode to: %s') % m,\n                             result={'output': m})\n\n\nclass Pipe(Command):\n    \"\"\"Pipe a command to a shell command, file or e-mail\"\"\"\n    SYNOPSIS = (None, 'pipe', None,\n                \"[e@mail.com|command|>filename] -- [<cmd> [args ... ]]\")\n    ORDER = ('Internals', 5)\n    CONFIG_REQUIRED = False\n    IS_USER_ACTIVITY = True\n    COMMAND_SECURITY = security.CC_ACCESS_FILESYSTEM\n\n    class CommandResult(Command.CommandResult):\n        def as_text(self):\n            result = self.result or self.error_info or {}\n            output = [\n                result.get('stderr') or '',\n                result.get('stdout') or '']\n            if result.get('return_code'):\n                output.extend([\n                    '(',\n                    _('Command returned non-zero exit code: %d') % result['return_code'],\n                    ')'])\n            return ''.join(output)\n\n    def command(self):\n        if '--' in self.args:\n            dashdash = self.args.index('--')\n            target = list(self.args[0:dashdash])\n            command, args = self.args[dashdash+1], self.args[dashdash+2:]\n        else:\n            target, command, args = [self.args[0]], self.args[1], self.args[2:]\n\n        output = ''\n        result = None\n        old_ui = self.session.ui\n        try:\n            from mailpile.ui import CapturingUserInteraction as CUI\n            self.session.ui = capture = CUI(self.session.config)\n            capture.render_mode = old_ui.render_mode\n            result = Action(self.session, command, ' '.join(args))\n            capture.display_result(result)\n            output = capture.captured\n        finally:\n            self.session.ui = old_ui\n\n        if target[0].startswith('>'):\n            t = ' '.join(target)\n            if t[0] == '>':\n                t = t[1:]\n            with vfs.open(t.strip(), 'w') as fd:\n                fd.write(output.encode('utf-8'))\n            return self._success(\n                'Wrote %d bytes to %s' % (len(output), t[1:]),\n                result={})\n\n        if '@' in target[0]:\n            from mailpile.plugins.compose import Compose\n            body = 'Result as %s:\\n%s' % (capture.render_mode, output)\n            if capture.render_mode != 'json' and output[0] not in ('{', '['):\n                body += '\\n\\nResult as JSON:\\n%s' % result.as_json()\n            composer = Compose(self.session, data={\n                'to': target,\n                'subject': ['Mailpile: %s %s' % (command, ' '.join(args))],\n                'body': [body]})\n            return self._success('Mailing output to %s' % ', '.join(target),\n                                 result=composer.run())\n\n        stdout = stderr = ''\n        target = [t.encode('utf-8') for t in target]\n        try:\n            self.session.ui.block()\n            popen_args = {'stdin': subprocess.PIPE}\n            if not self.session.ui.interactive:\n                popen_args.update({\n                    'stdout': subprocess.PIPE, 'stderr': subprocess.PIPE})\n\n            MakePopenUnsafe()\n            kid = subprocess.Popen(target, **popen_args)\n            MakePopenSafe()\n\n            stdout, stderr = kid.communicate(input=output.encode('utf-8'))\n        finally:\n            MakePopenSafe()\n            kid.wait()\n            self.session.ui.unblock()\n\n        result = {\n            'stdout': stdout,\n            'stderr': stderr,\n            'return_code': kid.returncode}\n        if kid.returncode != 0:\n            return self._error('Error piping to %s' % (target, ), info=result)\n        else:\n            return self._success(\n                'Wrote %d bytes to %s' % (len(output), ' '.join(target)),\n                result=result)\n\n\nclass Quit(Command):\n    \"\"\"Exit Mailpile, normal shutdown\"\"\"\n    SYNOPSIS = (\"q\", \"quit\", \"quitquitquit\", '[restart]')\n    ABOUT = (\"Quit mailpile\")\n    ORDER = (\"Internals\", 2)\n    CONFIG_REQUIRED = False\n    RAISES = (KeyboardInterrupt,)\n    HTTP_CALLABLE = ('POST',)\n    HTTP_POST_VARS = {\n        'restart': 'Set to restart instead of shutting down'\n    }\n    COMMAND_SECURITY = security.CC_QUIT\n\n    def command(self):\n        if 'restart' in self.args or self.data.get('restart', [False])[0]:\n            mailpile.util.QUITTING = 'restart'\n        else:\n            mailpile.util.QUITTING = mailpile.util.QUITTING or True\n\n        from mailpile.plugins.gui import UpdateGUIState\n        UpdateGUIState()\n\n        self._background_save(index=True, config='!FORCE', wait=True)\n        if self.session.config.http_worker:\n            self.session.config.http_worker.quit()\n\n        thread.interrupt_main()\n        return self._success(_('Shutting down...'))\n\n\nclass IdleQuit(Command):\n    \"\"\"Shut down Mailpile if it has been idle for a while\"\"\"\n    SYNOPSIS = (None, \"idlequit\", None, \"[<timeout>]\")\n    ORDER = (\"Internals\", 2)\n    CONFIG_REQUIRED = False\n\n    def check(self):\n        idle = time.time() - max(self.started, mailpile.util.LAST_USER_ACTIVITY)\n        if idle > self.timeout:\n            Quit(self.session, 'quit').run()\n\n    def command(self):\n        config = self.session.config\n        self.timeout = int(self.args[0]) if self.args else 600\n        self.started = time.time()\n        config.cron_worker.add_task('idlequit', self.timeout / 5, self.check)\n        return self._success(\n            _('Will shutdown if idle for over %s seconds') % self.timeout,\n            {'timeout': self.timeout})\n\n\nclass TrustingQQQ(Command):\n    \"\"\"Allow anybody to quit the app\"\"\"\n    SYNOPSIS = (None, \"trustingqqq\", None, None)\n    COMMAND_SECURITY = security.CC_QUIT\n\n    def command(self):\n        # FIXME: This is a hack to allow Windows deployments to shut\n        #        down cleanly. Eventually this will take an argument\n        #        specifying a random token that the launcher chooses.\n\n        Quit.HTTP_AUTH_REQUIRED = False\n        return self._success('OK, anybody can quit!')\n\n\nclass Abort(Command):\n    \"\"\"Force exit Mailpile (kills threads)\"\"\"\n    SYNOPSIS = (None, \"quit/abort\", \"abortabortabort\", None)\n    ABOUT = (\"Quit mailpile\")\n    ORDER = (\"Internals\", 2)\n    CONFIG_REQUIRED = False\n    HTTP_QUERY_VARS = {\n        'no_save': 'Do not try to save state'\n    }\n    COMMAND_SECURITY = security.CC_QUIT\n\n    def command(self):\n        mailpile.util.QUITTING = mailpile.util.QUITTING or True\n        if 'no_save' not in self.data:\n            self._background_save(index=True, config=True, wait=True,\n                                  wait_callback=lambda: os._exit(1))\n        else:\n            os._exit(1)\n\n        return self._success(_('Shutting down...'))\n\n\nclass Help(Command):\n    \"\"\"Print help on Mailpile or individual commands.\"\"\"\n    SYNOPSIS = ('h', 'help', 'help', '[<command-group>]')\n    ABOUT = ('This is Mailpile!')\n    ORDER = ('Config', 9)\n    CONFIG_REQUIRED = False\n    IS_USER_ACTIVITY = True\n\n    class CommandResult(Command.CommandResult):\n\n        def splash_as_text(self):\n            text = [\n                '=' * 77,\n                self.result['splash']\n            ]\n\n            if not self.result['web_terminal']:\n                if self.result['http_url']:\n                    text.append(_('The Web interface address is: %s'\n                                  ) % self.result['http_url'])\n                else:\n                    text.append(_('The Web interface is disabled,'\n                                  ' type `www` to turn it on.'))\n                text.append('')\n\n            b = '  * '\n            if self.result['web_terminal']:\n                text.append(_('Type `help` for instructions, or try these:'))\n                text.append(b + _('Terminal') +': '+\n                    '`/full`, `/small`, `/clear`, `/close`')\n            elif self.result['interactive']:\n                text.append(b + _(\n                    'Type `help` for instructions or `quit` to quit.'))\n                text.append(b + _(\n                    'Slow operations can be aborted by pressing: <CTRL-C>'))\n\n            if self.result['login_cmd'] and self.result['interactive']:\n                text.append(b +\n                    _('You can log in using the `%s` command.')\n                        % self.result['login_cmd'])\n            elif self.result['interactive'] or self.result['web_terminal']:\n                text.append(b + _('System') +': '+\n                    '`sendmail`, `rescan sources`, `set sys.debug = log`')\n                text.append(b + _('Search') +': '+\n                    '`inbox`, `trash`, `search is:unread from:john`')\n                text.append(b + _('Actions') +': '+\n                    '`tag -new these`, `view 2`, `tag -new +trash 1 3 7`')\n                text.append(b + _('Output') +': '+\n                    '`spam :json`, `view 1 :html`, `view raw 1 >/tmp/msg.eml`')\n\n            if self.result['in_browser']:\n                text.append(b + _('Check your web browser!'))\n\n            return '\\n'.join(text)\n\n        def variables_as_text(self):\n            text = []\n            for group in self.result['variables']:\n                text.append('%s (%s.*)' % (group['name'], group['category']))\n                for var in group['variables']:\n                    sep = ('=' in var['type']) and ': ' or ' = '\n                    text.append(('  %-35s %s'\n                                 ) % (('%s%s<%s>'\n                                       ) % (var['var'], sep,\n                                            var['type'].replace('=', '> = <')),\n                                      var['desc']))\n                text.append('')\n            return '\\n'.join(text)\n\n        def commands_as_text(self):\n            text = [_('Commands:')]\n            last_rank = None\n            cmds = self.result['commands']\n            width = self.result.get('width', 8)\n            ckeys = cmds.keys()\n            ckeys.sort(key=lambda k: (cmds[k][3], cmds[k][0]))\n            arg_width = min(50, max(14, self.session.ui.term.max_width-70))\n            for c in ckeys:\n                cmd, args, explanation, rank = cmds[c]\n                if not rank or not cmd:\n                    continue\n                if last_rank and int(rank / 10) != last_rank:\n                    text.append('')\n                last_rank = int(rank / 10)\n                if c[0] == '_':\n                    c = '  '\n                else:\n                    c = '%s|' % c[0]\n                fmt = '  %%s%%-%d.%ds' % (width, width)\n                if explanation:\n                    if len(args or '') <= arg_width:\n                        fmt += ' %%-%d.%ds %%s' % (arg_width, arg_width)\n                    else:\n                        pad = len(c) + width + 3 + arg_width\n                        fmt += ' %%s\\n%s %%s' % (' ' * pad)\n                else:\n                    explanation = ''\n                    fmt += ' %s %s '\n                text.append(fmt % (c, cmd.replace('=', ''),\n                                   args and ('%s' % (args, )) or '',\n                                   (explanation.splitlines() or [''])[0]))\n            if self.result.get('tags'):\n                text.extend([\n                    '',\n                    _('Tags:  (use a tag as a command to display tagged '\n                      'messages)'),\n                    '',\n                    self.result['tags'].as_text()\n                ])\n            return '\\n'.join(text)\n\n        def as_text(self):\n            if not self.result:\n                return _('Error')\n            return ''.join([\n                ('splash' in self.result) and self.splash_as_text() or '',\n                (('variables' in self.result) and self.variables_as_text()\n                 or ''),\n                ('commands' in self.result) and self.commands_as_text() or '',\n            ])\n\n    def command(self):\n        config = self.session.config\n        self.session.ui.reset_marks(quiet=True)\n        if self.args:\n            command = self.args[0]\n            for cls in COMMANDS:\n                name = cls.SYNOPSIS[1] or cls.SYNOPSIS[2]\n                width = len(name or '')\n                if name and command in cls.SYNOPSIS[1:3]:\n                    order = 1\n                    cmd_list = {'_main': (name, cls.SYNOPSIS[3],\n                                          cls.__doc__, order)}\n                    subs = [c for c in COMMANDS\n                            if (c.SYNOPSIS[1] or c.SYNOPSIS[2] or ''\n                                ).startswith(name + '/')]\n                    for scls in sorted(subs):\n                        sc, scmd, surl, ssynopsis = scls.SYNOPSIS[:4]\n                        order += 1\n                        cmd_list['_%s' % scmd] = (scmd, ssynopsis,\n                                                  scls.__doc__, order)\n                        width = max(len(scmd or surl), width)\n                    return self._success(_('Displayed help'), result={\n                        'pre': cls.__doc__,\n                        'commands': cmd_list,\n                        'width': width\n                    })\n            return self._error(_('Unknown command'))\n\n        else:\n            cmd_list = {}\n            count = 0\n            for grp in COMMAND_GROUPS:\n                count += 10\n                for cls in COMMANDS:\n                    if cls.CONFIG_REQUIRED and not config.loaded_config:\n                        continue\n                    c, name, url, synopsis = cls.SYNOPSIS[:4]\n                    if cls.ORDER[0] == grp and '/' not in (name or ''):\n                        cmd_list[c or '_%s' % name] = (name, synopsis,\n                                                       cls.__doc__,\n                                                       count + cls.ORDER[1])\n            if config.loaded_config:\n                tags = GetCommand('tags')(self.session).run(display=\"priority\")\n            else:\n                tags = {}\n            try:\n                index = self._idx()\n            except IOError:\n                index = None\n            return self._success(_('Displayed help'), result={\n                'commands': cmd_list,\n                'tags': tags,\n                'index': index\n            })\n\n    def _starting(self):\n        pass\n\n    def _finishing(self, rv, *args, **kwargs):\n        return self.CommandResult(self, self.session, self.name,\n                                  self.__doc__, rv,\n                                  self.status, self.message)\n\n\nclass HelpVars(Help):\n    \"\"\"Print help on Mailpile variables\"\"\"\n    SYNOPSIS = (None, 'help/variables', 'help/variables', None)\n    ABOUT = ('The available mailpile variables')\n    ORDER = ('Config', 9)\n    CONFIG_REQUIRED = False\n    IS_USER_ACTIVITY = True\n\n    def command(self):\n        config = self.session.config.rules\n        result = []\n        categories = [\"sys\", \"prefs\"]\n        for cat in categories:\n            variables = []\n            what = config[cat]\n            if isinstance(what[2], dict):\n                for ii, i in what[2].iteritems():\n                    stype = (\n                        _('(subsection)') if isinstance(i[1], dict) else\n                        '|'.join(i[1]) if isinstance(i[1], (list, tuple)) else\n                         str(i[1]))\n                    if '<type' in stype:\n                        stype = stype.replace('<type ', '')[1:-2]\n                    variables.append({\n                        'var': ii,\n                        'type': stype,\n                        'desc': i[0]\n                    })\n            variables.sort(key=lambda k: k['var'])\n            result.append({\n                'category': cat,\n                'name': config[cat][0],\n                'variables': variables\n            })\n        result.sort(key=lambda k: config[k['category']][0])\n        return self._success(_('Displayed variables'),\n                             result={'variables': result})\n\n\nclass HelpSplash(Help):\n    \"\"\"Print Mailpile splash screen\"\"\"\n    SYNOPSIS = (None, 'help/splash', 'help/splash', '[web_terminal]')\n    ORDER = ('Config', 9)\n    CONFIG_REQUIRED = False\n\n    def command(self, interactive=True):\n        from mailpile.auth import Authenticate\n        config = self.session.config\n\n        if not self.session.ui.interactive:\n            interactive = False\n        web_terminal = 'web_terminal' in self.args\n\n        in_browser = False\n        http_worker = config.http_worker\n        if http_worker and not web_terminal:\n            http_url = 'http://%s:%s%s/' % http_worker.httpd.sspec\n            if (mailpile.platforms.InDesktopEnvironment()\n                    and self.session.config.prefs.open_in_browser):\n                if BrowseOrLaunch.Browse(http_worker.httpd.sspec):\n                    in_browser = True\n                    time.sleep(2)\n        else:\n            http_url = ''\n\n        return self._success(_('Displayed welcome message'), result={\n            'splash': self.ABOUT,\n            'http_url': http_url,\n            'in_browser': in_browser,\n            'web_terminal': web_terminal,\n            'login_cmd': (Authenticate.SYNOPSIS[1]\n                          if not self.session.config.loaded_config else ''),\n            'interactive': interactive\n        })\n\n\n\n_plugins.register_commands(\n    Load, Optimize, Rescan, DeleteMessages,\n    BrowseOrLaunch, RunWWW, ProgramStatus, CronStatus, HealthCheck,\n    GpgCommand, ListDir, ChangeDir, CatFile, WritePID, Cleanup,\n    ConfigPrint, ConfigSet, ConfigAdd, ConfigUnset, ConfigureMailboxes,\n    ListLanguages, RenderPage, Output, Pipe,\n    Help, HelpVars, HelpSplash, Quit, IdleQuit, TrustingQQQ, Abort\n)\n"
  },
  {
    "path": "mailpile/plugins/crypto_autocrypt.py",
    "content": "# This file contains Autocrypt application logic: state database,\n# persistance, integration with Mailpile itself.\n#\n# Lower level code lives in mailpile.crypto.autocrypt, and some logic\n# has also leaked into mailpile.crypto.gpgi and mailpile.crypto.mime.\n#\n\"\"\"\nUsage examples and doctests for internal logic:\n\n>>> import time\n>>> state_db = {}\n>>> cfg = None\n>>> email = 'bre@mailpile.is'\n\n# Seed our fake state DB with some data, verify it's sane.\n>>> acr = AutocryptRecord(email, key_sig='123', prefer_encrypt='mutual')\n>>> (time.time() - acr.last_seen_ts) // 10\n0.0\n>>> (time.time() - acr.autocrypt_ts) // 10\n0.0\n>>> acr.float_ratio()  # Ratio of messages with Autocrypt?\n1.0\n>>> acr.save_to(state_db) is not None\nTrue\n>>> state_db[email][0]\n'123'\n>>> AutocryptRecord.Load(state_db, email).key_sig\n'123'\n\n# Unknown e-mails, give None as a recommendation.\n>>> str(autocrypt_recommendation(cfg,'foo@example.org', state_db=state_db))\n'None'\n\n# If prefer-encrypt is Mutual, we usually recommend encrypting...\n>>> str(autocrypt_recommendation(cfg, email, state_db=state_db))\n'encrypt (key=123)'\n\n# Recommendations change if we haven't seen any Autocrypt headers for\n# over 35 days or if we no longer have a key.\n>>> acr.autocrypt_ts -= (36 * 24 * 3600)\n>>> acr.save_to(state_db) and None\n>>> str(autocrypt_recommendation(cfg, email, state_db=state_db))\n'discourage (key=123)'\n>>> acr.key_sig = None\n>>> acr.save_to(state_db) and None\n>>> str(autocrypt_recommendation(cfg, email, state_db=state_db))\n'disable'\n\n\"\"\"\nimport base64\nimport copy\nimport datetime\nimport re\nimport time\nimport traceback\nimport urllib2\nfrom email import encoders\nfrom email.mime.base import MIMEBase\n\nimport mailpile.security as security\nfrom mailpile.conn_brokers import Master as ConnBroker\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.commands import Command\nfrom mailpile.crypto.autocrypt import *\nfrom mailpile.crypto.gpgi import GnuPG\nfrom mailpile.crypto.gpgi import OpenPGPMimeSigningWrapper\nfrom mailpile.crypto.gpgi import OpenPGPMimeEncryptingWrapper\nfrom mailpile.crypto.gpgi import OpenPGPMimeSignEncryptWrapper\nfrom mailpile.crypto.mime import UnwrapMimeCrypto, MessageAsString\nfrom mailpile.crypto.state import EncryptionInfo, SignatureInfo\nfrom mailpile.eventlog import GetThreadEvent\nfrom mailpile.mailutils.emails import Email, ExtractEmails, ClearParseCache\nfrom mailpile.mailutils.emails import MakeContentID\nfrom mailpile.plugins import PluginManager, EmailTransform\nfrom mailpile.plugins.crypto_policy import register_crypto_policy\nfrom mailpile.plugins.vcard_gnupg import PGPKeysImportAsVCards\nfrom mailpile.plugins.search import Search\nfrom mailpile.plugins.keylookup import register_crypto_key_lookup_handler\nfrom mailpile.plugins.keylookup.email_keylookup import get_pgp_key_keywords\nfrom mailpile.plugins.keylookup.email_keylookup import EmailKeyLookupHandler\nfrom mailpile.util import sha1b64\n\n\n##[ The Autocrypt State Database logic ]#####################################\n\n# FIXME: This really should be a record store, not an in-memory dict\ndef save_Autocrypt_DB(config):\n    if config.autocrypt_db:\n        config.save_pickle(config.autocrypt_db, 'autocrypt_db')\n\n\ndef get_Autocrypt_DB(config):\n    if not config.real_hasattr('autocrypt_db'):\n        try:\n            db = config.load_pickle('autocrypt_db')\n        except (IOError, EOFError):\n            db = {'state': {}}\n        config.real_setattr('autocrypt_db', db)\n    return config.autocrypt_db\n\n\nclass AutocryptRecord(object):\n    RATIO_MAX = 10000.0  # Force calculations to be floats\n    RATIO_INIT = 10000\n    RATIO_WINDOW = 100\n    INIT_ORDER = ('key_sig', 'autocrypt_ts', 'prefer_encrypt',\n                  'key_count', 'mid', 'last_seen_ts', 'seen_count',\n                  'imported_ts', 'key_ratio', 'key_info')\n\n    def __init__(self, to,\n                 key_sig=None, autocrypt_ts=None, prefer_encrypt=None,\n                 key_count=None, mid=None, last_seen_ts=None,\n                 seen_count=None, imported_ts=None, key_ratio=None,\n                 key_info=None):\n        if '@' not in to:\n            raise ValueError('To must be an e-mail address')\n        self.autocrypt_ts = int(key_sig and (autocrypt_ts or time.time()) or 0)\n        self.prefer_encrypt = prefer_encrypt or ''\n        self.key_sig = key_sig or ''\n        self.key_count = int(key_count or (key_sig and 1) or 0)\n        self.to = to\n        self.mid = mid or ''\n        self.last_seen_ts = int(last_seen_ts or autocrypt_ts or time.time())\n        self.seen_count = int(seen_count or 1)\n        self.imported_ts = (imported_ts or 0)\n        self.key_ratio = (key_ratio or (self.RATIO_INIT if key_sig else 0))\n        self.key_info = key_info or ''\n\n    def float_ratio(self):\n        return (self.key_ratio / self.RATIO_MAX)\n\n    def update_ratio(self, have_key=True):\n        window = float(min(self.seen_count, self.RATIO_WINDOW))\n        oratio = self.key_ratio\n        self.key_ratio = int(self.RATIO_MAX * (\n            ((self.key_ratio / self.RATIO_MAX) * (window-1) / window) +\n            ((1.0 if have_key else 0) / window)))\n        return (oratio != self.key_ratio)\n\n    def should_encrypt(self):\n        return (\n            self.key_sig and\n            self.prefer_encrypt == 'mutual' and\n            self.autocrypt_ts == self.last_seen_ts)\n\n    def as_list(self):\n        return [self.__getattribute__(k) for k in self.INIT_ORDER]\n\n    def as_dict(self):\n        return dict(\n            (k, self.__getattribute__(k))\n            for k in (['to'] + list(self.INIT_ORDER)))\n\n    def as_text(self):\n        return '%s' % self.as_dict()\n\n    def save_to(self, db):\n        db[canonicalize_email(self.to)] = self.as_list()\n        return self\n\n    @classmethod\n    def Load(cls, db, to, _raise=KeyError):\n        try:\n            return cls(to, *db[canonicalize_email(to)])\n        except (KeyError, AttributeError, TypeError):\n            if _raise is None:\n                return None\n            else:\n                raise _raise('Not Found')\n\n\ndef autocrypt_process_email(config, msg, msg_mid, msg_ts, sender_email,\n                            autocrypt_header=None, save_DB=False):\n    \"\"\"\n    Process an e-mail, updating the Autocrypt state database as appropriate.\n    If the state database has changed, return the new state. Otherwise None.\n    \"\"\"\n    if not config.prefs.key_tofu.autocrypt:\n        return None\n\n    db = get_Autocrypt_DB(config)['state']\n    changed = False\n    try:\n        existing = AutocryptRecord.Load(db, sender_email)\n    except KeyError:\n        existing = None\n\n    # Trying keep the DB Small: we don't store full keys, just hashes\n    # of them. When or if we actually decide to use the key it must\n    # either be findable in e-mail (not deleted) or in a keychain.\n    # Since Autocrypt is opportunistic, missing some chances to encrypt\n    # is by definition acceptable! We also deliberately do not use\n    # the key fingerprint here, as we would still like to detect and\n    # capture updates when subkeys or UIDs change.\n    try:\n        # Note: Fails if the sender_email doesn't match the addr= attribte.\n        autocrypt_header = (\n            autocrypt_header or\n            extract_autocrypt_header(msg, to=sender_email))\n\n        if autocrypt_header:\n            to = autocrypt_header['addr']\n            pe = autocrypt_header.get('prefer-encrypt')\n            key_data = autocrypt_header['keydata']\n            key_sig = sha1b64(key_data).strip()\n            # FIXME: Do we need to handle gossip headers here? If the way\n            #        we handle keys using keytofu and the search engine is\n            #        compatible with Autocrypt, then maybe no...?\n        else:\n            to = pe = key_data = key_sig = None\n\n        # Note: This algorithm differs from the update algorithm described in\n        #       the Autocrypt Level 1 spec, because we're also updating a\n        # counter that tells us how often we've seen a particular key.\n\n        # New entry or no-op; short circuit\n        if existing is None:\n            if key_sig:\n                existing = AutocryptRecord(to,\n                    key_sig=key_sig,\n                    autocrypt_ts=msg_ts,\n                    prefer_encrypt=pe,\n                    mid=msg_mid)\n                changed = True\n                return existing  # Will save in `finally` block\n            else:\n                return None\n\n        # Always update last-seen timestamp and seen counter\n        if existing.mid != msg_mid:\n            existing.seen_count += 1\n            changed = True\n        if existing.last_seen_ts < msg_ts:\n            existing.last_seen_ts = msg_ts\n            changed = True\n\n        # Same key: Update counts and policy/timestamp if newer\n        if existing.key_sig == key_sig:\n            if existing.mid != msg_mid:\n                if existing.autocrypt_ts < msg_ts:\n                    existing.autocrypt_ts = msg_ts\n                    existing.prefer_encrypt = pe\n                    existing.mid = msg_mid\n                existing.key_count += 1\n                changed = True\n\n        # Different key: If newer than what's on file, update\n        if key_sig and existing.autocrypt_ts < msg_ts:\n            existing.autocrypt_ts = msg_ts\n            existing.key_sig = key_sig\n            existing.mid = msg_mid\n            existing.prefer_encrypt = pe\n            existing.key_count = 1\n            existing.imported_ts = 0\n            changed = True\n\n        # Update our estimated ratio of how many mails have Autocrypt\n        if existing:\n            if existing.update_ratio(have_key=(key_sig is not None)):\n                changed = True\n\n        # If we made changes, return the current state. Else, None.\n        return (existing if changed else None)\n\n    except (TypeError, KeyError):\n        traceback.print_exc()\n        changed = False\n        return None\n\n    finally:\n        if changed and (existing is not None):\n            existing.save_to(db)\n            if save_DB:\n                save_Autocrypt_DB(config)\n\n\ndef autocrypt_recommendation(config, email, re_encrypted=False, state_db=None):\n    \"\"\"\n    Returns an Autocrypt Level 1 recommendation for a given e-mail address.\n    If the e-mail is not in the Autocrypt database, returns None.\n    \"\"\"\n    db = state_db or get_Autocrypt_DB(config)['state']\n    try:\n        acr = AutocryptRecord.Load(db, email)\n    except KeyError:\n        acr = None\n\n    # Not found in the Autocrypt DB, we have no opinion.\n    if not acr:\n        return None\n\n    # Notes:\n    #\n    #  - Checking whether keys are usable for encryption (expired, revoked)\n    #    is handled by mailpile.plugins.keylookup.KeyTofu; our Autocrypt\n    #    recommendations assume all that keys are usable.\n    #\n    #  - We are not handling Gossip here at all, but Gossip is handled\n    #    by Mailpile's fallback heuristics since Autocrypt Gossip headers\n    #    are considered as a source of keys to import when requested.\n    #\n    # This simplifies the logic somewhat.\n\n    # Determine if encryption is possible; short-circuit if not.\n    if not acr.key_sig:\n        return AutocryptRecommendation(AutocryptRecommendation.DISABLE)\n\n    # Phase 1: Preliminary recommendation\n    if acr.last_seen_ts - (35 * 24 * 3600) > acr.autocrypt_ts:\n        rec = AutocryptRecommendation.DISCOURAGE\n    else:\n        rec = AutocryptRecommendation.ENABLE\n\n    # Phase 2: Final recommendation\n    if re_encrypted or (\n            (rec == AutocryptRecommendation.ENABLE) and\n            ('mutual' == acr.prefer_encrypt)):\n        rec = AutocryptRecommendation.ENCRYPT\n\n    return AutocryptRecommendation(rec, key_sig=acr.key_sig)\n\n\ndef autocrypt_policy_checker(session, profile, emails):\n    AR = AutocryptRecommendation\n    acrs = []\n    baseline = 'sign' if ('S' in profile.crypto_format) else 'none'\n\n    if 'E' not in profile.crypto_format:\n        return (baseline, AR.ENABLE if profile.pgp_key else AR.DISABLE)\n\n    for email in (e for e in emails if e != profile.email):\n        acrs.append(autocrypt_recommendation(session.config, email))\n        if acrs[-1] is None:\n            return (baseline, AR.DISABLE)\n\n    policy = AR.Synchronize(*acrs)\n    return (\n        'sign-encrypt' if (policy == AR.ENCRYPT) else baseline,\n        policy)\n\n\n##[ Autocrypt integration and API commands ]###################################\n\nclass AutocryptSearch(Command):\n    \"\"\"Search for the Autocrypt database.\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/autocrypt/search', 'crypto/autocrypt/search', '<emails>')\n    HTTP_CALLABLE = ('GET', )\n    HTTP_QUERY_VARS = {'q': 'emails'}\n\n    class CommandResult(Command.CommandResult):\n        def as_text(self):\n            if self.result:\n                r = self.result\n                return '\\n'.join([\"%s: %s (should_encrypt=%s)\" % (\n                                      to,\n                                      r[to].as_dict(),\n                                      r[to].should_encrypt())\n                                  for to in sorted(r.keys())])\n            else:\n                return _(\"No results\")\n\n    def command(self):\n        args = list(self.args)\n        for q in self.data.get('q', []):\n            args.extend(q.split())\n\n        db = get_Autocrypt_DB(self.session.config)['state']\n        results = dict((e, AutocryptRecord.Load(db, e))\n                       for e in args if canonicalize_email(e) in db)\n\n        if results:\n            return self._success(_(\"Found %d results\") % len(results.keys()),\n                                 results)\n        else:\n            return self._error(_(\"Not found\"), results)\n\n\nclass AutocryptForget(Command):\n    \"\"\"Forget all Autocrypt state for a list of e-mail address.\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/autocrypt/forget', 'crypto/autocrypt/forget', '<emails>')\n    HTTP_CALLABLE = ('POST', )\n    HTTP_QUERY_VARS = {'email': 'emails'}\n\n    def command(self):\n        args = list(self.args)\n        args.extend(self.data.get('email', []))\n\n        forgot = []\n        db = get_Autocrypt_DB(self.session.config)['state']\n        for e in args:\n            if e in db:\n                del db[e]\n                forgot.append(e)\n\n        if forgot:\n            save_Autocrypt_DB(self.session.config)\n            return self._success(_(\"Forgot %d recipients\") % len(forgot),\n                                 forgot)\n        else:\n            return self._error(_(\"Not found\"))\n\n\nclass AutocryptParse(Command):\n    \"\"\"Parse the Autocrypt header from a message (or messages).\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/autocrypt/parse', 'crypto/autocrypt/parse', '<emails>')\n    HTTP_CALLABLE = ('POST', )\n\n    def command(self):\n        session, config, idx = self.session, self.session.config, self._idx()\n\n        updated = []\n        args = list(self.args)\n        for e in [Email(idx, i) for i in self._choose_messages(args)]:\n            autocrypt_meta_kwe(\n                idx, e.msg_mid(), e.get_msg(), None,\n                int(e.get_msg_info(e.index.MSG_DATE), 36),\n                update_cb=lambda u, k: updated.append((u, k)),\n                save_DB=False)\n\n        updated = [(u[0].as_dict(), sorted(list(u[1])))\n                   for u in updated if u[0] is not None]\n        if updated:\n            save_Autocrypt_DB(config)\n\n        return self._success(\"Updated %d records\" % len(updated), updated)\n\n\nclass AutocryptPeers(Command):\n    \"\"\"List known Autocrypt Peers and their state.\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/autocrypt/peers', 'crypto/autocrypt/peers', None)\n    HTTP_CALLABLE = ('POST', )\n\n    class CommandResult(Command.CommandResult):\n        def as_text(self):\n            if not self.result:\n                return _(\"No results\")\n            return '\\n'.join([\n                AutocryptRecord.Load(self.result, r).as_text()\n                for r in self.result])\n\n    def command(self):\n        session, config, idx = self.session, self.session.config, self._idx()\n        args = list(self.args)\n\n        db = get_Autocrypt_DB(config)['state']\n\n        return self._success(_(\"Found %d peers\") % len(db), db)\n\n\ndef autocrypt_meta_kwe(index, msg_mid, msg, msg_size, msg_ts,\n                       body_info=None, update_cb=None, save_DB=True):\n    \"\"\"\n    This extracts search keywords from the Autocrypt headers, and\n    updates the Autocrypt state database as a side-effect.\n    \"\"\"\n    keywords = set([])\n    config = index.config\n    if not config.prefs.key_tofu.autocrypt:\n        return keywords\n\n    mimetype = (msg.get_content_type() or '').lower()\n    if mimetype not in AUTOCRYPT_IGNORE_MIMETYPES:\n        autocrypt_header = sender = None\n\n        senders = ExtractEmails(msg['from'] or '')  # FIXME: Shitty parser?\n        if len(senders) == 1:\n            sender = senders[0]\n            autocrypt_header = extract_autocrypt_header(msg, to=sender)\n\n        if autocrypt_header:\n            keywords.add('pgp:has')\n            keywords.add('autocrypt:has')\n            key_data = autocrypt_header.get('keydata')\n            if key_data:\n                keywords |= set(get_pgp_key_keywords(key_data))\n\n            for gh in extract_autocrypt_gossip_headers(msg):\n                key_data = gh.get('keydata')\n                if key_data:\n                    keywords.add('autocrypt-gossip:has')\n                    keywords |= set(get_pgp_key_keywords(key_data))\n\n        update = autocrypt_process_email(config, msg, msg_mid, msg_ts, sender,\n                                         autocrypt_header=autocrypt_header,\n                                         save_DB=save_DB)\n        if update_cb is not None:\n            update_cb(update, keywords)\n\n    return keywords\n\n\nclass AutocryptTxf(EmailTransform):\n    \"\"\"\n    This is an outgoing email content transform for adding autocrypt headers.\n\n    Note: This transform relies on Memory Hole code elsewhere to correctly\n    obscure Gossip headers. Plugin/hook priorities must be set accordingly.\n    \"\"\"\n    def TransformOutgoing(self, sender, rcpts, msg, **kwargs):\n        matched = False\n        keydata = mutual = sender_keyid = key_binary = None\n\n        gnupg = GnuPG(self.config, event=GetThreadEvent())\n        profile = self._get_sender_profile(sender, kwargs)\n        vcard = profile['vcard']\n        if vcard is not None:\n            crypto_format = vcard.crypto_format\n            sender_keyid = vcard.pgp_key\n            if sender_keyid and 'autocrypt' in crypto_format:\n                key_binary = gnupg.get_minimal_key(key_id=sender_keyid,\n                                                   user_id=sender,\n                                                   armor=False)\n\n            if key_binary:\n                mutual = 'E' in crypto_format.split('+')[0].split(':')[-1]\n                msg[\"Autocrypt\"] = make_autocrypt_header(\n                    sender, key_binary, prefer_encrypt_mutual=mutual)\n\n                if 'encrypt' in msg.get('Encryption', '').lower():\n                    gossip_list = []\n                    for rcpt in rcpts:\n                        # FIXME: Check if any of the recipients are in the BCC\n                        #        header; omit their keys if so?\n                        try:\n                            # This *should* always succeed: if we are encrypting,\n                            # then the key we encrypt to should already be in\n                            # the keychain.\n                            if '#' in rcpt:\n                                rcpt, rcpt_keyid = rcpt.split('#')\n                            else:\n                                # This happens when composing in the CLI.\n                                rcpt_keyid = rcpt\n                            if (rcpt != sender) and rcpt_keyid:\n                                kb = gnupg.get_minimal_key(key_id=rcpt_keyid,\n                                                           user_id=rcpt,\n                                                           armor=False)\n                                if kb:\n                                    gossip_list.append(make_autocrypt_header(\n                                        rcpt, kb, prefix='Autocrypt-Gossip'))\n                        except (ValueError, IndexError):\n                            pass\n                    if len(gossip_list) > 1:\n                        # No point gossiping peoples keys back to them alone.\n                        for hdr in gossip_list:\n                            msg.add_header('Autocrypt-Gossip', hdr)\n\n                matched = True\n\n        return sender, rcpts, msg, matched, True\n\n\nclass AutocryptKeyLookupHandler(EmailKeyLookupHandler):\n    NAME = _(\"Autocrypt\")\n    PRIORITY = 4\n    TIMEOUT = 25  # 5 seconds per message we are willing to parse\n    LOCAL = True\n    PRIVACY_FRIENDLY = True\n    SCORE = 1\n\n    def __init__(self, session, *args, **kwargs):\n        EmailKeyLookupHandler.__init__(self, session, *args, **kwargs)\n\n    def _score(self, key):\n        return (self.SCORE, _('Found key using Autocrypt'))\n\n    def _db_and_acr(self, address):\n        db = get_Autocrypt_DB(self.session.config)['state']\n        try:\n            return db, AutocryptRecord.Load(db, address)\n        except KeyError:\n            return db, None\n\n    def _getkey(self, email, keyinfo):\n        db, acr = self._db_and_acr(email)\n        if acr:\n            rv = EmailKeyLookupHandler._getkey(self, email, keyinfo)\n            if self._gk_succeeded(rv):\n                acr.imported_ts = int(time.time())\n                acr.save_to(db)\n                save_Autocrypt_DB(self.session.config)\n            return rv\n        else:\n            raise ValueError('Not found in Autocrypt DB: %s' % email)\n\n    def _lookup(self, address, strict_email_match=False):\n        config, ui = self.session.config, self.session.ui\n        results = {}\n        if not (address and config.prefs.key_tofu.autocrypt):\n            return results\n\n        db, acr = self._db_and_acr(address)\n        if acr is None or not acr.key_sig or not acr.mid:\n            return results\n\n        # Note: Autocrypt gossip is handled by the normal e-mail lookups\n        for key_info, raw_key in self._get_message_keys(int(acr.mid, 36),\n                autocrypt=True, autocrypt_gossip=False, attachments=False):\n            key_sig = sha1b64(raw_key).strip()\n            if key_sig == acr.key_sig:\n                fp = key_info.fingerprint\n                results[fp] = results[key_sig] = copy.copy(key_info)\n                self.key_cache[fp] = self.key_cache[key_sig] = raw_key\n                if 'keylookup' in config.sys.debug:\n                    ui.debug('Got key from =%s: %s' % (acr.mid, key_sig,))\n            elif 'keylookup' in config.sys.debug:\n                ui.debug('Key sig %s != %s' % (key_sig, acr.key_sig))\n\n        return results\n\n\nif __name__ == \"__main__\":\n    import sys\n    import doctest\n\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS)\n    print '%s' % (results, )\n    if results.failed:\n        sys.exit(1)\n\nelse:\n    _plugins = PluginManager(builtin=__file__)\n\n    _plugins.register_meta_kw_extractor('autocrypt', autocrypt_meta_kwe)\n    _plugins.register_commands(\n        AutocryptSearch,\n        AutocryptForget,\n        AutocryptParse,\n        AutocryptPeers)\n    register_crypto_key_lookup_handler(AutocryptKeyLookupHandler)\n    register_crypto_policy('autocrypt', autocrypt_policy_checker)\n\n    # Note: we perform our transformations BEFORE the GnuPG transformations\n    # (prio 500), so the memory hole transformation can take care of hiding\n    # the Autocrypt-Gossip headers.\n    _plugins.register_outgoing_email_content_transform(\n        '400_autocrypt', AutocryptTxf)\n"
  },
  {
    "path": "mailpile/plugins/crypto_gnupg.py",
    "content": "from __future__ import print_function\nimport copy\nimport datetime\nimport re\nimport time\nimport urllib2\nfrom email import encoders\nfrom email.mime.base import MIMEBase\n\nimport mailpile.security as security\nfrom mailpile.conn_brokers import Master as ConnBroker\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.commands import Command\nfrom mailpile.crypto.gpgi import GnuPG\nfrom mailpile.crypto.gpgi import OpenPGPMimeSigningWrapper\nfrom mailpile.crypto.gpgi import OpenPGPMimeEncryptingWrapper\nfrom mailpile.crypto.gpgi import OpenPGPMimeSignEncryptWrapper\nfrom mailpile.crypto.mime import UnwrapMimeCrypto, MessageAsString\nfrom mailpile.crypto.mime import OBSCURE_HEADERS_MILD, OBSCURE_HEADERS_EXTREME\nfrom mailpile.crypto.mime import OBSCURE_HEADERS_REQUIRED, ObscureSubject\nfrom mailpile.crypto.state import EncryptionInfo, SignatureInfo\nfrom mailpile.eventlog import GetThreadEvent\nfrom mailpile.mailutils.addresses import AddressHeaderParser\nfrom mailpile.mailutils.emails import Email, MakeContentID, ClearParseCache\nfrom mailpile.plugins import PluginManager, EmailTransform\nfrom mailpile.plugins.vcard_gnupg import PGPKeysImportAsVCards\nfrom mailpile.plugins.search import Search\n\n_plugins = PluginManager(builtin=__file__)\n\n\n##[ GnuPG e-mail processing ]#################################################\n\nclass ContentTxf(EmailTransform):\n    def _wrap_key_in_html(self, title, keydata):\n        return ((\n            \"<html><head><meta charset='utf-8'></head><body>\\n\"\n            \"<h1>%(title)s</h1><p>\\n\\n%(description)s\\n\\n</p>\"\n            \"<pre>\\n%(key)s\\n</pre><hr>\"\n            \"<i><a href='%(ad_url)s'>%(ad)s</a>.</i></body></html>\"\n            ) % self._wrap_key_in_html_vars(title, keydata)).encode('utf-8')\n\n    def _wrap_key_in_html_vars(self, title, keydata):\n        return {\n            \"title\": title,\n            \"description\": _(\n                \"This is a digital encryption key, which you can use to send\\n\"\n                \"confidential messages to the owner, or to verify their\\n\"\n                \"digital signatures. You can safely discard or ignore this\\n\"\n                \"file if you do not use e-mail encryption or signatures.\"),\n            \"ad\": _(\"Generated by Mailpile and GnuPG\"),\n            \"ad_url\": \"https://www.mailpile.is/\",  # FIXME: Link to help?\n            \"key\": keydata}\n\n    def TransformOutgoing(self, sender, rcpts, msg, **kwargs):\n        # *** msg is email.mime.multipart.MIMEMultipart\n        matched = False\n        gnupg = None\n        sender_keyid = None\n\n        # Prefer to just get everything from the profile VCard, in the\n        # common case...\n        profile = self._get_sender_profile(sender, kwargs)\n        if profile['vcard'] is not None:\n            sender_keyid = profile['vcard'].pgp_key\n        crypto_format = profile.get('crypto_format') or 'none'\n\n        # Parse the openpgp_header data from the crypto_format\n        openpgp_header = [p.split(':')[-1]\n                          for p in crypto_format.split('+')\n                          if p.startswith('openpgp_header:')]\n        if not openpgp_header:\n            openpgp_header = self.config.prefs.openpgp_header and ['CFG']\n\n        if openpgp_header[0] != 'N' and not sender_keyid:\n            # This is a fallback: this shouldn't happen much in normal use\n            try:\n                gnupg = gnupg or GnuPG(self.config, event=GetThreadEvent())\n                seckeys = dict([(uid[\"email\"], fp) for fp, key\n                                in gnupg.list_secret_keys().iteritems()\n                                if key[\"capabilities_map\"].get(\"encrypt\")\n                                and key[\"capabilities_map\"].get(\"sign\")\n                                for uid in key[\"uids\"]])\n                sender_keyid = seckeys.get(sender)\n            except (KeyError, TypeError, IndexError, ValueError):\n                traceback.print_exc()\n\n        if sender_keyid and openpgp_header:\n            preference = {\n                'ES': 'signencrypt',\n                'SE': 'signencrypt',\n                'E': 'encrypt',\n                'S': 'sign',\n                'N': 'unprotected',\n                'CFG': self.config.prefs.openpgp_header\n            }[openpgp_header[0].upper()]\n            msg[\"OpenPGP\"] = (\"id=%s; preference=%s\"\n                              % (sender_keyid, preference))\n\n        if ('attach-pgp-pubkey' in msg and\n                msg['attach-pgp-pubkey'][:3].lower() in ('yes', 'tru')):\n            gnupg = gnupg or GnuPG(self.config, event=GetThreadEvent())\n            if sender_keyid:\n                keys = gnupg.list_keys(selectors=[sender_keyid])\n            else:\n                keys = gnupg.address_to_keys(AddressHeaderParser(sender).addresses_list()[0])\n\n            key_count = 0\n            for fp, key in keys.iteritems():\n                if not any(key[\"capabilities_map\"].values()):\n                    continue\n                # We should never really hit this more than once. But if we\n                # do, should still be fine.\n                keyid = key[\"keyid\"]\n                data = gnupg.get_pubkey(keyid)\n\n                try:\n                    from_name = key[\"uids\"][0][\"name\"]\n                    filename = _('Encryption key for %s') % from_name\n                except:\n                    filename = _('My encryption key')\n\n                if self.config.prefs.gpg_html_wrap:\n                    data = self._wrap_key_in_html(filename, data)\n                    ext = 'html'\n                else:\n                    ext = 'asc'\n\n                att = MIMEBase('application', 'pgp-keys')\n                att.set_payload(data)\n                encoders.encode_base64(att)\n                del att['MIME-Version']\n                att.add_header('Content-Id', MakeContentID())\n                att.add_header('Content-Disposition', 'attachment',\n                               filename=filename + '.' + ext)\n                att.signature_info = SignatureInfo(parent=msg.signature_info)\n                att.encryption_info = EncryptionInfo(parent=msg.encryption_info)\n                msg.attach(att)\n                key_count += 1\n\n            if key_count > 0:\n                msg['x-mp-internal-pubkeys-attached'] = \"Yes\"\n\n        return sender, rcpts, msg, matched, True\n\nclass CryptoTxf(EmailTransform):\n    def TransformOutgoing(self, sender, rcpts, msg,\n                          crypto_policy='none',\n                          crypto_format='default',\n                          cleaner=lambda m: m,\n                          **kwargs):\n        matched = False\n        if 'pgp' in crypto_policy or 'gpg' in crypto_policy:\n            wrapper = None\n\n            # Set defaults\n            prefer_inline = kwargs.get('prefer_inline', False)\n            if 'obscure_all_meta' in crypto_format:\n                obscured = OBSCURE_HEADERS_EXTREME\n            elif 'obscure_meta' in crypto_format:\n                obscured = OBSCURE_HEADERS_MILD\n            elif self.config.prefs.encrypt_subject:\n                obscured = copy.copy(OBSCURE_HEADERS_REQUIRED)\n                obscured['subject'] = ObscureSubject\n            else:\n                obscured = OBSCURE_HEADERS_REQUIRED\n\n            if 'sign' in crypto_policy and 'encrypt' in crypto_policy:\n                wrapper = OpenPGPMimeSignEncryptWrapper\n                prefer_inline = 'prefer_inline' in crypto_format\n            elif 'encrypt' in crypto_policy:\n                wrapper = OpenPGPMimeEncryptingWrapper\n                prefer_inline = 'prefer_inline' in crypto_format\n            elif 'sign' in crypto_policy:\n                # When signing only, we 1) prefer inline by default, based\n                # on this: https://github.com/mailpile/Mailpile/issues/1693\n                # and 2) don't obscure any headers as that's pointless.\n                wrapper = OpenPGPMimeSigningWrapper\n                prefer_inline = 'pgpmime' not in crypto_format\n                obscured = {}\n\n            if wrapper:\n                msg = wrapper(self.config,\n                              sender=sender,\n                              cleaner=cleaner,\n                              recipients=rcpts,\n                              use_html_wrapper=self.config.prefs.gpg_html_wrap,\n                              obscured_headers=obscured\n                              ).wrap(msg, prefer_inline=prefer_inline)\n                matched = True\n\n        return sender, rcpts, msg, matched, (not matched)\n\n\n_plugins.register_outgoing_email_content_transform('500_gnupg', ContentTxf)\n_plugins.register_outgoing_email_crypto_transform('500_gnupg', CryptoTxf)\n\n##[ Misc. GPG-related API commands ]##########################################\n\nclass GPGKeySearch(Command):\n    \"\"\"Search for a GPG Key.\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/gpg/searchkey', 'crypto/gpg/searchkey', '<terms>')\n    HTTP_CALLABLE = ('GET', )\n    HTTP_QUERY_VARS = {'q': 'search terms'}\n\n    class CommandResult(Command.CommandResult):\n        def as_text(self):\n            if self.result:\n                return '\\n'.join([\"%s: %s <%s>\" % (keyid, x[\"name\"], x[\"email\"]) for keyid, det in self.result.iteritems() for x in det[\"uids\"]])\n            else:\n                return _(\"No results\")\n\n    def command(self):\n        args = list(self.args)\n        for q in self.data.get('q', []):\n            args.extend(q.split())\n\n        return self._gnupg().search_key(\" \".join(args))\n\n\nclass GPGKeyReceive(Command):\n    \"\"\"Fetch a GPG Key.\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/gpg/receivekey', 'crypto/gpg/receivekey', '<keyid>')\n    HTTP_CALLABLE = ('POST', )\n    HTTP_QUERY_VARS = {'keyid': 'ID of key to fetch'}\n    COMMAND_SECURITY = security.CC_CHANGE_GNUPG\n\n    def command(self):\n        keyid = self.data.get(\"keyid\", self.args)\n        res = []\n        for key in keyid:\n            res.append(self._gnupg().recv_key(key))\n\n        # Previous crypto evaluations may now be out of date, so we\n        # clear the cache so users can see results right away.\n        ClearParseCache(pgpmime=True)\n\n        return res\n\n\nclass GPGKeyImport(Command):\n    \"\"\"Import a GPG Key.\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/gpg/importkey', 'crypto/gpg/importkey',\n                '<key_file>')\n    HTTP_CALLABLE = ('POST', )\n    HTTP_QUERY_VARS = {\n        'key_data': 'ASCII armor of public key to be imported',\n        'key_file': 'Location of file containing the public key',\n        'key_url': 'URL of file containing the public key',\n        'name': '(ignored)'\n    }\n    COMMAND_SECURITY = security.CC_CHANGE_GNUPG\n\n    def command(self):\n        key_files = self.data.get(\"key_file\", []) + [a for a in self.args\n                                                     if not '://' in a]\n        key_urls = self.data.get(\"key_url\", []) + [a for a in self.args\n                                                   if '://' in a]\n        key_data = []\n        key_data.extend(self.data.get(\"key_data\", []))\n        for key_file in key_files:\n            with open(key_file) as file:\n                key_data.append(file.read())\n        for key_url in key_urls:\n            with ConnBroker.context(need=[ConnBroker.OUTGOING_HTTP]):\n                uo = urllib2.urlopen(key_url)\n            key_data.append(uo.read())\n\n        rv = self._gnupg().import_keys('\\n'.join(key_data))\n\n        # Previous crypto evaluations may now be out of date, so we\n        # clear the cache so users can see results right away.\n        ClearParseCache(pgpmime=True)\n\n        # Update the VCards!\n        PGPKeysImportAsVCards(self.session,\n                              arg=([i['fingerprint'] for i in rv['updated']] +\n                                   [i['fingerprint'] for i in rv['imported']])\n                              ).run()\n\n        return self._success(_(\"Imported %d keys\") % len(key_data), rv)\n\n\nclass GPGKeySign(Command):\n    \"\"\"Sign a key.\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/gpg/signkey', 'crypto/gpg/signkey', '<keyid> [<signingkey>]')\n    HTTP_CALLABLE = ('POST',)\n    HTTP_QUERY_VARS = {'keyid': 'The key to sign',\n                       'signingkey': 'The key to sign with'}\n    COMMAND_SECURITY = security.CC_CHANGE_GNUPG\n\n    def command(self):\n        signingkey = None\n        keyid = None\n        args = list(self.args)\n        try: keyid = args.pop(0)\n        except: keyid = self.data.get(\"keyid\", None)\n        try: signingkey = args.pop(0)\n        except: signingkey = self.data.get(\"signingkey\", None)\n\n        print(keyid)\n        if not keyid:\n            return self._error(\"You must supply a keyid\", None)\n        rv = self._gnupg().sign_key(keyid, signingkey)\n\n        # Previous crypto evaluations may now be out of date, so we\n        # clear the cache so users can see results right away.\n        ClearParseCache(pgpmime=True)\n\n        return rv\n\n\nclass GPGKeyImportFromMail(Search):\n    \"\"\"Import a GPG Key.\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/gpg/importkeyfrommail',\n                'crypto/gpg/importkeyfrommail', '<mid>')\n    HTTP_CALLABLE = ('POST', )\n    HTTP_QUERY_VARS = {'mid': 'Message ID', 'att': 'Attachment ID'}\n    COMMAND_CACHE_TTL = 0\n    COMMAND_SECURITY = security.CC_CHANGE_GNUPG\n\n    class CommandResult(Command.CommandResult):\n        def __init__(self, *args, **kwargs):\n            Command.CommandResult.__init__(self, *args, **kwargs)\n\n        def as_text(self):\n            if self.result:\n                return \"Imported %d keys (%d updated, %d unchanged) from the mail\" % (\n                    self.result[\"results\"][\"count\"],\n                    self.result[\"results\"][\"imported\"],\n                    self.result[\"results\"][\"unchanged\"])\n            return \"\"\n\n    def command(self):\n        session, config, idx = self.session, self.session.config, self._idx()\n        args = list(self.args)\n        if args and args[-1][0] == \"#\":\n            attid = args.pop()\n        else:\n            attid = self.data.get(\"att\", 'application/pgp-keys')\n        args.extend([\"=%s\" % x for x in self.data.get(\"mid\", [])])\n        eids = self._choose_messages(args)\n        if not eids:\n            return self._error(\"No messages selected\", None)\n        elif len(eids) > 1:\n            return self._error(\"One message at a time, please\", None)\n\n        email = Email(idx, list(eids)[0])\n        fn, attr = email.extract_attachment(session, attid, mode='inline')\n        if attr and attr[\"data\"]:\n            res = self._gnupg().import_keys(attr[\"data\"])\n\n            # Previous crypto evaluations may now be out of date, so we\n            # clear the cache so users can see results right away.\n            ClearParseCache(pgpmime=True)\n\n            return self._success(\"Imported key\", res)\n\n        return self._error(\"No results found\", None)\n\n\nclass GPGKeyList(Command):\n    \"\"\"List GPG Keys.\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/gpg/keylist',\n                'crypto/gpg/keylist', '<address>')\n    HTTP_CALLABLE = ('GET', )\n    HTTP_QUERY_VARS = {'address': 'E-mail address'}\n\n    def command(self):\n        args = list(self.args)\n        if len(args) > 0:\n            addr = args[0]\n        else:\n            addr = self.data.get(\"address\", None)\n\n        if addr is None:\n            return self._error(\"Must supply e-mail address\", None)\n\n        res = self._gnupg().address_to_keys(addr)\n        return self._success(\"Searched for keys for e-mail address\", res)\n\n\nclass GPGKeyExport(Command):\n    \"\"\"Export a GPG Public Key.\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/gpg/key', 'crypto/gpg/key', '<id>')\n    HTTP_CALLABLE = ('GET', )\n    HTTP_QUERY_VARS = {\n        'key': 'ID of key to fetch; passed directly to GnuPG',\n        'download': 'Set to trigger a download instead of inline'\n    }\n\n    class CommandResult(Command.CommandResult):\n        def __init__(self, *args, **kwargs):\n            Command.CommandResult.__init__(self, *args, **kwargs)\n\n        def as_text(self):\n            if self.result:\n                return self.result[\"public_key\"]\n            return \"\"\n\n    def command(self):\n        args = list(self.args)\n        if len(args) > 0:\n            keyid = args[0]\n        else:\n            keyid = self.data.get(\"key\", None)\n\n        if keyid is None:\n            return self._error(\"Which key?\", None)\n\n        keydata = self._gnupg().export_pubkeys([keyid])\n        if not keydata:\n            return self._error(\"Key not found\", None)\n\n        if self.data.get('download', [False])[0]:\n            filename, fd = self.session.ui.open_for_data(\n                attributes={\n                    'filename': '%s.asc' % keyid,\n                    'mimetype': 'application/pgp-keys',\n                    'disposition': 'attachment'})\n            fd.write(keydata)\n            fd.close()\n\n        return self._success(_(\"Exported key\"), {\n            'key_id': keyid,\n            'public_key': keydata})\n\n\nclass GPGKeyListSecret(Command):\n    \"\"\"List secret GPG Keys, --usable omits disabled, revoked, expired.\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/gpg/keylist/secret',\n                                    'crypto/gpg/keylist/secret', '[--usable]')\n    HTTP_CALLABLE = ('GET', )\n\n    def command(self):\n    \n        all = self._gnupg().list_secret_keys()\n        if '--usable' in self.args:\n            res = {fprint : all[fprint] for fprint in all if not (\n                        all[fprint]['disabled'] or all[fprint]['revoked'] or\n                        all[fprint]['expired'])}\n        else:\n            res = all\n        return self._success(\"Searched for secret keys\", res)\n\n\nclass GPGUsageStatistics(Search):\n    \"\"\"Get usage statistics from mail, given an address\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/gpg/statistics',\n                'crypto/gpg/statistics', '<address>')\n    HTTP_CALLABLE = ('GET', )\n    HTTP_QUERY_VARS = {'address': 'E-mail address'}\n    COMMAND_CACHE_TTL = 0\n\n    class CommandResult(Command.CommandResult):\n        def __init__(self, *args, **kwargs):\n            Command.CommandResult.__init__(self, *args, **kwargs)\n\n        def as_text(self):\n            if self.result:\n                return \"%d%% of e-mail from %s has PGP signatures (%d/%d)\" % (\n                    100*self.result[\"ratio\"],\n                    self.result[\"address\"],\n                    self.result[\"pgpsigned\"],\n                    self.result[\"messages\"])\n            return \"\"\n\n    def command(self):\n        args = list(self.args)\n        if len(args) > 0:\n            addr = args[0]\n        else:\n            addr = self.data.get(\"address\", None)\n\n        if addr is None:\n            return self._error(\"Must supply an address\", None)\n\n        session, idx = self._do_search(search=[\"from:%s\" % addr])\n        total = 0\n        for messageid in session.results:\n            total += 1\n\n        session, idx = self._do_search(search=[\"from:%s\" % addr,  \"has:pgp\"])\n        pgp = 0\n        for messageid in session.results:\n            pgp += 1\n\n        if total > 0:\n            ratio = float(pgp)/total\n        else:\n            ratio = 0\n\n        res = {\"messages\": total,\n               \"pgpsigned\": pgp,\n               \"ratio\": ratio,\n               \"address\": addr}\n\n        return self._success(\"Got statistics for address\", res)\n\n\nclass GPGCheckKeys(Search):\n    \"\"\"Sanity check your keys and profiles\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/gpg/check_keys', 'crypto/gpg/check_keys',\n                '[--all-keys]')\n    HTTP_CALLABLE = ('GET', )\n    COMMAND_CACHE_TTL = 0\n\n    MIN_KEYSIZE = 2048\n\n    class CommandResult(Command.CommandResult):\n        def __init__(self, *args, **kwargs):\n            Command.CommandResult.__init__(self, *args, **kwargs)\n\n        def as_text(self):\n            if not isinstance(self.result, dict):\n                return ''\n            if self.result.get('details'):\n                message = '%s.\\n - %s' % (self.message, '\\n - '.join(\n                    p['description'] for p in self.result['details']\n                ))\n            else:\n                message = '%s. %s' % (self.message, _('Looks good!'))\n            if self.result.get('fixes'):\n                message += '\\n\\n%s\\n - %s' % (_('Proposed fixes:'),\n                                            '\\n - '.join(\n                    '\\n    * '.join(f) for f in self.result['fixes']\n                ))\n            return message\n\n    def _fix_gen_key(self, min_bits=2048):\n        return [\n            _(\"You need a new key!\"),\n            _(\"Run: %s\") % '`gpg --gen-key`',\n            _(\"Answer the tool\\'s questions: use RSA and RSA, %d bits or more\"\n              ) % min_bits]\n\n    def _fix_mp_config(self, good_key=None):\n        fprint = (good_key['fingerprint'] if good_key else '<FINGERPRINT>')\n        return [\n           _('Update the Mailpile config to use a good key:'),\n           _('IMPORTANT: This MUST be done before disabling the key!'),\n           _('Run: %s') % ('`set prefs.gpg_recipient = %s`' % fprint),\n           _('Run: %s') % ('`optimize`'),\n           _('This key\\'s passphrase will be used to log in to Mailpile')]\n\n    def _fix_revoke_key(self, fprint, comment=''):\n        return [\n            _('Revoke bad keys:') + ('  ' + comment if comment else ''),\n            _('Run: %s') % ('`gpg --gen-revoke %s`' % fprint),\n            _('Say yes to the first question, then follow the instructions'),\n            _('A revocation certificate will be shown on screen'),\n            _('Copy & paste that, save, and send to people who have the old key'),\n            _('You can search for %s to find such people'\n              ) % '`is:encrypted to:me`']\n\n    def _fix_disable_key(self, fprint, comment=''):\n        return [\n            _('Disable bad keys:') + ('  ' + comment if comment else ''),\n            _('Run: %s') % ('`gpg --edit-key %s`' % fprint),\n            _('Type %s') % '`disable`',\n            _('Type %s') % '`save`']\n\n    def command(self):\n        session, config = self.session, self.session.config\n        args = list(self.args)\n\n        all_keys = '--all-keys' in args\n        quiet = '--quiet' in args\n\n        date = datetime.date.today()\n        today = date.strftime(\"%Y-%m-%d\")\n        date += datetime.timedelta(days=14)\n        fortnight = date.strftime(\"%Y-%m-%d\")\n\n        serious = 0\n        details = []\n        fixes = []\n        bad_keys = {}\n        good_key = None\n        good_keys = {}\n        secret_keys = self._gnupg().list_secret_keys()\n\n        for fprint, info in secret_keys.iteritems():\n            k_info = {\n                'description': None,\n                'key': fprint,\n                'keysize': int(info.get('keysize', 0)),\n            }\n            is_serious = True\n            exp = info.get('expiration_date')\n            if info[\"disabled\"]:\n                k_info['description'] = _('%s: --- Disabled.') % fprint\n                is_serious = False\n            elif (not info['capabilities_map'].get('encrypt') or\n                    not info['capabilities_map'].get('sign')):\n                if info.get(\"revoked\"):\n                    k_info['description'] = _('%s: --- Revoked.'\n                                              ) % fprint\n                    is_serious = False\n                elif exp and exp <= today:\n                    k_info['description'] = _('%s: Bad: Expired on %s'\n                                              ) % (fprint,\n                                                   info['expiration_date'])\n                else:\n                    k_info['description'] = _('%s: Bad: Key is useless'\n                                              ) % fprint\n            elif exp and exp <= fortnight:\n                k_info['description'] = _('%s: Bad: Expires on %s'\n                                          ) % (fprint, info['expiration_date'])\n            elif k_info['keysize'] < self.MIN_KEYSIZE:\n                k_info['description'] = _('%s: Bad: Too small (%d bits)'\n                                          ) % (fprint, k_info['keysize'])\n            else:\n                good_keys[fprint] = info\n                if (not good_key\n                        or int(good_key['keysize']) < k_info['keysize']):\n                    good_key = info\n                k_info['description'] = _('%s: OK: %d bits, looks good!'\n                                          ) % (fprint, k_info['keysize'])\n                is_serious = False\n\n            if k_info['description'] is not None:\n                details.append(k_info)\n            if is_serious:\n                fixes += [self._fix_revoke_key(fprint, _('(optional)')),\n                          self._fix_disable_key(fprint)]\n                serious += 1\n            if fprint not in good_keys:\n                bad_keys[fprint] = info\n\n        bad_recipient = False\n        if config.prefs.gpg_recipient:\n            for k in bad_keys:\n                if k.endswith(config.prefs.gpg_recipient):\n                    details.append({\n                        'gpg_recipient': True,\n                        'description': _('%s: Mailpile config uses bad key'\n                                         ) % k,\n                        'key': k\n                    })\n                    bad_recipient = True\n                    serious += 1\n\n        if bad_recipient and good_key:\n            fixes[:0] = [self._fix_mp_config(good_key)]\n\n        profiles = config.vcards.find_vcards([], kinds=['profile'])\n        for vc in profiles:\n            p_info = {\n                'profile': vc.get('x-mailpile-rid').value,\n                'email': vc.email,\n                'fn': vc.fn\n            }\n            try:\n                if all_keys:\n                    vcls = [k.value for k in vc.get_all('key') if k.value]\n                else:\n                    vcls = [vc.get('key').value]\n            except (IndexError, AttributeError):\n                vcls = []\n            for key in vcls:\n                fprint = key.split(',')[-1]\n                if fprint and fprint in bad_keys:\n                    p_info['key'] = fprint\n                    p_info['description'] = _('%(key)s: Bad key in profile'\n                                              ' %(fn)s <%(email)s>'\n                                              ' (%(profile)s)') % p_info\n                    details.append(p_info)\n                    serious += 1\n            if not vcls:\n                p_info['description'] = _('No key for %(fn)s <%(email)s>'\n                                          ' (%(profile)s)') % p_info\n                details.append(p_info)\n                serious += 1\n\n        if len(good_keys) == 0:\n            fixes[:0] = [self._fix_gen_key(min_bits=self.MIN_KEYSIZE),\n                         self._fix_mp_config()]\n\n        if quiet and not serious:\n            return self._success('OK')\n\n        ret = self._error if serious else self._success\n        return ret(_('Sanity checked: %d keys in GPG keyring, %d profiles')\n                     % (len(secret_keys), len(profiles)),\n                   result={'passed': not serious,\n                           'details': details,\n                           'fixes': fixes})\n\n\n_plugins.register_commands(GPGKeySearch)\n_plugins.register_commands(GPGKeyReceive)\n_plugins.register_commands(GPGKeyImport)\n_plugins.register_commands(GPGKeyImportFromMail)\n_plugins.register_commands(GPGKeySign)\n_plugins.register_commands(GPGKeyList)\n_plugins.register_commands(GPGKeyExport)\n_plugins.register_commands(GPGUsageStatistics)\n_plugins.register_commands(GPGKeyListSecret)\n_plugins.register_commands(GPGCheckKeys)\n"
  },
  {
    "path": "mailpile/plugins/crypto_policy.py",
    "content": "from datetime import datetime, timedelta\n\nimport mailpile.security as security\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.vcard import VCardLine, AddressInfo\nfrom mailpile.commands import Command\nfrom mailpile.crypto.autocrypt import AutocryptRecommendation\nfrom mailpile.mailutils.emails import Email\n\n\n_plugins = PluginManager(builtin=__file__)\n\n\nVCARD_CRYPTO_POLICY = 'X-MAILPILE-CRYPTO-POLICY'\nCRYPTO_POLICIES = ['none', 'sign', 'encrypt', 'sign-encrypt',\n                   'best-effort', 'default']\n\nCRYPTO_POLICY_CHECKERS = {}\n\n\n##[ Commands ]################################################################\n\n\ndef register_crypto_policy(name, checker):\n    global CRYPTO_POLICY_CHECKERS\n    global CRYPTO_POLICIES\n    CRYPTO_POLICY_CHECKERS[name] = checker\n    if name not in CRYPTO_POLICIES:\n        CRYPTO_POLICIES.append(name)\n\n\nclass CryptoPolicyBaseAction(Command):\n    \"\"\" Base class for crypto policy commands \"\"\"\n    pass\n\n\nclass UpdateCryptoPolicyForUser(CryptoPolicyBaseAction):\n    \"\"\" Update crypto policy for a single user \"\"\"\n    SYNOPSIS = (None, 'crypto_policy/set', 'crypto_policy/set',\n                '<email address> none|sign|encrypt|sign-encrypt|default')\n    ORDER = ('Internals', 9)\n    HTTP_CALLABLE = ('POST',)\n    HTTP_QUERY_VARS = {'email': 'contact email', 'policy': 'new policy'}\n    COMMAND_SECURITY = security.CC_CHANGE_CONTACTS\n\n    def command(self):\n        email, policy = self._parse_args()\n\n        if policy not in CRYPTO_POLICIES:\n            return self._error('Policy has to be one of %s' %\n                               '|'.join(CRYPTO_POLICIES))\n\n        vcard = self.session.config.vcards.get_vcard(email)\n        if vcard:\n            with vcard:\n                vcard.crypto_policy = policy\n                vcard.save()\n            return self._success(_('Set crypto policy for %s to %s'\n                                   ) % (email, policy),\n                                 result={'email': email, 'policy:': policy})\n        else:\n            return self._error(_('No vCard for email %s!') % email)\n\n    def _parse_args(self):\n        if self.data:\n            email = unicode(self.data['email'][0])\n            policy = unicode(self.data['policy'][0])\n        else:\n            if len(self.args) != 2:\n                return self._error(_('Please provide email address and policy!'))\n\n            email = self.args[0]\n            policy = self.args[1]\n        return email, policy\n\n\n# FIXME: These decisions belong in mailpile.security!\nclass CryptoPolicy(CryptoPolicyBaseAction):\n    \"\"\"Calculate the aggregate crypto policy for a set of users\"\"\"\n    SYNOPSIS = (None, 'crypto_policy', 'crypto_policy', '[<emailaddresses>]')\n    ORDER = ('Internals', 9)\n    HTTP_CALLABLE = ('GET',)\n    HTTP_QUERY_VARS = {\n        'email': 'e-mail addresses',\n        'should-encrypt': 'Assume a base-line policy of wanting encryption'}\n\n    @classmethod\n    def ShouldAttachKey(cls, config, vcards=None, emails=None, ttl=90):\n        now = datetime.now()\n        offset = timedelta(days=ttl)  # FIXME: Magic number!\n        never = datetime.fromtimestamp(0)\n        dates = []\n\n        # who = dict of email -> vcard\n        who = dict((vc.email, vc) for vc in (vcards or []) if vc)\n        for e in emails or []:\n            if e not in who:\n                who[e] = config.vcards.get(e)\n\n        # Examine each one. The policy is to only attach a key if everyone\n        # can use keys AND someone needs a key.\n        needs_key = 0\n        for email, vc in who.iteritems():\n\n            if vc and vc.kind == 'profile':\n                continue  # Ignore self\n\n            ts = None\n            if vc:\n                try:\n                    # FIXME: This doesn't check *which* key we sent! This\n                    #        needs to be made smarter for key rollover and\n                    #        mutliple-key scenarios.\n                    ts = datetime.fromtimestamp(float(vc.pgp_key_shared))\n                except (ValueError, TypeError, AttributeError):\n                    pass\n\n            if (ts or never) + offset < now:\n                # This user hasn't been sent a key recently.\n                needs_key += 1\n\n                # Can they do crypto?\n                ratio = cls._encryption_ratio(\n                    config.background, config.index, email, minimum=1)\n                if ratio <= 0:\n                    # Nope, let's not spam them with keys\n                    return False\n\n        # Someone needs a key update, attach.\n        return (needs_key > 0)\n\n    @classmethod\n    def _vcard_policy(self, config, email):\n        vcard = config.vcards.get_vcard(email)\n        if vcard:\n            return (vcard, vcard.kind, email,\n                    vcard.crypto_policy or 'default',\n                    vcard.crypto_format or 'default')\n        return (None, None, email, 'default', 'default')\n\n    @classmethod\n    def _encryption_ratio(self, session, idx, email, minimum=5):\n        # This method needs to return quickly, so we perform the more\n        # restrictive search first before calculating a proper ratio.\n        crypto = idx.search(session, ['from:' + email, 'has:crypto'])\n        if len(crypto) < minimum:\n            return 0.0\n\n        # We also assume index order is roughly date order, which will\n        # only be true once users have used the app for a while...\n        recent = idx.search(session, ['from:' + email]).as_set()\n        recent = set(sorted(list(recent))[-25:])\n        crypto = crypto.as_set() & recent\n\n        return float(len(crypto)) / len(recent)\n\n    @classmethod\n    def crypto_policy(cls, session, idx, emails, should_encrypt=False):\n        config = session.config\n        for i in range(0, len(emails)):\n            if '<' in emails[i]:\n                emails[i] = (emails[i].split('<', 1)[1]\n                                      .split('>', 1)[0]\n                                      .rsplit('#', 1)[0].strip())\n        policies = [cls._vcard_policy(config, e) for e in set(emails)]\n        default = [(v, k, e, p, f) for v, k, e, p, f in policies\n                   if k == 'profile']\n        default = default[0] if default else (None, None, None,\n                                              'best-effort', 'send_keys')\n\n        recommendation = AutocryptRecommendation.ENABLE\n        cpolicy = default[-2]\n        cformat = default[-1]\n\n        if cpolicy in CRYPTO_POLICY_CHECKERS:\n            cpolicy, recommendation = (\n                CRYPTO_POLICY_CHECKERS[cpolicy](session, default[0], emails))\n\n        if should_encrypt and ('encrypt' not in cpolicy):\n            if 'sign' in cpolicy or 'best-effort' == cpolicy:\n                cpolicy = 'sign-encrypt'\n            else:\n                cpolicy = 'encrypt'\n\n        # Try and merge all the user policies into one. This may lead\n        # to conflicts which cannot be resolved.\n        policy = cpolicy\n        reason = None\n        for vc, kind, email, cpol, cfmt in policies:\n            if cpol and cpol not in ('default', 'best-effort', 'autocrypt'):\n                if policy in ('default', 'best-effort', 'autocrypt'):\n                    policy = cpol\n                elif policy != cpol:\n                    reason = _('Recipients have conflicting encryption '\n                               'policies.')\n                    policy = 'conflict'\n        if policy == 'default':\n            policy = 'best-effort'\n        if not reason:\n            reason = _('The encryption policy for these recipients is: %s'\n                       ) % policy\n\n        # If we don't have a key ourselves, that limits our options...\n        if default[0]:\n            if default[0].get_all('KEY'):\n                can_sign = True\n                can_encrypt = None  # not False and not True\n            else:\n                can_sign = False\n                can_encrypt = False\n                if policy in ('sign', 'sign-encrypt', 'encrypt'):\n                    reason = _('This account does not have an encryption key.')\n                    policy = 'conflict'\n                elif policy in ('default', 'best-effort'):\n                    reason = _('This account does not have an encryption key.')\n                    policy = 'none'\n        else:\n            can_sign = False\n            can_encrypt = False\n        if can_encrypt is not False:\n            can_encrypt = len([1 for v, k, e, p, f in policies\n                               if not v or not v.get_all('KEY')]) == 0\n        if not can_encrypt and 'encrypt' in policy:\n            policy = 'conflict'\n            if 'encrypt' in cpolicy:\n                reason = _('Your policy is to always encrypt, '\n                           'but we do not have keys for everyone!')\n            else:\n                reason = _('Some recipients require encryption, '\n                           'but we do not have keys for everyone!')\n\n        # If the policy is \"best-effort\", then we would like to sign and\n        # encrypt if possible/safe. The bar for signing is lower.\n        if policy == 'best-effort':\n            should_encrypt = can_encrypt\n            if should_encrypt:\n                for v, k, e, p, f in policies:\n                    if k and k == 'profile':\n                        pass\n                    elif cls._encryption_ratio(session, idx, e) < 0.8:\n                        should_encrypt = False\n                        break\n                if should_encrypt:\n                    policy = 'sign-encrypt'\n                    reason = _('We have keys for everyone!')\n            if can_sign and not should_encrypt:\n                # FIXME: Should we check if anyone is using a lame MUA?\n                policy = 'sign'\n                if can_encrypt:\n                    reason = _('Will not encrypt because '\n                               'historic data is insufficient.')\n                else:\n                    reason = _('Cannot encrypt because we '\n                               'do not have keys for all recipients.')\n\n        if 'send_keys' in cformat:\n            send_keys = cls.ShouldAttachKey(\n                config,\n                vcards=[p[0] for p in policies],\n                emails=[p[2] for p in policies if not p[0]])\n        else:\n            send_keys = False\n\n        return {\n          'reason': reason,\n          'can-sign': can_sign,\n          'can-encrypt': can_encrypt,\n          'crypto-policy': policy,\n          'crypto-format': cformat,\n          'recommendation': recommendation,\n          'send-keys': send_keys,\n          'addresses': dict([(e, AddressInfo(e, vc.fn if vc else e, vcard=vc))\n                             for vc, k, e, p, f in policies if vc])}\n\n    def command(self):\n        emails = list(self.args) + self.data.get('email', [])\n        should_encrypt = self.data.get('should-encrypt', False)\n        if len(emails) < 1:\n            return self._error('Please provide at least one email address!')\n\n        result = self.crypto_policy(self.session, self._idx(), emails,\n                                    should_encrypt=should_encrypt)\n        return self._success(result['reason'], result=result)\n\n\n_plugins.register_commands(CryptoPolicy, UpdateCryptoPolicyForUser)\n"
  },
  {
    "path": "mailpile/plugins/cryptostate.py",
    "content": "from __future__ import print_function\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.crypto.state import EncryptionInfo, SignatureInfo\n\n\n_plugins = PluginManager(builtin=__file__)\n\n\n##[ Keywords ]################################################################\n\ndef text_kw_extractor(index, msg, ctype, text, **kwargs):\n    kw = set()\n    if ('-----BEGIN PGP' in text and '\\n-----END PGP' in text):\n        kw.add('pgp:has')\n        kw.add('crypto:has')\n    return kw\n\n\ndef meta_kw_extractor(index, msg_mid, msg, msg_size, msg_ts, **kwargs):\n    kw, enc, sig = set(), set(), set()\n    def crypto_eval(part):\n        # This is generic\n        if part.encryption_info.get('status') != 'none':\n            enc.add('mp_%s-%s' % ('enc', part.encryption_info['status']))\n            kw.add('crypto:has')\n            kw.add('encryption:has')\n\n        if part.signature_info.get('status') != 'none':\n            sig.add('mp_%s-%s' % ('sig', part.signature_info['status']))\n            kw.add('crypto:has')\n            kw.add('signature:has')\n            keyinfo = part.signature_info.get('keyinfo')\n            if keyinfo:\n                kw.add('%s:sig' % keyinfo[-16:].lower())\n\n        if 'cryptostate' in index.config.sys.debug:\n            print('part status(=%s): enc=%s sig=%s' % (msg_mid,\n                part.encryption_info.get('status'),\n                part.signature_info.get('status')\n            ))\n\n        # This is OpenPGP-specific\n        if (part.encryption_info.get('protocol') == 'openpgp'\n                or part.signature_info.get('protocol') == 'openpgp'):\n            kw.add('pgp:has')\n\n        # FIXME: Other encryption protocols?\n\n    def choose_one(fmt, statuses, ordering):\n        for o in ordering:\n            for mix in ('', 'mixed-'):\n                status = (fmt % (mix+o))\n                if status in statuses:\n                    return set([status])\n        return set(list(statuses)[:1])\n\n    # Evaluate all the message parts\n    crypto_eval(msg)\n    for p in msg.walk():\n        crypto_eval(p)\n\n    # OK, we should have exactly encryption state...\n    if len(enc) < 1:\n        enc.add('mp_enc-none')\n    elif len(enc) > 1:\n        enc = choose_one('mp_enc-%s', enc, EncryptionInfo.STATUSES)\n\n    # ... and exactly one signature state.\n    if len(sig) < 1:\n        sig.add('mp_sig-none')\n    elif len(sig) > 1:\n        sig = choose_one('mp_sig-%s', sig, SignatureInfo.STATUSES)\n\n    # Emit tags for our states\n    for tname in (enc | sig):\n        tag = index.config.get_tags(slug=tname)\n        if tag:\n            kw.add('%s:in' % tag[0]._key)\n\n    if 'cryptostate' in index.config.sys.debug:\n        print('part crypto state(=%s): %s' % (msg_mid, ','.join(list(kw))))\n\n    return list(kw)\n\n_plugins.register_text_kw_extractor('crypto_tkwe', text_kw_extractor)\n_plugins.register_meta_kw_extractor('crypto_mkwe', meta_kw_extractor)\n\n\n##[ Search helpers ]##########################################################\n\ndef search(config, idx, term, hits):\n    #\n    # FIXME: Translate things like pgp:signed into a search for all the\n    #        tags that have signatures (good or bad).\n    #\n    return []\n\n_plugins.register_search_term('crypto', search)\n_plugins.register_search_term('pgp', search)\n"
  },
  {
    "path": "mailpile/plugins/dates.py",
    "content": "import time\nimport datetime\n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\n\n\n_plugins = PluginManager(builtin=__name__)\n\n\n##[ Keywords ]################################################################\n\ndef meta_kw_extractor(index, msg_mid, msg, msg_size, msg_ts, **kwargs):\n    mdate = datetime.date.fromtimestamp(msg_ts)\n    keywords = [\n        '%s:year' % mdate.year,\n        '%s:month' % mdate.month,\n        '%s:day' % mdate.day,\n        '%s-%s:yearmonth' % (mdate.year, mdate.month),\n        '%s-%s-%s:date' % (mdate.year, mdate.month, mdate.day)\n    ]\n    return keywords\n\n_plugins.register_meta_kw_extractor('dates', meta_kw_extractor)\n\n\n##[ Search terms ]############################################################\n\ndef _adjust(d):\n    if d[2] > 31:\n        d[1] += 1\n        d[2] -= 31\n    if d[1] > 12:\n        d[0] += 1\n        d[1] -= 12\n\n\ndef _mk_date(ts):\n    mdate = datetime.date.fromtimestamp(ts)\n    return '%d-%d-%d' % (mdate.year, mdate.month, mdate.day)\n\n\n_date_offsets = {\n    'today': 0,\n    'yesterday': 1,\n    'd': 1,\n    'w': 7,\n    'm': 31,\n    'q': 91\n}\n\n\ndef search(config, idx, term, hits):\n    try:\n        word = term.split(':', 1)[1].lower()\n        if '..' in term:\n            start, end = word.split('..')\n        else:\n            start = end = word\n\n        if end in _date_offsets:\n            end = _mk_date(time.time() - _date_offsets[end]*24*3600)\n        elif end[-1:] in _date_offsets:\n            do = _date_offsets[end[-1:]]\n            end = _mk_date(time.time() - int(end[:-1])*do*24*3600)\n        elif len(end) >= 9 and '-' not in end:\n            end = _mk_date(long(end))\n\n        if start in _date_offsets:\n            start = _mk_date(time.time() - _date_offsets[start]*24*3600)\n        elif start[-1:] in _date_offsets:\n            do = _date_offsets[start[-1:]]\n            start = _mk_date(time.time() - int(start[:-1])*do*24*3600)\n        elif len(start) >= 9 and '-' not in start:\n            start = _mk_date(long(start))\n\n        start = [int(p) for p in start.split('-')][:3]\n        end = [int(p) for p in end.split('-')[:3]]\n        while len(start) < 3:\n            start.append(1)\n        if len(end) == 1:\n            end.extend([12, 31])\n        elif len(end) == 2:\n            end.append(31)\n        if not start <= end:\n            raise ValueError()\n\n        terms = []\n        while start <= end:\n            # Move forward one year?\n            if start[1:] == [1, 1]:\n                ny = [start[0], 12, 31]\n                if ny <= end:\n                    terms.append('%d:year' % start[0])\n                    start[0] += 1\n                    continue\n\n            # Move forward one month?\n            if start[2] == 1:\n                nm = [start[0], start[1], 31]\n                if nm <= end:\n                    terms.append('%d-%d:yearmonth' % (start[0], start[1]))\n                    start[1] += 1\n                    _adjust(start)\n                    continue\n\n            # Move forward one day...\n            terms.append('%d-%d-%d:date' % tuple(start))\n            start[2] += 1\n            _adjust(start)\n\n        rt = []\n        for t in terms:\n            rt.extend(hits(t))\n        return rt\n    except:\n        raise ValueError('Invalid date range: %s' % term)\n\n\n_plugins.register_search_term('dates', search)\n_plugins.register_search_term('date', search)\n"
  },
  {
    "path": "mailpile/plugins/eventlog.py",
    "content": "import time\n\nimport mailpile.util\nimport mailpile.security as security\nfrom mailpile.commands import Command\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.util import *\n\n\n_plugins = PluginManager(builtin=__file__)\n\n\nclass Events(Command):\n    \"\"\"Display events from the event log\"\"\"\n    SYNOPSIS = (None, 'eventlog', 'logs/events',\n                '[incomplete] [wait] [<count>] '\n                '[<field>=<val> <f>!=<v> <f>=~<re> ...]')\n    ORDER = ('Internals', 9)\n    HTTP_CALLABLE = ('GET', )\n    HTTP_QUERY_VARS = {\n        'wait': 'seconds to wait for new data',\n        'gather': 'gather time (minimum wait), seconds',\n        'debuglog': 'include current debug-log ring buffer',\n        'incomplete': 'incomplete events only?',\n        # Filtering by event attributes\n        'event_id': 'an event ID',\n        'flag': 'require a flag',\n        'flags': 'match all flags',\n        'since': 'wait for new data?',\n        'source': 'source class',\n        # Filtering by event data (syntax is a bit weird)\n        'data': 'var:value',\n        'private_data': 'var:value'\n    }\n    LOG_NOTHING = True\n    IS_HANGING_ACTIVITY = True\n    IS_USER_ACTIVITY = False\n\n    DEFAULT_WAIT_TIME = 10.0\n    GATHER_TIME = 0.5\n\n    def command(self):\n        session, config, index = self.session, self.session.config, self._idx()\n        event_log = config.event_log\n\n        debuglog = truthy(self.data.get('debuglog', ['no'])[0])\n        incomplete = truthy(self.data.get('incomplete', ['no'])[0])\n        waiting = int(self.data.get('wait', [0])[0])\n        gather = float(self.data.get('gather', [self.GATHER_TIME])[0])\n\n        limit = 0\n        filters = {}\n        for arg in self.args:\n            if arg.lower() == 'incomplete':\n                incomplete = True\n            elif arg.lower() == 'wait':\n                waiting = self.DEFAULT_WAIT_TIME\n            elif arg.lower() == 'debuglog':\n                debuglog = True\n            elif '=' in arg:\n                field, value = arg.split('=', 1)\n                filters[unicode(field)] = unicode(value)\n            else:\n                try:\n                    limit = int(arg)\n                except ValueError:\n                    raise UsageError('Bad argument: %s' % arg)\n\n        # Handle args from the web\n        def fset(arg, val):\n            if val.startswith('!'):\n                filters[arg+'!'] = val[1:]\n            else:\n                filters[arg] = val\n        for arg in self.data:\n            if arg in ('source', 'flags', 'flag', 'since', 'event_id'):\n                fset(arg, self.data[arg][0])\n            elif arg in ('data', 'private_data'):\n                for data in self.data[arg]:\n                    var, val = data.split(':', 1)\n                    fset('%s_%s' % (arg, var), val)\n\n        # Compile regular expression matches\n        for arg in filters:\n            if filters[arg][:1] == '~':\n                filters[arg] = re.compile(filters[arg][1:])\n\n        now = time.time()\n        expire = now + waiting - gather\n        if waiting:\n            # JS sometimes sends us \"undefined\", handle it gracefully...\n            if filters.get('since', 'undefined') == 'undefined':\n                filters['since'] = now\n            if float(filters['since']) < 0:\n                filters['since'] = float(filters['since']) + now\n            time.sleep(gather)\n\n        events = []\n        while not waiting or (expire + gather) > time.time():\n            if incomplete:\n                events = list(config.event_log.incomplete(**filters))\n            else:\n                events = list(config.event_log.events(**filters))\n            if events or not waiting:\n                break\n            else:\n                config.event_log.wait(expire - time.time())\n                time.sleep(gather)\n\n        if limit:\n            if 'since' in filters:\n                events = events[:limit]\n            else:\n                events = events[-limit:]\n\n        result = {\n            'count': len(events),\n            'ts': max([0] + [e.ts for e in events]) or time.time(),\n            'events': [e.as_dict() for e in events]}\n        if debuglog:\n            log_ui = self.session.ui\n            result['since'] = since = float(filters.get('since', 0))\n            result['debuglog'] = sorted([\n                (c, t, l) for c, t, l in log_ui.term.ring_buffer if t > since])\n\n        return self._success(_('Found %d events') % len(events), result=result)\n\n\nclass Cancel(Command):\n    \"\"\"Cancel events\"\"\"\n    SYNOPSIS = (None, 'eventlog/cancel', 'logs/events/cancel', 'all|<eventIDs>')\n    ORDER = ('Internals', 9)\n    HTTP_CALLABLE = ('POST', )\n    HTTP_POST_VARS = {\n        'event_id': 'Event ID'\n    }\n    IS_USER_ACTIVITY = False\n    COMMAND_SECURITY = security.CC_CHANGE_CONFIG\n\n    def command(self):\n        if self.args and 'all' in self.args:\n            events = self.session.config.event_log.events()\n        else:\n            events = [self.session.config.event_log.get(eid)\n                      for eid in (list(self.args) +\n                                  self.data.get('event_id', []))]\n        canceled = []\n        for event in events:\n            if event and event.COMPLETE not in event.flags:\n                try:\n                    event.source_class.Cancel(self, event)\n                except (NameError, AttributeError):\n                    event.flags = event.COMPLETE\n                    self.session.config.event_log.log_event(event)\n                canceled.append(event.event_id)\n        return self._success(_('Canceled %d events') % len(canceled),\n                             canceled)\n\n\nclass Undo(Command):\n    \"\"\"Undo either the last action or one specified by Event ID\"\"\"\n    SYNOPSIS = ('u', 'undo', 'logs/events/undo', '[<Event ID>]')\n    ORDER = ('Internals', 9)\n    HTTP_CALLABLE = ('POST', )\n    HTTP_POST_VARS = {\n        'event_id': 'Event ID'\n    }\n    IS_USER_ACTIVITY = False\n\n    def command(self):\n        event_id = (self.data.get('event_id', [None])[0]\n                    or (self.args and self.args[0])\n                    or (self.session.last_event_id))\n        if not event_id:\n            return self._error(_('Need an event ID!'))\n        event = self.session.config.event_log.get(event_id)\n        if event:\n            try:\n                sc = event.source_class\n                forbid = security.forbid_command(self, sc.COMMAND_SECURITY)\n                if forbid:\n                    return self._error(forbid)\n                else:\n                    return sc.Undo(self, event)\n            except (NameError, AttributeError):\n                self._ignore_exception()\n                return self._error(_('Event %s is not undoable') % event_id)\n        else:\n            return self._error(_('Event %s not found') % event_id)\n\n\nclass Watch(Command):\n    \"\"\"Watch the events fly by\"\"\"\n    SYNOPSIS = (None, 'eventlog/watch', None, None)\n    ORDER = ('Internals', 9)\n    IS_USER_ACTIVITY = False\n    CONFIG_REQUIRED = False\n\n    def command(self):\n        config = self.session.config\n        unregister = False\n        self.session.ui.notify(\n            _('Watching logs: Press CTRL-C to return to the CLI'))\n        try:\n            while not mailpile.util.QUITTING and not config.event_log:\n                time.sleep(1)\n            unregister = (config.event_log and\n                config.event_log.ui_watch(self.session.ui))\n            self.session.ui.unblock(force=True)\n            while not mailpile.util.QUITTING:\n                time.sleep(1)\n        except KeyboardInterrupt:\n            pass\n        finally:\n            if unregister:\n                config.event_log.ui_unwatch(self.session.ui)\n        return self._success(_('That was fun!'))\n\n\n_plugins.register_commands(Events, Cancel, Undo, Watch)\n"
  },
  {
    "path": "mailpile/plugins/exporters.py",
    "content": "import mailbox\nimport os\nimport time\n\nimport mailpile.security as security\nfrom mailpile.commands import Command\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.mailutils.emails import Email\nfrom mailpile.plugins import PluginManager\nfrom mailpile.util import *\n\n\n_plugins = PluginManager(builtin=os.path.basename(__file__)[:-3])\n\n\n##[ Configuration ]###########################################################\n\nMAILBOX_FORMATS = ('mbox', 'maildir')\n\n_plugins.register_config_variables('prefs', {\n    'export_format': ['Default format for exporting mail',\n                      MAILBOX_FORMATS, 'mbox'],\n})\n\n\n##[ Commands ]################################################################\n\nclass ExportMail(Command):\n    \"\"\"Export messages to an external mailbox\"\"\"\n    SYNOPSIS = (None, 'export', None, '[-flat] [-notags] <msgs> [<fmt>:<path>]')\n    ORDER = ('Searching', 99)\n    COMMAND_SECURITY = security.CC_ACCESS_FILESYSTEM\n\n    def export_path(self, mbox_type):\n        if mbox_type == 'mbox':\n            return 'mailpile-%d.mbx' % time.time()\n        else:\n            return 'mailpile-%d'\n\n    def create_mailbox(self, mbox_type, path):\n        if mbox_type == 'mbox':\n            return mailbox.mbox(path)\n        elif mbox_type == 'maildir':\n            return mailbox.Maildir(path)\n        raise UsageError('Invalid mailbox type: %s (must be mbox or maildir)'\n                         % mbox_type)\n\n    def command(self, save=True):\n        session, config, idx = self.session, self.session.config, self._idx()\n        mbox_type = config.prefs.export_format\n\n        args = list(self.args)\n        if args and ':' in args[-1]:\n            mbox_type, path = args.pop(-1).split(':', 1)\n        else:\n            path = self.export_path(mbox_type)\n\n        flat = notags = False\n        while args and args[0][:1] == '-':\n            option = args.pop(0).replace('-', '')\n            if option == 'flat':\n                flat = True\n            elif option == 'notags':\n                notags = True\n\n        if os.path.exists(path):\n            return self._error('Already exists: %s' % path)\n\n        msg_idxs = list(self._choose_messages(args))\n        if not msg_idxs:\n            session.ui.warning('No messages selected')\n            return False\n\n        # Exporting messages without their threads barely makes any\n        # sense.\n        if not flat:\n            for i in reversed(range(0, len(msg_idxs))):\n                mi = msg_idxs[i]\n                msg_idxs[i:i+1] = [int(m[idx.MSG_MID], 36)\n                                   for m in idx.get_conversation(msg_idx=mi)]\n\n        # Let's always export in the same order. Stability is nice.\n        msg_idxs.sort()\n\n        try:\n            mbox = self.create_mailbox(mbox_type, path)\n        except (IOError, OSError):\n            mbox = None\n        if mbox is None:\n            if not os.path.exists(os.path.dirname(path)):\n                reason = _('Parent directory does not exist.')\n            else:\n                reason = _('Is the disk full? Are permissions lacking?')\n            return self._error(_('Failed to create mailbox: %s') % reason)\n\n        exported = {}\n        failed = []\n        while msg_idxs:\n            msg_idx = msg_idxs.pop(0)\n            if msg_idx not in exported:\n                e = Email(idx, msg_idx)\n                session.ui.mark(_('Exporting message =%s ...') % e.msg_mid())\n                fd = e.get_file()\n                if fd is None:\n                    failed.append(e.msg_mid())\n                    session.ui.warning(_('Message =%s is unreadable! Skipping.'\n                                         ) % e.msg_mid())\n                    continue\n                try:\n                    data = fd.read()\n                    if not notags:\n                        tags = [tag.slug for tag in\n                                (self.session.config.get_tag(t) or t for t\n                                 in e.get_msg_info(idx.MSG_TAGS).split(',')\n                                 if t)\n                                if hasattr(tag, 'slug')]\n                        lf = '\\r\\n' if ('\\r\\n' in data[:200]) else '\\n'\n                        header, body = data.split(lf+lf, 1)\n                        data = str(lf.join([\n                            header,\n                            'X-Mailpile-Tags: ' + '; '.join(sorted(tags)\n                                                            ).encode('utf-8'),\n                            '',\n                            body\n                        ]))\n                    mbox.add(data.replace('\\r\\n', '\\n'))\n                    exported[msg_idx] = 1\n                finally:\n                    fd.close()\n\n        mbox.flush()\n        result = {\n            'exported': len(exported),\n            'created': path\n        }\n        if failed:\n            result['failed'] = failed\n        return self._success(\n            _('Exported %d messages to %s') % (len(exported), path),\n            result)\n\n_plugins.register_commands(ExportMail)\n"
  },
  {
    "path": "mailpile/plugins/groups.py",
    "content": "from mailpile.commands import Command\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.plugins.tags import AddTag, DeleteTag, Filter\nfrom mailpile.plugins.contacts import *\n\n\n_plugins = PluginManager(builtin=__file__)\n\n\n##[ Search terms ]############################################################\n\ndef search(config, idx, term, hits):\n    group = config._vcards.get(term.split(':', 1)[1])\n    rt, emails = [], []\n    if group and group.kind == 'group':\n        for email, attrs in group.get('EMAIL', []):\n            group = config._vcards.get(email.lower(), None)\n            if group:\n                emails.extend([e[0].lower() for e in group.get('EMAIL', [])])\n            else:\n                emails.append(email.lower())\n    fromto = term.startswith('group:') and 'from' or 'to'\n    for email in set(emails):\n        rt.extend(hits('%s:%s' % (email, fromto)))\n    return rt\n\n_plugins.register_search_term('group', search)\n_plugins.register_search_term('togroup', search)\n\n\n##[ Commands ]################################################################\n\ndef GroupVCard(parent):\n    \"\"\"A factory for generating group commands\"\"\"\n\n    class GroupVCardCommand(parent):\n        SYNOPSIS = tuple([(t and t.replace('vcard', 'group') or t)\n                          for t in parent.SYNOPSIS])\n        KIND = 'group'\n        ORDER = ('Tagging', 4)\n\n        def _valid_vcard_handle(self, vc_handle):\n            # If there is already a tag by this name, complain.\n            return (vc_handle and\n                   ('-' != vc_handle[0]) and\n                   ('@' not in vc_handle) and\n                   (not self.session.config.get_tag_id(vc_handle)))\n\n        def _prepare_new_vcard(self, vcard):\n            session, handle = self.session, vcard.nickname\n            return (AddTag(session, arg=[handle]).run() and\n                    Filter(session, arg=['add', 'group:%s' % handle,\n                                         '+%s' % handle, vcard.fn]).run())\n\n        def _add_from_messages(self):\n            raise ValueError('Invalid group ids: %s' % self.args)\n\n        def _pre_delete_vcard(self, vcard):\n            session, handle = self.session, vcard.nickname\n            return (Filter(session, arg=['delete',\n                                         'group:%s' % handle]).run() and\n                    DeleteTag(session, arg=[handle]).run())\n\n    return GroupVCardCommand\n\n\nclass Group(GroupVCard(VCard)):\n    \"\"\"View groups\"\"\"\n\n\nclass AddGroup(GroupVCard(AddVCard)):\n    \"\"\"Add groups\"\"\"\n\n\nclass GroupAddLines(GroupVCard(VCardAddLines)):\n    \"\"\"Add lines to a group VCard\"\"\"\n\n\nclass RemoveGroup(GroupVCard(RemoveVCard)):\n    \"\"\"Remove groups\"\"\"\n\n\nclass ListGroups(GroupVCard(ListVCards)):\n    \"\"\"Find groups\"\"\"\n\n\n_plugins.register_commands(Group, AddGroup, GroupAddLines,\n                           RemoveGroup, ListGroups)\n"
  },
  {
    "path": "mailpile/plugins/gui.py",
    "content": "import json\nimport select\nimport socket\nimport threading\nimport time\n\nimport mailpile.auth\nimport mailpile.util\nfrom mailpile.conn_brokers import Master as ConnBroker\nfrom mailpile.commands import Command\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.security import GetUserSecret\nfrom mailpile.ui import Session\nfrom mailpile.util import *\n\n\n_plugins = PluginManager(builtin=__file__)\n_GUIS = {}\n\n\ndef UpdateGUIState():\n    for gui in _GUIS.values():\n        gui.change_state()\n\n\nclass GuiOMaticConnection(threading.Thread):\n    def __init__(self, config, sock, main=False):\n        threading.Thread.__init__(self)\n        self.daemon = True\n        self.config = config\n        self._am_main = True # main\n        self._sock = sock\n        self._state = self._state_startup\n        self._lock = threading.Lock()\n        self._notified = {}\n\n    def _do(self, command, **args):\n        try:\n            if self._sock:\n                self._sock.sendall('%s %s\\n' % (command, json.dumps(args)))\n        except IOError:\n            if self._am_main:\n                from mailpile.plugins.core import Quit\n                Quit(self.config.background, 'quit').run()\n            self._sock = False\n\n    def _select_sleep(self, seconds):\n        select.select([self._sock], [], [self._sock], seconds)\n\n    def _state_startup(self, in_state):\n        if in_state:\n            self._do('set_status', status='startup')\n            self._do('notify_user', message=_('Connected'))\n            if self._am_main:\n                self._do('set_item', id='quit', label=_(\"Shutdown Mailpile\"))\n                self._do('set_item', id='quit_button', label=_(\"Shutdown\"))\n            for ss in ('mailpile', 'logged-in', 'remote-access'):\n                self._do('set_status_display', id=ss, color='#999')\n        else:\n            self._select_sleep(1)\n            self._do('hide_splash_screen')\n            self._do('show_main_window')\n            self._do('set_item', id='main', sensitive=True)\n            self._do('set_item', id='browse', sensitive=True)\n            self._do('set_status_display',\n                id='mailpile',\n                color='#777',\n                icon='image:logo',\n                title=_('Welcome to Mailpile!'),\n                details=_('Mailpile is now running on this computer.'))\n\n    def _state_need_setup(self, in_state):\n        if in_state:\n            self._do('set_status', status='attention')\n            self._do('set_status_display',\n                id=\"logged-in\",\n                color='#333',\n                icon='image:new-setup',\n                title=_('Brand new installation!'),\n                details=(\n                    _('This appears to be a new installation of Mailpile!')\n                    + '\\n' +\n                    _('You need to choose a language, password and privacy policy.')\n                    + '\\n' +\n                    _('To proceed, open Mailpile in your web browser.')))\n            self._do('set_item', id='open', label=_(\"Get Started!\"))\n\n    def _state_please_log_in(self, in_state):\n        if in_state:\n            self._do('set_status', status='attention')\n            self._do('set_status_display',\n                id=\"logged-in\",\n                color='#333',\n                icon='image:logged-out',\n                title=_('Not logged in'),\n                details=(_('Your data is stored encrypted and is'\n                           ' inaccessible until you log in.')\n                         + '\\n' +\n                         _('To proceed, open Mailpile in your web browser.')))\n            self._do('set_item', id='open', label=_(\"Log In\"))\n\n    def _state_loading_index(self, in_state):\n        if in_state:\n            self._do('set_status', status='working')\n            self._do('set_status_display',\n                id='logged-in',\n                color='#444',\n                title=_('Logging you in...'))\n            self._do('set_item', id='open', sensitive=False)\n\n    def _state_logged_in(self, in_state):\n        if in_state:\n            self._do('set_status', status='normal')\n            self._do('set_status_display',\n                id='logged-in',\n                icon='image:logged-in',\n                color='#666',\n                title=_('You are logged in'),\n                details=_('Mailpile can now process and display your e-mail.'))\n            self._do('set_item',\n                id='open',\n                sensitive=True,\n                label=_(\"Open E-mail\"))\n            self._do('set_status_display',\n                id='mailpile',\n                color='#333')\n\n    def _state_shutting_down(self, in_state):\n        if in_state:\n            self._do('set_status', status='shutdown')\n            self._do('notify_user', message=_('Shutting down'))\n            self._do('set_item', id='open', sensitive=False)\n\n    def _choose_state(self):\n        from mailpile.plugins.setup_magic import Setup\n        if mailpile.util.QUITTING:\n            return self._state_shutting_down\n        elif Setup.Next(self.config, 'anything') != 'anything':\n            return self._state_need_setup\n        elif not self.config.loaded_config:\n            return self._state_please_log_in\n        elif self.config.index_loading:\n            return self._state_loading_index\n        else:\n            return self._state_logged_in\n\n    def change_state(self):\n        with self._lock:\n            next_state = self._choose_state()\n            if next_state != self._state:\n                self._state(False)\n                self._state = next_state\n                self._state(True)\n                return True\n            else:\n                label = None\n\n                if self.config.index_loading:\n                    pass  # new_mail_notifications handles this\n                elif self._state in (self._state_need_setup,):\n                    label =  _('Mailpile') + ': ' + _('New Installation')\n                elif self._state not in (\n                        self._state_logged_in, self._state_shutting_down):\n                    label = _('Mailpile') + ': ' + _('Please log in')\n\n                # FIXME: We rely on sending garbage over the socket\n                #        regularly to check for errors. When that is\n                #        gone we might not need to be so chatty.\n                #        Until then: do not remove, it breaks shutdown!\n                if label:\n                    self._do('set_item', id=\"notification\", label=label)\n\n                return False\n\n    def new_mail_notifications(self, summarize=False):\n        # FIXME: This is quite a lot of set operations that don't really\n        #        belong here. Or do they? This feels out of place.\n        if self.config.index_loading:\n            self._do('set_status_display',\n                id='logged-in',\n                details=_(\n                    'Loaded metadata for {num} messages so far, please wait.'\n                    ).format(num=len(self.config.index_loading.INDEX)))\n            return\n        if self._state != self._state_logged_in:\n            return\n        if not self._notified:\n            summarize = True\n\n        new_messages = set([])\n        for tag in self.config.get_tags(type='unread'):\n            new_messages |= self.config.index.TAGS.get(tag._key, set([]))\n\n        hidden_messages = set([])\n        for tag in self.config.get_tags(flag_hides=True):\n            hidden_messages |= self.config.index.TAGS.get(tag._key, set([]))\n\n        notify = {}\n        for tag in self.config.get_tags(notify_new=True):\n            already_notified = self._notified.get(tag._key, set([]))\n            all_in_tag = (self.config.index.TAGS.get(tag._key, set([]))\n                          - hidden_messages)\n            new_in_tag = (all_in_tag & new_messages)\n            new_new_in_tag = (all_in_tag - already_notified)\n            if new_in_tag or new_new_in_tag:\n                notify[tag._key] = (tag, new_in_tag, new_new_in_tag)\n            self._notified[tag._key] = all_in_tag\n\n        all_new = set([])\n        all_new_new = set([])\n        for tag, new_in_tag, new_new_in_tag in notify.values():\n            all_new |= new_in_tag\n            all_new_new |= new_new_in_tag\n        unread = len(all_new)\n        count = len(all_new_new)\n\n        if count == 1:\n            # FIXME: There is only one brand new message.\n            #        Tell the user more about it.\n            pass\n\n        tag_count = len(notify.keys())\n        if (tag_count == 0) and (count == 0):\n            message=_('No new mail, {num} messages total.'\n                      ).format(num=len(self.config.index.INDEX))\n\n        elif tag_count == 1:\n            tag, new_msgs, new_new_msgs = notify.values()[0]\n            if new_new_msgs and not summarize:\n                message=_('{tagName}: {new} new messages ({num} unread)'\n                          ).format(new=len(new_new_msgs),\n                                   num=len(new_msgs),\n                                   tagName=tag.name)\n            else:\n                message=_('{tagName}: {num} unread messages'\n                          ).format(num=len(new_msgs), tagName=tag.name)\n        else:\n            message=_('You have {num} unread messages in {tags} tags'\n                      ).format(num=unread, tags=tag_count)\n\n        self._do('notify_user', popup=(count > 0), message=message)\n\n\n    def run(self):\n        tid = self.ident\n        try:\n            with self._lock:\n                _GUIS[tid] = self\n                self._state(True)\n            self.new_mail_notifications(summarize=True)\n            loop_count = 0\n            while self._sock:\n                loop_count += 1\n                self._select_sleep(1)  # FIXME: Lengthen this when possible\n                self.change_state()\n                if loop_count % 5 == 0:\n                    # FIXME: This involves a fair number of set operations,\n                    #        should only do this after new mail has arrived.\n                    self.new_mail_notifications()\n        finally:\n            del _GUIS[tid]\n\n\nclass ConnectToGuiOMatic(Command):\n    \"\"\"Connect to a waiting gui-o-matic GUI\"\"\"\n    SYNOPSIS = (None, 'gui', 'gui', '[<secret>] [main|watch] <port>')\n    ORDER = ('Internals', 9)\n    CONFIG_REQUIRED = False\n    IS_USER_ACTIVITY = False\n    HTTP_CALLABLE = ('GET', 'POST')\n    HTTP_AUTH_REQUIRED = False\n\n    def command(self):\n        if self.data.get('_method'):\n            secret, style, port = self.args\n            if secret != GetUserSecret(self.session.config.workdir):\n                raise AccessError('Invalid User Secret')\n        elif len(self.args) == 2:\n            style, port = self.args\n        elif len(self.args) == 1:\n            style, port = 'main', self.args[0]\n\n        with ConnBroker.context(need=[ConnBroker.OUTGOING_RAW]):\n            guic = GuiOMaticConnection(\n                self.session.config,\n                socket.create_connection(('localhost', int(port))),\n                main=(style == 'main'))\n        guic.start()\n\n        return self._success(\"OK\")\n\n\n_plugins.register_commands(ConnectToGuiOMatic)\n"
  },
  {
    "path": "mailpile/plugins/html_magic.py",
    "content": "# This plugin generates Javascript, HTML or CSS fragments based on the\n# current theme, skin and active plugins.\n#\n# It also takes care of safely downloading random stuff from the Internet,\n# using the appropriate proxying policies.\n#\nfrom urllib2 import urlopen, HTTPError\n\nimport mailpile.security as security\nfrom mailpile.commands import Command\nfrom mailpile.conn_brokers import Master as ConnBroker\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.plugins.core import RenderPage\nfrom mailpile.ui import SuppressHtmlOutput\nfrom mailpile.urlmap import UrlMap\nfrom mailpile.util import *\n\n\n_plugins = PluginManager(builtin=__file__)\n\n\n##[ Commands ]################################################################\n\nclass JsApi(RenderPage):\n    \"\"\"Output API bindings, plugin code and CSS as CSS or Javascript\"\"\"\n    SYNOPSIS = (None, None, 'jsapi', None)\n    ORDER = ('Internals', 0)\n    HTTP_CALLABLE = ('GET', )\n    HTTP_AUTH_REQUIRED = 'Maybe'\n    HTTP_QUERY_VARS = {'ts': 'Cache busting timestamp'}\n\n    def max_age(self):\n        # Set a long TTL if we know which version of the config this request\n        # applies to, as changed config should avoid the outdated cache.\n        if 'ts' in self.data:\n            return 7 * 24 * 3600\n        else:\n            return 30\n\n    def etag_data(self):\n        # This summarizes the config state this page depends on, for\n        # generating an ETag which the HTTPD can use for caching.\n        config = self.session.config\n        return ([config.version,\n                 config.timestamp,\n                 # The above should be enough, the rest is belt & suspenders\n                 config.prefs.language,\n                 config.web.setup_complete] +\n                sorted(config.sys.plugins))\n\n    def command(self, save=True, auto=False):\n        res = {\n            'api_methods': [],\n            'javascript_classes': [],\n            'css_files': []\n        }\n        if self.args:\n            # Short-circuit if we're serving templates...\n            return self._success(_('Serving up API content'), result=res)\n\n        session, config = self.session, self.session.config\n        urlmap = UrlMap(session)\n        for method in ('GET', 'POST', 'UPDATE', 'DELETE'):\n            for cmd in urlmap._api_commands(method, strict=True):\n                cmdinfo = {\n                    \"url\": cmd.SYNOPSIS[2],\n                    \"method\": method\n                }\n                if hasattr(cmd, 'HTTP_QUERY_VARS'):\n                    cmdinfo[\"query_vars\"] = cmd.HTTP_QUERY_VARS\n                if hasattr(cmd, 'HTTP_POST_VARS'):\n                    cmdinfo[\"post_vars\"] = cmd.HTTP_POST_VARS\n                if hasattr(cmd, 'HTTP_OPTIONAL_VARS'):\n                    cmdinfo[\"optional_vars\"] = cmd.OPTIONAL_VARS\n                res['api_methods'].append(cmdinfo)\n\n        created_js = []\n        for cls, filename in sorted(list(\n                config.plugins.get_js_classes().iteritems())):\n            try:\n                parts = cls.split('.')[:-1]\n                for i in range(1, len(parts)):\n                    parent = '.'.join(parts[:i+1])\n                    if parent not in created_js:\n                        res['javascript_classes'].append({\n                            'classname': parent,\n                            'code': ''\n                        })\n                        created_js.append(parent)\n                with open(filename, 'rb') as fd:\n                    res['javascript_classes'].append({\n                        'classname': cls,\n                        'code': fd.read().decode('utf-8')\n                    })\n                    created_js.append(cls)\n            except (OSError, IOError, UnicodeDecodeError):\n                self._ignore_exception()\n\n        for cls, filename in sorted(list(\n                config.plugins.get_css_files().iteritems())):\n            try:\n                with open(filename, 'rb') as fd:\n                    res['css_files'].append({\n                        'classname': cls,\n                        'css': fd.read().decode('utf-8')\n                    })\n            except (OSError, IOError, UnicodeDecodeError):\n                self._ignore_exception()\n\n        return self._success(_('Generated Javascript API'), result=res)\n\n\nclass ProgressiveWebApp(RenderPage):\n    \"\"\"Output PWA Manifest\"\"\"\n    SYNOPSIS = (None, None, 'jsapi/pwa', None)\n    ORDER = ('Internals', 0)\n    HTTP_CALLABLE = ('GET', )\n    HTTP_AUTH_REQUIRED = False\n    HTTP_QUERY_VARS = {'ts': 'Cache busting timestamp'}\n\n    def command(self):\n        return self._success(_('Rendered Progressive Web App Data'), result={})\n\n\nclass HttpProxyGetRequest(Command):\n    \"\"\"HTTP GET content from the public web\"\"\"\n    SYNOPSIS = (None, None, 'http_proxy', None)\n    ORDER = ('Internals', 0)\n    RAISES = (AccessError, SuppressHtmlOutput)\n    HTTP_CALLABLE = ('GET', )\n    HTTP_AUTH_REQUIRED = True\n    HTTP_QUERY_VARS = {\n        'ts': 'Cache busting timestamp',\n        'timeout': 'Timeout in seconds',\n        'url': 'URL to fetch',\n        'csrf': 'CSRF token'\n    }\n\n    def command(self):\n        session = self.session\n        html_variables = session.ui.html_variables\n\n        if not (html_variables and\n                session.ui.valid_csrf_token(self.data.get('csrf', [''])[0])):\n            raise AccessError('Invalid CSRF token')\n\n        url = self.data['url'][0]\n        timeout = float(self.data.get('timeout', ['10'])[0])\n\n        conn_reject = []  # FIXME: reject ConnBroker.OUTGOING_TRACKABLE ?\n        if url[:6].lower() == 'https:':\n            conn_need = [ConnBroker.OUTGOING_HTTP]\n        elif url[:5].lower() == 'http:':\n            conn_need = [ConnBroker.OUTGOING_HTTPS]\n        else:\n            raise AccessError('Invalid URL scheme')\n\n        try:\n            with ConnBroker.context(need=conn_need, reject=conn_reject) as ctx:\n                session.ui.mark('Getting: %s' % url)\n                response = urlopen(url, data=None, timeout=timeout)\n        except HTTPError as e:\n            response = e\n\n        data = response.read()\n        headers = response.headers\n        contenttype = headers.get('content-type', 'application/octet-stream')\n\n        request = html_variables['http_request']\n        request.send_http_response(response.code, response.msg)\n        request.send_standard_headers(mimetype=contenttype,\n                                      header_list=[('Content-Length',\n                                                    len(data))])\n        request.wfile.write(data)\n\n        raise SuppressHtmlOutput()\n\n\n_plugins.register_commands(JsApi, ProgressiveWebApp, HttpProxyGetRequest)\n"
  },
  {
    "path": "mailpile/plugins/keylookup/__init__.py",
    "content": "# FIXME: We would like to implement the following priority for sources of keys:\n#\n#       1. Local validated        +User Prefs First, +Happy experts\n#       2. Autocrypt              +Direct knowledge of e-mail client config\n#       3. Autocrypt gossip       +Direct knowledge of thead requirements\n#       - - - - - - - - - - -[ The metadata leakage line ]- - - - - - - - - -\n#       4. WKD                    +Organizational validity, -Admin abuse\n#       5. Validating keyserver   +User control, +Confirmed validity, -Central\n#       - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n#       6. Local unvalidated      +User Prefs, -Lots of garbage?\n#\n# Our current strategy is close, but is missing the nuanced interaction with the\n# local shared keychain.\n#\nimport copy\nimport math\nimport traceback\nimport ssl\nimport urllib\nimport urllib2\nfrom mailpile.commands import Command\nfrom mailpile.conn_brokers import Master as ConnBroker\nfrom mailpile.crypto import gpgi\nfrom mailpile.crypto.gpgi import GnuPG\nfrom mailpile.crypto.keyinfo import KeyUID, KeyInfo, MailpileKeyInfo\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.mailutils.emails import ClearParseCache\nfrom mailpile.plugins import PluginManager\nfrom mailpile.plugins.vcard_gnupg import PGPKeysImportAsVCards\nfrom mailpile.security import secure_urlget\nfrom mailpile.util import *\nfrom mailpile.vcard import AddressInfo, VCardLine, MailpileVCard\n\n\n__all__ = ['email_keylookup', 'wkd']\n\nKEY_LOOKUP_HANDLERS = []\nTOFU_CHECK_HISTORY = {}\n\n\n##[ Internal code, functions ]################################################\n\ndef register_crypto_key_lookup_handler(handler):\n    if handler not in KEY_LOOKUP_HANDLERS:\n        KEY_LOOKUP_HANDLERS.append(handler)\n    KEY_LOOKUP_HANDLERS.sort(\n        key=lambda h: (0 if h.LOCAL else 1, h.PRIORITY, -h.SCORE))\n\n\ndef _score_validity(validity, local=False):\n    if \"r\" in validity:\n        return (-1000, _('Encryption key is revoked'))\n    elif \"d\" in validity:\n        return (-1000, _('Encryption key is disabled'))\n    elif \"e\" in validity:\n        return (-100, _('Encryption key has expired'))\n    elif local and (\"f\" in validity or \"u\" in validity):\n        return (50, _('Encryption key has been imported and verified'))\n    return (0, '')\n\n\n# FIXME: https://leap.se/en/docs/design/transitional-key-validation\n#        ... provides a very structured ranking for keys coming from\n#        different types of sources.  Check it!\ndef _update_scores(session, key_id, key_info, known_keys_list):\n    \"\"\"Update scores and score explanations\"\"\"\n\n    # This is done here, potentially overriding the keychain lookup handler,\n    # in case for some reason (e.g. UID changes on source keys) remote sources\n    # suggest matches which our local search doesn't catch.\n    if key_id in known_keys_list:\n        score, reason = _score_validity(known_keys_list[key_id][\"validity\"],\n                                        local=True)\n        if score == 0:\n            score = 9\n            reason = _('Encryption key has been imported')\n\n        if score > 0 and key_info.is_pinned:\n            score = 99\n            reason = _('Encryption key has been imported and pinned')\n\n        key_info.on_keychain = True\n        key_info.scores['Known encryption keys'] = [score, reason]\n\n    # FIXME: For this to work better, we need a list of signing subkeys.\n    #        However, if a match is found then that counts, so we use it.\n    if session:\n        msgs = session.config.index.search(\n            session, ['sig:' + key_id[-16:].lower()]).as_set()\n        score = int(math.log(len(msgs) + 1, 2))\n        if score:\n            reason = _('Signature seen on %d messages') % len(msgs)\n            key_info.scores['Used to sign e-mail'] = [score, reason]\n\n    if key_info.keysize:\n        bits = int(key_info.keysize)\n\n        if key_info.keytype_name.startswith('Ed'):\n            score = 4\n        else:\n            score = min(3, bits // 1024)  # Cap RSA keys at score=3\n\n        if score >= 4:\n          key_strength = _('Encryption key is very strong')\n        elif score >= 3:\n          key_strength = _('Encryption key is strong')\n        elif score >= 2:\n          key_strength = _('Encryption key is strong enough')\n        else:\n          key_strength = _('Encryption key is weak')\n\n        key_info.scores['Encryption key strength'] = [score, key_strength]\n\n    key_info.score = sum(score for source, (score, reason)\n                         in key_info.scores.iteritems())\n\n    sc, reason = max([(abs(score), reason)\n                     for score, reason in key_info['scores'].values()])\n    key_info.score_reason = '%s' % reason\n\n    log_score = math.log(3 * abs(key_info.score), 3)\n    key_info.score_stars = (max(1, min(int(round(log_score)), 5))\n                            * (-1 if (key_info['score'] < 0) else 1))\n\n\ndef _normalize_key(session, key_info):\n    \"\"\"Make sure expected attributes are on all keys\"\"\"\n    if not key_info.uids:\n        key_info.uids.append(KeyUID())\n    for uid in key_info.uids:\n        uid.name = uid.name or _('Anonymous')\n        e = uid.email\n        if e and e not in key_info.vcards:\n            vcard = session.config.vcards.get_vcard(e)\n            if vcard:\n                ai = AddressInfo(e, uid.name, vcard=vcard)\n                key_info.vcards[e] = ai\n                if vcard.pgp_key == key_info.fingerprint:\n                    key_info.is_preferred = True\n                    if vcard.pgp_key_pinned:\n                        key_info.is_pinned = True\n    key_info.origins = list(set(key_info.origins))\n    if not key_info.is_pinned:\n        key_info.is_pinned = False\n    if not key_info.is_preferred:\n        key_info.is_preferred = False\n    if not key_info.is_autocrypt:\n        key_info.is_autocrypt = False\n\n\ndef _mailpile_key_list(gpgi_key_list):\n    result = {}\n    for info in gpgi_key_list.values():\n        mki = MailpileKeyInfo.FromGPGI(info)\n        result[mki.summary()] = mki\n    return result\n\n\ndef lookup_crypto_keys(session, address,\n                       event=None, strict_email_match=False, allowremote=True,\n                       origins=None, get=None, vcard=None, only_good=False,\n                       pin_key=False, known_keys_list=None):\n    config = (session and session.config)\n    found_keys = {}\n    ordered_keys = []\n    known_keys_list = known_keys_list or _mailpile_key_list(\n        GnuPG(config or None).list_keys())\n\n    if origins:\n        handlers = [h for h in KEY_LOOKUP_HANDLERS\n                    if (h.NAME in origins) or (h.NAME.lower() in origins)\n                    or (h.SHORTNAME in origins)]\n    else:\n        handlers = KEY_LOOKUP_HANDLERS\n\n    ungotten = get and get[:] or []\n    progress = [ ]\n\n    # If the user has disabled \"web content\", only allow local requests\n    if config and allowremote and config.prefs.web_content == 'off':\n        if ('keylookup' in config.sys.debug\n                or 'keytofu' in config.sys.debug):\n            session.ui.debug(\"Remote key lookups disabled by prefs.web_content.\")\n        allowremote = False\n\n    for handler in handlers:\n        if get and not ungotten:\n            # We have all the keys!\n            break\n\n        try:\n            h = handler(session, known_keys_list)\n            if not allowremote and not h.LOCAL:\n                continue\n\n            if allowremote and config and config.prefs.web_content == 'anon':\n                if (config.sys.proxy.protocol not in ('tor', 'tor-risky')\n                        or not h.PRIVACY_FRIENDLY):\n                    if ('keylookup' in config.sys.debug\n                            or 'keytofu' in config.sys.debug):\n                        session.ui.debug(\n                            \"Origin %s disabled by prefs.web_content\" % h.NAME)\n                    continue\n\n            if found_keys and (not h.PRIVACY_FRIENDLY) and (not origins):\n                # We only try the privacy-hostile methods if we haven't\n                # found any keys (unless origins were specified).\n                if not ungotten:\n                    continue\n\n            progress.append(h.NAME)\n            if event and config:\n                ordered_keys.sort(key=lambda k: -k[\"score\"])\n                event.message = _('Searching for encryption keys in: %s'\n                                  ) % _(h.NAME)\n                event.private_data = {\"result\": ordered_keys,\n                                      \"progress\": progress,\n                                      \"runningsearch\": h.NAME}\n                config.event_log.log_event(event)\n\n            # We allow for more time when importing keys\n            timeout = h.TIMEOUT\n            if ungotten:\n                timeout *= 4\n\n            # h.lookup will remove found keys from the wanted list,\n            # but we have to watch out for the effects of timeouts.\n            wanted = ungotten[:]\n            results = RunTimed(timeout, h.lookup, address,\n                               strict_email_match=strict_email_match,\n                               get=(wanted if (get is not None) else None))\n            ungotten[:] = wanted\n        except KeyboardInterrupt:\n            raise\n        except:\n            if config and config.sys.debug:\n                traceback.print_exc()\n            results = {}\n\n        # FIXME: This merging of info about keys is probably misguided.\n        for key_id, key_info in results.iteritems():\n            if key_id in found_keys:\n                old_scores = found_keys[key_id].scores\n                old_uids = found_keys[key_id].uids\n                found_keys[key_id].update(key_info)\n                found_keys[key_id].scores.update(old_scores)\n\n                # Merge in the old UIDs\n                uid_emails = [u.email for u in key_info.uids]\n                for uid in old_uids:\n                    email = uid.email\n                    if email and email not in uid_emails:\n                        found_keys[key_id].uids.append(uid)\n            else:\n                found_keys[key_id] = key_info\n            found_keys[key_id].origins.append(h.NAME)\n\n        for key_id in found_keys.keys():\n            _normalize_key(session, found_keys[key_id])\n            _update_scores(session, key_id, found_keys[key_id], known_keys_list)\n\n        # This updates and sorts ordered_keys in place. This will magically\n        # also update the data on the viewable event, because Python.\n        ordered_keys[:] = found_keys.values()\n        ordered_keys.sort(key=lambda k: -k.score)\n\n    if only_good:\n        ordered_keys = [k for k in ordered_keys if k.score > 0]\n\n    if get and vcard and ordered_keys:\n        with vcard:\n            vcard.pgp_key = ordered_keys[0].fingerprint\n            vcard.pgp_key_pinned = 'true' if pin_key else 'false'\n            vcard.save()\n        ordered_keys[0].is_preferred = True\n        ordered_keys[0].is_pinned = pin_key\n        for k in ordered_keys[1:]:\n            k.is_preferred = k.is_pinned = False\n\n    if event and config:\n        event.private_data = {\"result\": ordered_keys, \"runningsearch\": False}\n        config.event_log.log_event(event)\n    return ordered_keys\n\n\n##[ API endpoints / commands ]#################################################\n\nclass KeyLookup(Command):\n    \"\"\"Perform a key lookup\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/keylookup', 'crypto/keylookup',\n        '<address> [<allowremote>] [-- <origin>, <origin>, ...]')\n    HTTP_CALLABLE = ('GET',)\n    HTTP_QUERY_VARS = {\n        'email': 'The address to find a encryption key for (strict)',\n        'address': 'The nick or address to find a encryption key for (fuzzy)',\n        'allowremote': 'Whether to permit remote key lookups (default=Yes)',\n        'origins': 'Specify which origins to check (or * for all)'}\n\n    def command(self):\n        args = list(self.args)\n\n        if '--' in args:\n            spoint = args.index('--')\n            origins = (' '.join(args[(spoint+1):])).split(', ')\n            args = args[:spoint]\n        else:\n            origins = self.data.get('origins')\n\n        if len(args) > 1:\n            allowremote = args.pop()\n        else:\n            allowremote = self.data.get('allowremote', ['Y'])[0]\n        if allowremote.lower()[:1] in ('n', 'f'):\n            allowremote = False\n\n        if '*' in (origins or []):\n            origins = [h.NAME for h in KEY_LOOKUP_HANDLERS]\n\n        email = \" \".join(self.data.get('email', []))\n        address = \" \".join(self.data.get('address', args))\n        result = dict((k.summary(), k) for k in\n            lookup_crypto_keys(self.session, email or address,\n                               strict_email_match=email,\n                               event=self.event,\n                               allowremote=allowremote,\n                               origins=origins))\n\n        return self._success(_n('Found %d encryption key',\n                                'Found %d encryption keys',\n                                len(result)) % len(result),\n                             result=result)\n\n\nclass KeyImport(Command):\n    \"\"\"Import keys\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/keyimport', 'crypto/keyimport',\n                      '<address> <fingerprint,...> <origins ...>')\n    HTTP_CALLABLE = ('POST',)\n    HTTP_POST_VARS = {\n        'address': 'The nick/address to find an encryption key for',\n        'pinned': 'Pin this key?',\n        'fingerprints': 'List of required fingerprints or key summary strings',\n        'origins': 'List of origins to search'\n    }\n\n    def _get_or_create_vcard(self, address):\n        vcard = self.session.config.vcards.get_vcard(address)\n        if not vcard:\n            vcard = MailpileVCard(\n                VCardLine(name='email', value=address, type='pref'),\n                VCardLine(name='kind', value='individual'),\n                config=self.session.config)\n            self.session.config.vcards.add_vcards(vcard)\n        return vcard\n\n    def command(self):\n        args = list(self.args)\n        if args:\n            pin_key = False\n            address, fprints, origins = args[0], args[1].split(','), args[2:]\n        else:\n            pin_key = self.data.get('pinned', [''])[0].lower()[:1] in ('y', 't')\n            address = self.data.get('address', [''])[0]\n            fprints = self.data.get('fingerprints', [])\n            origins = self.data.get('origins', [])\n        safe_assert(address or fprints or origins)\n\n        result = lookup_crypto_keys(\n            self.session, address,\n            get=[f.strip() for f in fprints],\n            pin_key=pin_key,\n            vcard=self._get_or_create_vcard(address),\n            origins=origins,\n            event=self.event)\n\n        if len(result) > 0:\n            # Update the VCards!\n            PGPKeysImportAsVCards(self.session,\n                                  arg=[k['fingerprint'] for k in result]\n                                  ).run()\n\n            # The key was looked up based on the given address, so it must have\n            # a user id containing that address, so when it is imported to\n            # VCards, the VCard for that address will list the key.\n            for k in result:\n                k.in_vcards = True\n            # Previous crypto evaluations may now be out of date, so we\n            # clear the cache so users can see results right away.\n            ClearParseCache(pgpmime=True)\n\n        return self._success(_n('Imported %d encryption key',\n                                'Imported %d encryption keys',\n                                len(result)) % len(result),\n                             result=result)\n\n\n# FIXME: This seems to want to update keys TOO OFTEN. There is probably a\n#        mismatch in which fingerprints we are using/storing how/where.\nclass KeyTofu(Command):\n    \"\"\"Import or refresh keys\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/keytofu', 'crypto/keytofu', '[--dry] [--should] <emails>')\n    HTTP_CALLABLE = ('POST',)\n    HTTP_POST_VARS = {\n        'email': 'E-mail addresses to find or update encryption keys for',\n        'should-encrypt': 'Is encryption a priority?'}\n\n    def _key_can_encrypt(self, gnupg, fingerprint):\n        rc, data = gnupg.encrypt(\"hello\", tokeys=[fingerprint])\n        return (rc == 0)\n\n    def _recently_used_crypto(self, tofu_cfg, idx, email):\n        session = self.session\n        crypto = idx.search(session, ['from:' + email, 'has:crypto']).as_set()\n\n        # We are confident a user is \"using crypto\" iff:\n        #   1) their most recent e-mail had signs of crypto\n        #   2) the were N crypto messages in their last M mails\n        if len(crypto) > tofu_cfg.hist_min:\n            recent = sorted(list(idx.search(session, ['from:' + email]).as_set()))\n        else:\n            recent = []\n        last1 = set(recent[-1:]) & crypto\n        recent_crypto = crypto & set(recent[-tofu_cfg.hist_recent:])\n\n        if 'keytofu' in self.session.config.sys.debug:\n            self.session.ui.debug(\n                'keytofu(%s): seen %d crypto e-mails, %d/%d recent, last=%s'\n                % (email, len(crypto), len(recent_crypto), len(recent),\n                   last1 and 'yes' or 'no'))\n\n        return (last1\n            and (len(recent_crypto) >= tofu_cfg.hist_min)\n            and len(recent_crypto)) or False\n\n    def _key_is_trusted(self, fingerprint, known_keys_list):\n        for summary, key_info in known_keys_list.iteritems():\n            if key_info['fingerprint'] == fingerprint:\n                return (key_info['validity'] in KeyInfo.KEY_TRUSTED_CODES)\n        return False\n\n    def _seen_enough_signatures(self, tofu_cfg, idx, email, keyinfo):\n        keyid = keyinfo.fingerprint[-16:].lower()\n        signed = idx.search(\n            self.session, ['from:' + email, 'sig:' + keyid]).as_set()\n        if 'keytofu' in self.session.config.sys.debug:\n            self.session.ui.debug(\n                'keytofu(%s): seen %d signatures, need %d'\n                % (email, len(signed), tofu_cfg.hist_min))\n        return (len(signed) >= tofu_cfg.hist_min)\n\n    def command(self):\n        tofu_cfg = self.session.config.prefs.key_tofu\n        if not (tofu_cfg.autocrypt or tofu_cfg.historic):\n            return self._success('Key TOFU disabled. Check prefs.key_tofu.* settings.')\n\n        args = list(self.args)\n        if args and (args[0] == '--dry'):\n            dry_run = args.pop(0)\n        else:\n            dry_run = 'keytofudry' in self.session.config.sys.debug\n\n        if args and (args[0] == '--should'):\n            should_encrypt = args.pop(0)\n        else:\n            should_encrypt = self.data.get('should-encrypt', ['N'])[0]\n            should_encrypt = should_encrypt.lower()[:1] in ('y', 't')\n\n        emails = set(args) | set(self.data.get('email', []))\n        if not emails:\n            return self._success('Nothing Happened')\n\n        if tofu_cfg.autocrypt:\n            from mailpile.plugins.crypto_autocrypt import get_Autocrypt_DB, save_Autocrypt_DB, AutocryptRecord, AutocryptKeyLookupHandler\n            acState = get_Autocrypt_DB(self.session.config)['state']\n        else:\n            acState = None\n\n\n        idx = self._idx()\n        gnupg = self._gnupg()\n        known_keys_list = _mailpile_key_list(gnupg.list_keys())\n\n        missing, old, status = [], {}, {}\n        for email in emails:\n            vc = self.session.config.vcards.get_vcard(email)\n            fp = vc.pgp_key if vc else None\n            if vc and fp:\n                if self._key_can_encrypt(gnupg, fp):\n                    old[email] = fp\n                    status[email] = 'Key is already on our key-chain'\n                    if vc.pgp_key_pinned:\n                        status[email] = 'Key is pinned'\n                    elif self._key_is_trusted(fp, known_keys_list):\n                        status[email] = 'Key is trusted by GnuPG'\n                    elif acState:\n                        acr = AutocryptRecord.Load(acState, email, _raise=None)\n                        if acr and acr.should_encrypt() and not acr.imported_ts:\n                            missing.append(email)\n                            status[email] = 'Obsolete key is on our key-chain'\n                else:\n                    # FIXME: Should we remove the bad key from the vcard?\n                    # FIXME: Should we blacklist the bad key?\n                    # FIXME: Should this trigger a notification, per. #1869?\n                    missing.append(email)\n                    status[email] = 'Obsolete key is on our key-chain'\n            else:\n                missing.append(email)\n                status[email] = 'We have no key for this person'\n\n        global TOFU_CHECK_HISTORY\n        now = time.time()\n        interval_cutoff = now - tofu_cfg.min_interval\n        for k in TOFU_CHECK_HISTORY.keys():\n            if TOFU_CHECK_HISTORY.get(k, now) < interval_cutoff:\n                del TOFU_CHECK_HISTORY[k]\n\n        should_import = {}\n        hist_origins = [o.strip() for o in tofu_cfg.hist_origins.split(',')]\n        for email in missing:\n            checking = '%s/%s' % (email, tofu_cfg)\n            last_check = TOFU_CHECK_HISTORY.get(checking, 0)\n            if (not should_encrypt) and last_check and last_check > interval_cutoff:\n                status[email] += ' (checked recently)'\n                continue\n            elif not dry_run:\n                TOFU_CHECK_HISTORY[checking] = now\n\n            if tofu_cfg.autocrypt and acState:\n                acr = AutocryptRecord.Load(acState, email, _raise=None)\n                if acr and acr.should_encrypt() and not acr.imported_ts:\n                    should_import[email] = (acr.key_sig, AutocryptKeyLookupHandler.NAME)\n\n            if tofu_cfg.historic and hist_origins and email not in should_import:\n                count = self._recently_used_crypto(tofu_cfg, idx, email)\n                if count or should_encrypt:\n                    keys = lookup_crypto_keys(self.session, email,\n                                              origins=hist_origins,\n                                              strict_email_match=True,\n                                              known_keys_list=known_keys_list,\n                                              event=self.event,\n                                              only_good=True) or []\n                    for keyinfo in keys:\n                        if should_encrypt or self._seen_enough_signatures(\n                                tofu_cfg, idx, email, keyinfo):\n                            should_import[email] = (keyinfo['fingerprint'],\n                                                    hist_origins)\n                            break\n                    if keys and 'email' not in should_import:\n                        status[email] = 'Found keys, but none in active use'\n                else:\n                    status[email] = 'Have not seen enough PGP messages'\n\n        imported = {}\n        for email, (key_id, origins) in should_import.iteritems():\n            if 'keytofu' in self.session.config.sys.debug:\n                self.session.ui.debug('keytofu(%s): importing %s from %s'\n                                      % (email, key_id, origins))\n            if dry_run:\n                status[email] = 'Should import %s from %s' % (key_id, origins)\n            else:\n                keys = lookup_crypto_keys(\n                    self.session, email,\n                    get=[key_id],\n                    vcard=self.session.config.vcards.get_vcard(email),\n                    strict_email_match=True,\n                    origins=origins,\n                    known_keys_list=known_keys_list,\n                    event=self.event)\n                if keys:\n                    # FIXME: This should trigger a notification, per. #1869\n                    imported[email] = keys\n                    status[email] = 'Imported key!'\n                    vc = self.session.config.vcards.get_vcard(email)\n                    if vc and key_id in keys:\n                        with vc:\n                            vc.pgp_key = keys[key_id].fingerprint\n                            vc.save()\n                else:\n                    status[email] = 'Failed to import key'\n\n        for email in imported:\n            if email in missing:\n                missing.remove(email)\n\n        if len(imported) > 0:\n            # Update the VCards!\n            fingerprints = []\n            for keys in imported.values():\n                fingerprints.extend(k['fingerprint'] for k in keys)\n            PGPKeysImportAsVCards(self.session, arg=fingerprints).run()\n            # Previous crypto evaluations may now be out of date, so we\n            # clear the cache so users can see results right away.\n            ClearParseCache(pgpmime=True)\n\n        # i18n note: Not translating things here, since messages are not\n        #            generally user-facing and we want to reduce load on\n        #            our translators.\n        return self._success('Evaluated key TOFU', result={\n            'missing_keys': missing,\n            'imported_keys': imported,\n            'status': status,\n            'on_keychain': old})\n\n\nPluginManager(builtin=__file__).register_commands(\n    KeyLookup, KeyImport, KeyTofu)\n\n\n##[ Basic lookup handlers ]###################################################\n\nclass LookupHandler:\n    NAME = \"NONE\"\n    SHORTNAME = \"NONE\"\n    TIMEOUT = 2\n    PRIORITY = 10000\n    PRIVACY_FRIENDLY = False\n    LOCAL = False\n    SCORE = 0\n\n    def __init__(self, session, known_keys_list):\n        self.session = session\n        self.known_keys = known_keys_list\n\n    def _gnupg(self):\n        return GnuPG(self.session and self.session.config or None)\n\n    def _score(self, key):\n        raise NotImplemented(\"Subclass and override _score\")\n\n    def _getkey(self, email, key):\n        raise NotImplemented(\"Subclass and override _getkey\")\n\n    def _gk_succeeded(self, result):\n        return (result and 0 < (len(result.get('imported', [])) +\n                                len(result.get('updated', []))))\n\n    def _lookup(self, address, strict_email_match=False):\n        raise NotImplemented(\"Subclass and override _lookup\")\n\n    def lookup(self, address, strict_email_match=False, get=None):\n        all_keys = self._lookup(address, strict_email_match=strict_email_match)\n        keys = {}\n        if get is not None:\n            get = [unicode(g).upper() for g in get]\n        for key_id, key_info in all_keys.iteritems():\n            fprint = unicode(key_info.fingerprint).upper()\n            summary = key_info.summary()\n            if ((get is None) or\n                    (fprint and fprint in get) or\n                    (summary in get) or\n                    (unicode(key_id).upper() in get)):  # FIXME: This is messy.\n                score, reason = self._score(key_info)\n                vscore, vreason = _score_validity(key_info['validity'])\n                if abs(vscore) > abs(score):\n                    reason = vreason\n                score += vscore\n\n                key_info.score = score\n                key_info.scores = {\n                    self.NAME: [score, reason]}\n\n                if get is not None:\n                    if key_id in get: get.remove(key_id)\n                    if fprint in get: get.remove(fprint)\n                    if summary in get: get.remove(summary)\n                    if self._gk_succeeded(self._getkey(address, key_info)):\n                        keys[key_id] = key_info\n                else:\n                    keys[key_id] = key_info\n\n        return keys\n\n    def key_import(self, address):\n        return True\n\n\nclass KeychainLookupHandler(LookupHandler):\n    NAME = \"GnuPG keychain\"\n    SHORTNAME = \"gnupg\"\n    LOCAL = True\n    PRIVACY_FRIENDLY = True\n    PRIORITY = 0\n    SCORE = 8\n\n    def _score(self, key):\n        return (self.SCORE, _('Found encryption key in keychain'))\n\n    def _lookup(self, address, strict_email_match):\n        address = address.lower()\n        results = {}\n        vcard = self.session.config.vcards.get_vcard(address)\n        for key_id, key_info in self.known_keys.iteritems():\n            match = False\n            for uid in key_info.uids:\n                if not strict_email_match:\n                    match = (address in uid.name.lower() or\n                             address in uid.email.lower())\n                else:\n                    match = (address == uid.email.lower())\n                if match:\n                    results[key_id] = key_info\n                    break\n            if vcard and (vcard.pgp_key == key_info.fingerprint) and not match:\n                key_info.uids.append(\n                    KeyUID(email=address, name=vcard.fn, comment='Mailpile'))\n                results[key_id] = key_info\n        return results\n\n    def _getkey(self, email, key):\n        # Returns dict like those returned by KeyserverLookupHandler._getkey()\n        # and EmailKeyLookupHandler._getkey(). Even though the key is already\n        # on the keychain, this is needed so KeyImport will create VCard(s)\n        # from the key to indicate that it can be used for encrypting.\n\n        if key['fingerprint'] in self.known_keys:\n            return {'updated':[{'fingerprint':key['fingerprint']}]}\n        else:\n            return {}\n\n\nclass KeyserverLookupHandler(LookupHandler):\n    NAME = \"PGP Keyservers\"\n    SHORTNAME = \"sks\"\n    LOCAL = False\n    TIMEOUT = 30  # We know these are slow...\n    PRIVACY_FRIENDLY = False\n    PRIORITY = 200\n    SCORE = 1\n\n    # People with really big keys are just going to have to publish in WKD\n    # or something, unless or until the SKS keyservers get fixed somehow.\n    MAX_KEY_SIZE = 1500000\n\n    # During testing, there were frequent HTTP gateway errors returned from\n    # hkps.pool.sks-keyservers.net so sks-keyservers.net was added too.\n    KEY_SERVER_BASE_URLS = [\n        \"https://sks-keyservers.net/pks/lookup\",\n        \"https://hkps.pool.sks-keyservers.net/pks/lookup\"]\n\n    def _score(self, key):\n        return (self.SCORE, _('Found encryption key in keyserver'))\n\n    def _lookup_url(self, url_base, address):\n        return \"{}?{}\".format(url_base, urllib.urlencode({\n            \"search\": address,\n            \"op\": \"index\",\n            \"fingerprint\": \"on\",\n            \"options\": \"mr\"}))\n\n    def _lookup(self, address, strict_email_match=False):\n        error = None\n        for url_base in self.KEY_SERVER_BASE_URLS:\n            url = self._lookup_url(url_base, address)\n            if 'keylookup' in self.session.config.sys.debug:\n                self.session.ui.debug('[%s] Fetching: %s' % (self.NAME, url))\n            try:\n                raw_result = secure_urlget(self.session, url,\n                                           maxbytes=self.MAX_KEY_SIZE+1)\n                error = None\n                break\n            except urllib2.HTTPError as e:\n                error = str(e)\n                if e.code == 404:\n                    # If a server reports the key was not found, let's stop\n                    # because the servers are supposed to be in sync.\n                    break;\n            except (IOError, urllib2.URLError, ssl.SSLError, ssl.CertificateError) as e:\n                error = str(e)\n\n        if not error and len(raw_result) > self.MAX_KEY_SIZE:\n            error = \"Response too big (>%d bytes), ignoring\" % self.MAX_KEY_SIZE\n            if 'keyservers' in self.session.config.sys.debug:\n                self.session.ui.debug('[%s] %s' % (self.NAME, error))\n\n        if error:\n            if 'keylookup' in self.session.config.sys.debug:\n                self.session.ui.debug('Error: %s' % error)\n            if 'Error 404' in error:\n                return {}\n            raise ValueError(error)\n\n        if 'keylookup' in self.session.config.sys.debug:\n            self.session.ui.debug('[%s] DATA: %s' % (self.NAME, raw_result[:200]))\n        results = _mailpile_key_list(\n            self._gnupg().parse_hpk_response(raw_result.split('\\n')))\n\n        if strict_email_match:\n            for key in results.keys():\n                match = [u for u in results[key].uids\n                         if u.email.lower() == address]\n                if not match:\n                    if 'keylookup' in self.session.config.sys.debug:\n                        self.session.ui.debug('[%s] No UID for %s, ignoring key'\n                                              % (self.NAME, address))\n                    del results[key]\n\n        if 'keylookup' in self.session.config.sys.debug:\n            self.session.ui.debug('[%s] Results=%d' % (self.NAME, len(results)))\n\n        return results\n\n    def _getkey_url(self, url_base, email, key):\n        # Note: keys.openpgp.org keeps barfing if we change the order of\n        #       the query string parameters. So using an API call to\n        #       generate and escape things causes breakage. We know the\n        #       fingerprint is URL-safe, so we just stick with it.\n        return \"{}?op=get&options=mr&search=0x{}\".format(\n            url_base, key['fingerprint'])\n\n    def _getkey(self, email, key):\n        error = None\n        key_data = None\n        for url_base in self.KEY_SERVER_BASE_URLS:\n            url = self._getkey_url(url_base, email, key)\n            if 'keylookup' in self.session.config.sys.debug:\n                self.session.ui.debug('Fetching: %s' % url)\n            try:\n                key_data = secure_urlget(self.session, url,\n                                         maxbytes=self.MAX_KEY_SIZE+1)\n                error = None\n                break\n            except urllib2.HTTPError as e:\n                error = e\n                if e.code == 404:\n                    # If a server reports the key was not found, let's stop\n                    # because the servers are supposed to be in sync.\n                    break;\n            except (IOError, urllib2.URLError, ssl.SSLError, ssl.CertificateError) as e:\n                error = e\n\n        if not key_data:\n            error = 'No key data!'\n\n        if not error and len(key_data) > self.MAX_KEY_SIZE:\n            error = \"Key too big (>%d bytes), ignoring\" % self.MAX_KEY_SIZE\n            if 'keylookup' in self.session.config.sys.debug:\n                self.session.ui.debug(error)\n\n        if error:\n            raise ValueError(str(error))\n\n        return self._gnupg().import_keys(key_data, filter_uid_emails=[email])\n\n\nclass VerifyingKeyserverLookupHandler(KeyserverLookupHandler):\n    NAME = \"keys.OpenPGP.org\"\n    SHORTNAME = \"koo\"\n    PRIVACY_FRIENDLY = False\n    LOCAL = False\n    TIMEOUT = 15\n    PRIORITY = 75  # Better than SKS keyservers and better than DNS\n    SCORE = 5      # Treat these as valid as WKD, yay e-mail vetting!\n\n    KEY_SERVER_BASE_URLS = [\n        \"http://zkaan2xfbuxia2wpf7ofnkbz6r5zdbbvxbunvp5g2iebopbfc4iqmbad.onion/pks/lookup\",\n        \"https://keys.openpgp.org/pks/lookup\"]\n\n    def _lookup_url(self, url_base, address):\n        # This deliberately avoids any escaping of the e-mail address; k.o.o.\n        # can't handle such things at the moment.\n        return \"{}?op=index&options=mr&search={}\".format(url_base, address)\n\n    def _score(self, key):\n        return (self.SCORE, _('Found encryption key in keys.OpenPGP.org'))\n\n\nregister_crypto_key_lookup_handler(KeychainLookupHandler)\nregister_crypto_key_lookup_handler(KeyserverLookupHandler)\nregister_crypto_key_lookup_handler(VerifyingKeyserverLookupHandler)\n\n# We do this down here, as that seems to make the Python module loader\n# things happy enough with the circular dependencies...\nfrom mailpile.plugins.keylookup.email_keylookup import EmailKeyLookupHandler\nfrom mailpile.plugins.keylookup.wkd import WKDLookupHandler\n"
  },
  {
    "path": "mailpile/plugins/keylookup/email_keylookup.py",
    "content": "import datetime\nimport time\nimport copy\n\nfrom mailpile.crypto.autocrypt import *\nfrom mailpile.crypto.keyinfo import get_keyinfo, MailpileKeyInfo\nfrom mailpile.i18n import gettext\nfrom mailpile.plugins import PluginManager\nfrom mailpile.plugins.keylookup import LookupHandler\nfrom mailpile.plugins.keylookup import register_crypto_key_lookup_handler\nfrom mailpile.plugins.search import Search\nfrom mailpile.mailutils.emails import Email\n\n\n_ = lambda t: t\n_plugins = PluginManager(builtin=__file__)\n\n\nGLOBAL_KEY_CACHE = {}\n\n\ndef _PRUNE_GLOBAL_KEY_CACHE():\n    global GLOBAL_KEY_CACHE\n    for k in GLOBAL_KEY_CACHE.keys()[10:]:\n        del GLOBAL_KEY_CACHE[k]\n\n\nPGP_KEY_SUFFIXES = ('pub', 'asc', 'key', 'pgp')\n\ndef _might_be_pgp_key(filename, mimetype):\n    filename = (filename or '').lower()\n    return ((mimetype == \"application/pgp-keys\") or\n            (filename.lower().split('.')[-1] in PGP_KEY_SUFFIXES and\n             'encrypted' not in filename and\n             'signature' not in filename))\n\n\nclass EmailKeyLookupHandler(LookupHandler, Search):\n    NAME = _(\"E-mail keys\")\n    SHORTNAME = 'e-mail'\n    PRIORITY = 5\n    TIMEOUT = 25  # 5 seconds per message we are willing to parse\n    LOCAL = True\n    PRIVACY_FRIENDLY = True\n    SCORE = 1\n\n    def __init__(self, session, *args, **kwargs):\n        LookupHandler.__init__(self, session, *args, **kwargs)\n        Search.__init__(self, session)\n\n        global GLOBAL_KEY_CACHE\n        self.key_cache = GLOBAL_KEY_CACHE\n        _PRUNE_GLOBAL_KEY_CACHE()\n\n    def _score(self, key):\n        return (self.SCORE, _('Found key in local e-mail'))\n\n    def _lookup(self, address, strict_email_match=False):\n        results = {}\n        canon_address = canonicalize_email(address)\n        terms = ['from:%s' % address, 'has:pgpkey', '+pgpkey:%s' % address]\n        session, idx = self._do_search(search=terms)\n        deadline = time.time() + (0.75 * self.TIMEOUT)\n        for messageid in session.results[:5]:\n            for key_info, raw_key in self._get_message_keys(\n                    messageid, autocrypt=False, autocrypt_gossip=True):\n                if strict_email_match:\n                    match = [u for u in key_info.uids\n                             if canonicalize_email(u.email) == canon_address]\n                    if not match:\n                        continue\n                fp = key_info.fingerprint\n                results[fp] = copy.copy(key_info)\n                self.key_cache[fp] = raw_key\n            if len(results) > 5 or time.time() > deadline:\n                break\n        return results\n\n    def _getkey(self, email, keyinfo):\n        data = self.key_cache.get(keyinfo.fingerprint)\n        if data:\n            return self._gnupg().import_keys(data, filter_uid_emails=[email])\n        else:\n            raise ValueError(\"Key not found\")\n\n    def _get_message_keys(self, messageid,\n                          autocrypt=True, autocrypt_gossip=True,\n                          attachments=True):\n        keys = self.key_cache.get(messageid, [])\n        if not keys:\n            email = Email(self._idx(), messageid)\n\n            # First we check the Autocrypt headers\n            loop_count = 0\n            msg = email.get_msg(pgpmime='all')\n            ac_headers = []\n            if autocrypt:\n                ac_headers.append(extract_autocrypt_header(msg))\n            if autocrypt_gossip:\n                ac_headers.extend(extract_autocrypt_gossip_headers(msg))\n            for ach in ac_headers:\n                loop_count += 1\n                if 'keydata' in ach:\n                    for keyinfo in get_keyinfo(ach['keydata'],\n                                               autocrypt_header=ach,\n                                               key_info_class=MailpileKeyInfo):\n                        keyinfo.is_autocrypt = True\n                        keyinfo.is_gossip = (loop_count > 1)\n                        keys.append((keyinfo, ach['keydata']))\n\n            # Then go looking at the attachments\n            atts = []\n            if attachments:\n                atts.extend(email.get_message_tree(want=[\"attachments\"]\n                                                   )[\"attachments\"])\n            for part in atts:\n                if len(keys) > 100:  # Just to set some limit...\n                    break\n                if _might_be_pgp_key(part[\"filename\"], part[\"mimetype\"]):\n                    key = part[\"part\"].get_payload(None, True)\n                    for keyinfo in get_keyinfo(key,\n                                               key_info_class=MailpileKeyInfo):\n                        keys.append((keyinfo, key))\n            self.key_cache[messageid] = keys\n        return keys\n\n\ndef get_pgp_key_keywords(data):\n    kws = []\n    for keyinfo in get_keyinfo(data):\n        for uid in keyinfo.uids:\n            if uid.email:\n                kws.append('%s:pgpkey' % uid.email.lower())\n        fingerprint = keyinfo.fingerprint.lower()\n        kws.append('pgpkey:has')\n        kws.append('%s:pgpkey' % fingerprint)\n        kws.append('%s:pgpkey' % fingerprint[-16:])\n        for sk in keyinfo.subkeys:\n           kws.append('%s:pgpkey' % sk.fingerprint)\n           kws.append('%s:pgpkey' % sk.fingerprint[-16:])\n    return kws\n\n\ndef has_pgpkey_data_kw_extractor(index, msg, mimetype, filename, part, loader,\n                                 body_info=None, **kwargs):\n    kws = []\n    if _might_be_pgp_key(filename, mimetype):\n        new_kws = get_pgp_key_keywords(part.get_payload(None, True))\n        if new_kws:\n            body_info['pgp_key'] = filename\n            kws += new_kws\n    return kws\n\n\nregister_crypto_key_lookup_handler(EmailKeyLookupHandler)\n_plugins.register_data_kw_extractor('pgpkey', has_pgpkey_data_kw_extractor)\n_ = gettext\n"
  },
  {
    "path": "mailpile/plugins/keylookup/wkd.py",
    "content": "import hashlib\nimport ssl\nimport urllib\nimport urllib2\n\nfrom mailpile.security import secure_urlget\nfrom mailpile.commands import Command\nfrom mailpile.conn_brokers import Master as ConnBroker\nfrom mailpile.crypto.keyinfo import get_keyinfo, MailpileKeyInfo\nfrom mailpile.i18n import gettext\nfrom mailpile.plugins import PluginManager\nfrom mailpile.plugins.keylookup import LookupHandler\nfrom mailpile.plugins.keylookup import register_crypto_key_lookup_handler\n\n_ = lambda t: t\n\n\nWKD_URL_FORMATS = (\n        'https://openpgpkey.%(d)s/.well-known/openpgpkey/%(d)s/hu/%(l)s?%(q)s',\n        'https://%(d)s/.well-known/openpgpkey/hu/%(l)s?%(q)s')\n\nALPHABET = \"ybndrfg8ejkmcpqxot1uwisza345h769\"\nSHIFT = 5\nMASK = 31\n\n\n#  Encodes data using ZBase32 encoding\n#  See: https://tools.ietf.org/html/rfc6189#section-5.1.6\n#\ndef _zbase_encode(data):\n    if len(data) == 0:\n        return \"\"\n    buffer = ord(data[0])\n    index = 1\n    bitsLeft = 8\n    result = \"\"\n    while bitsLeft > 0 or index < len(data):\n        if bitsLeft < SHIFT:\n            if index < len(data):\n                buffer = buffer << 8\n                buffer = buffer | (ord(data[index]) & 0xFF)\n                bitsLeft = bitsLeft + 8\n                index = index + 1\n            else:\n                pad = SHIFT - bitsLeft\n                buffer = buffer << pad\n                bitsLeft = bitsLeft + pad\n        bitsLeft = bitsLeft - SHIFT\n        result = result + ALPHABET[MASK & (buffer >> bitsLeft)]\n    return result\n\n\ndef WebKeyDirectoryURLs(address, plusmagic=True):\n    local, _, domain = address.partition(\"@\")\n    encoded_parts = [(local, _zbase_encode(\n        hashlib.sha1(local.lower().encode('utf-8')).digest()))]\n    if plusmagic and '+' in local:\n        local = local.split('+')[0]\n        encoded_parts.append((local, _zbase_encode(\n            hashlib.sha1(local.lower().encode('utf-8')).digest())))\n    for lp, lpe in encoded_parts:\n        for urlfmt in WKD_URL_FORMATS:\n            yield urlfmt % {\n                'd': domain,  # FIXME: Should this be punycoded?\n                'l': lpe,\n                'q': urllib.urlencode({'l': lp})}\n\n\n#  Support for Web Key Directory (WKD) lookup for keys.\n#  See: https://wiki.gnupg.org/WKD and https://datatracker.ietf.org/doc/draft-koch-openpgp-webkey-service/\n#\nclass WKDLookupHandler(LookupHandler):\n    NAME = _(\"Web Key Directory\")\n    SHORTNAME = 'wkd'\n    TIMEOUT = 12\n    PRIORITY = 50  # WKD is better than keyservers and better than DNS\n    PRIVACY_FRIENDLY = True  # These lookups can go over Tor\n    SCORE = 5\n\n    # People with really big keys are just going to have to publish in WKD\n    # or something, unless or until the SKS keyservers get fixed somehow.\n    MAX_KEY_SIZE = 1500000\n\n    # Avoid lookups to certain domains. The rationale here is these are large\n    # providers which are unlikely to implement WKD any time soon, so we would\n    # rather not leak details of our activities to them. This list is a subset\n    # of the list found here:\n    #  - https://github.com/mailcheck/mailcheck/wiki/List-of-Popular-Domains\n    DOMAIN_BLACKLIST = [\n        \"aol.com\", \"att.net\", \"comcast.net\", \"facebook.com\", \"gmail.com\",\n        \"gmx.com\", \"googlemail.com\", \"google.com\", \"hotmail.com\", \"hotmail.co.uk\",\n        \"mac.com\", \"me.com\", \"mail.com\", \"msn.com\", \"live.com\", \"sbcglobal.net\",\n        \"verizon.net\", \"yahoo.com\", \"yahoo.co.uk\", \"email.com\",\n        \"games.com\", \"gmx.net\", \"icloud.com\", \"iname.com\", \"inbox.com\", \"love.com\",\n        \"outlook.com\", \"pobox.com\", \"rocketmail.com\", \"wow.com\", \"ygm.com\",\n        \"ymail.com\", \"zoho.com\", \"zohomail.eu\", \"yandex.com\", \"bellsouth.net\",\n        \"charter.net\", \"cox.net\", \"earthlink.net\", \"juno.com\", \"btinternet.com\",\n        \"virginmedia.com\", \"blueyonder.co.uk\", \"freeserve.co.uk\", \"live.co.uk\",\n        \"ntlworld.com\", \"o2.co.uk\", \"orange.net\", \"sky.com\", \"talktalk.co.uk\",\n        \"tiscali.co.uk\", \"virgin.net\", \"wanadoo.co.uk\", \"bt.com\", \"sina.com\",\n        \"sina.cn\", \"qq.com\", \"naver.com\", \"hanmail.net\", \"daum.net\", \"nate.com\",\n        \"yahoo.co.jp\", \"yahoo.co.kr\", \"yahoo.co.id\", \"yahoo.co.in\", \"yahoo.com.sg\",\n        \"yahoo.com.ph\", \"163.com\", \"yeah.net\", \"126.com\", \"21cn.com\", \"aliyun.com\",\n        \"foxmail.com\", \"hotmail.fr\", \"live.fr\", \"laposte.net\", \"yahoo.fr\",\n        \"wanadoo.fr\", \"orange.fr\", \"gmx.fr\", \"sfr.fr\", \"neuf.fr\", \"free.fr\", \"gmx.de\",\n        \"hotmail.de\", \"live.de\", \"online.de\", \"t-online.de\", \"web.de\", \"yahoo.de\",\n        \"libero.it\", \"virgilio.it\", \"hotmail.it\", \"aol.it\", \"tiscali.it\", \"alice.it\",\n        \"live.it\", \"yahoo.it\", \"email.it\", \"tin.it\", \"poste.it\", \"teletu.it\",\n        \"mail.ru\", \"rambler.ru\", \"yandex.ru\", \"ya.ru\", \"list.ru\", \"hotmail.be\",\n        \"live.be\", \"skynet.be\", \"voo.be\", \"tvcablenet.be\", \"telenet.be\",\n        \"hotmail.com.ar\", \"live.com.ar\", \"yahoo.com.ar\", \"fibertel.com.ar\",\n        \"speedy.com.ar\", \"arnet.com.ar\", \"yahoo.com.mx\", \"live.com.mx\", \"hotmail.es\",\n        \"hotmail.com.mx\", \"prodigy.net.mx\", \"yahoo.ca\", \"hotmail.ca\", \"bell.net\",\n        \"shaw.ca\", \"sympatico.ca\", \"rogers.com\", \"yahoo.com.br\", \"hotmail.com.br\",\n        \"outlook.com.br\", \"uol.com.br\", \"bol.com.br\", \"terra.com.br\", \"ig.com.br\",\n        \"itelefonica.com.br\", \"r7.com\", \"zipmail.com.br\", \"globo.com\", \"globomail.com\",\n        \"oi.com.br\"]\n\n    def __init__(self, *args, **kwargs):\n        LookupHandler.__init__(self, *args, **kwargs)\n        self.key_cache = { }\n\n    def _score(self, key):\n        return (self.SCORE, _('Found key in Web Key Directory'))\n\n    def _lookup(self, address, strict_email_match=True):\n        local, _, domain = address.partition(\"@\")\n        if domain.lower() in self.DOMAIN_BLACKLIST:\n            # FIXME: Maybe make this dynamic; check for the WKD policy file and\n            #        if it is present remove the provider from the blacklist.\n            self.session.ui.debug(\n                '[%s] Blacklisted domain, skipping: %s' % (self.NAME, domain))\n            return {}\n\n        # FIXMEs:\n        #   - Check the spec and make sure we are doing the right thing when\n        #     comes to redirects. Probably switch off. But Linus! They seem\n        #     broken now, wah, wah, wah.\n        #   - Check the policy file, if it doesn't exist don't leak the\n        #     e-mail address to the server? Cache this? Counter-argument,\n        #     shame if user has no policy file but has a published key.\n        #   - Check content-type, because some sites return weird crap.\n\n        local_part_encoded = _zbase_encode(\n            hashlib.sha1(local.lower().encode('utf-8')).digest())\n        error = None\n        keyinfo = None\n        for url in WebKeyDirectoryURLs(address):\n            try:\n                if 'keylookup' in self.session.config.sys.debug:\n                    self.session.ui.debug('[%s] Fetching %s' % (self.NAME, url))\n                key_data = secure_urlget(self.session, url,\n                                         maxbytes=self.MAX_KEY_SIZE+1,\n                                         timeout=int(self.TIMEOUT / 3))\n                if key_data:\n                    keyinfo = get_keyinfo(key_data,\n                        key_source=(self.SHORTNAME, url),\n                        key_info_class=MailpileKeyInfo\n                        )[0]\n                    error = None\n                    break\n                else:\n                    error = 'Key not found'\n            except urllib2.HTTPError as e:\n                if e.code == 404 and '+' not in address:\n                    error = '404: %s' % e\n                    # Since we are testing openpgpkey.* first, if we actually get a\n                    # valid response back we should treat that as authoritative and\n                    # not waste cycles checking the bare domain too.\n                    break\n                else:\n                    error = str(e)\n            except ssl.CertificateError as e:\n                error = 'TLS: %s' % e\n            except (urllib2.URLError, ValueError, KeyError) as e:\n                error = 'FAIL: %s' % e\n\n        if not error and len(key_data) > self.MAX_KEY_SIZE:\n            error = \"Key too big (>%d bytes), ignoring\" % self.MAX_KEY_SIZE\n            if 'keylookup' in self.session.config.sys.debug:\n                self.session.ui.debug(error)\n\n        if error and 'keylookup' in self.session.config.sys.debug:\n            self.session.ui.debug('[%s] Error: %s' % (self.NAME, error))\n        if not error:\n            self.key_cache[keyinfo[\"fingerprint\"]] = key_data\n        elif error[:3] in ('TLS', 'FAI', '404'):\n            return {}  # Suppress these errors, they are common.\n        else:\n            raise ValueError(error)\n\n        # FIXME: Key refreshes will need to know where this key came\n        #        from, we should record this somewhere. Should WKD\n        #        keys be considered ephemeral? What about revocations?\n        #        What about signatures? What if we get back multiple\n        #        keys/certs? What if we get back a revocation?\n\n        return {keyinfo[\"fingerprint\"]: keyinfo}\n\n    def _getkey(self, email, keyinfo):\n        # FIXME: Consider cleaning up the key before we import it, to\n        #        get rid of signatures and UIDs we don't care about.\n        data = self.key_cache.pop(keyinfo[\"fingerprint\"])\n        if data:\n            return self._gnupg().import_keys(data, filter_uid_emails=[email])\n        else:\n            raise ValueError(\"Key not found\")\n\n\nclass GetWebKeyDirectoryURLs(Command):\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/wkd/urls', None, '<emails>')\n\n    def command(self):\n        return self._success(_(\"Generated WKD URLs\"),\n            dict((addr, list(WebKeyDirectoryURLs(addr))) for addr in self.args))\n\n\n_ = gettext\n\n_plugins = PluginManager(builtin=__file__)\n_plugins.register_commands(GetWebKeyDirectoryURLs)\n\nregister_crypto_key_lookup_handler(WKDLookupHandler)\n\n"
  },
  {
    "path": "mailpile/plugins/migrate.py",
    "content": "import mailpile.security as security\nfrom mailpile.commands import Command\nfrom mailpile.config.defaults import APPVER\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.mail_source.local import LocalMailSource\nfrom mailpile.plugins import PluginManager\nfrom mailpile.util import *\nfrom mailpile.vcard import *\n\n\n_plugins = PluginManager(builtin=__file__)\n\n# We might want to do this differently at some point, but\n# for now it's fine.\n\n\ndef migrate_routes(session):\n    # Migration from route string to messageroute structure\n    def route_parse(route):\n        if route.startswith('|'):\n            command = route[1:].strip()\n            return {\n                \"name\": command.split()[0],\n                \"protocol\": \"local\",\n                \"command\": command\n            }\n        else:\n            res = re.split(\n                \"([\\w]+)://([^:]+):([^@]+)@([\\w\\d.]+):([\\d]+)[/]{0,1}\", route)\n            if len(res) >= 5:\n                return {\n                    \"name\": _(\"%(user)s on %(host)s\"\n                              ) % {\"user\": res[2], \"host\": res[4]},\n                    \"protocol\": res[1],\n                    \"username\": res[2],\n                    \"password\": res[3],\n                    \"host\": res[4],\n                    \"port\": res[5]\n                }\n            else:\n                session.ui.warning(_('Could not migrate route: %s') % route)\n        return None\n\n    def make_route_name(route_dict):\n        # This will always return the same hash, no matter how Python\n        # decides to order the dict internally.\n        return md5_hex(str(sorted(list(route_dict.iteritems()))))[:8]\n\n    if session.config.prefs.get('default_route'):\n        route_dict = route_parse(session.config.prefs.default_route)\n        if route_dict:\n            route_name = make_route_name(route_dict)\n            session.config.routes[route_name] = route_dict\n            session.config.prefs.default_messageroute = route_name\n\n    return True\n\n\ndef migrate_mailboxes(session):\n    config = session.config\n\n    # FIXME: This should be using mailpile.vfs.FilePath\n    # FIXME: Link new mail sources to a profile... any profile?\n\n    def _common_path(paths):\n        common_head, junk = os.path.split(paths[0])\n        for path in paths:\n            head, junk = os.path.split(path)\n            while (common_head and common_head != '/' and\n                   head and head != '/' and\n                   head != common_head):\n                # First we try shortening the target path...\n                while head and head != '/' and head != common_head:\n                    head, junk = os.path.split(head)\n                # If that failed, lop one off the common path and try again\n                if head != common_head:\n                    common_head, junk = os.path.split(common_head)\n                    head, junk = os.path.split(path)\n        return common_head\n\n    mailboxes = []\n    thunderbird = []\n\n    spam_tids = [tag._key for tag in config.get_tags(type='spam')]\n    trash_tids = [tag._key for tag in config.get_tags(type='trash')]\n    inbox_tids = [tag._key for tag in config.get_tags(type='inbox')]\n\n    # Iterate through config.sys.mailbox, sort mailboxes by type\n    for mbx_id, path, src in config.get_mailboxes(with_mail_source=False):\n        if (path.startswith('src:') or\n                config.is_editable_mailbox(mbx_id)):\n            continue\n        elif 'thunderbird' in path.lower():\n            thunderbird.append((mbx_id, path))\n        else:\n            mailboxes.append((mbx_id, path))\n\n    if thunderbird:\n        # Create basic mail source...\n        if 'tbird' not in config.sources:\n            config.sources['tbird'] = {\n                'name': 'Thunderbird',\n                'protocol': 'mbox',\n            }\n            config.sources.tbird.discovery.create_tag = True\n\n        config.sources.tbird.discovery.policy = 'read'\n        config.sources.tbird.discovery.process_new = True\n        tbird_src = LocalMailSource(session, config.sources.tbird)\n\n        # Configure discovery policy?\n        root = _common_path([path for mbx_id, path in thunderbird])\n        if 'thunderbird' in root.lower():\n            # FIXME: This is wrong, we should create a mailbox entry\n            #        with the policy 'watch'.\n            tbird_src.my_config.discovery.path = root\n\n        # Take over all the mailboxes\n        for mbx_id, path in thunderbird:\n            mbx = tbird_src.take_over_mailbox(mbx_id)\n            if 'inbox' in path.lower():\n                mbx.apply_tags.extend(inbox_tids)\n            elif 'spam' in path.lower() or 'junk' in path.lower():\n                mbx.apply_tags.extend(spam_tids)\n            elif 'trash' in path.lower():\n                mbx.apply_tags.extend(trash_tids)\n\n        tbird_src.my_config.discovery.policy = 'unknown'\n\n    for name, proto, description, cls in (\n        ('mboxes', 'local', 'Local mailboxes', LocalMailSource),\n    ):\n        if mailboxes:\n            # Create basic mail source...\n            if name not in config.sources:\n                config.sources[name] = {\n                    'name': description,\n                    'protocol': proto\n                }\n                config.sources[name].discovery.create_tag = False\n            config.sources[name].discovery.policy = 'read'\n            config.sources[name].discovery.process_new = True\n            config.sources[name].discovery.apply_tags = inbox_tids[:]\n            src = cls(session, config.sources[name])\n            for mbx_id, path in mailboxes:\n                mbx = src.take_over_mailbox(mbx_id)\n            config.sources[name].discovery.policy = 'unknown'\n\n    return True\n\n\ndef migrate_cleanup(session):\n    config = session.config\n\n    # Clean the autotaggers\n    autotaggers = [t for t in config.prefs.autotag.values() if t.tagger]\n    config.prefs.autotag = autotaggers\n\n    # Clean the vcards:\n    #   - Prefer vcards with valid key info\n    #   - De-dupe everything based on name/email combinations\n    def cardprint(vc):\n        emails = set([v.value for v in vc.get_all('email')])\n        return '/'.join([vc.fn] + sorted(list(emails)))\n    vcards = all_vcards = set(config.vcards.values())\n    keepers = set()\n    for vc in vcards:\n        keys = vc.get_all('key')\n        for k in keys:\n            try:\n                mime, fp = k.value.split('data:')[1].split(',')\n                if fp:\n                    keepers.add(vc)\n            except (ValueError, IndexError):\n                pass\n    for p in (1, 2):\n        prints = set([cardprint(vc) for vc in keepers])\n        for vc in vcards:\n            cp = cardprint(vc)\n            if cp not in prints:\n                keepers.add(vc)\n                prints.add(cp)\n        vcards = keepers\n        keepers = set()\n    # Deleted!!\n    config.vcards.del_vcards(*list(all_vcards - vcards))\n\n    return True\n\n\nMIGRATIONS_BEFORE_SETUP = [migrate_routes]\nMIGRATIONS_AFTER_SETUP = [migrate_cleanup]\nMIGRATIONS = {\n    'routes': migrate_routes,\n    'sources': migrate_mailboxes,\n    'cleanup': migrate_cleanup\n}\n\n\nclass Migrate(Command):\n    \"\"\"Perform any needed migrations\"\"\"\n    SYNOPSIS = (None, 'setup/migrate', None,\n                '[' + '|'.join(sorted(MIGRATIONS.keys())) + ']')\n    ORDER = ('Internals', 0)\n    COMMAND_SECURITY = security.CC_CHANGE_CONFIG\n\n    def command(self, before_setup=True, after_setup=True):\n        session = self.session\n        err = cnt = 0\n\n        migrations = []\n        for a in self.args:\n            if a in MIGRATIONS:\n                migrations.append(MIGRATIONS[a])\n            else:\n                raise UsageError(_('Unknown migration: %s (available: %s)'\n                                   ) % (a, ', '.join(MIGRATIONS.keys())))\n\n        if not migrations:\n            migrations = ((before_setup and MIGRATIONS_BEFORE_SETUP or []) +\n                          (after_setup and MIGRATIONS_AFTER_SETUP or []))\n\n        for mig in migrations:\n            try:\n                if mig(session):\n                    cnt += 1\n                else:\n                    err += 1\n            except:\n                self._ignore_exception()\n                err += 1\n\n        self.session.config.version = APPVER  # We've migrated to this!\n\n        self._background_save(config=True)\n        return self._success(_('Performed %d migrations, failed %d.'\n                               ) % (cnt, err))\n\n\n_plugins.register_commands(Migrate)\n"
  },
  {
    "path": "mailpile/plugins/motd.py",
    "content": "import os\nimport json\nimport sys\nfrom datetime import datetime as dtime\nfrom urllib2 import urlopen\n\nfrom mailpile.commands import Command\nfrom mailpile.config.base import PublicConfigRule as p\nfrom mailpile.config.defaults import APPVER\nfrom mailpile.conn_brokers import Master as ConnBroker\nfrom mailpile.plugins import PluginManager\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.util import *\n\n\n_ = lambda t: t\n_plugins = PluginManager(builtin=__file__)\n\n\n# MARS is the Mailpile Analytics Reporting System. Pretty fancy, huh?\n#\n# Details:\n#  https://github.com/mailpile/Mailpile/wiki/Mailpile-Analytics-Reporting-System\n#\nMOTD_MARS    = '/motd/%(ver)s-%(os)s/motd.json?lang=%(lang)s&py=%(py)s'\nMOTD_NO_MARS = '/motd/latest/motd.json'\n#\nMOTD_URL_DEFAULT          = 'https://www.mailpile.is' + MOTD_MARS\nMOTD_URL_TOR_ONLY         = 'http://clgs64523yi2bkhz.onion' + MOTD_MARS\nMOTD_URL_NO_MARS          = 'https://www.mailpile.is' + MOTD_NO_MARS\nMOTD_URL_TOR_ONLY_NO_MARS = 'http://clgs64523yi2bkhz.onion' + MOTD_NO_MARS\nMOTD_URLS = {\n    \"default\": MOTD_URL_DEFAULT,\n    \"tor-only\": MOTD_URL_TOR_ONLY,\n    \"generic\": MOTD_URL_NO_MARS,\n    \"tor-generic\": MOTD_URL_TOR_ONLY_NO_MARS,\n    \"unknown\": \"\",\n    \"none\": \"\"\n}\n\n\n_plugins.register_config_variables('prefs', {\n    'motd_url': p(_('URL to the Message Of The Day'), 'str', 'unknown')\n})\n\n\nclass MessageOfTheDay(Command):\n    \"\"\"Download and/or display the Message Of The Day\"\"\"\n    SYNOPSIS = (None, 'motd', 'motd', '[--silent|--ifnew] [--[no]update|--check]')\n    ORDER = ('Internals', 6)\n    CONFIG_REQUIRED = False\n    IS_USER_ACTIVITY = False\n\n    @classmethod\n    def _disable_updates(cls, session):\n        # Don't auto-update the MOTD if the user hasn't configured any\n        # accounts yet - no point bothering the user or the MOTD server.\n        #\n        # FIXME: Check other conditions?\n        #\n        return (len(session.config.sources) < 1)\n\n    @classmethod\n    def update(cls, session):\n        if not cls._disable_updates(session):\n            cls(session, arg=['--silent', '--check']).run()\n\n    def _get(self, url):\n        if url.startswith('file:'):\n            return open(url[5:], 'r').read()\n\n        if url.startswith('https:'):\n            conn_need = [ConnBroker.OUTGOING_HTTPS]\n        elif url.startswith('http:'):\n            conn_need = [ConnBroker.OUTGOING_HTTP]\n        else:\n            return _('Unsupported URL for message of the day: %s') % url\n\n        with ConnBroker.context(need=conn_need) as ctx:\n            self.session.ui.mark('Getting: %s' % url)\n            return urlopen(url, data=None, timeout=10).read()\n\n    class CommandResult(Command.CommandResult):\n        def as_text(self):\n            motd = (self.result or {}).get('_motd')\n            if not motd:\n                return ''\n\n            date = dtime.fromtimestamp(self.result.get('timestamp', 0))\n            return '%s, %4.4d-%2.2d-%2.2d:\\n\\n    %s\\n\\n*** %s ***\\n' % (\n                _('Message Of The Day'),\n                date.year, date.month, date.day,\n                motd.replace('\\n', '\\n    '),\n                self.result.get('_version_info')\n            )\n\n    def command(self):\n        session, config = self.session, self.session.config\n\n        # If not configured, do nothing.\n        url = MOTD_URLS.get(config.prefs.motd_url, config.prefs.motd_url)\n        if not url:\n            return self._success('', result={})\n\n        old_motd = motd = None\n        try:\n            old_motd = motd = config.load_pickle('last_motd')\n            message = '%s: %s' % (_('Message Of The Day'), _('Loaded'))\n        except (OSError, IOError):\n            pass\n\n        if '--update' in self.args:\n            motd = None\n        elif motd and '--check' in self.args:\n            if motd['_updated'] < (time.time() - 23.5 * 3600):\n                motd = None\n\n        if motd is None:\n            if (('--update' not in self.args and self._disable_updates(session))\n                    or '--noupdate' in self.args):\n                return self._success('', result={})\n\n            try:\n                motd = json.loads(self._get(url % {\n                    'ver': APPVER,\n                    'lang': config.prefs.language or 'en',\n                    'os': sys.platform,\n                    'py': sys.version.split()[0]\n                }))\n                motd['_updated'] = int(time.time())\n                motd['_is_new'] = False\n                if (not old_motd\n                        or old_motd.get(\"timestamp\") != motd.get(\"timestamp\")):\n                    motd['_is_new'] = True\n                    message = '%s: %s' % (_('Message Of The Day'), _('Updated'))\n                config.save_pickle(motd, 'last_motd', encrypt=False)\n            except (IOError, OSError, ValueError):\n                pass\n\n        if not motd:\n            motd = old_motd\n\n        if motd:\n            self.event.data['motd'] = motd\n\n            lang = config.prefs.language or 'en'\n            motd['_motd'] = motd.get(lang, motd.get('en'))\n\n            latest = motd.get('latest_version')\n            if not latest:\n                motd['_version_info'] = _('Mailpile update info unavailable')\n            elif latest == APPVER:\n                motd['_version_info'] = _('Your Mailpile is up to date')\n            else:\n                motd['_version_info'] = _('An upgrade for Mailpile is '\n                                          'available, version %s'\n                                          ) % latest\n\n            if '--silent' in self.args:\n                motd = {}\n            elif '--ifnew' in self.args and not motd.get('_is_new'):\n                motd = {}\n\n            return self._success(message, result=motd)\n        else:\n            message = '%s: %s' % (_('Message Of The Day'), _('Unknown'))\n            return self._error(message, result={})\n\n\n_plugins.register_commands(MessageOfTheDay)\n_plugins.register_slow_periodic_job('motd', 3600, MessageOfTheDay.update)\n"
  },
  {
    "path": "mailpile/plugins/oauth.py",
    "content": "from __future__ import print_function\nimport json\nimport time\nimport traceback\nfrom urllib import urlencode, quote_plus\n\nfrom mailpile.i18n import gettext\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.plugins.setup_magic import TestableWebbable\nfrom mailpile.util import *\n\n\n_ = lambda s: s\n_plugins = PluginManager(builtin=__file__)\n\n\n##[ Configuration ]###########################################################\n\n_plugins.register_config_section(\n    'oauth', ['OAuth configuration and state', False, {\n        'providers': ('Known OAuth providers', {\n            'protocol':      ('OAuth* protocol', str, ''),\n            'server_re':     ('Regular expression to match servers', str, ''),\n            'client_id':     ('The OAuth Client ID', str, ''),\n            'client_secret': ('The OAuth token itself', str, ''),\n            'redirect_re':   ('Regexp of URLs we can redirect to', str, ''),\n            'token_url':     ('The OAuth token endpoint', str, ''),\n            'oauth_url':     ('The OAuth authentication endpoint', str, ''),\n        }, {}),\n\n        'tokens': ('Current OAuth tokens', {\n            'provider':      ('Provider ID', str, ''),\n            'access_token':  ('Access token', str, ''),\n            'token_type':    ('Access token type', str, ''),\n            'expires_at':    ('Access token expiration time', int, 0),\n            'refresh_token': ('Refresh token', str, '')\n        }, {})\n    }])\n\n\n##[ Commands ]################################################################\n\nclass OAuth2(TestableWebbable):\n    SYNOPSIS = (None, None, 'setup/oauth2', None)\n    RAISES = (AccessError,)\n    HTTP_CALLABLE = ('GET', 'POST')\n    HTTP_QUERY_VARS = {\n        'mailsource': 'Mail source ID',\n        'mailroute': 'Mail route ID',\n        'hostname': 'Mail server',\n        'username': 'User name',\n        'code': 'Authorization code',\n        'error': 'Error code',\n        'scope': 'OAuth2 scope (ignored)',\n        'state': 'State token'\n    }\n    HARD_CODED_OAUTH2 = {\n        'GMail': {\n            'protocol': 'Google',\n            'server_re': '.*\\\\.(google|gmail)\\\\.com$',\n            'client_id': ('174733765695-1dnhaq06gt61tg432t0d6jlt76nng2t1'\n                          '.apps.googleusercontent.com'),\n            'client_secret': 'vbUxR2Dvqb1c-nI2-X_7NvCu',\n            'redirect_re': '^http://localhost[:/]',\n            'token_url': ('https://www.googleapis.com/oauth2/v4/token'),\n            'oauth_url': ('https://accounts.google.com/o/oauth2/v2/auth'\n                          '?response_type=code'\n                          '&access_type=offline'\n                          '&scope=https://mail.google.com/'\n                          '&redirect_uri=%(redirect_uri)s'\n                          '&client_id=%(client_id)s'\n                          '&state=%(state)s'\n                          '&login_hint=%(username)s')}}\n    OAUTH2_OOB_REDIRECT = \"urn:ietf:wg:oauth:2.0:oob\"\n\n    @classmethod\n    def RedirectURI(cls, config, oauth2_cfg, http_host=None):\n        if not http_host:\n            http_host = \"%s:%s\" % config.http_worker.httpd.sspec[:2]\n        meth = 'http' if http_host.startswith('localhost:') else 'https'\n        url = '/'.join([\n            '%s://%s%s' % (meth, http_host, config.http_worker.httpd.sspec[2]),\n            cls.SYNOPSIS[2],\n            ''])\n\n        url_re = oauth2_cfg.get('redirect_re')\n        if url_re and not re.match(url_re, url, re.DOTALL):\n            return cls.OAUTH2_OOB_REDIRECT\n\n        return url\n\n    @classmethod\n    def ActivateHardCodedOAuth(cls, config):\n        for name, cfg in cls.HARD_CODED_OAUTH2.iteritems():\n            if name not in config.oauth.providers.keys():\n                config.oauth.providers[name] = cfg\n\n    @classmethod\n    def GetOAuthConfig(cls, config, hostname=None, oname=None):\n        cls.ActivateHardCodedOAuth(config)\n        if oname:\n            return (oname, config.oauth.providers[oname])\n        for name, cfg in config.oauth.providers.iteritems():\n            if re.match(cfg['server_re'], hostname):\n                 return (name, cfg)\n        return (None, None)\n\n    @classmethod\n    def GetOAuthURLVars(cls, session, ocfg, username):\n        # FIXME: Make this a custom token just for OAuth\n        state = '%s/%s/%s' % (\n            username, ocfg._key, session.ui.html_variables['csrf_token'])\n        http_host = session.ui.html_variables.get(\"http_host\")\n        return {\n            'redirect_uri': quote_plus(\n                 cls.RedirectURI(session.config, ocfg, http_host)),\n            'client_id': quote_plus(ocfg['client_id']),\n            'username': quote_plus(username),\n            'state': state}\n\n    @classmethod\n    def GetOAuthURL(cls, session, ocfg, username, url_vars=None):\n        if url_vars is None:\n            url_vars = cls.GetOAuthURLVars(session, ocfg, username)\n        return ocfg['oauth_url'] % url_vars\n\n    @classmethod\n    def XOAuth2Response(cls, username, token_info):\n        return 'user=%s\\x01auth=Bearer %s\\x01\\x01' % (\n            username, token_info.access_token)\n\n    @classmethod\n    def GetToken(cls, session, oauth2_cfg, code, tok_id=None):\n        \"\"\"\n        Fetch a token and associated details from an authorization server.\n\n        Returns something like this:\n          {\n            'access_token': 'tsbk6pcSPSNffdzEkxVicwf...',\n            'token_type': 'Bearer',\n            'expires_at': 123456789,\n            'refresh_token': '1CtboygBSKA-Ut1e7...'\n          }\n        \"\"\"\n        post_data = urlencode([\n            ('code', code),\n            ('client_id', oauth2_cfg['client_id']),\n            ('client_secret', oauth2_cfg['client_secret']),\n            ('redirect_uri', cls.RedirectURI(\n                session.config, oauth2_cfg,\n                session.ui.html_variables.get(\"http_host\"))),\n            ('grant_type', 'authorization_code')])\n\n        data = json.loads(cls.URLGet(\n           session, oauth2_cfg['token_url'], data=post_data))\n\n        tok_id = tok_id or ('%x' % time.time())\n        session.config.oauth.tokens[tok_id] = {}\n        tok_info = session.config.oauth.tokens[tok_id]\n        tok_info.provider = oauth2_cfg._key\n        tok_info.token_type = data['token_type']\n        tok_info.access_token = data['access_token']\n        tok_info.expires_at = int(time.time() + data['expires_in'])\n        tok_info.refresh_token = data['refresh_token']\n        if 'oauth' in session.config.sys.debug:\n            session.ui.debug(\"Fetched OAuth2 token for %s\" % tok_id)\n\n        return tok_id, tok_info\n\n    @classmethod\n    def GetFreshTokenInfo(cls, session, tok_id):\n        oauth2_cfg, post_data = {}, None\n        try:\n            tok_info = session.config.oauth.tokens[tok_id]\n            if (tok_info.expires_at > (time.time() + 300)\n                    or not tok_info.refresh_token):\n                return tok_info\n\n            oauth2_cfg = session.config.oauth.providers[tok_info.provider]\n            post_data = urlencode([\n                ('refresh_token', tok_info.refresh_token),\n                ('client_id', oauth2_cfg['client_id']),\n                ('client_secret', oauth2_cfg['client_secret']),\n                ('grant_type', 'refresh_token')])\n            data = json.loads(cls.URLGet(\n               session, oauth2_cfg['token_url'], data=post_data))\n\n            tok_info.access_token = data['access_token']\n            tok_info.expires_at = int(time.time() + data['expires_in'])\n            if 'oauth' in session.config.sys.debug:\n                session.ui.debug(\"Refreshed OAuth2 token for %s\" % tok_id)\n\n            return tok_info\n        except:\n            if 'oauth' in session.config.sys.debug:\n                session.ui.debug(traceback.format_exc())\n                session.ui.debug('Failed: POST %s, data=%s' % (\n                    oauth2_cfg.get('token_url'), post_data))\n            return False\n\n    def setup_command(self, session):\n        config = session.config\n\n        code = self.data.get('code', [''])[0]\n        msid = self.data.get('mailsource', [''])[0]\n        rtid = self.data.get('mailroute', [''])[0]\n        username = self.data.get('username', [''])[0]\n        hostname = self.data.get('hostname', [''])[0]\n        state = self.data.get('state', [''])[0]\n        results = { 'error': self.data.get('error', [''])[0] }\n\n        if msid:\n            username = config.sources[msid].username\n            hostname = config.sources[msid].host\n        elif rtid:\n            username = config.routes[rtid].username\n            hostname = config.routes[rtid].host\n\n        if code:\n            username, oname, csrf = state.split('/', 2)\n            if not session.ui.valid_csrf_token(csrf):\n                print('Invalid CSRF token: %s' % csrf)\n                raise AccessError('Invalid CSRF token')\n\n            oname, ocfg = self.GetOAuthConfig(config, oname=oname)\n            tok_id, tok_info = self.GetToken(session, ocfg, code,\n                                             tok_id=username)\n\n            # This helps the mail sources/routes detect that it may\n            # be worth trying the connection again...\n            for msid, source in config.sources.iteritems():\n                if source.username == username:\n                    source.password = tok_info.access_token\n            for msid, route in config.routes.iteritems():\n                if route.username == username:\n                    route.password = tok_info.access_token\n\n            results['success'] = True\n\n        elif username and hostname:\n            oname, ocfg = self.GetOAuthConfig(config, hostname)\n            uv = self.GetOAuthURLVars(session, ocfg, username)\n            hr = (uv['redirect_uri'] != quote_plus(self.OAUTH2_OOB_REDIRECT))\n            results.update(uv)\n            results.update({\n                'have_redirect': hr,\n                'username': username,\n                'oauth_url': self.GetOAuthURL(session, ocfg, username,\n                                              url_vars=uv) })\n\n        return self._success(_('OAuth2 Authorization'), results)\n\n\n_ = gettext\n_plugins.register_commands(OAuth2)\n"
  },
  {
    "path": "mailpile/plugins/plugins.py",
    "content": "import os\n\nimport mailpile.commands\nimport mailpile.security as security\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\n\n\n_plugins = PluginManager(builtin=__file__)\n\n\nclass Plugins(mailpile.commands.Command):\n    \"\"\"List the currently available plugins.\"\"\"\n    SYNOPSIS = (None, 'plugins', None, '[<plugins>]')\n    ORDER = ('Config', 9)\n    HTTP_CALLABLE = ('GET',)\n    CONFIG_REQUIRED = False\n\n    def command(self):\n        pm = self.session.config.plugins\n        wanted = self.args\n\n        info = dict((d, {\n            'loaded': d in pm.LOADED,\n            'builtin': d not in pm.DISCOVERED\n        }) for d in pm.available() if (not wanted or d in wanted))\n\n        for plugin in info:\n            if plugin in pm.DISCOVERED:\n                info[plugin]['manifest'] = pm.DISCOVERED[plugin][1]\n\n        return self._success(_('Listed available plugins'), info)\n\n\nclass LoadPlugin(mailpile.commands.Command):\n    \"\"\"Load and enable a given plugin.\"\"\"\n    SYNOPSIS = (None, 'plugins/load', 'plugins/load', '<plugin>')\n    ORDER = ('Config', 9)\n    HTTP_CALLABLE = ('POST',)\n    HTTP_POST_VARS = {\n        'plugin': '<plugin name>'\n    }\n    COMMAND_SECURITY = security.CC_CHANGE_CONFIG\n\n    def command(self):\n        config = self.session.config\n        plugins = config.plugins\n        args = list(self.args) + self.data.get('plugin', [])\n\n        for plugin in args:\n            if plugin in plugins.LOADED:\n                return self._error(_('Already loaded: %s') % plugin,\n                                   info={'loaded': plugin})\n\n        for plugin in args:\n            try:\n                # FIXME: This fails to update the ConfigManger\n                # FIXME: This fails to start workers\n                discovered = plugins.DISCOVERED\n                if plugins.load(plugin, process_manifest=True, config=config):\n                    if (plugin in discovered and not\n                            discovered[plugin][1].get('require_login', True)):\n                        config.sys.plugins_early.append(plugin)\n                    config.sys.plugins.append(plugin)\n                else:\n                    raise ValueError('Loading failed')\n            except Exception as e:\n                self._ignore_exception()\n                return self._error(_('Failed to load plugin: %s') % plugin,\n                                   info={'failed': plugin})\n\n        config.save()\n        return self._success(_('Loaded plugins: %s') % ', '.join(self.args),\n                             {'loaded': self.args})\n\n\nclass DisablePlugin(mailpile.commands.Command):\n    \"\"\"Disable a plugin.\"\"\"\n    SYNOPSIS = (None, 'plugins/disable', 'plugins/disable', '<plugin>')\n    ORDER = ('Config', 9)\n    HTTP_CALLABLE = ('POST',)\n    HTTP_POST_VARS = {\n        'plugin': '<plugin name>'\n    }\n    COMMAND_SECURITY = security.CC_CHANGE_CONFIG\n\n    def command(self):\n        config = self.session.config\n        plugins = config.plugins\n        args = list(self.args) + self.data.get('plugin', [])\n        for plugin in args:\n            if plugin in plugins.REQUIRED:\n                return self._error(_('Required plugins can not be disabled: %s'\n                                     ) % plugin)\n            if plugin not in config.sys.plugins:\n                return self._error(_('Plugin not loaded: %s') % plugin)\n\n        for plugin in args:\n            while plugin in config.sys.plugins:\n                config.sys.plugins.remove(plugin)\n            while plugin in config.sys.plugins_early:\n                config.sys.plugins_early.remove(plugin)\n\n        config.save()\n        return self._success(_('Disabled plugins: %s (restart required)'\n                               ) % ', '.join(self.args),\n                             {'disabled': self.args})\n\n\n_plugins.register_commands(Plugins, LoadPlugin, DisablePlugin)\n"
  },
  {
    "path": "mailpile/plugins/search.py",
    "content": "import datetime\nimport json\nimport re\nimport time\nimport traceback\nimport unicodedata\n\nimport mailpile.security as security\nfrom mailpile.commands import Command\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.mailutils import MBX_ID_LEN, FormatMbxId\nfrom mailpile.mailutils.addresses import AddressHeaderParser\nfrom mailpile.mailutils.emails import Email, ExtractEmails, ExtractEmailAndName\nfrom mailpile.plugins import PluginManager\nfrom mailpile.search import MailIndex\nfrom mailpile.security import evaluate_sender_trust\nfrom mailpile.urlmap import UrlMap\nfrom mailpile.util import *\nfrom mailpile.ui import SuppressHtmlOutput\nfrom mailpile.vfs import vfs, FilePath\nfrom mailpile.vcard import AddressInfo\n\n\n_plugins = PluginManager(builtin=__file__)\n\n\n##[ Shared basic Search Result class]#########################################\n\nclass SearchResults(dict):\n\n    _NAME_TITLES = ('the', 'mr', 'ms', 'mrs', 'sir', 'dr', 'lord')\n\n    def _name(self, sender, short=True, full_email=False):\n        words = re.sub('[\"<>]', '', sender).split()\n        nomail = [w for w in words if not '@' in w]\n        if nomail:\n            if short:\n                if len(nomail) > 1 and nomail[0].lower() in self._NAME_TITLES:\n                    return nomail[1]\n                return nomail[0]\n            return ' '.join(nomail)\n        elif words:\n            if not full_email:\n                return words[0].split('@', 1)[0]\n            return words[0]\n        return '(nobody)'\n\n    def _names(self, senders):\n        if len(senders) > 1:\n            names = {}\n            for sender in senders:\n                sname = self._name(sender)\n                names[sname] = names.get(sname, 0) + 1\n            namelist = names.keys()\n            namelist.sort(key=lambda n: -names[n])\n            return ', '.join(namelist)\n        if len(senders) < 1:\n            return '(no sender)'\n        if senders:\n            return self._name(senders[0], short=False)\n        return ''\n\n    def _compact(self, namelist, maxlen):\n        l = len(namelist)\n        while l > maxlen:\n            namelist = re.sub(', *[^, \\.]+, *', ',,', namelist, 1)\n            if l == len(namelist):\n                break\n            l = len(namelist)\n        namelist = re.sub(',,,+, *', ' .. ', namelist, 1)\n        return namelist\n\n    TAG_TYPE_FLAG_MAP = {\n        'trash': 'trash',\n        'spam': 'spam',\n        'ham': 'ham',\n        'drafts': 'draft',\n        'blank': 'draft',\n        'sent': 'from_me',\n        'unread': 'unread',\n        'outbox': 'from_me',\n        'replied': 'replied',\n        'fwded': 'forwarded'\n    }\n\n    def _metadata(self, msg_info):\n        msg_mid = msg_info[MailIndex.MSG_MID]\n        if '-' in msg_mid:\n            # Ephemeral...\n            msg_idx = None\n        else:\n            msg_idx = int(msg_mid, 36)\n            cache = self.idx.CACHE.get(msg_idx, {})\n            if 'metadata' in cache:\n                return cache['metadata']\n\n        nz = lambda l: [v for v in l if v]\n        msg_ts = long(msg_info[MailIndex.MSG_DATE], 36)\n        msg_date = datetime.datetime.fromtimestamp(msg_ts)\n\n        fe, fn = ExtractEmailAndName(msg_info[MailIndex.MSG_FROM])\n        f_info = self._address(e=fe, n=fn)\n        f_info['aid'] = (self._msg_addresses(msg_info, no_to=True, no_cc=True)\n                         or [''])[0]\n        thread_mid = parent_mid = msg_info[MailIndex.MSG_THREAD_MID]\n        if '/' in thread_mid:\n            thread_mid, parent_mid = thread_mid.split('/')\n        expl = {\n            'mid': msg_mid,\n            'id': msg_info[MailIndex.MSG_ID],\n            'timestamp': msg_ts,\n            'from': f_info,\n            'to_aids': self._msg_addresses(msg_info, no_from=True, no_cc=True),\n            'cc_aids': self._msg_addresses(msg_info, no_from=True, no_to=True),\n            'msg_kb': int(msg_info[MailIndex.MSG_KB], 36),\n            'tag_tids': sorted(self._msg_tags(msg_info)),\n            'thread_mid': thread_mid,\n            'parent_mid': parent_mid,\n            'subject': msg_info[MailIndex.MSG_SUBJECT],\n            'body': MailIndex.get_body(msg_info),\n            'flags': {\n            },\n            'crypto': {\n            }\n        }\n\n        # Ephemeral messages do not have URLs\n        if '-' in msg_info[MailIndex.MSG_MID]:\n            expl['flags'].update({\n                'ephemeral': True,\n                'draft': True,\n            })\n        else:\n            expl['urls'] = {\n                'thread': self.urlmap.url_thread(msg_info[MailIndex.MSG_MID]),\n                'source': self.urlmap.url_source(msg_info[MailIndex.MSG_MID]),\n            }\n\n        # Support rich snippets\n        if expl['body']['snippet'].startswith('{'):\n            try:\n                expl['body'] = json.loads(expl['body']['snippet'])\n            except ValueError:\n                pass\n\n        # Misc flags\n        sender_vcard = self.idx.config.vcards.get_vcard(fe.lower())\n        if sender_vcard:\n            if sender_vcard.kind == 'profile':\n                expl['flags']['from_me'] = True\n            else:\n                expl['flags']['from_contact'] = True\n\n        tag_types = [self.idx.config.get_tag(t).type for t in expl['tag_tids']]\n        for t in self.TAG_TYPE_FLAG_MAP:\n            if t in tag_types:\n                expl['flags'][self.TAG_TYPE_FLAG_MAP[t]] = True\n\n        # Check tags for signs of encryption or signatures\n        tag_slugs = [self.idx.config.get_tag(t).slug for t in expl['tag_tids']]\n        for t in tag_slugs:\n            if t.startswith('mp_sig'):\n                expl['crypto']['signature'] = t[7:]\n            elif t.startswith('mp_enc'):\n                expl['crypto']['encryption'] = t[7:]\n\n        # Extra behavior for editable messages\n        if 'draft' in expl['flags']:\n            if 'ephemeral' in expl['flags']:\n                pass\n            elif self.idx.config.is_editable_message(msg_info):\n                expl['urls']['editing'] = self.urlmap.url_edit(expl['mid'])\n            else:\n                del expl['flags']['draft']\n\n        if msg_idx is not None:\n            cache['metadata'] = expl\n            self.idx.CACHE[msg_idx] = cache\n        return expl\n\n    def _msg_addresses(self, msg_info=None, addresses=[],\n                       no_from=False, no_to=False, no_cc=False):\n        cids = set()\n\n        for ai in addresses:\n            eid = self.idx.EMAIL_IDS.get(ai.address.lower())\n            cids.add(b36(self.idx.add_email(ai.address, name=ai.fn, eid=eid)))\n\n        if msg_info:\n            if not no_to:\n                to = [t for t in msg_info[MailIndex.MSG_TO].split(',') if t]\n                cids |= set(to)\n            if not no_cc:\n                cc = [t for t in msg_info[MailIndex.MSG_CC].split(',') if t]\n                cids |= set(cc)\n            if not no_from:\n                fe, fn = ExtractEmailAndName(msg_info[MailIndex.MSG_FROM])\n                if fe:\n                    eid = self.idx.EMAIL_IDS.get(fe.lower())\n                    cids.add(b36(self.idx.add_email(fe, name=fn, eid=eid)))\n\n        return sorted(list(cids))\n\n    def _address(self, cid=None, e=None, n=None):\n        if cid and not (e and n):\n            e, n = ExtractEmailAndName(self.idx.EMAILS[int(cid, 36)])\n        vcard = self.session.config.vcards.get_vcard(e)\n        if vcard and '@' in n:\n            n = vcard.fn\n        return AddressInfo(e, n, vcard=vcard)\n\n    def _msg_tags(self, msg_info):\n        tids = [t for t in msg_info[MailIndex.MSG_TAGS].split(',')\n                if t and self.session.config.tags.get(t)]\n        return tids\n\n    def _tag(self, tid, attributes={}):\n        return dict_merge(self.session.config.get_tag_info(tid), attributes)\n\n    _BAR = u'\\u2502'\n    _FORK = u'\\u251c'\n    _FIRST = u'\\u250c'\n    _LAST = u'\\u2514'\n    _BLANK = u' '\n    _DASH = u'\\u2500'\n    _TEE = u'\\u252c'\n\n    def _thread(self, thread_mid):\n        thr_info = self.idx.get_conversation(msg_idx=int(thread_mid, 36))\n        thr_info.sort(key=lambda i: long(i[self.idx.MSG_DATE], 36))\n        if not thr_info:\n            return []\n\n        # Map messages to parents\n        par_map = {}\n        for info in thr_info:\n            parent = (info[self.idx.MSG_THREAD_MID].split('/') + [None])[1]\n            par_map[info[self.idx.MSG_MID]] = (parent, info)\n\n        # Reverse the mapping\n        thr_map = {}\n        first_mid = thr_info[0][self.idx.MSG_MID]\n        for msg_mid, (par_mid, m_info) in par_map.iteritems():\n            if par_mid is None:\n                # If we have no parent, pretend the first message in the\n                # thread is the parent.\n                par_mid = first_mid\n            if par_mid != msg_mid:\n                thr_map[par_mid] = (thr_map.get(par_mid, []) + [msg_mid])\n            else:\n                thr_map[par_mid] = thr_map.get(par_mid, [])\n\n        # Render threads in thread order, including ascii art\n        thread = []\n        seen = set()\n        def by_date(p):\n            if p not in par_map:\n                return 0;\n            return int(par_map[p][1][self.idx.MSG_DATE], 36)\n        def render(prefix, mid, first=False):\n            kids = thr_map.get(mid, [])\n            if mid not in seen:\n                # This guarantees that if we somehow end up with a loop, we\n                # just skip over the repeated message and make progress.\n                seen.add(mid)\n                thread.append([mid,\n                               prefix + ((self._FIRST if first else self._TEE)\n                                         if kids else ''),\n                               kids])\n                if prefix.endswith(self._LAST):\n                    prefix = prefix[:-len(self._BAR)] + self._BLANK\n                elif prefix:\n                    prefix = prefix[:-len(self._BAR)] + self._BAR\n\n            if kids:\n                # This delete also prevents us from repeating ourselves.\n                del thr_map[mid]\n                kids.sort(key=by_date)\n                for i, kmid in enumerate(kids):\n                    if prefix.endswith(self._BLANK):\n                        if thread[-1][1].endswith(self._TEE):\n                            thread[-1][1] = thread[-1][1][:-len(self._TEE)]\n                            thread[-1][1] = thread[-1][1][:-len(self._LAST)]\n                            thread[-1][1] += self._FORK\n                            prefix = prefix[:-len(self._BLANK)]\n                    if i < len(kids) - 1:\n                        render(prefix + self._FORK, kmid)\n                    else:\n                        if prefix.endswith(self._BLANK):\n                            if thread[-1][1].endswith(self._LAST):\n                                thread[-1][1] = thread[-1][1][:-len(self._LAST)]\n                                prefix = prefix[:-len(self._BLANK)]\n                        render(prefix + self._LAST, kmid)\n        thr_keys = thr_map.keys()\n        thr_keys.sort(key=by_date)\n        first = True\n        for par_mid in thr_keys:\n            if par_mid in thr_map:\n                render('', par_mid, first=first)\n                first = False\n\n        return thread\n\n    WANT_MSG_TREE = ('attachments', 'html_parts', 'text_parts', 'vcal_parts',\n                     'header_list', 'headerprints', 'editing_strings', 'crypto',\n                     '_cleaned', 'trust')\n    PRUNE_MSG_TREE = ('headers', )  # Added by editing_strings\n\n    def _prune_msg_tree(self, tree):\n        for k in tree.keys():\n            if k not in self.WANT_MSG_TREE or k in self.PRUNE_MSG_TREE:\n                del tree[k]\n        for att in tree.get('attachments', []):\n            if 'part' in att:\n                del att['part']\n        return tree\n\n    def _troubleshoot_missing_message(self, email, tree):\n        mi = email.get_msg_info()\n        details = {\"locations\": []}\n        problems = []\n        for msg_ptr, mbox, fd in email.index.enumerate_ptrs_mboxes_fds(mi):\n            pf = None\n            mbox_ptr = msg_ptr[:MBX_ID_LEN]\n            mail_ptr = msg_ptr[MBX_ID_LEN:]\n            info = {\n                'msg_ptr': msg_ptr,\n                'mbox_key': mbox_ptr,\n                'mail_key': mail_ptr}\n\n            if fd:\n                try:\n                    fd.read(10240)\n                except:\n                    pf = _(\"Failed to read message {mail_key}, {mail_desc}.\")\n                    info['mail_desc'] = _('unknown error')\n            elif mbox is not None:\n                pf = _(\"Failed to open message {mail_key}, {mail_desc}.\")\n            else:\n                pf = _(\"Failed to open mailbox {mbox_key}\")\n\n            if mbox is not None:\n                info.update({\n                    'mail_desc': mbox.describe_msg_by_ptr(msg_ptr),\n                    'mbox_desc': unicode(mbox)})\n            if pf:\n                problems.append(pf.format(**info))\n            details[\"locations\"].append(info)\n\n        if problems:\n            details['details'] = ' '.join(problems)\n\n        return details\n\n    def _message(self, email):\n        problem, tree = None, {}\n        try:\n            # We load the message in stages (relying on the internal cache\n            # to make this not slow), so we can report more accurately what\n            # has failed.\n            problem = _('Failed to load and parse message data.')\n            msg = email.get_msg(pgpmime=False)\n\n            problem = _('Failed process message crypto (decrypt, etc).')\n            msg = email.get_msg(pgpmime='default')\n\n            problem = _('Failed to parse message.')\n            tree = email.get_message_tree(want=(email.WANT_MSG_TREE_PGP +\n                                                self.WANT_MSG_TREE))\n\n            problem = _('Failed process message crypto (decrypt, etc).')\n            email.evaluate_pgp(tree, decrypt=True)\n\n            problem = _(\"Failed to evalute sender trust\")\n            evaluate_sender_trust(self.session.config, email, tree)\n\n            editing_strings = tree.get('editing_strings')\n            if editing_strings:\n                for key in ('from', 'to', 'cc', 'bcc'):\n                    problem = _(\"Failed to parse %s headers.\" % key)\n                    if key in editing_strings:\n                        cids = self._msg_addresses(\n                            addresses=AddressHeaderParser(\n                                unicode_data=editing_strings[key]))\n                        editing_strings['%s_aids' % key] = cids\n                        for cid in cids:\n                            if cid not in self['data']['addresses']:\n                                self['data']['addresses'\n                                             ][cid] = self._address(cid=cid)\n            problem = None\n\n        except Exception as e:\n            if problem:\n                problem += ' ' + _('Message may be corrupt!')\n            details = {\n                'error': unicode(e),\n                 'details': problem,\n                'traceback': traceback.format_exc(e)}\n            details.update(self._troubleshoot_missing_message(email, tree))\n            self['errors'] = self.get('errors', {})\n            self['errors'][email.msg_mid()] = details\n\n        return self._prune_msg_tree(tree)\n\n    def __init__(self, session, idx,\n                 results=None, start=0, end=None, num=None,\n                 emails=None, view_pairs=None, people=None,\n                 suppress_data=False, full_threads=True,\n                 unwrap_pgp='all'):\n        dict.__init__(self)\n        self.session = session\n        self.people = people\n        self.emails = emails or []\n        self.unwrap_pgp = unwrap_pgp\n        self.view_pairs = view_pairs or {}\n        self.idx = idx\n        self.urlmap = UrlMap(self.session)\n\n        results = self.results = results or session.results or []\n\n        num = num or session.config.prefs.num_results\n        if end:\n            start = end - num\n        if start > len(results):\n            start = len(results)\n        if start < 0:\n            start = 0\n\n        try:\n            threads = [b36(r) for r in results[start:start + num]]\n        except TypeError:\n            results = threads = []\n            start = end = 0\n\n        self.session.ui.mark(_('Parsing metadata for %d results '\n                               '(full_threads=%s)') % (len(threads),\n                                                       full_threads))\n\n        self.update({\n            'summary': _('Search: %s') % ' '.join(session.searched),\n            'stats': {\n                'count': len(threads),\n                'start': start + 1,\n                'end': start + min(num, len(results)-start),\n                'total': len(results),\n            },\n            'search_order': session.order,\n            'search_terms': session.searched,\n            'index_capabilities': dict((c, True) for c in idx.CAPABILITIES),\n            'tag_capabilities': {},\n            'address_ids': [],\n            'message_ids': [],\n            'view_pairs': view_pairs,\n            'thread_ids': threads,\n        })\n        if 'tags' in self.session.config:\n            search_tags = [idx.config.get_tag(t.split(':')[1], {})\n                           for t in session.searched\n                           if t.startswith('in:') or t.startswith('tag:')]\n            search_tag_ids = [t._key for t in search_tags if t]\n            self.update({\n                'search_tag_ids': search_tag_ids,\n            })\n            if search_tag_ids:\n                self['summary'] = ' & '.join([t.name for t\n                                              in search_tags if t])\n            for attr in ('hides', 'editable', 'msg_only',\n                         'allow_add', 'allow_del'):\n                tags = [t for t in search_tags if t.get('flag_' + attr)]\n                self['tag_capabilities'][attr] = (len(tags) > 0)\n\n            templates = [t.get('template') for t in search_tags]\n            if templates and templates[0] not in ('index', '', None):\n                self['tag_capabilities']['template'] = templates[0]\n\n        else:\n            search_tag_ids = []\n\n        if suppress_data or (not results and not emails):\n            return\n\n        self.update({\n            'data': {\n                'addresses': {},\n                'metadata': {},\n                'messages': {},\n                'threads': {}\n            }\n        })\n        if 'tags' in self.session.config:\n            th = self['data']['tags'] = {}\n            for tid in search_tag_ids:\n                if tid not in th:\n                    th[tid] = self._tag(tid, {'searched': True})\n\n        idxs = results[start:start + num]\n\n        for e in emails or []:\n            self.add_email(e, idxs)\n\n        done_idxs = set()\n        while idxs:\n            idxs = list(set(idxs) - done_idxs)\n            for idx_pos in idxs:\n                done_idxs.add(idx_pos)\n                msg_info = idx.get_msg_at_idx_pos(idx_pos)\n                self.add_msg_info(b36(idx_pos), msg_info,\n                                  full_threads=full_threads, idxs=idxs)\n\n        if emails and len(emails) == 1:\n            self['summary'] = emails[0].get_msg_info(MailIndex.MSG_SUBJECT)\n\n    def add_msg_info(self, mid, msg_info, full_threads=False, idxs=None):\n        # Populate data.metadata\n        self['data']['metadata'][mid] = self._metadata(msg_info)\n\n        # Populate data.thread\n        thread_mid = parent_mid = msg_info[MailIndex.MSG_THREAD_MID]\n        if '/' in thread_mid:\n            thread_mid, parent_mid = thread_mid.split('/')\n        if thread_mid not in self['data']['threads']:\n            thread = self._thread(thread_mid)\n            self['data']['threads'][thread_mid] = thread\n            if full_threads and idxs:\n                idxs.extend([int(t, 36) for t, bar, kids in thread\n                             if t not in self['data']['metadata']])\n\n        # Populate data.person\n        for cid in self._msg_addresses(msg_info):\n            if cid not in self['data']['addresses']:\n                self['data']['addresses'][cid] = self._address(cid=cid)\n\n        # Populate data.tag\n        if 'tags' in self.session.config:\n            for tid in self._msg_tags(msg_info):\n                if tid not in self['data']['tags']:\n                    self['data']['tags'][tid] = self._tag(tid,\n                                                          {\"searched\": False})\n\n    def add_email(self, e, idxs=None):\n        if e not in self.emails:\n            self.emails.append(e)\n        mid = e.msg_mid()\n        if mid not in self['data']['messages']:\n            self['data']['messages'][mid] = self._message(e)\n        if mid not in self['message_ids']:\n            self['message_ids'].append(mid)\n        # This happens last, as the parsing above may have side-effects\n        # which matter once we get this far.\n        self.add_msg_info(mid, e.get_msg_info(uncached=True),\n                          full_threads=True, idxs=idxs)\n\n    def __nonzero__(self):\n        return True\n\n    def next_set(self):\n        stats = self['stats']\n        return SearchResults(self.session, self.idx,\n                             start=stats['start'] - 1 + stats['count'],\n                             unwrap_pgp=self.unwrap_pgp)\n\n    def previous_set(self):\n        stats = self['stats']\n        return SearchResults(self.session, self.idx,\n                             end=stats['start'] - 1,\n                             unwrap_pgp=self.unwrap_pgp)\n\n    def _fix_width(self, text, width):\n        chars = []\n        for c in unicode(text):\n            cwidth = 2 if (unicodedata.east_asian_width(c) in 'WF') else 1\n            if cwidth <= width:\n                chars.append(c)\n                width -= cwidth\n            else:\n                break\n        if width:\n            chars += [' ' * width]\n        return ''.join(chars)\n\n    def as_text(self):\n        from mailpile.www.jinjaextensions import MailpileCommand as JE\n        clen = max(3, len('%d' % len(self.session.results)))\n        cfmt = '%%%d.%ds' % (clen, clen)\n\n        term_width = self.session.ui.term.max_width\n        fs_width = int((22 + 53) * (term_width / 79.0))\n        f_width = min(32, int(0.30 * fs_width))\n        s_width = fs_width - f_width\n\n        text = []\n        count = self['stats']['start']\n        expand_ids = [e.msg_idx_pos for e in self.emails]\n        addresses = self.get('data', {}).get('addresses', {})\n\n        for mid in self['thread_ids']:\n            m = self['data']['metadata'][mid]\n            tags = [self['data']['tags'].get(t) for t in m['tag_tids']]\n            tags = [t for t in tags if t]\n            tag_names = [t['name'] for t in tags\n                         if not t.get('searched', False)\n                         and t.get('label', True)\n                         and t.get('display', '') != 'invisible']\n            tag_new = [t for t in tags if t.get('type') == 'unread']\n            tag_names.sort()\n            msg_meta = tag_names and ('  (' + '('.join(tag_names)) or ''\n\n            # FIXME: this is a bit ugly, but useful for development\n            es = ['', '']\n            for t in [t['slug'] for t in tags]:\n                if t.startswith('mp_enc') and 'none' not in t:\n                    es[1] = 'E'\n                if t.startswith('mp_sig') and 'none' not in t:\n                    es[0] = 'S'\n            es = ''.join([e for e in es if e])\n            if es:\n                msg_meta = (msg_meta or '  ') + ('[%s]' % es)\n            elif msg_meta:\n                msg_meta += ')'\n            else:\n                msg_meta += '  '\n            msg_meta += elapsed_datetime(m['timestamp'])\n\n            from_info = (m['from'].get('fn') or m['from'].get('email')\n                         or '(anonymous)')\n            if from_info[:1] in ('<', '\"', '\\''):\n                from_info = from_info[1:]\n                if from_info[-1:] in ('>', '\"', '\\''):\n                    from_info = from_info[:-1]\n            if '@' in from_info and len(from_info) > 18:\n                e, d = from_info.split('@', 1)\n                if d in ('gmail.com', 'yahoo.com', 'hotmail.com'):\n                    from_info = '%s@%s..' % (e, d[0])\n                else:\n                    from_info = '%s..@%s' % (e[0], d)\n\n            if not expand_ids:\n                def gg(pos):\n                    return (pos < 10) and pos or '>'\n                thr_mid = m['thread_mid']\n                thread = [ti[0] for ti in self['data']['threads'][thr_mid]]\n                if m['mid'] not in thread:\n                    thread.append(m['mid'])\n                pos = thread.index(m['mid']) + 1\n                if pos > 1:\n                    from_info = '%s>%s' % (gg(pos-1), from_info)\n                else:\n                    from_info = '  ' + from_info\n                if pos < len(thread):\n                    from_info = '%s>%s' % (from_info[:20], gg(len(thread)-pos))\n\n            subject = re.sub('^(\\\\[[^\\\\]]{6})[^\\\\]]{3,}\\\\]\\\\s*', '\\\\1..] ',\n                             JE._nice_subject(m.get('subject')))\n            subject_width = max(1, s_width - (clen + len(msg_meta)))\n            subject = self._fix_width(subject, subject_width)\n            from_info = self._fix_width(from_info, f_width)\n\n            #sfmt = '%%s%%s' % (subject_width, subject_width)\n            #ffmt = ' %%s%%s' % (f_width, f_width)\n            tfmt = cfmt + ' %s%s%s%s'\n            text.append(tfmt % (count, from_info, tag_new and '*' or ' ',\n                                subject, msg_meta))\n\n            if mid in self['data'].get('messages', {}):\n                exp_email = self.emails[expand_ids.index(int(mid, 36))]\n                msg_tree = exp_email.get_message_tree(pgpmime=self.unwrap_pgp)\n                text.append('-' * term_width)\n                text.append(exp_email.get_editing_string(msg_tree,\n                    attachment_headers=False).strip())\n                if msg_tree['attachments']:\n                    text.append('\\nAttachments:')\n                    for a in msg_tree['attachments']:\n                        text.append('%5.5s %s' % ('#%s' % a['count'],\n                                                  a['filename']))\n                text.append('-' * term_width)\n\n            count += 1\n        if not count:\n            text = ['(No messages found)']\n        return '\\n'.join(text) + '\\n'\n\n\n##[ Commands ]################################################################\n\nclass Search(Command):\n    \"\"\"Search your mail!\"\"\"\n    SYNOPSIS = ('s', 'search', 'search', '[@<start>] <terms>')\n    ORDER = ('Searching', 0)\n    HTTP_CALLABLE = ('GET', )\n    HTTP_QUERY_VARS = {\n        'q': 'search terms',\n        'qr': 'search refinements',\n        'order': 'sort order',\n        'start': 'start position',\n        'end': 'end position',\n        'full': 'return all metadata',\n        'view': 'MID/MID pairs to expand in place',\n        'parent': 'Parent folder, in browse mode',\n        'context': 'refine or redisplay an older search'\n    }\n    IS_USER_ACTIVITY = True\n    COMMAND_CACHE_TTL = 900\n    CHANGES_SESSION_CONTEXT = True\n    UNWRAP_PGP = 'default'\n\n    class CommandResult(Command.CommandResult):\n        def __init__(self, *args, **kwargs):\n            Command.CommandResult.__init__(self, *args, **kwargs)\n            self.fixed_up = False\n            if isinstance(self.result, dict):\n                self.message = self.result.get('summary', '')\n            elif isinstance(self.result, list):\n                self.message = ', '.join([r.get('summary', '')\n                                          for r in self.result])\n\n        def _fixup(self):\n            if self.fixed_up:\n                return self\n            self.fixed_up = True\n            return self\n\n        def as_text(self):\n            if self.result:\n                if isinstance(self.result, (bool, str, unicode, int, float)):\n                    return unicode(self.result)\n                elif isinstance(self.result, (list, set)):\n                    return '\\n'.join([r.as_text() for r in self.result])\n                elif hasattr(self.result, 'as_text'):\n                    return self.result.as_text()\n                return _('Unprintable results')\n            else:\n                return _('No results')\n\n        def as_html(self, *args, **kwargs):\n            return Command.CommandResult.as_html(self._fixup(),\n                                                 *args, **kwargs)\n\n        def as_dict(self, *args, **kwargs):\n            return Command.CommandResult.as_dict(self._fixup(),\n                                                 *args, **kwargs)\n\n    def __init__(self, *args, **kwargs):\n        Command.__init__(self, *args, **kwargs)\n        self._email_views = []\n        self._email_view_pairs = {}\n        self._emails = []\n\n    def _idx(self, **kwargs):\n        return self.session.search_index or Command._idx(self)\n\n    def state_as_query_args(self):\n        try:\n            return self._search_state\n        except (AttributeError, NameError):\n            return Command.state_as_query_args(self)\n\n    def _starting(self):\n        Command._starting(self)\n        session, idx = self.session, self._idx()\n        self._search_args = args = []\n\n        def _viewpair(m):\n            return tuple((m.split('/') + ['*'])[:2])\n\n        self._email_views = self.data.get('view', [])\n        self._email_view_pairs = dict(_viewpair(m) for m in self._email_views)\n        self._emails = []\n\n        self.context = self.data.get('context', [None])[0]\n        if self.context:\n            args += self.session.searched\n\n        def nq(t):\n            p = t[0] if (t and t[0] in '-+') else ''\n            t = t[len(p):]\n            if t.startswith('tag:') or t.startswith('in:'):\n                try:\n                    raw_tag = session.config.get_tag(t.split(':')[1])\n                    if raw_tag and raw_tag.hasattr(slug):\n                        t = 'in:%s' % raw_tag.slug\n                except (IndexError, KeyError, TypeError):\n                    pass\n            return p+t\n\n        args += [a for a in list(nq(a) for a in self.args) if a not in args]\n        for q in self.data.get('q', []):\n            ext = [nq(a) for a in q.split()]\n            args.extend([a for a in ext if a not in args])\n\n        # Query refinements...\n        qrs = []\n        for qr in self.data.get('qr', []):\n            qrs.extend(nq(a) for a in qr.split())\n        args.extend(qrs)\n\n        for order in self.data.get('order', []):\n            session.order = order\n\n        num = def_num = session.config.prefs.num_results\n        d_start = int(self.data.get('start', [0])[0])\n        d_end = int(self.data.get('end', [0])[0])\n        if d_start and d_end:\n            args[:0] = ['@%s' % d_start]\n            num = d_end - d_start + 1\n        elif d_start:\n            args[:0] = ['@%s' % d_start]\n        elif d_end:\n            args[:0] = ['@%s' % (d_end - num + 1)]\n\n        start = 0\n        self._default_position = True\n        while args and args[0].startswith('@'):\n            spoint = args.pop(0)[1:]\n            try:\n                start = int(spoint) - 1\n                self._default_position = False\n            except ValueError:\n                raise UsageError(_('Weird starting point: %s') % spoint)\n\n        session.order = session.order or session.config.prefs.default_order\n        self._start = start\n        self._num = num\n        self._search_state = {\n            'q': [q for q in args if q not in qrs],\n            'qr': qrs,\n            'order': [session.order],\n            'start': [str(start + 1)] if start else [],\n            'view': self._email_views,\n            'end': [str(start + num)] if (num != def_num) else [],\n            'parent': self.data.get('parent', '')\n        }\n        if self.context:\n            self._search_state['context'] = [self.context]\n\n    def _email_view_side_effects(self, emails):\n        session, config, idx = self.session, self.session.config, self._idx()\n        msg_idxs = [e.msg_idx_pos for e in emails]\n        if 'tags' in config and config.prefs.auto_mark_as_read:\n            for tag in config.get_tags(type='unread'):\n                idx.remove_tag(session, tag._key, msg_idxs=msg_idxs)\n            for tag in config.get_tags(type='read'):\n                idx.add_tag(session, tag._key, msg_idxs=msg_idxs)\n\n        idx.apply_filters(session, '@read',\n                          msg_idxs=[e.msg_idx_pos for e in emails])\n        return None\n\n    def switch_indexes(self, path):\n        if path in ('default', 'mailpile'):\n             self.session.search_index = None\n        else:\n             config = self.session.config\n             self.session.search_index = config.get_path_index(\n                 self.session, path)\n\n        # Note: this falls back to default index if we set to None\n        return self._idx()\n\n    def _do_search(self, search=None, process_args=False):\n        session = self.session\n\n        if (self.context is None\n                or search\n                or session.searched != self._search_args):\n            session.searched = search or []\n            want_index = 'default'\n            if search is None or process_args:\n                prefix = ''\n                for arg in self._search_args:\n                    if arg.endswith(':'):\n                        prefix = arg.lower()\n                    elif ':' in arg or (arg and arg[0] in ('-', '+')):\n                        if arg.startswith('index:'):\n                            want_index = arg[6:]\n                        else:\n                            prefix = ''\n                            session.searched.append(arg.lower())\n                    elif prefix and '@' in arg:\n                        session.searched.append(prefix + arg.lower())\n                    else:\n                        words = re.findall(WORD_REGEXP, arg.lower())\n                        session.searched.extend([prefix + word\n                                                 for word in words])\n            if not session.searched:\n                session.searched = ['all:mail']\n\n            idx = self.switch_indexes(want_index)\n            context = session.results if self.context else None\n            session.results = list(idx.search(session, session.searched,\n                                              context=context).as_set())\n\n            if '*' in self._email_view_pairs.values():\n                # If we are auto-choosing which message from a thread to\n                # display, then we want the raw results so we can only\n                # choose from messages that matched our search. We have to\n                # save this, since the sort below may collapse the results.\n                raw_results = list(session.results)\n\n            for pmid, emid in list(self._email_view_pairs.iteritems()):\n                # Make sure all our requested messages are amongst results\n                pmid_idx = int(pmid, 36)\n                if pmid_idx not in session.results:\n                    session.results.append(pmid_idx)\n\n                if ('flat' in session.order) and emid != '*':\n                    # Flat mode doesn't really use view pairs, so also make\n                    # sure our actual view target is among results.\n                    emid_idx = int(emid, 36)\n                    if emid_idx not in session.results:\n                        session.results.append(emid_idx)\n\n            if session.order:\n                idx.sort_results(session, session.results, session.order)\n        else:\n            idx = self._idx()\n\n        self._emails = []\n        pivot_pos = any_pos = len(session.results)\n        if self._email_view_pairs:\n            new_tids = set(\n                [t._key for t in session.config.get_tags(type='unread')])\n        for pmid, emid in list(self._email_view_pairs.iteritems()):\n            try:\n                if emid == '*':\n                    pmid_idx = int(pmid, 36)\n                    conversation = idx.get_conversation(msg_idx=pmid_idx)\n                    # Find oldest message in conversation that is unread\n                    # and matches our search criteria...\n                    matches = []\n                    for info in conversation:\n                        if new_tids & set(info[idx.MSG_TAGS].split(',')):\n                            imid_idx = int(info[idx.MSG_MID], 36)\n                            if imid_idx in raw_results:\n                                matches.append((int(info[idx.MSG_DATE], 36),\n                                                imid_idx))\n                    if matches:\n                        emid_idx = min(matches)[1]\n                        emid = b36(emid_idx)\n                    else:\n                        emid = pmid\n                        emid_idx = pmid_idx\n                    self._email_view_pairs[pmid] = emid\n                else:\n                    emid_idx = int(emid, 36)\n                    conversation = idx.get_conversation(msg_idx=emid_idx)\n\n                if 'flat' not in session.order:\n                    for info in conversation:\n                        cmid = info[idx.MSG_MID]\n                        self._email_view_pairs[cmid] = emid\n\n                # Calculate visibility...\n                for cmid in self._email_view_pairs:\n                    try:\n                        cpos = session.results.index(int(cmid, 36))\n                    except ValueError:\n                        cpos = -1\n                    if cpos >= 0:\n                        any_pos = min(any_pos, cpos)\n                    if (cpos > self._start and\n                            cpos < self._start + self._num + 1):\n                        pivot_pos = min(cpos, pivot_pos)\n                self._emails.append(Email(idx, emid_idx))\n            except ValueError:\n                self._email_view_pairs = {}\n\n        if 'flat' in (session.order or ''):\n            # Above we have guaranteed that the target message is in the\n            # result set; unset this dictionary to force a flat display\n            # of the chosen message.\n            self._email_view_pairs = {}\n\n        # Adjust the visible window of results if we are expanding an\n        # individual message, to guarantee visibility.\n        if pivot_pos < len(session.results):\n            self._start = max(0, pivot_pos - max(self._num // 5, 2))\n        elif any_pos < len(session.results):\n            self._start = max(0, any_pos - max(self._num // 5, 2))\n\n        if self._emails:\n            self._email_view_side_effects(self._emails)\n\n        return session, idx\n\n    def cache_id(self, *args, **kwargs):\n        if self._emails or self.session.search_index:\n            return ''\n        return Command.cache_id(self, *args, **kwargs)\n\n    def cache_requirements(self, result):\n        msgs = self.session.results[self._start:self._start + self._num]\n        def fix_term(term):\n            # Terms are reversed in the search engine...\n            if term[:1] in ['-', '+']:\n                term = term[1:]\n            if term[:4] == 'vfs:':\n                raise ValueError('VFS searches are not cached')\n            term = ':'.join(reversed(term.split(':', 1)))\n            return unicode(term)\n        reqs = set(['!config'] +\n                   [fix_term(t) for t in self.session.searched] +\n                   [u'%s:msg' % i for i in msgs])\n        if self.session.displayed:\n            reqs |= set(u'%s:thread' % int(tmid, 36) for tmid in\n                        self.session.displayed.get('thread_ids', []))\n            reqs |= set(u'%s:msg' % int(tmid, 36) for tmid in\n                        self.session.displayed.get('message_ids', []))\n        return reqs\n\n    def command(self):\n        session, idx = self._do_search()\n        full_threads = self.data.get('full', False)\n        session.displayed = SearchResults(session, idx,\n                                          start=self._start,\n                                          num=self._num,\n                                          emails=self._emails,\n                                          view_pairs=self._email_view_pairs,\n                                          full_threads=full_threads,\n                                          unwrap_pgp=self.UNWRAP_PGP)\n        session.ui.mark(_('Prepared %d search results (context=%s)'\n                          ) % (len(session.results), self.context))\n        return self._success(_('Found %d results in %.3fs'\n                               ) % (len(session.results),\n                                    session.ui.report_marks(quiet=True)),\n                             result=session.displayed)\n\n\nclass Next(Search):\n    \"\"\"Display next page of results\"\"\"\n    SYNOPSIS = ('n', 'next', None, None)\n    ORDER = ('Searching', 1)\n    HTTP_CALLABLE = ()\n    COMMAND_CACHE_TTL = 0\n\n    def command(self):\n        session = self.session\n        try:\n            session.displayed = session.displayed.next_set()\n        except AttributeError:\n            session.ui.error(_(\"You must perform a search before \"\n                               \"requesting the next page.\"))\n            return False\n        return self._success(_('Displayed next page of results.'),\n                             result=session.displayed)\n\n\nclass Previous(Search):\n    \"\"\"Display previous page of results\"\"\"\n    SYNOPSIS = ('p', 'previous', None, None)\n    ORDER = ('Searching', 2)\n    HTTP_CALLABLE = ()\n    COMMAND_CACHE_TTL = 0\n\n    def command(self):\n        session = self.session\n        try:\n            session.displayed = session.displayed.previous_set()\n        except AttributeError:\n            session.ui.error(_(\"You must perform a search before \"\n                               \"requesting the previous page.\"))\n            return False\n        return self._success(_('Displayed previous page of results.'),\n                             result=session.displayed)\n\n\nclass Order(Search):\n    \"\"\"Sort by: date, from, subject, random or index\"\"\"\n    SYNOPSIS = ('o', 'order', None, '<how>')\n    ORDER = ('Searching', 3)\n    HTTP_CALLABLE = ()\n    COMMAND_CACHE_TTL = 0\n\n    def command(self):\n        session, idx = self.session, self._idx()\n        session.order = self.args and self.args[0] or None\n        idx.sort_results(session, session.results, session.order)\n        session.displayed = SearchResults(session, idx)\n        return self._success(_('Changed sort order to %s') % session.order,\n                             result=session.displayed)\n\n\nclass View(Search):\n    \"\"\"View one or more messages\"\"\"\n    SYNOPSIS = ('v', 'view', 'message', '[raw] <message>')\n    ORDER = ('Searching', 4)\n    HTTP_QUERY_VARS = {\n        'mid': 'metadata-ID'\n    }\n    COMMAND_CACHE_TTL = 0\n\n    class RawResult(dict):\n        def _decode(self):\n            try:\n                return self['source'].decode('utf-8')\n            except UnicodeDecodeError:\n                try:\n                    return self['source'].decode('iso-8859-1')\n                except:\n                    return '(MAILPILE FAILED TO DECODE MESSAGE)'\n\n        def as_text(self, *args, **kwargs):\n            return self._decode()\n\n        def as_html(self, *args, **kwargs):\n            return '<pre>%s</pre>' % escape_html(self._decode())\n\n    def _side_effects(self, emails):\n        # A compatibility stub only\n        return self._email_view_side_effects(emails)\n\n    def state_as_query_args(self):\n        return Command.state_as_query_args(self)\n\n    def command(self):\n        session, config, idx = self.session, self.session.config, self._idx()\n        results = []\n        args = list(self.args)\n        args.extend(['=%s' % mid.replace('=', '')\n                     for mid in self.data.get('mid', [])])\n        if args and args[0].lower() == 'raw':\n            raw = args.pop(0)\n        else:\n            raw = False\n        emails = [Email(idx, mid) for mid in self._choose_messages(args)]\n\n        rv = self._side_effects(emails)\n        if rv is not None:\n            # This is here so derived classes can do funky things.\n            return rv\n\n        for email in emails:\n            if raw:\n                subject = email.get_msg_info(idx.MSG_SUBJECT)\n                results.append(self.RawResult({\n                    'summary': _('Raw message: %s') % subject,\n                    'source': email.get_file().read()\n                }))\n            else:\n                old_result = None\n                for result in results:\n                    if email.msg_idx_pos in result.results:\n                        old_result = result\n                if old_result:\n                    old_result.add_email(email)\n                    continue\n\n                # Get conversation\n                conv = idx.get_conversation(msg_idx=email.msg_idx_pos)\n\n                # Sort our results by date...\n                def sort_conv_key(info):\n                    return -int(info[idx.MSG_DATE], 36)\n                conv.sort(key=sort_conv_key)\n\n                # Convert to index positions only\n                conv = [int(info[idx.MSG_MID], 36) for info in conv]\n\n                session.results = conv\n                results.append(SearchResults(session, idx,\n                                             emails=[email],\n                                             num=len(conv)))\n        if len(results) == 1:\n            return self._success(_('Displayed a single message'),\n                                 result=results[0])\n        else:\n            session.results = []\n            return self._success(_('Displayed %d messages') % len(results),\n                                 result=results)\n\n\nclass Extract(Command):\n    \"\"\"Extract attachment(s) to file(s)\"\"\"\n    SYNOPSIS = ('e', 'extract', 'message/download', '<msgs> <att> [><fn>]')\n    ORDER = ('Searching', 5)\n    RAISES = (SuppressHtmlOutput, UrlRedirectException)\n    IS_USER_ACTIVITY = True\n\n    class CommandResult(Command.CommandResult):\n        def __init__(self, *args, **kwargs):\n            self.fixed_up = False\n            Command.CommandResult.__init__(self, *args, **kwargs)\n\n        def _fixup(self):\n            if self.fixed_up:\n                return self\n            for result in (self.result or []):\n                if 'data' in result:\n                    result['data'] = result['data'].encode('base64'\n                                                           ).replace('\\n', '')\n            self.fixed_up = True\n            return self\n\n        def as_html(self, *args, **kwargs):\n            return Command.CommandResult.as_html(self._fixup(),\n                                                 *args, **kwargs)\n\n        def as_dict(self, *args, **kwargs):\n            return Command.CommandResult.as_dict(self._fixup(),\n                                                 *args, **kwargs)\n\n    def command(self):\n        session, config, idx = self.session, self.session.config, self._idx()\n        mode = 'download'\n        name_fmt = None\n\n        args = list(self.args)\n        if args[0] in ('inline', 'inline-preview', 'preview',\n                       'get', 'download'):\n            mode = args.pop(0)\n\n        if len(args) > 0 and args[-1].startswith('>'):\n            forbid = security.forbid_command(self,\n                                             security.CC_ACCESS_FILESYSTEM)\n            if forbid:\n                return self._error(forbid)\n            name_fmt = args.pop(-1)[1:]\n\n        if (args[0].startswith('#') or\n                args[0].startswith('part-') or\n                args[0].startswith('ext:')):\n            cid = args.pop(0)\n        else:\n            cid = args.pop(-1)\n\n        emails = [Email(idx, i) for i in self._choose_messages(args)]\n        results = []\n        for e in emails:\n            if cid[0] == '*':\n                tree = e.get_message_tree(want=['attachments'])\n                cids = [('#%s' % a['count']) for a in tree['attachments']\n                        if a['filename'].lower().endswith(cid[1:].lower())]\n            else:\n                cids = [cid]\n\n            for c in cids:\n                fn, info = e.extract_attachment(session, c,\n                                                name_fmt=name_fmt, mode=mode)\n                if info:\n                    info['idx'] = e.msg_idx_pos\n                    if fn:\n                        info['created_file'] = fn\n                    results.append(info)\n        return results\n\n\n_plugins.register_commands(Extract, Next, Order, Previous, Search, View)\n\n\n##[ Search terms ]############################################################\n\ndef mailbox_search(config, idx, term, hits):\n    word = term.split(':', 1)[1].lower()\n    try:\n        mbox_id = FormatMbxId(b36(int(word, 36)))\n    except ValueError:\n        mbox_id = None\n\n    mailboxes = []\n    for m in config.sys.mailbox.keys():\n        fn = FilePath(config.sys.mailbox[m]).display().lower()\n        if (mbox_id == m) or word in fn:\n            mailboxes.append(m)\n\n    rt = []\n    for mbox_id in mailboxes:\n        mbox_id = FormatMbxId(mbox_id)\n        rt.extend(hits('%s:mailbox' % mbox_id))\n\n    return rt\n\n\n_plugins.register_search_term('mailbox', mailbox_search)\n"
  },
  {
    "path": "mailpile/plugins/setup_magic.py",
    "content": "from __future__ import print_function\nimport copy\nimport datetime\nimport os\nimport random\nimport socket\nimport sys\nimport time\nfrom urllib import urlencode\nfrom urllib2 import urlopen\nfrom lxml import objectify\n\nimport mailpile.auth\nimport mailpile.security as security\nfrom mailpile.conn_brokers import Master as ConnBroker\nfrom mailpile.config.defaults import CONFIG_RULES, APPVER\nfrom mailpile.i18n import ListTranslations, ActivateTranslation, gettext\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.plugins import PLUGINS\nfrom mailpile.plugins.contacts import AddProfile, ListProfiles\nfrom mailpile.plugins.contacts import ListProfiles\nfrom mailpile.plugins.migrate import Migrate\nfrom mailpile.plugins.motd import MOTD_URL_TOR_ONLY_NO_MARS\nfrom mailpile.plugins.setup_magic_ispdb import STATIC_ISPDB\nfrom mailpile.plugins.tags import AddTag\nfrom mailpile.commands import Command\nfrom mailpile.crypto.gpgi import SignatureInfo, EncryptionInfo\nfrom mailpile.eventlog import Event\nfrom mailpile.httpd import BLOCK_HTTPD_LOCK, Idle_HTTPD\nfrom mailpile.smtp_client import SendMail, SendMailError\nfrom mailpile.urlmap import UrlMap\nfrom mailpile.ui import Session, SilentInteraction\nfrom mailpile.util import *\n\n\n_ = lambda s: s\n_plugins = PluginManager(builtin=__file__)\n\n\n##[ Commands ]################################################################\n\nclass SetupMagic(Command):\n    \"\"\"Perform initial setup\"\"\"\n    SYNOPSIS = (None, None, None, None)\n    ORDER = ('Internals', 0)\n    LOG_PROGRESS = True\n    COMMAND_SECURITY = security.CC_CHANGE_CONFIG\n\n    TAGS = {\n        'New': {\n            'type': 'unread',\n            'label': False,\n            'display': 'invisible',\n            'icon': 'icon-new',\n            'label_color': '03-gray-dark',\n            'name': _('New'),\n        },\n        'Inbox': {\n            'type': 'inbox',\n            'display': 'priority',\n            'display_order': 2,\n            'icon': 'icon-inbox',\n            'label_color': '06-blue',\n            'notify_new': True,\n            'name': _('Inbox'),\n        },\n        'Blank': {\n            'type': 'blank',\n            'flag_editable': True,\n            'flag_msg_only': True,\n            'flag_allow_add': False,\n            'display': 'invisible',\n            'template': 'outgoing',\n            'name': _('Blank'),\n        },\n        'Drafts': {\n            'type': 'drafts',\n            'flag_editable': True,\n            'flag_msg_only': True,\n            'flag_allow_add': False,\n            'display': 'priority',\n            'display_order': 1,\n            'template': 'drafts',\n            'icon': 'icon-compose',\n            'label_color': '03-gray-dark',\n            'name': _('Drafts'),\n        },\n        'Outbox': {\n            'type': 'outbox',\n            'flag_msg_only': True,\n            'flag_allow_add': False,\n            'display': 'priority',\n            'display_order': 3,\n            'template': 'outbox',\n            'icon': 'icon-outbox',\n            'label_color': '06-blue',\n            'name': _('Outbox'),\n        },\n        'Sent': {\n            'type': 'sent',\n            'flag_msg_only': True,\n            'display': 'priority',\n            'display_order': 4,\n            'template': 'sent',\n            'icon': 'icon-sent',\n            'label_color': '03-gray-dark',\n            'name': _('Sent'),\n        },\n        'Spam': {\n            'slug': 'spam',\n            'type': 'spam',\n            'flag_hides': True,\n            'display': 'priority',\n            'display_order': 5,\n            'icon': 'icon-spam',\n            'label_color': '10-orange',\n            'name': _('Spam'),\n            'auto_after': 30,\n            'auto_action': '-spam +trash',\n            'auto_tag': 'fancy'\n        },\n        'Ham': {\n            'type': 'ham',\n            'display': 'invisible',\n            'name': _('Ham'),\n        },\n        'Trash': {\n            'slug': 'trash',\n            'type': 'trash',\n            'flag_hides': True,\n            'display': 'priority',\n            'display_order': 6,\n            'template': 'trash',\n            'icon': 'icon-trash',\n            'label_color': '13-brown',\n            'auto_after': 91,\n            'auto_action': '!delete',\n            'name': _('Trash'),\n        },\n        # These are magical tags that perform searches and show\n        # messages in contextual views.\n# FIXME: This is a good idea, but not quite ready to ship.\n#       'Conversations': {\n#           'type': 'replied',\n#           'icon': 'icon-forum',\n#           'label': False,\n#           'label_color': '05-blue-light',\n#           'name': _('Conversations'),\n#           'display_order': 1001,\n#       },\n        'Photos': {\n            'type': 'search',\n            'icon': 'icon-photos',\n            'label': False,\n            'label_color': '08-green',\n            'template': 'photos',\n            'name': _('Photos'),\n            'display_order': 1002,\n            '_filters': ['att:jpg is:personal'],\n        },\n        'Documents': {\n            'type': 'search',\n            'icon': 'icon-document',\n            'label': False,\n            'label_color': '06-blue',\n            'template': 'atts',\n            'name': _('Documents'),\n            'display_order': 1003,\n            '_filters': ['has:document is:personal'],\n        },\n        # These are placeholder tags that perform searches - these are\n        # generally to be avoided as they break the user expectation of\n        # how tags behave. A normal tag + filter is almost always the\n        # right choice!\n        'All Mail': {\n            'type': 'search',\n            'icon': 'icon-logo',\n            'label': False,\n            'label_color': '06-blue',\n            'search_terms': 'all:mail',\n            'name': _('All Mail'),\n            'display_order': 1100,\n        },\n        # These are internal tags, used for tracking user actions on\n        # messages, as input for machine learning algorithms. These get\n        # automatically added, and may be automatically removed as well\n        # to keep the working sets reasonably small.\n        'mp_rpl': {'type': 'replied', 'label': False, 'display': 'invisible',\n                   'flag_msg_only': True},\n        'mp_fwd': {'type': 'fwded', 'label': False, 'display': 'invisible',\n                   'flag_msg_only': True},\n        'mp_tag': {'type': 'tagged', 'label': False, 'display': 'invisible',\n                   'flag_msg_only': True},\n        'mp_read': {'type': 'read', 'label': False, 'display': 'invisible',\n                   'flag_msg_only': True},\n        'mp_ham': {'type': 'ham', 'label': False, 'display': 'invisible',\n                   'flag_msg_only': True},\n    }\n\n    def basic_app_config(self, session,\n                         save_and_update_workers=True,\n                         want_daemons=True):\n        session.ui.notify(_('Disabling lockdown'))\n        security.DISABLE_LOCKDOWN = True\n        # Create local mailboxes\n        session.config.open_local_mailbox(session)\n\n        # Create standard tags and filters\n        created = []\n        for t, tag_settings in self.TAGS.iteritems():\n            tag_settings = copy.copy(tag_settings)\n\n            tid = session.config.get_tag_id(t.replace(' ', '-'))\n            if not tid:\n                AddTag(session, arg=[t]).run(save=False)\n                tid = session.config.get_tag_id(t)\n                created.append(t)\n            if not tid:\n                session.ui.notify(_('Failed to create tag: %s') % t)\n                continue\n\n            tag_info = session.config.tags[tid]\n\n            # Delete any old filters...\n            old_fids = [f for f, v in session.config.filters.iteritems()\n                        if v.primary_tag == tid]\n            if old_fids:\n                session.config.filter_delete(*old_fids)\n\n            # Create new ones?\n            tag_filters = tag_settings.get('_filters', [])\n            for search in tag_filters:\n                session.config.filters.append({\n                    'type': 'system',\n                    'terms': search,\n                    'tags': '+%s' % tid,\n                    'primary_tag': tid,\n                    'comment': t\n                })\n            if tag_filters:\n                del tag_settings['_filters']\n            for k in ('magic_terms', 'search_terms', 'search_order'):\n                if k in tag_info:\n                    del tag_info[k]\n            tag_info.update(tag_settings)\n\n        for stype, statuses in (('sig', SignatureInfo.STATUSES),\n                                ('enc', EncryptionInfo.STATUSES)):\n            for status in statuses:\n                tagname = 'mp_%s-%s' % (stype, status)\n                if not session.config.get_tag_id(tagname):\n                    AddTag(session, arg=[tagname]).run(save=False)\n                    created.append(tagname)\n                session.config.get_tag(tagname).update({\n                    'type': 'attribute',\n                    'flag_msg_only': True,\n                    'display': 'invisible',\n                    'label': False,\n                })\n\n        if 'New' in created:\n            session.ui.notify(_('Created default tags'))\n\n        # Import all the basic plugins\n        reload_config = False\n        for plugin in PLUGINS:\n            if plugin not in session.config.sys.plugins:\n                session.config.sys.plugins.append(plugin)\n                reload_config = True\n        for plugin in session.config.plugins.WANTED:\n            if plugin in session.config.plugins.available():\n                session.config.sys.plugins.append(plugin)\n        if reload_config:\n            with session.config._lock:\n                session.config.save()\n                session.config.load(session)\n\n        try:\n            # If spambayes is not installed, this will fail\n            import mailpile.plugins.autotag_sb\n            if 'autotag_sb' not in session.config.sys.plugins:\n                session.config.sys.plugins.append('autotag_sb')\n                session.ui.notify(_('Enabling SpamBayes autotagger'))\n        except ImportError:\n            session.ui.warning(_('Please install SpamBayes '\n                                 'for super awesome spam filtering'))\n\n        vcard_importers = session.config.prefs.vcard.importers\n        if not vcard_importers.gravatar:\n            vcard_importers.gravatar.append({'active': True})\n            session.ui.notify(_('Enabling Gravatar image importer'))\n        if not vcard_importers.libravatar:\n            vcard_importers.libravatar.append({'active': True})\n            session.ui.notify(_('Enabling Libravatar image importer'))\n\n        gpg_home = os.path.expanduser('~/.gnupg')\n        if not vcard_importers.gpg:\n            vcard_importers.gpg.append({'active': True,\n                                        'gpg_home': gpg_home})\n            session.ui.notify(_('Importing contacts from GPG keyring'))\n\n        if ('autotag_sb' in session.config.sys.plugins and\n                len(session.config.prefs.autotag) == 0):\n            session.config.prefs.autotag.append({\n                'match_tag': 'spam',\n                'tagger': 'spambayes',\n                'trainer': 'spambayes'\n            })\n            session.config.prefs.autotag[0].exclude_tags[0] = 'ham'\n\n        # Mark config as up-to-date\n        session.config.version = APPVER\n\n        if save_and_update_workers:\n            session.config.save()\n            session.config.prepare_workers(session, daemons=want_daemons)\n\n        # Enable Tor in the background, if we have it...\n        session.config.slow_worker.add_unique_task(\n            session, 'tor-autoconfig', lambda: SetupTor.autoconfig(session))\n\n        session.ui.notify(_('Reenabling lockdown'))\n        security.DISABLE_LOCKDOWN = False\n\n    def make_master_key(self):\n        session = self.session\n        if (session.config.prefs.gpg_recipient not in (None, '', '!CREATE')\n                and not session.config.get_master_key()\n                and not session.config.prefs.obfuscate_index):\n            #\n            # This secret is arguably the most critical bit of data in the\n            # app, it is used as an encryption key and to seed hashes in\n            # a few places.  As such, the user may need to type this in\n            # manually as part of data recovery, so we keep it reasonably\n            # sized and devoid of confusing chars.\n            #\n            # They okay_random() function uses os.urandom() and mixes with\n            # the seed data we provide (misc app state), as well as the\n            # current full-resolution time.  The output is suitable for use\n            # as a password (alphanumeric, avoiding O01l).\n            #\n            # It should give about 281 bits of randomness:\n            #\n            #   import math\n            #   math.log((25 + 25 + 8) ** (12 * 4), 2) == 281.183...\n            #\n            session.config.set_master_key(okay_random(12 * 4,\n                                          '%s' % session.config,\n                                          '%s' % self.session,\n                                          '%s' % self.data))\n            if self._idx() and self._idx().INDEX:\n                session.ui.warning(_('Unable to obfuscate search index '\n                                     'without losing data. Not indexing '\n                                     'encrypted mail.'))\n            else:\n                session.config.prefs.obfuscate_index = True\n                session.config.prefs.index_encrypted = True\n                session.ui.notify(_('Obfuscating search index and enabling '\n                                    'indexing of encrypted e-mail. Yay!'))\n            return True\n        else:\n            return False\n\n    @classmethod\n    def URLGet(cls, session, url, data=None):\n        if url.lower().startswith('https'):\n            conn_needs = [ConnBroker.OUTGOING_HTTPS]\n        else:\n            conn_needs = [ConnBroker.OUTGOING_HTTP]\n        session.ui.mark('Getting: %s' % url)\n        with ConnBroker.context(need=conn_needs) as context:\n            return urlopen(url, data=data, timeout=10).read()\n\n    def _urlget(self, url, data=None):\n        return self.URLGet(self.session, url, data=data)\n\n    def setup_command(self, session):\n        pass  # Overridden by children\n\n    def command(self, *args, **kwargs):\n        return self.setup_command(self.session, *args, **kwargs)\n\n\nclass TestableWebbable(SetupMagic):\n    HTTP_AUTH_REQUIRED = 'Maybe'\n    HTTP_CALLABLE = ('GET', )\n    HTTP_QUERY_VARS = {\n        '_path': 'Redirect path'\n    }\n    HTTP_POST_VARS = {\n        'testing': 'Yes or No, if testing',\n        'advance': 'Yes or No, advance setup flow',\n    }\n\n    def _advance(self):\n        path = self.data.get('_path', [None])[0]\n        data = dict([(k, v) for k, v in self.data.iteritems()\n                     if k not in self.HTTP_POST_VARS\n                     and k not in ('_method',)])\n\n        nxt = Setup.Next(self.session.config, None, needed_auth=False)\n        if nxt:\n            url = '/%s/' % nxt.SYNOPSIS[2]\n        elif path and path != '/%s/' % Setup.SYNOPSIS[2]:\n            # Use the same redirection logic as the Authenticator\n            mailpile.auth.Authenticate.RedirectBack(path, data)\n        else:\n            url = '/'\n\n        qs = urlencode([(k, v) for k, vl in data.iteritems() for v in vl])\n        raise UrlRedirectException(''.join([self.session.config.sys.http_path, url, '?%s' % qs if qs else '']))\n\n    def _success(self, message, result=True, advance=False):\n        if advance or truthy(self.data.get('advance', ['no'])[0], default=False):\n            self._advance()\n        return SetupMagic._success(self, message, result=result)\n\n    def _testing(self):\n        self._testing_yes(lambda: True)\n        return (self.testing is not None)\n\n    def _testing_yes(self, method, *args, **kwargs):\n        testination = self.data.get('testing')\n        if testination:\n            self.testing = random.randint(0, 1)\n            self.testing = truthy(testination[0], default=self.testing)\n            return self.testing\n        self.testing = None\n        return method(*args, **kwargs)\n\n    def _testing_data(self, method, tdata, *args, **kwargs):\n        result = self._testing_yes(method, *args, **kwargs) or []\n        return (result\n                if (self.testing is None) else\n                (self.testing and tdata or []))\n\n    def setup_command(self, session):\n        raise Exception('FIXME')\n\n\nclass SetupGetEmailSettings(TestableWebbable):\n    \"\"\"Lookup, guess, test server details for an e-mail address\"\"\"\n    SYNOPSIS = (None, 'setup/email_servers', 'setup/email_servers',\n                \"<email> <password>\")\n    HTTP_CALLABLE = ('GET', )\n    HTTP_QUERY_VARS = dict_merge(TestableWebbable.HTTP_QUERY_VARS, {\n        'email': 'E-mail address',\n        'timeout': 'Seconds',\n        'password': 'Account password',\n        'track-id': 'Tracking ID for event log'\n    })\n    TEST_DATA = {\n        'imap_host': 'imap.wigglebonk.com',\n        'imap_port': 993,\n        'imap_tls': True,\n        'pop3_host': 'pop3.wigglebonk.com',\n        'pop3_port': 110,\n        'pop3_tls': False,\n        'smtp_host': 'smtp.wigglebonk.com',\n        'smtp_port': 465,\n        'smtp_tls': False\n    }\n    ISPDB_URL = 'https://autoconfig.thunderbird.net/v1.1/%(domain)s'\n    AUTOCONFIG_URL = '%(protocol)s://autoconfig.%(domain)s/mail/config-v1.1.xml?emailaddress=%(email)s'\n    AUTOCONFIG_ALT_URL = '%(protocol)s://%(domain)s/.well-known/autoconfig/mail/config-v1.1.xml'\n\n    def _progress(self, message):\n        if self.event and self.tracking_id:\n            self.event.private_data = {\"track-id\": self.tracking_id}\n            if 'log' in self.event.data:\n                self.event.data['log'].append([int(time.time()), message])\n            else:\n                self.event.data['log'] = [[int(time.time()), message]]\n            self.event.message = message\n            self._update_event_state(self.event.RUNNING, log=True)\n        else:\n            self.session.ui.mark(message)\n\n    def _log_result(self, message):\n        if self.event and self.tracking_id:\n            if self.event.data.get('log'):\n                self.event.data['log'][-1].append(message)\n        else:\n            self.session.ui.mark(message)\n\n    def _username(self, val, email):\n        lpart = email.split('@')[0]\n        return str(val).replace('%EMAILADDRESS%', email\n                                ).replace('%EMAILLOCALPART%', lpart)\n\n    def _guess_username(self, email):\n        lpart, domain = email.split('@', 1)\n        if not '.' in domain:\n            # Localhost or other \"local\" names, assume just local part\n            return lpart\n        else:\n            return email\n\n    def _source_proto(self, insrv):\n        sockettype = str(insrv.socketType)\n        servertype = str(insrv.get('type', ''))\n        if sockettype.lower() == 'ssl':\n            servertype += '_ssl'\n        elif sockettype.lower() == 'starttls':\n            servertype += '_tls'\n        else:\n            print('FIXME/SOURCE: %s/%s' % (sockettype, servertype))\n        return servertype.lower()\n\n    def _route_proto(self, outsrv):\n        sockettype = str(outsrv.socketType)\n        servertype = str(outsrv.get('type', 'smtp'))\n        if sockettype.lower() == 'ssl':\n            servertype += 'ssl'\n        elif sockettype.lower() == 'starttls':\n            servertype += 'tls'\n        else:\n            print('FIXME/ROUTE: %s/%s' % (sockettype, servertype))\n        return servertype.lower()\n\n    def _rank(self, entry):\n        rank = 0\n        proto = entry.get('protocol', 'unknown').lower()\n        auth = entry.get('auth_type', 'unknown').lower()\n        # Deprioritize encrypted services on localhost, because they will\n        # generally have a certificate mismatch and the crypto doesn't\n        # do anything anyway.\n        lmul = -1 if (entry.get('hostname', '') == 'localhost') else 1\n        for srch, score in [('pop3', 1),\n                            ('imap', 2),\n                            ('ssl', 10 * lmul),\n                            ('tls', 5 * lmul),\n                            ('oauth2', 10),\n                            ('password', 0)]:\n            if srch in proto or srch in auth:\n                rank -= score\n        return rank\n\n    def _clean_domain(self, domain):\n        domain = domain.lower()\n\n        # Shortcuts, to save some cycles & speed things up...\n        for shortcut in ('.google.com', ):\n            if domain.endswith(shortcut):\n                domain = shortcut[1:]\n        for prefix in ('mx.', 'mx1.', 'mail.', 'smtp.'):\n            if domain.startswith(prefix) and '.' in domain[len(prefix):]:\n                domain = domain[len(prefix):]\n\n        return domain\n\n    def _get_xml_autoconfig(self, url, domain, email):\n        try:\n            result = {'sources': [], 'routes': []}\n\n            xml_data = STATIC_ISPDB.get(domain)\n            if not xml_data:\n                xml_data = self._urlget(url)\n\n            if xml_data:\n                data = objectify.fromstring(xml_data)\n# FIXME: Massage these so they match the format of the routes and\n#        sources more closely. Also look out and report the visiturl to\n#        handle GMail. OAuth2 is coming up as an auth mech, we will need to\n#        support it: https://bugzilla.mozilla.org/show_bug.cgi?id=1166625\n                try:\n                    for enable in data.emailProvider.enable:\n                        result['enable'] = result.get('enable', [])\n                        result['enable'].append({\n                            'url': enable.get('visiturl', ''),\n                            'description': str(enable.instruction)\n                        })\n                except AttributeError:\n                    pass\n                try:\n                    for docs in data.emailProvider.documentation:\n                        result['docs'] = result.get('docs', [])\n                        result['docs'].append({\n                            'url': docs.get('url', ''),\n                            'description': docs.descr.text\n                        })\n                except AttributeError:\n                    pass\n                for insrv in data.emailProvider.incomingServer:\n                    for auth in insrv.authentication:\n                        result['sources'].append({\n                            'protocol': self._source_proto(insrv),\n                            'username': self._username(insrv.username, email),\n                            'auth_type': str(auth),\n                            'host': str(insrv.hostname),\n                            'port': str(insrv.port)})\n                for outsrv in data.emailProvider.outgoingServer:\n                    for auth in outsrv.authentication:\n                        result['routes'].append({\n                            'protocol': self._route_proto(outsrv),\n                            'username': self._username(outsrv.username, email),\n                            'auth_type': str(auth),\n                            'host': str(outsrv.hostname),\n                            'port': str(outsrv.port)})\n                result['sources'].sort(key=self._rank)\n                result['routes'].sort(key=self._rank)\n                return result\n        except (IOError, ValueError, AttributeError):\n            return None\n\n    def _get_ispdb(self, email, domain):\n        domain = self._clean_domain(domain)\n\n        if domain in ('localhost',):\n            return None\n\n        self._progress(_('Checking ISPDB for %s') % domain)\n        settings = self._get_xml_autoconfig(\n            self.ISPDB_URL % {'domain': domain}, domain, email)\n        if settings:\n            self._log_result(_('Found %s in ISPDB') % domain)\n            return settings\n        dparts = domain.split('.')\n        if len(dparts) > 2:\n            domain = '.'.join(dparts[1:])\n            # FIXME: Make a longer list of 2nd-level public TLDs to ignore\n            if domain not in ('co.uk', 'pagekite.me'):\n                return self._get_xml_autoconfig(\n                    self.ISPDB_URL % {'domain': domain}, domain, email)\n        return None\n\n    def _want_anonymity(self):\n        return (self.session.config.sys.proxy.protocol in ('tor', 'tor-risky')\n                and not self.session.config.sys.proxy.fallback)\n\n    def _get_mx1(self, domain):\n        if domain in ('localhost',):\n            return None\n\n        # This would bypasses the connection broker and is not secured or\n        # anonymized, so if the user really wants anonymity we just punt.\n        if self._want_anonymity():\n            return None\n\n        import DNS\n        DNS.DiscoverNameServers()\n        try:\n            timeout = (self.deadline - time.time()) // 2\n            mxlist = DNS.DnsRequest(name=domain, qtype=DNS.Type.MX,\n                                    timeout=timeout).req()\n            mxs = sorted([m['data'] for m in mxlist.answers if 'data' in m])\n            return mxs[0][1] if mxs else None\n        except socket.error:\n            return None\n\n    def _get_domain_autoconfig(self, email, domain, mx1, ssl=True):\n        protocol = 'https'\n        if not ssl:\n            protocol = 'http'\n\n        for url in (self.AUTOCONFIG_URL, self.AUTOCONFIG_ALT_URL):\n            for dom in (domain, mx1):\n                if dom:\n                    self._progress(_('Checking for autoconfig on %s') % dom)\n                    settings = self._get_xml_autoconfig(\n                        url % {'protocol': protocol, 'domain': dom, 'email': email},\n                        dom, email)\n                    if settings:\n                        self._log_result(_('Found autoconfig on %s') % dom)\n                        return settings\n\n        return None\n\n    def _guess_service_domains(self, domain,\n                               mx=None, service_domains=None):\n        if domain in ('localhost',):\n            return {'pop3': [domain], 'imap': [domain], 'smtp': [domain]}\n\n        import socket\n        seen_ips = {'pop3': set(), 'imap': set(), 'smtp': set()}\n        service_domains = {} if (service_domains is None) else service_domains\n        # FIXME: Also check DNS service records?\n        for prefix, protos in (('pop',  ('pop3',)),\n                               ('pop3', ('pop3',)),\n                               ('imap', ('imap',)),\n                               ('smtp', ('smtp',)),\n                               ('mail', ('imap', 'pop3', 'smtp')),\n                               (None, ('imap', 'pop3', 'smtp'))):\n            try:\n                if prefix:\n                    name = '%s.%s' % (prefix, domain)\n                else:\n                    name = domain\n\n                if not self._want_anonymity():\n                    ip = socket.gethostbyname(name)\n                else:\n                    # We just try to connect to everything if anonymity\n                    # was requested - otherwise we'd be leaking over DNS.\n                    ip = '%s-%s' % (prefix, protos)\n\n                if ip:\n                    for proto in protos:\n                        if ip not in seen_ips[proto]:\n                            seen_ips[proto].add(ip)\n                            if proto in service_domains:\n                                if name not in service_domains[proto]:\n                                    service_domains[proto].append(name)\n                            else:\n                                service_domains[proto] = [name]\n            except socket.gaierror:\n                pass\n        if mx:\n            isp_domain = self._clean_domain(mx)\n            self._guess_service_domains(isp_domain,\n                                        service_domains=service_domains)\n\n        return service_domains\n\n    def _probe_port(self, host, port, encrypted=False):\n        import socket\n        if encrypted:\n            needs = [ConnBroker.OUTGOING_RAW, ConnBroker.OUTGOING_ENCRYPTED]\n        else:\n            needs = [ConnBroker.OUTGOING_RAW, ConnBroker.OUTGOING_CLEARTEXT]\n        with ConnBroker.context(need=needs) as cb:\n            try:\n                # FIXME: magic number follows\n                socket.create_connection((host, port), timeout=15).close()\n                return True\n            except (AssertionError, IOError, OSError, socket.error):\n                pass\n        return False\n\n    def _guess_settings(self, email, domain, mx1):\n        # Strategy:\n        #\n        # 1. Look up possible service names...\n        # 2. Attempt connections on well-known service ports\n        #\n        # Passwords and usernames are checked later, as is STARTTLS.\n\n        args_hash = {'domain': domain, 'email': email}\n        self._progress(_('Guessing settings for %(email)s') % args_hash)\n\n        service_domains = self._guess_service_domains(domain, mx=mx1)\n        self._log_result(_('Found %d potential servers')\n                         % len(service_domains))\n        if not service_domains:\n            return None\n\n        self._progress(_('Probing for services...'))\n        result = {'sources': [], 'routes': []}\n        for section, service, port, proto, auth_type in (\n                ('sources', 'imap', '993', 'imap_ssl', 'password'),\n                ('sources', 'pop3', '995', 'pop3_ssl', 'password'),\n                ('sources', 'imap', '143', 'imap', 'password'),\n                ('sources', 'pop3', '110', 'pop3', 'password'),\n                ('routes', 'smtp', '465', 'smtpssl', 'password-cleartext'),\n                ('routes', 'smtp', '587', 'smtp', 'password-cleartext'),\n                ('routes', 'smtp', '25', 'smtp', 'password-cleartext')):\n            for host in service_domains.get(service, []):\n                if len(result[section]) > 3:\n                    break\n                if self._probe_port(host, port, encrypted=('ssl' in proto)):\n                    result[section].append({\n                        'protocol': proto,\n                        'username': self._guess_username(email),\n                        'auth_type': auth_type,\n                        'host': str(host),\n                        'port': str(port),\n                    })\n                    self._progress(_('Found %(service)s server on '\n                                     '%(host)s:%(port)s')\n                                   % {'service': service.upper(),\n                                      'host': host,\n                                      'port': port})\n\n        return result\n\n    def _get_email_settings(self, email):\n        # Thunderbird does this:\n        #  - tb-install-dir/isp/example.com.xml on the harddisk\n        #  - check for autoconfig.example.com\n        #  - look up of \"example.com\" in the ISPDB\n        #  - look up \"MX example.com\" in DNS, and for mx1.mail.hoster.com,\n        #    look up \"hoster.com\" in the ISPDB\n        #  - try to guess (imap.example.com, smtp.example.com etc.)\n        #\n        # We mostly follow Thunderbird's design, except we give the ISPDB\n        # priority: if it has an entry, don't try autoconfig.example.com.\n\n        domain = email.split('@')[-1].lower()\n        settings = None\n        mx1 = None\n\n        if not settings and self.deadline > time.time():\n            # FIXME: actually we want mx1 here but since DNS lack security that\n            #        would compromise security when ISPDB gives us a result\n            settings = self._get_domain_autoconfig(email, domain, None, ssl=True)\n\n        if not settings and self.deadline > time.time():\n            settings = self._get_ispdb(email, domain)\n\n        if not settings and self.deadline > time.time():\n            mx1 = self._get_mx1(domain)\n            if mx1 and not mx1.endswith('.' + domain):\n                settings = self._get_ispdb(email, mx1)\n\n        if not settings and self.deadline > time.time():\n            settings = self._get_domain_autoconfig(email, None, mx1, ssl=True)\n\n        # Try the unencrypted lookups next...\n        if not settings and self.deadline > time.time():\n            settings = self._get_domain_autoconfig(email, domain, mx1, ssl=False)\n        if not settings and self.deadline > time.time():\n            settings = self._get_domain_autoconfig(email, None, mx1, ssl=False)\n\n        if not settings and self.deadline > time.time():\n            settings = self._guess_settings(email, domain, mx1)\n\n        if self.deadline < time.time():\n            self._progress(_('Ran out of time, results may be incomplete'))\n\n        return settings\n\n    def _test_login_and_proto(self, email, settings):\n        event = Event(data={})\n\n        if settings['protocol'].startswith('smtp'):\n            try:\n                safe_assert(\n                    SendMail(self.session, None,\n                             [(email,\n                               [email, 'test@mailpile.is'], None,\n                               [event])],\n                             test_only=True, test_route=settings))\n                return True, True\n            except (IOError, OSError, AssertionError, SendMailError):\n                pass\n\n        if settings['protocol'].startswith('imap'):\n            from mailpile.mail_source.imap import TestImapSettings\n            if TestImapSettings(self.session, settings, event):\n                return True, True\n\n        if settings['protocol'].startswith('pop3'):\n            from mailpile.mail_source.pop3 import TestPop3Settings\n            if TestPop3Settings(self.session, settings, event):\n                return True, True\n\n        if ('connection' in event.data and\n                event.data['connection']['error'][0] == 'auth'):\n            return False, True\n\n        if ('last_error' in event.data and event.data.get('auth')):\n            return False, True\n\n        return False, False\n\n    def _probe_account_settings(self, email, results):\n        result = results[email]\n        userpart = email.split('@')[0]\n        user_info = {'userpart': userpart, 'email': email}\n        login_errors_total = 0\n        route_open_relay = False\n        for cleartext, which in ((False, 'routes'),\n                                 (False, 'sources'),\n                                 (True, 'routes'),\n                                 (True, 'sources')):\n            login_errors = 0\n            servers = result[which]\n            if cleartext and ((not servers) or\n                              (which == 'routes' and route_open_relay) or\n                              servers[0].get('username')):\n                # If we have already found combinations that work for both\n                # incoming and outgoing, don't send the password over the\n                # network in the clear; just stop here.\n                continue\n\n            self._progress(_('Probing %s, cleartext=%s')\n                           % (which, cleartext))\n\n            for details in servers:\n                for starttls, userfmt in ((True, '%(email)s'),\n                                          (True, '%(userpart)s'),\n                                          (True, ''),\n                                          (False, '%(email)s'),\n                                          (False, '%(userpart)s'),\n                                          (False, '')):\n                    # Skip some combinations...\n                    has_ssl = (('ssl' in details['protocol']) or\n                               ('tls' in details['protocol']))\n                    crypto = True if (starttls or has_ssl) else False\n                    if starttls and has_ssl:\n                        # No STARTTLS if this server already uses TLS\n                        continue\n                    if crypto is cleartext:\n                        # Cleartext pass: ignore starttls and ssl conns\n                        # Crypto pass: require starttls OR has_ssl\n                        continue\n                    if time.time() > self.deadline:\n                        continue\n                    if not userfmt and (details['protocol'][:4] != 'smtp'\n                                        or login_errors_total == 0):\n                        continue\n\n                    server_info = copy.copy(details)\n                    server_info['username'] = userfmt % user_info\n                    if userfmt:\n                        server_info['password'] = self.password\n                    if starttls:\n                        if server_info['protocol'] == 'smtp':\n                            server_info['protocol'] += 'tls'\n                        else:\n                            server_info['protocol'] += '_tls'\n                        pmsg = _('Testing %(protocol)4.4s '\n                                 'on %(host)s:%(port)s '\n                                 'with STARTTLS as %(username)s')\n                    else:\n                        pmsg = _('Testing %(protocol)4.4s '\n                                 'on %(host)s:%(port)s '\n                                 'as %(username)s')\n                    if not crypto:\n                        pmsg += ' (' + _('insecure') + ')'\n\n                    # FIXME: Unsupported protocol...\n                    if server_info['protocol'] == 'pop3_tls':\n                        continue\n\n                    self._progress(pmsg % server_info)\n                    lok, pok = self._test_login_and_proto(email, server_info)\n                    if lok and pok:\n                        self._log_result(_('Success'))\n                        details.update(server_info)\n                        if not userfmt:\n                            route_open_relay = True\n                        break\n                    elif pok:\n                        self._log_result(_('Protocol is OK'))\n                        details['protocol'] = server_info['protocol']\n                    if not lok:\n                        self._log_result(_('Login failed'))\n                        login_errors += 1\n            if login_errors:\n                # Sort the results; prefer the ones with a successful login\n                order = list(range(0, len(servers)))\n                order.sort(key=lambda i: (\n                    0 if servers[i].get('username') else 1,\n                    0 if servers[i]['protocol'][-3:] in ('ssl', 'tls') else 1,\n                    0 if 'imap' in servers[i]['protocol'] else 1,\n                    i))\n                servers[:] = [servers[i] for i in order]\n                login_errors_total += login_errors\n        return login_errors_total\n\n    def setup_command(self, session):\n        results = {}\n        args = list(self.args)\n        self.deadline = time.time() + float(self.data.get('timeout', [60])[0])\n        self.tracking_id = self.data.get('track-id', [None])[0]\n        self.password = self.data.get('password', [None])[0]\n        if not self.password and len(args) > 1:\n            self.password = args.pop(-1)\n\n        emails = args + self.data.get('email', [])\n        if self.password and len(emails) != 1:\n            return self._error(_('Can only test settings for one account '\n                                 'at a time'))\n\n        for email in emails:\n            settings = self._testing_data(self._get_email_settings,\n                                          self.TEST_DATA, email)\n            if settings:\n                results[email] = settings\n                if self.password and self.deadline > time.time():\n                    errors = self._probe_account_settings(email, results)\n                    if errors:\n                        for k in ('routes', 'sources'):\n                            if (settings.get(k) and\n                                    not settings[k][0].get('username')):\n                                results['login_failed'] = True\n\n            if time.time() >= self.deadline:\n                break\n        if results:\n            return self._success(\n                _('Found settings for %d addresses') % len(results),\n                result=results)\n        else:\n            return self._error(_('No settings found'))\n\n\nclass SetupWelcome(TestableWebbable):\n    SYNOPSIS = (None, None, 'setup/welcome', None)\n    HTTP_CALLABLE = ('GET', 'POST')\n    HTTP_POST_VARS = dict_merge(TestableWebbable.HTTP_POST_VARS, {\n        'language': 'Language selection'\n    })\n\n    def bg_setup_stage_1(self):\n        # Wait a bit, so the user has something to look at befor we\n        # block the web server and do real work.\n        time.sleep(2)\n\n        # Intial configuration of app goes here...\n        if not self.session.config.tags:\n            with BLOCK_HTTPD_LOCK, Idle_HTTPD(allowed=0):\n                self.basic_app_config(self.session)\n\n    def configure_language(self, session, config, language, save=True):\n        try:\n            i18n = lambda: ActivateTranslation(session, config, language)\n            if not self._testing_yes(i18n):\n                raise ValueError('Failed to configure i18n')\n            config.prefs.language = language\n            if save and not self._testing():\n                self._background_save(config='!FORCE')\n            return True\n        except ValueError:\n            return self._error(_('Invalid language: %s') % language)\n\n    def setup_command(self, session):\n        config = session.config\n        if self.data.get('_method') == 'POST' or self._testing():\n            language = self.data.get('language', [''])[0]\n            if language:\n                rv = self.configure_language(session, config, language)\n                if rv is not True:\n                    return rv\n\n            config.slow_worker.add_unique_task(\n                session, 'Setup, Stage 1', lambda: self.bg_setup_stage_1())\n\n        languages = [(l, n) for l, n in ListTranslations(config).iteritems()]\n        languages.sort(key=lambda k: (k[1], k[0]))\n        results = {\n            'languages': languages,\n            'language': config.prefs.language\n        }\n        return self._success(_('Welcome to Mailpile!'), results)\n\n\nclass CreatePassword(TestableWebbable):\n    SYNOPSIS = (None, None, 'setup/mkpass', None)\n    HTTP_CALLABLE = ('GET', 'POST')\n    HTTP_POST_VARS = dict_merge(TestableWebbable.HTTP_POST_VARS, {\n        'dict': 'Word list to use'\n    })\n    PATHS = ['/etc/dictionaries-common', '/usr/dict', '/usr/share/dict']\n\n    def find_dictionaries(self):\n        dictionaries = set([])\n        for path in (p for p in self.PATHS if os.path.exists(p)):\n            for fn in (os.path.join(path, f) for f in os.listdir(path)):\n                fpath = os.path.realpath(fn)\n                ext = fpath.split('.')[-1]\n                if (not os.path.isdir(fpath)\n                        and ext not in ('aff', 'hash')):\n                    stat = os.stat(fpath)\n                    if stat.st_size > 100000:\n                        dictionaries.add((stat.st_size, fpath))\n        return sorted(list(dictionaries))\n\n    def load_dictionary(self, dpath, maxlen=6):\n        return list(w for w in open(dpath, 'rb')\n                    if \"'\" not in w and ' ' not in w and len(w) <= (maxlen+1))\n\n    def setup_command(self, session):\n        from mailpile.crypto.aes_utils import getrandbits\n        from math import log\n\n        dictionaries = self.find_dictionaries()\n        dictionary = dictionaries[-1][1]\n        words = self.load_dictionary(dictionary, maxlen=6)\n        wanted_bits = 64\n        passphrase = []\n        results = {\n            'dictionaries': dictionaries,\n            'dictionary': dictionary\n        }\n\n        # This is our random word generation; first we shuffle the\n        # dictionary (poorly), because we're going to only use the first\n        # power of 2 words.\n        random.shuffle(words)\n\n        # Figure out how many bits index neatly into the file\n        filebits = int(log(len(words), 2))\n        filemask = (2 ** filebits) - 1\n\n        # Encode strongly random bits using the shuffled dictionary\n        while wanted_bits > 0:\n            wanted_bits -= filebits\n            word = words[getrandbits(filebits) & filemask].strip().lower()\n            passphrase.append(word.decode('utf-8'))\n\n        results.update({\n            'dictionary_bits': filebits,\n            'passphrase': ' '.join(passphrase),\n            'bits': filebits * len(passphrase)\n        })\n        return self._success(_('Welcome to Mailpile!'), results)\n\n\nclass SetupPassword(TestableWebbable):\n    SYNOPSIS = (None, None, 'setup/password', None)\n    HTTP_CALLABLE = ('GET', 'POST')\n    HTTP_POST_VARS = dict_merge(TestableWebbable.HTTP_POST_VARS, {\n        'existing': 'Old Mailpile password',\n        'password1': 'New Mailpile password',\n        'password2': 'Confirmation password'\n    })\n\n    PASSWORD_LOCK = CryptoLock()\n\n    def setup_command(self, session):\n        config = session.config\n        current_passphrase = config.passphrases['DEFAULT']\n        need_password = current_passphrase.is_set()\n        incorrect = mismatch = done = False\n        if self.data.get('_method') == 'POST' or self._testing():\n            with SetupPassword.PASSWORD_LOCK:\n                if need_password:\n                    ex = self.data.get('existing', [''])[0]\n                    if not current_passphrase.compare(ex):\n                        incorrect = True\n                        time.sleep(1)\n\n                if not incorrect:\n                    p1 = self.data.get('password1', [''])[0]\n                    p2 = self.data.get('password2', [''])[0]\n                    if p1 and p2 and p1 == p2:\n                        config.passphrases['DEFAULT'].set_passphrase(p1)\n                        config.prefs.gpg_recipient = '!PASSWORD'\n                        self.make_master_key()\n                        self._background_save(config='!FORCE')\n                        mailpile.auth.LogoutAll()\n                        done = True\n                else:\n                    mismatch = True\n\n        results = {\n            'need_password': need_password,\n            'configured': done,\n            'incorrect': incorrect,\n            'mismatch': mismatch\n        }\n        return self._success(_('Welcome to Mailpile!'), results)\n\n\nclass SetupTestRoute(TestableWebbable):\n    SYNOPSIS = (None, None, 'setup/test_route', None)\n\n    HTTP_AUTH_REQUIRED = True\n    HTTP_CALLABLE = ('POST', )\n    HTTP_POST_VARS = dict_merge(TestableWebbable.HTTP_POST_VARS,\n                                dict((k, v[0]) for k, v in\n                                     CONFIG_RULES['routes'][1].iteritems()),\n                                {'route_id': 'ID of existing route'})\n    TEST_DATA = {}\n\n    def setup_command(self, session):\n\n        if self.args:\n            route_id = self.args[0]\n        elif 'route_id' in self.data:\n            route_id = self.data['route_id'][0]\n        else:\n            route_id = None\n\n        if route_id:\n            route = self.session.config.routes[route_id]\n            safe_assert(route)\n        else:\n            route = {}\n            for k in CONFIG_RULES['routes'][1]:\n                if k not in self.data:\n                    pass\n                elif CONFIG_RULES['routes'][1][k][1] in (int, 'int'):\n                    route[k] = int(self.data[k][0])\n                else:\n                    route[k] = self.data[k][0]\n\n        fromaddr = route.get('username', '')\n        if '@' not in fromaddr:\n            fromaddr = self.session.config.get_profile()['email']\n        if not fromaddr or '@' not in fromaddr:\n            fromaddr = '%s@%s' % (route.get('username', 'test'),\n                                  route.get('host', 'example.com'))\n        safe_assert(fromaddr)\n\n        error_info = {'error': _('Unknown error')}\n        try:\n            safe_assert(\n                SendMail(self.session, None,\n                         [(fromaddr,\n                           [fromaddr, 'test@mailpile.is'],\n                           None,\n                           [self.event])],\n                         test_only=True, test_route=route))\n            return self._success(_('Route is working'),\n                                 result=route)\n        except OSError:\n            error_info = {'error': _('Invalid command'),\n                          'invalid_command': True}\n        except SendMailError as e:\n            error_info = {'error': e.message,\n                          'sendmail_error': True}\n            error_info.update(e.error_info)\n        except:\n            import traceback\n            traceback.print_exc()\n\n        return self._error(_('Route is not working'),\n                           result=route, info=error_info)\n\n\nclass SetupTor(TestableWebbable):\n    \"\"\"Check for Tor and auto-configure if possible.\"\"\"\n    SYNOPSIS = (None, 'setup/tor', 'setup/tor', \"[--auto] [--shared]\")\n    HTTP_CALLABLE = ('POST',)\n    HTTP_POST_VARS = {\n        'prefer_shared': 'If set, prefer a shared Tor instance'}\n\n    @classmethod\n    def autoconfig(cls, session):\n        cls(session, arg=['--auto']).run()\n\n    def auto_configure_tor(self, session):\n        if session.config.tor_worker is not None:\n            if session.config.tor_worker.isReady(wait=True):\n                time.sleep(0.1)\n                hostport = ('127.0.0.1', session.config.tor_worker.socks_port)\n                success, message = self._configure_tor(session, hostport,\n                                                       port_zero=True)\n                if success:\n                    return message\n\n        if session.config.sys.tor.systemwide:\n            hostport = ('127.0.0.1', 9050)\n            success, message = self._configure_tor(session, hostport)\n            if success:\n                return message\n\n        if session.config.tor_worker is None:\n            if session.config.start_tor_worker().isReady(wait=True):\n                time.sleep(0.1)\n                hostport = ('127.0.0.1', session.config.tor_worker.socks_port)\n                success, message = self._configure_tor(session, hostport,\n                                                       port_zero=True)\n                if success:\n                    session.config.sys.tor.systemwide = False\n\n        return message\n\n    def _configure_tor(self, session, hostport, port_zero=False):\n        try:\n            with ConnBroker.context(need=[ConnBroker.OUTGOING_RAW]) as ctx:\n                tor = socket.create_connection(hostport, timeout=10)\n        except IOError:\n            return (False,\n                _('Failed to connect to Tor on %s:%s. Is it installed?')\n                % hostport)\n\n        # If that succeeded, we might have Tor!\n        old_proto = session.config.sys.proxy.protocol\n        session.config.sys.proxy.protocol = 'tor'\n        session.config.sys.proxy.host = hostport[0]\n        session.config.sys.proxy.port = 0 if port_zero else hostport[1]\n        session.config.sys.proxy.fallback = True\n\n        # Configure connection broker, revert settings while we test\n        ConnBroker.configure()\n        session.config.sys.proxy.protocol = old_proto\n\n        # Test it...\n        need_tor = [ConnBroker.OUTGOING_HTTPS]\n        try:\n            with ConnBroker.context(need=need_tor) as context:\n                motd = urlopen(MOTD_URL_TOR_ONLY_NO_MARS,\n                               data=None, timeout=10).read()\n                safe_assert(motd.strip().endswith('}'))\n            session.config.sys.proxy.protocol = 'tor'\n            return (True, _('Successfully configured and enabled Tor!'))\n        except (IOError, AssertionError):\n            ConnBroker.configure()\n            return (False,\n                _('Failed to configure Tor on %s:%s. Is the network down?')\n                % hostport)\n\n    def setup_command(self, session):\n        if (\"--auto\" not in self.args\n                or session.config.sys.proxy.protocol == 'unknown'):\n            message = self.auto_configure_tor(session)\n        else:\n            message = _('Proxy settings have already been configured.')\n\n        if session.config.sys.proxy.protocol == 'tor':\n            return self._success(message, result=session.config.sys.proxy)\n        else:\n            return self._error(message, result=session.config.sys.proxy)\n\n\nclass Setup(SetupWelcome):\n    \"\"\"Enter setup flow\"\"\"\n    SYNOPSIS = (None, 'setup', 'setup', '')\n\n    ORDER = ('Internals', 0)\n    LOG_PROGRESS = True\n    HTTP_POST_VARS = TestableWebbable.HTTP_POST_VARS\n    HTTP_CALLABLE = ('GET',)\n    HTTP_AUTH_REQUIRED = True\n\n    # These are a global, may be modified...\n    KEY_WORKER_LOCK = CryptoRLock()\n    KEY_CREATING_THREAD = None\n    KEY_EDITING_THREAD = None\n\n    @classmethod\n    def _CHECKPOINTS(self, config):\n        return [\n            # Stage 0: Welcome: Choose app language\n            ('language', lambda: config.prefs.language, SetupWelcome),\n\n            # Stage 1: Basic security - a password\n            ('security', lambda: config.get_master_key(), SetupPassword)\n        ]\n\n    @classmethod\n    def Next(cls, config, default, needed_auth=True):\n        if not config.loaded_config:\n            return default\n\n        for name, guard, step in cls._CHECKPOINTS(config):\n            auth_required = (step.HTTP_AUTH_REQUIRED is True\n                             or (config.prefs.gpg_recipient and\n                                 step.HTTP_AUTH_REQUIRED == 'Maybe'))\n            if not guard():\n                if (not needed_auth) or (not auth_required):\n                    return step\n\n        return default\n\n    def cli_setup_command(self, session):\n        # Stop the workers...\n        want_daemons = session.config.cron_worker is not None\n        session.config.stop_workers()\n\n        # Perform any required migrations\n        Migrate(session).run(before_setup=True, after_setup=False)\n\n        # Basic app config, tags, plugins, etc.\n        self.basic_app_config(session,\n                              save_and_update_workers=False,\n                              want_daemons=want_daemons)\n\n        # Set language from environment\n        if not session.config.prefs.language:\n            lang = os.getenv('LANG', '').split('.')[0] or 'en'\n            if self.configure_language(session, session.config, lang,\n                                       save=False) is True:\n                session.ui.notify(_('Language set to: %s') % lang)\n\n        # Ask the user for a password, if we don't have security already\n        if (not session.config.passphrases['DEFAULT'].is_set() and\n                not session.config.prefs.gpg_recipient):\n            p1 = session.ui.get_password(_('Choose a password for Mailpile: '))\n            if p1:\n                p2 = session.ui.get_password(_('Confirm password: '))\n            if p1 and p2 and p1 == p2:\n                session.config.passphrases['DEFAULT'].set_passphrase(p1)\n                session.config.prefs.gpg_recipient = '!PASSWORD'\n                self.make_master_key()\n            else:\n                session.ui.error(\n                    _('Passwords did not match! Please try again.'))\n\n        # Perform any required migrations\n        Migrate(session).run(before_setup=False, after_setup=True)\n\n        session.config.save()\n        session.config.prepare_workers(session, daemons=want_daemons)\n\n        return self._success(_('Performed initial Mailpile setup'))\n\n    def setup_command(self, session):\n        if '_method' in self.data:\n            return self._success(_('Entering setup flow'), result=dict(\n                ((c[0], c[1]() and True or False)\n                 for c in self._CHECKPOINTS(session.config)\n            )))\n        else:\n            return self.cli_setup_command(session)\n\n\n_ = gettext\n_plugins.register_commands(SetupMagic,\n                           SetupGetEmailSettings,\n                           SetupWelcome,\n                           CreatePassword,\n                           SetupPassword,\n                           SetupTestRoute,\n                           SetupTor,\n                           Setup)\n"
  },
  {
    "path": "mailpile/plugins/setup_magic_ispdb.py",
    "content": "##[ Static ISPDB entries ]######################################################\n\n# https://autoconfig.thunderbird.net/v1.1/gmail.com - as of 2017-05-01\nSTATIC_ISPDB_GOOGLEMAIL = \"\"\"\n<clientConfig version=\"1.1\">\n  <emailProvider id=\"googlemail.com\">\n    <domain>gmail.com</domain>\n    <domain>googlemail.com</domain>\n    <!-- MX, for Google Apps -->\n    <domain>google.com</domain>\n    <displayName>Google Mail</displayName>\n    <displayShortName>GMail</displayShortName>\n    <incomingServer type=\"imap\">\n      <hostname>imap.gmail.com</hostname>\n      <port>993</port>\n      <socketType>SSL</socketType>\n      <username>%EMAILADDRESS%</username>\n      <authentication>OAuth2</authentication>\n      <authentication>password-cleartext</authentication>\n    </incomingServer>\n    <incomingServer type=\"pop3\">\n      <hostname>pop.gmail.com</hostname>\n      <port>995</port>\n      <socketType>SSL</socketType>\n      <username>%EMAILADDRESS%</username>\n      <authentication>password-cleartext</authentication>\n      <pop3>\n        <leaveMessagesOnServer>true</leaveMessagesOnServer>\n      </pop3>\n    </incomingServer>\n    <outgoingServer type=\"smtp\">\n      <hostname>smtp.gmail.com</hostname>\n      <port>465</port>\n      <socketType>SSL</socketType>\n      <username>%EMAILADDRESS%</username>\n      <authentication>OAuth2</authentication>\n      <authentication>password-cleartext</authentication>\n    </outgoingServer>\n    <enable visiturl=\"https://mail.google.com/mail/?ui=2&amp;shva=1#settings/fwdandpop\">\n      <instruction>You need to enable IMAP access</instruction>\n    </enable>\n    <documentation url=\"http://mail.google.com/support/bin/answer.py?answer=13273\">\n      <descr>How to enable IMAP/POP3 in GMail</descr>\n    </documentation>\n    <documentation url=\"http://mail.google.com/support/bin/topic.py?topic=12806\">\n      <descr>How to configure email clients for IMAP</descr>\n    </documentation>\n    <documentation url=\"http://mail.google.com/support/bin/topic.py?topic=12805\">\n      <descr>How to configure email clients for POP3</descr>\n    </documentation>\n    <documentation url=\"http://mail.google.com/support/bin/answer.py?answer=86399\">\n      <descr>How to configure TB 2.0 for POP3</descr>\n    </documentation>\n  </emailProvider>\n\n  <webMail>\n    <loginPage url=\"https://accounts.google.com/ServiceLogin?service=mail&amp;continue=http://mail.google.com/mail/\"/>\n    <loginPageInfo url=\"https://accounts.google.com/ServiceLogin?service=mail&amp;continue=http://mail.google.com/mail/\">\n      <username>%EMAILADDRESS%</username>\n      <usernameField id=\"Email\"/>\n      <passwordField id=\"Passwd\"/>\n      <loginButton id=\"signIn\"/>\n    </loginPageInfo>\n  </webMail>\n\n</clientConfig>\n\"\"\"\n\nSTATIC_ISPDB = {\n    'gmail.com': STATIC_ISPDB_GOOGLEMAIL,\n    'google.com': STATIC_ISPDB_GOOGLEMAIL,\n    'googlemail.com': STATIC_ISPDB_GOOGLEMAIL}\n\n"
  },
  {
    "path": "mailpile/plugins/sizes.py",
    "content": "import math\nimport time\nimport datetime\n\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\n\n\n_plugins = PluginManager(builtin=__file__)\n\n\n##[ Keywords ]################################################################\n\ndef meta_kw_extractor(index, msg_mid, msg, msg_size, msg_ts, **kwargs):\n    \"\"\"Create a search term with the floored log2 size of the message.\"\"\"\n    if msg_size <= 0:\n        return []\n    return ['%s:ln2sz' % int(math.log(msg_size, 2))]\n\n_plugins.register_meta_kw_extractor('sizes', meta_kw_extractor)\n\n\n##[ Search terms ]############################################################\n\n\n_size_units = {\n    't': 40,\n    'g': 30,\n    'm': 20,\n    'k': 10,\n    'b': 0\n}\n_range_keywords = [\n    '..',\n    '-'\n]\n\n\ndef _mk_logsize(size, default_unit=0):\n    if not size:\n        return 0\n    unit = 0\n    size = size.lower()\n    if size[-1].isdigit():  # ends with a number\n        unit = default_unit\n    elif len(size) >= 2 and size[-2] in _size_units and size[-1] == 'b':\n        unit = _size_units[size[-2]]\n        size = size[:-2]\n    elif size[-1] in _size_units:\n        unit = _size_units[size[-1]]\n        size = size[:-1]\n    try:\n        return int(math.log(float(size), 2) + unit)\n    except ValueError:\n        return 1 + unit\n\n\ndef search(config, idx, term, hits):\n    try:\n        word = term.split(':', 1)[1].lower()\n\n        for range_keyword in _range_keywords:\n            if range_keyword in term:\n                start, end = word.split(range_keyword)\n                break\n        else:\n            start = end = word\n\n        # if no unit is setup in the start term, use the unit from the end term\n        end_unit_size = end.lower()[-1]\n        end_unit = 0\n        if end_unit_size in _size_units:\n            end_unit = _size_units[end_unit_size]\n\n        start = _mk_logsize(start, end_unit)\n        end = _mk_logsize(end)\n        terms = ['%s:ln2sz' % sz for sz in range(start, end+1)]\n\n        rt = []\n        for t in terms:\n            rt.extend(hits(t))\n        return rt\n    except:\n        raise ValueError('Invalid size: %s' % term)\n\n\n_plugins.register_search_term('size', search)\n"
  },
  {
    "path": "mailpile/plugins/smtp_server.py",
    "content": "import asyncore\nimport email.parser\nimport random\nimport smtpd\nimport threading\nimport traceback\n\nimport mailpile.security as security\nfrom mailpile.commands import Command\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.mailutils.emails import Email\nfrom mailpile.plugins import PluginManager\nfrom mailpile.smtp_client import sha512_512kCheck, sha512_512kCollide\nfrom mailpile.smtp_client import SMTORP_HASHCASH_RCODE, SMTORP_HASHCASH_FORMAT\nfrom mailpile.util import *\n\n\n_plugins = PluginManager(builtin=__file__)\n\n\n##[ Configuration ]##########################################################\n\n_plugins.register_config_section(\n    'sys', 'smtpd', [_('SMTP Daemon'), False, {\n        'host': (_('Listening host for SMTP daemon'), 'hostname', 'localhost'),\n        'port': (_('Listening port for SMTP daemon'), int, 0),\n    }])\n\n\nclass SMTPChannel(smtpd.SMTPChannel):\n    MAX_MESSAGE_SIZE = 1024 * 1024 * 50\n    HASHCASH_WANT_BITS = 8  # Only 128-or-so expensive sha512_512k ops\n    HASHCASH_URL = 'https://www.mailpile.is/hashcash/'\n\n    def __init__(self, session, *args, **kwargs):\n        smtpd.SMTPChannel.__init__(self, *args, **kwargs)\n        self.session = session\n        # Lie lie lie lie...\n        self.__fqdn = 'cs.utah.edu'\n        self.too_much_data = False\n        self.is_spam = False\n        self.want_hashcash = {}\n\n    def _is_dangerous_address(self, address):\n        return False  # FIXME\n\n    def _is_spam_address(self, address):\n        return False  # FIXME\n\n    def push(self, msg):\n        play_nice_with_threads()\n        if msg.startswith('220'):\n            # This is a hack, because these days it is no longer considered\n            # reasonable to tell everyone your hostname and version number.\n            # Lie lie lie lie! ... https://snowplow.org/tom/worm/worm.html\n            smtpd.SMTPChannel.push(self, ('220 cs.utah.edu SMTP '\n                                          'Sendmail 5.67; '\n                                          'Wed, 2 Nov 1988 20:49'))\n        else:\n            smtpd.SMTPChannel.push(self, msg)\n\n    def _address_ok(self, address):\n        if self._is_dangerous_address(address):\n            self.is_spam = True\n        elif self._is_spam_address(address):\n            self.is_spam = True\n        return True\n\n    def _challenge(self):\n        return '-'.join([str(random.randint(0, 0xfffffff)),\n                         str(random.randint(0, 0xfffffff)),\n                         str(random.randint(0, 0xfffffff))])\n\n    def _hashgrey_ok(self, address):\n        if '#' in address:\n            address, solution = address.split('##', 1)\n        else:\n            solution = None\n\n        want_bits = self.HASHCASH_WANT_BITS\n        addrpair = '%s, %s' % (self.__mailfrom, address)\n        if solution and addrpair in self.want_hashcash:\n            if sha512_512kCheck(self.want_hashcash[addrpair],\n                               want_bits, solution):\n                return address\n            else:\n                self.push('550 Hashcash is null and void')\n                self.close_when_done()\n                return None\n        else:\n            ch = self.want_hashcash[addrpair] = self._challenge()\n            self.push(str(SMTORP_HASHCASH_RCODE) + ' ' +\n                      SMTORP_HASHCASH_FORMAT\n                      % {'bits': want_bits,\n                         'challenge': ch,\n                         'url': self.HASHCASH_URL})\n            return None\n\n    def smtp_MAIL(self, arg):\n        address = self.__getaddr('FROM:', arg) if arg else None\n        if not address:\n            self.push('501 Syntax: MAIL FROM:<address>')\n            return\n        if self.__mailfrom:\n            self.push('503 Error: nested MAIL command')\n            return\n        if self._address_ok(address):\n            self.__mailfrom = address\n            self.push('250 Ok')\n\n    def smtp_RCPT(self, arg):\n        if not self.__mailfrom:\n            self.push('503 Error: need MAIL command')\n            return\n        address = self.__getaddr('TO:', arg) if arg else None\n        if not address:\n            self.push('501 Syntax: RCPT TO: <address>')\n            return\n        if len(self.__rcpttos) > 0:\n            self.push(\"553 One mail at a time, please\")\n            self.close_when_done()\n        if not self.is_spam:\n            address = self._hashgrey_ok(address)\n        if address and self._address_ok(address) and not self.is_spam:\n            self.__rcpttos.append(address)\n            self.push('250 Ok')\n\n    def smtp_DATA(self, arg):\n        if self.is_spam:\n            self.push(\"450 I don't like spam!\")\n            self.close_when_done()\n        else:\n            smtpd.SMTPChannel.smtp_DATA(arg)\n\n    def collect_incoming_data(self, data):\n        if (self.__line and\n                sum((len(l) for l in self.__line)) > self.MAX_MESSAGE_SIZE):\n            self.push('552 Error: too much data')\n            self.close_when_done()\n        else:\n            smtpd.SMTPChannel.collect_incoming_data(self, data)\n\n\nclass SMTPServer(smtpd.SMTPServer):\n    def __init__(self, session, localaddr, **kwargs):\n        self.session = session\n        smtpd.SMTPServer.__init__(self, localaddr, None)\n\n    def handle_accept(self):\n        pair = self.accept()\n        if pair is not None:\n            conn, addr = pair\n            channel = SMTPChannel(self.session, self, conn, addr)\n\n    def process_message(self, peer, mailfrom, rcpttos, data):\n        # We can assume that the mailfrom and rcpttos have checked out\n        # and this message is indeed intended for us. Spool it to disk\n        # and add to the index!\n        session, config = self.session, self.session.config\n        blank_tid = config.get_tags(type='blank')[0]._key\n        idx = config.index\n        play_nice_with_threads()\n        try:\n            message = email.parser.Parser().parsestr(data)\n            lid, lmbox = config.open_local_mailbox(session)\n            e = Email.Create(idx, lid, lmbox, ephemeral_mid=False)\n            idx.add_tag(session, blank_tid, msg_idxs=[e.msg_idx_pos],\n                        conversation=False)\n            e.update_from_msg(session, message)\n            idx.remove_tag(session, blank_tid, msg_idxs=[e.msg_idx_pos],\n                           conversation=False)\n            return None\n        except:\n            traceback.print_exc()\n            return '400 Oops wtf'\n\n\nclass SMTPWorker(threading.Thread):\n    def __init__(self, session):\n        self.session = session\n        self.quitting = False\n        threading.Thread.__init__(self)\n\n    def run(self):\n        cfg = self.session.config.sys.smtpd\n        if cfg.host and cfg.port:\n            server = SMTPServer(self.session, (cfg.host, cfg.port))\n            while not self.quitting:\n                asyncore.poll(timeout=1.0)\n            asyncore.close_all()\n\n    def quit(self, join=True):\n        self.quitting = True\n        if join and self.isAlive():\n            self.join()\n\n\nclass HashCash(Command):\n    \"\"\"Try to collide a hash using the SMTorP algorithm\"\"\"\n    SYNOPSIS = (None, 'hashcash', None, '<bits> <challenge>')\n    ORDER = ('Internals', 9)\n    HTTP_CALLABLE = ()\n    COMMAND_SECURITY = security.CC_CPU_INTENSIVE\n\n    def command(self):\n        bits, challenge = int(self.args[0]), self.args[1]\n        expected = 2 ** bits\n        def marker(counter):\n            progress = ((1024.0 * counter) / expected) * 100\n            self.session.ui.mark('Finding a %d-bit collision for %s (%d%%)'\n                                 % (bits, challenge, progress))\n        collision = sha512_512kCollide(challenge, bits, callback1k=marker)\n        return self._success({\n            'challenge': challenge,\n            'collision': collision\n        })\n\n\n_plugins.register_worker(SMTPWorker)\n_plugins.register_commands(HashCash)\n"
  },
  {
    "path": "mailpile/plugins/tags.py",
    "content": "# -*- coding: utf-8 -*-\nimport mailpile.security as security\nfrom mailpile.commands import Command\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.plugins.core import DeleteMessages\nfrom mailpile.urlmap import UrlMap\nfrom mailpile.util import *\n\nfrom mailpile.plugins.search import Search\n\n\n_plugins = PluginManager(builtin=__file__)\n\n\n##[ Configuration ]###########################################################\n\n\nFILTER_TYPES = ('user',      # These are the default, user-created filters\n                'incoming',  # These filters are only applied to new messages\n                'system',    # Mailpile core internal filters\n                'plugin')    # Filters created by plugins\n\n_plugins.register_config_section('tags', [\"Tags\", {\n    'name': ['Tag name', 'str', ''],\n    'slug': ['URL slug', 'slashslug', ''],\n\n    # Functional attributes\n    'type': ['Tag type', [\n        'tag', 'group', 'attribute', 'unread', 'inbox', 'search',\n        # Maybe TODO: 'folder', 'shadow',\n        'profile', 'mailbox',                         # Accounts, Mailboxes\n        'drafts', 'blank', 'outbox', 'sent',          # composing and sending\n        'replied', 'fwded', 'tagged', 'read', 'ham',  # behavior tracking tags\n        'trash', 'spam'                               # junk mail tags\n    ], 'tag'],\n    'flag_hides': ['Hide tagged messages from searches?', 'bool', False],\n    'flag_editable': ['Mark tagged messages as editable?', 'bool', False],\n    'flag_msg_only': ['Never apply to entire conversations', 'bool', False],\n    'flag_allow_add': ['Allow users to apply this tag', 'bool', True],\n    'flag_allow_del': ['Allow users to remove this tag', 'bool', True],\n\n    # Tag display attributes for /in/tag or searching in:tag\n    'template': ['Default tag display template', 'str', 'index'],\n    'search_terms': ['Terms to search for on /in/tag/', 'str', 'in:%(slug)s'],\n    'search_order': ['Default search order for /in/tag/', 'str', ''],\n    'magic_terms': ['Extra terms to search for', 'str', ''],\n\n    # Tag display attributes for search results/lists/UI placement\n    'icon': ['URL to default tag icon', 'str', 'icon-tag'],\n    'label': ['Display as label in results', 'bool', True],\n    'label_color': ['Color to use in label', 'str', '#4D4D4D'],\n    'toolbar': ['Display in selection toolbar', 'bool', False],\n    'display': ['Display context in UI', ['priority', 'tag', 'subtag',\n                                          'archive', 'invisible'], 'tag'],\n    'display_order': ['Order in lists', 'float', 0],\n    'parent': ['ID of parent tag, if any', 'str', ''],\n    'notify_new': ['Notify users about new messages', 'bool', False],\n\n    # Automation settings\n    'auto_after': ['After N days, perform automatic action', 'int', 0],\n    'auto_action': ['Action to perform', 'str', ''],\n    'auto_tag': ['Enable machine-learning for this tag', 'str', '']\n}, {}])\n\n_plugins.register_config_section('filters', [\"Filters\", {\n    'tags': ['Tag/untag actions', 'str', ''],\n    'terms': ['Search terms', 'str', ''],\n    'comment': ['Human readable description', 'str', ''],\n    'type': ['Filter type', FILTER_TYPES, FILTER_TYPES[0]],\n    'primary_tag': ['Tag dedicated to this filter', 'str', ''],\n}, {}])\n\n\ndef GetFilters(cfg, filter_on=None, types=FILTER_TYPES[:1]):\n    filters = cfg.filters.keys()\n    filters.sort(key=lambda k: int(k, 36))\n    flist = []\n    tset = set(types)\n    for fid in filters:\n        terms = cfg.filters[fid].get('terms', '')\n        ftype = cfg.filters[fid]['type']\n        if not (set([ftype, 'any', 'all', None]) & tset):\n            continue\n        if filter_on is not None and terms != filter_on:\n            continue\n        flist.append((fid, terms,\n                      cfg.filters[fid].get('tags', ''),\n                      cfg.filters[fid].get('comment', ''),\n                      ftype))\n    return flist\n\n\ndef FilterMove(cfg, filter_id, filter_new_id):\n    def swap(f1, f2):\n        tmp = cfg.filters[f1]\n        cfg.filters[f1] = cfg.filters[f2]\n        cfg.filters[f2] = tmp\n    ffrm = int(filter_id, 36)\n    fto = int(filter_new_id, 36)\n    if ffrm > fto:\n        for fid in reversed(range(fto, ffrm)):\n            swap(b36(fid + 1), b36(fid))\n    elif ffrm < fto:\n        for fid in range(ffrm, fto):\n            swap(b36(fid), b36(fid + 1))\n\n\ndef FilterDelete(cfg, *filter_ids):\n    filter_ids = list(filter_ids)\n    filter_ids.sort(key=lambda fid: int(fid, 36))\n    filters = cfg.filters\n    for fid in reversed(filter_ids):\n        lastid = b36(len(filters)-1).lower()\n        if fid <= lastid:\n            if lastid != fid:\n                cfg.filter_move(fid, lastid)\n            del filters[lastid]\n\n\ndef GetTags(cfg, tn=None, default=None, **kwargs):\n    results = []\n    tv = None\n    if tn is not None:\n        #\n        # Hack, allow the tn= to be any of: TID, name or slug.\n        #\n        # However, the most precise style of match wins - so TID lookups\n        # will never get confused by slugs, and slug lookups will never\n        # get confused by names.\n        #\n        tn = tn.lower()\n        try:\n            if tn in cfg.tags:\n                results.append([cfg.tags[tn]._key])\n        except (KeyError, IndexError, AttributeError):\n            pass\n        if not results:\n            tv = cfg.tags.values()\n            tags = ([t._key for t in tv if t.slug.lower() == tn] or\n                    [t._key for t in tv if t.name.lower() == tn])\n            results.append(tags)\n\n    if kwargs:\n        tv = tv or cfg.tags.values()\n        for kw in kwargs:\n            want = kwargs[kw]\n            if not isinstance(want, (list, tuple)):\n                want = [want]\n            want = [unicode(w).lower() for w in want]\n            if kw == 'tid':\n                results.append([str(k) for k in want if str(k) in cfg.tags])\n            else:\n                want = set(want)\n                if '*' in want:\n                    results.append(cfg.tags.keys())\n                else:\n                    results.append([t._key for t in tv\n                                    if (unicode(t[kw]).lower() in want)])\n\n    if (tn or kwargs) and not results:\n        return default\n    else:\n        tags = set(cfg.tags.keys())\n        for r in results:\n            tags &= set(r)\n        tags = [cfg.tags[t] for t in tags]\n        return tags\n\n\ndef GetTag(cfg, tn, default=None):\n    return (GetTags(cfg, tn, default=None) or [default])[0]\n\n\ndef GetTagID(cfg, tn):\n    tags = GetTags(cfg, tn=tn, default=[None])\n    return tags and (len(tags) == 1) and tags[0]._key or None\n\n\ndef GuessTags(cfg, name):\n    tags = set()\n    name = name.lower()\n    # When guessing tag/folder names not in localized language\n    # let's also try some common mappings/translations\n    # FIXME: decide how to tread 'trash' folders on servers\n    TYPICAL_FOLDER_NAMES = {u'gesendet': 'sent',\n                            u'gelöscht': 'trash',\n                            u'entwürfe': 'drafts',\n                            u'spamverdacht': 'spam',\n                            u'éléments envoyés': 'sent',\n                            u'éléments supprimés': 'trash',\n                            u'brouillons': 'drafts',\n    }\n    try:\n        tname = TYPICAL_FOLDER_NAMES[name]\n    except KeyError:\n        tname = ''\n    for tagtype in ('inbox', 'drafts', 'sent', 'spam'):\n        for tag in cfg.get_tags(type=tagtype):\n            if (name.endswith(tag.name.lower()) or\n                    name.endswith(_(tag.name).lower()) or\n                    tname.endswith(tag.name.lower())):\n                tags.add(tag._key)\n    return tags\n\n\ndef Slugify(tag_name, tags=None):\n    slug = CleanText(tag_name.lower().replace(' ', '-').replace('@', '-'),\n                     banned=CleanText.NONDNS.replace('/', '')\n                     ).clean.lower() or 'tag'\n    n = 1\n    while tags and slug in [t.slug for t in tags.values()]:\n        n += 1\n        slug = Slugify('%s.%s' % (tag_name or 'tag', n))\n    return slug\n\n\ndef GetTagInfo(cfg, tn, stats=False, unread=None, exclude=None, subtags=None):\n    tag = GetTag(cfg, tn)\n    tid = tag._key\n    info = {\n        'tid': tid,\n        'url': UrlMap(config=cfg).url_tag(tid),\n    }\n    for k in tag.all_keys():\n        if k in ('display_order', ):\n            if str(tag[k]) == 'nan':\n                tag[k] = 0.01 * len(info)\n        info[k] = tag[k]\n    if subtags:\n        info['subtag_ids'] = [t._key for t in subtags]\n    exclude = exclude or set()\n    if stats and (unread is not None):\n        messages = (cfg.index.TAGS.get(tid, set()) - exclude)\n        stats_all = len(messages)\n        info['name'] = _(info['name'])\n        info['stats'] = {\n            'all': stats_all,\n            'new': len(messages & unread),\n            'not': len(cfg.index.INDEX) - stats_all\n        }\n        if subtags:\n            for subtag in subtags:\n                messages |= cfg.index.TAGS.get(subtag._key, set())\n            info['stats'].update({\n                'sum_all': len(messages),\n                'sum_new': len(messages & unread),\n            })\n\n    return info\n\n\n# FIXME: This is dumb.\nimport mailpile.config.manager\nmailpile.config.manager.ConfigManager.get_tag = GetTag\nmailpile.config.manager.ConfigManager.get_tags = GetTags\nmailpile.config.manager.ConfigManager.get_tag_id = GetTagID\nmailpile.config.manager.ConfigManager.get_tag_info = GetTagInfo\nmailpile.config.manager.ConfigManager.guess_tags = GuessTags\nmailpile.config.manager.ConfigManager.get_filters = GetFilters\nmailpile.config.manager.ConfigManager.filter_move = FilterMove\nmailpile.config.manager.ConfigManager.filter_delete = FilterDelete\n\n\n##[ Commands ]################################################################\n\nclass TagCommand(Command):\n    def _reorder_all_tags(self):\n        taglist = [(t.display, t.display_order, t.slug, t._key)\n                   for t in self.session.config.tags.values()]\n        taglist.sort()\n        order = 1\n        for td, tdo, ts, tid in taglist:\n            self.session.config.tags[tid].display_order = order\n            order += 1\n\n    def finish(self, save=True):\n        if save:\n            self._background_save(config=True, index=True)\n        return True\n\n\nclass Tag(TagCommand):\n    \"\"\"Add or remove tags on a set of messages\"\"\"\n    SYNOPSIS = (None, 'tag', 'tag', '[--conversations|--messages|--force] '\n                                    '<[+|-]tags> <msgs>')\n    ORDER = ('Tagging', 0)\n    HTTP_CALLABLE = ('POST', )\n    HTTP_POST_VARS = {\n        'mid': 'message-ids',\n        'add': 'tags',\n        'del': 'tags',\n        'conversations': '[yes|no|auto]',\n        'context': 'search context, for tagging relative results',\n        'force': 'Force changes'\n    }\n    COMMAND_SECURITY = security.CC_TAG_EMAIL\n\n    class CommandResult(TagCommand.CommandResult):\n        def as_text(self):\n            if not self.result:\n                return 'Failed'\n            if not self.result['msg_ids']:\n                return 'Nothing happened'\n            what = []\n            if self.result['tagged']:\n                what.append('Tagged ' +\n                            ', '.join([k['name'] for k, ids\n                                       in self.result['tagged']]))\n            if self.result['untagged']:\n                what.append('Untagged ' +\n                            ', '.join([k['name'] for k, ids\n                                       in self.result['untagged']]))\n            count = len(self.result['msg_ids'])\n            whats = ', '.join(what)\n            convs = (_n('%d conversation', '%d conversation', count)\n                     if self.result.get('conversations') else\n                     _n('%d message', '%d messages', count)) % count\n            return '%s (%s)' % (whats, convs)\n\n    def _get_ops_and_msgids(self, words):\n        # If we are asked to both add and remove a tag, we do neither as\n        # that is nonsense without knowing the order of the operations.\n        deling = set(self.data.get('del', []))\n        adding = set(self.data.get('add', []))\n        ops = (['-%s' % t for t in (deling-adding) if t] +\n               ['+%s' % t for t in (adding-deling) if t])\n        force = truthy(self.data.get('force', ['no'])[0])\n        conversations = truthy(self.data.get('conversations', ['auto'])[0],\n                               special={'auto': None})\n        if 'mid' in self.data:\n            words = ['=%s' % m for m in self.data['mid']]\n        else:\n            while words and words[0][:1] in ('-', '+'):\n                op = words.pop(0)\n                if op in ('--conversations', '--messages'):\n                    conversations = True if (op[:3] == '--c') else False\n                elif op == '--force':\n                    force = True\n                else:\n                    ops.append(op)\n\n        # Allow ops that select tags by attribute instead of slug/id/name\n        expanded_ops = []\n        for op in ops:\n            if ':' in op:\n                sign = op[:1]\n                tvar, tval = op[1:].split(':', 1)\n                expanded_ops.extend('%s%s' % (sign, tag._key)\n                    for tag in self.session.config.get_tags(**{tvar: tval}))\n            else:\n                expanded_ops.append(op)\n\n        # Make op list unique and sort so removals happen first\n        expanded_ops = list(set(expanded_ops))\n        expanded_ops.sort(key=lambda k:\n            ({'-': 1, '+': 2}.get(k[:1], 8), k))\n\n        msg_ids = self._choose_messages(words)\n        return expanded_ops, msg_ids, conversations\n\n    def _do_tagging(self, ops, msg_ids, conversations,\n                    save=True, auto=False, force=False):\n        idx = self._idx()\n        rv = {\n            'conversations': False,\n            'msg_ids': [b36(i) for i in msg_ids],\n            'tagged': [],\n            'untagged': [],\n            'ignored': []\n        }\n\n        for op in ops:\n            tag = self.session.config.get_tag(op[1:])\n            if tag:\n                # FIXME: This should depend on more factors!\n                #    - Tags should have metadata about default scope\n                if conversations is None:\n                    conversation = ('flat' not in (self.session.order or ''))\n                    if (tag.flag_msg_only or\n                            tag.flag_editable or\n                            tag.type == 'attribute'):\n                        conversation = False\n                else:\n                    conversation = conversations\n                if conversation:\n                    rv['conversations'] = True\n\n                ignored = False\n                tag_id = tag._key\n                tag_cfg = tag\n                tag = tag.copy()\n                tag[\"tid\"] = tag_id\n                if op[0] == '-':\n                    if force or tag_cfg['flag_allow_del']:\n                        removed = idx.remove_tag(self.session, tag_id,\n                                                 msg_idxs=msg_ids,\n                                                 conversation=conversation)\n                        rv['untagged'].append(\n                            (tag, sorted([b36(i) for i in removed])))\n                    else:\n                        rv['ignored'].append((op, tag))\n                        ignored = True\n                else:\n                    if force or tag_cfg['flag_allow_add']:\n                        added = idx.add_tag(self.session, tag_id,\n                                            msg_idxs=msg_ids,\n                                            conversation=conversation)\n                        rv['tagged'].append(\n                            (tag, sorted([b36(i) for i in added])))\n                    else:\n                        rv['ignored'].append((op, tag))\n                        ignored = True\n\n                # Record behavior\n                if len(msg_ids) < 15 and not ignored:\n                    for t in self.session.config.get_tags(type='tagged',\n                                                          default=[]):\n                        idx.add_tag(self.session, t._key, msg_idxs=msg_ids)\n            else:\n                self.session.ui.warning('Unknown tag: %s' % op)\n\n\n        if rv['ignored'] and (len(rv['tagged']) == len(rv['untagged']) == 0):\n            self.event.private_data['ignored'] = rv['ignored']\n            return self._error(_('Nothing Happened'), result=rv)\n\n        if rv['conversations']:\n            undo_msg = _n('Untag %d conversation',\n                          'Untag %d conversations',\n                          len(msg_ids)) % len(msg_ids)\n            done_msg = _n('Tagged %d conversation',\n                          'Tagged %d conversations',\n                          len(msg_ids)) % len(msg_ids)\n        else:\n            undo_msg = _n('Untag %d message',\n                          'Untag %d messages', len(msg_ids)) % len(msg_ids)\n            done_msg = _n('Tagged %d message',\n                          'Tagged %d messages', len(msg_ids)) % len(msg_ids)\n\n        rv['undo_msg'] = undo_msg\n        self.event.data['undo'] = undo_msg\n        self.event.private_data['undo'] = {\n            'tagged': [[t['tid'], mids] for t, mids in rv['tagged']],\n            'untagged': [[t['tid'], mids] for t, mids in rv['untagged']],\n        }\n        if rv['ignored']:\n            self.event.private_data['ignored'] = rv['ignored']\n\n        self.finish(save=save)\n        return self._success(done_msg, rv)\n\n    @classmethod\n    def Undo(cls, undo, event):\n        idx = undo._idx()\n        rv = {\n            'tagged': [],\n            'untagged': []\n        }\n        for tid, msg_mids in event.private_data['undo']['tagged']:\n            removed = idx.remove_tag(undo.session, tid,\n                                     msg_idxs=[int(i, 36) for i in msg_mids],\n                                     conversation=False)\n            rv['untagged'].append((tid, sorted([b36(i) for i in removed])))\n\n        for tid, msg_mids in event.private_data['undo']['untagged']:\n            added = idx.add_tag(undo.session, tid,\n                                msg_idxs=[int(i, 36) for i in msg_mids],\n                                conversation=False)\n            rv['tagged'].append((tid, sorted([b36(i) for i in added])))\n        return undo._success(_('Undid tagging operation'), rv)\n\n    def command(self, **kwargs):\n        return self._do_tagging(*self._get_ops_and_msgids(list(self.args)),\n                                **kwargs)\n\n\n# FIXME\nclass TagLater(Tag):\n    \"\"\"Schedule a tag operation to happen later.\"\"\"\n    SYNOPSIS = (None, 'tag/later', 'tag/later', '<seconds> <[+|-]tags> <msgs>')\n\n    def command(self, **kwargs):\n        args = list(self.args)\n        seconds = args.pop(0)\n        ops, msg_ids, conversations = self._get_ops_and_msgids(args)\n        # FIXME: Schedule event!\n        return self._success(_('Scheduled %d messages for future tagging')\n                             % len(msg_ids), {\n            'msg_ids': [b36(i) for i in msg_ids],\n            'seconds': seconds\n        })\n\n\n# FIXME\nclass TagTemporarily(Tag):\n    \"\"\"Temporarily add or remove tags.\"\"\"\n    SYNOPSIS = (None, 'tag/tmp', 'tag/tmp', '<seconds> <[+|-]tags> <msgs>')\n\n    def command(self, **kwargs):\n        args = list(self.args)\n        seconds = args.pop(0)\n        rv = self._do_tagging(*self._get_ops_and_msgids(args), **kwargs)\n        # FIXME: Schedule undo event!\n        return rv\n\n\nclass AddTag(TagCommand):\n    \"\"\"Create a new tag\"\"\"\n    SYNOPSIS = (None, 'tags/add', 'tags/add', '<tag>')\n    ORDER = ('Tagging', 0)\n    HTTP_CALLABLE = ('GET', 'POST')\n    HTTP_POST_VARS = {\n        'name': 'tag name',\n        'slug': 'tag slug',\n        # Optional initial attributes of tags\n        'icon': 'icon-tag',\n        'type': 'Internal tag type',\n        'label': 'display as label in search results, or not',\n        'label_color': 'the color of the label',\n        'display': 'tag display type',\n        'toolbar': 'display in toolbar?',\n        'template': 'tag template type',\n        'search_terms': 'default search associated with this tag',\n        'magic_terms': 'magic search terms associated with this tag',\n        'parent': 'parent tag ID',\n        'auto_tag': 'enable or disable auto-tagging',\n        'auto_after': 'perform auto-action after this time',\n        'auto_action': 'auto-action to perform',\n    }\n    COMMAND_SECURITY = security.CC_CHANGE_TAGS\n\n    OPTIONAL_VARS = ['icon', 'type', 'parent',\n                     'display', 'toolbar', 'label', 'label_color',\n                     'template', 'search_terms', 'magic_terms',\n                     'auto_tag', 'auto_after', 'auto_action']\n\n    class CommandResult(TagCommand.CommandResult):\n        def as_text(self):\n            if not self.result:\n                return 'Failed'\n            if not self.result['added']:\n                return 'Nothing happened'\n            return ('Added tags: ' +\n                    ', '.join([k['name'] for k in self.result['added']]))\n\n    def command(self, save=True):\n        config = self.session.config\n\n        if self.data.get('_method', 'not-http').upper() == 'GET':\n            return self._success(_('Add tags here!'), {\n                'form': self.HTTP_POST_VARS,\n                'rules': self.session.config.tags.rules['_any'][1]\n            })\n\n        # Check arguments/POST data, and make sure we have matching numbers\n        # of names and slugs for the tags we're about to create.\n        slugs = self.data.get('slug', [])\n        names = self.data.get('name', [])\n        if slugs and len(names) != len(slugs):\n            return self._error('Name/slug pairs do not match')\n        elif names and not slugs:\n            slugs = [Slugify(n, config.tags) for n in names]\n        # This adds CLI-style arguments to the list\n        slugs.extend([Slugify(s, config.tags) for s in self.args])\n        names.extend(self.args)\n\n        # Check Slug is valid\n        for slug in slugs:\n            if slug != Slugify(slug, config.tags):\n                return self._error('Invalid tag slug: %s' % slug)\n\n        # Check Tag is unique\n        for tag in config.tags.values():\n            if tag.slug in slugs:\n                return self._error('Tag already exists: %s/%s' % (tag.slug,\n                                                                  tag.name))\n\n        tags = [\n            {'name': (n or _('New Tag')), 'slug': (s or 'tag')}\n            for (n, s) in zip(names, slugs)]\n        for v in self.OPTIONAL_VARS:\n            for i in range(0, len(tags)):\n                vlist = self.data.get(v, [])\n                if len(vlist) > i and vlist[i]:\n                    tags[i][v] = vlist[i]\n        if tags:\n            # Add Tag to config\n            config.tags.extend(tags)\n            if save:\n                self._reorder_all_tags()\n            self.finish(save=save)\n\n        # Get full Tag objects of added tags to return\n        results = []\n        for tag in tags:\n            results.append(GetTagInfo(self.session.config, tag['slug']))\n\n        # Return success\n        return self._success(_('Added %d tags') % len(results),\n                             {'added': results})\n\n\nclass ListTags(TagCommand):\n    \"\"\"List tags\"\"\"\n    SYNOPSIS = (None, 'tags', 'tags', '[<wanted>|!<wanted>] [...]')\n    ORDER = ('Tagging', 0)\n    HTTP_STRICT_VARS = False\n    COMMAND_CACHE_TTL = 3600\n    LOG_NOTHING = True  # Avoid gunking up the event log with Boring Stuff\n\n    def cache_requirements(self, result):\n        if result:\n            return set([u'!config'] +\n                       [u'%s:in' % ti['slug'] for ti in result.result['tags']])\n        else:\n            return set([u'!config'])\n\n    class CommandResult(TagCommand.CommandResult):\n        def as_text(self):\n            if not self.result:\n                return 'Failed'\n            tags = self.result['tags']\n            wrap = int(min(23*5, (self.session.ui.term.max_width-1)) / 23)\n            text = []\n            for i in range(0, len(tags)):\n                stats = tags[i]['stats']\n                text.append(('%s%5.5s %-18.18s'\n                             ) % ((i % wrap) == 0 and '  ' or '',\n                                  '%s' % (stats.get('sum_new', stats['new'])\n                                          or ''),\n                                  tags[i]['name'])\n                            + ((i % wrap) == (wrap - 1) and '\\n' or ''))\n            return ''.join(text) + '\\n'\n\n    def command(self, **kwargs):\n        result, idx = [], self._idx()\n\n        args = []\n        search = kwargs\n        for arg in self.args:\n            if '=' in arg:\n                kw, val = arg.split('=', 1)\n                search[kw.strip()] = val.strip()\n            else:\n                args.append(arg)\n        for kw in self.data:\n            if kw in ('tid', 'mode') or kw in self.session.config.tags.rules:\n                search[kw] = self.data[kw]\n            elif kw not in ('only', 'not', '_method', '_recursion', 'context'):\n                from mailpile.urlmap import BadDataError\n                raise BadDataError('Bad variable (%s)' % (kw))\n\n        wanted = [t.lower() for t in args if not t.startswith('!')]\n        unwanted = [t[1:].lower() for t in args if t.startswith('!')]\n        wanted.extend([t.lower() for t in self.data.get('only', [])])\n        unwanted.extend([t.lower() for t in self.data.get('not', [])])\n\n        unread_messages = set()\n        for tag in self.session.config.get_tags(type='unread', default=[]):\n            unread_messages |= idx.TAGS.get(tag._key, set())\n\n        excluded_messages = set()\n        for tag in self.session.config.get_tags(flag_hides=True, default=[]):\n            excluded_messages |= idx.TAGS.get(tag._key, set())\n\n        mode = search.get('mode', 'default')\n        if 'mode' in search:\n            del search['mode']\n\n        search['default'] = []\n        for tag in self.session.config.get_tags(**search):\n            if wanted and tag.slug.lower() not in wanted:\n                continue\n            if unwanted and tag.slug.lower() in unwanted:\n                continue\n            if mode == 'tree' and tag.parent and not wanted:\n                continue\n\n            # Hide invisible tags by default, any search terms at all will\n            # disable this behavior\n            if (not wanted and not unwanted and not search\n                    and tag.display == 'invisible'):\n                continue\n\n            recursion = self.data.get('_recursion', 0)\n            tid = tag._key\n\n            # List subtags...\n            if recursion == 0:\n                subtags = self.session.config.get_tags(parent=tid)\n                subtags.sort(key=lambda k: k.get('slug', 'zzzz'))\n            else:\n                subtags = None\n\n            # Get tag info (how depends on whether this is a hiding tag)\n            if tag.flag_hides:\n                info = GetTagInfo(self.session.config, tid, stats=True,\n                                  unread=unread_messages,\n                                  subtags=subtags)\n            else:\n                info = GetTagInfo(self.session.config, tid, stats=True,\n                                  unread=unread_messages,\n                                  exclude=excluded_messages,\n                                  subtags=subtags)\n\n            # This expands out the full tree\n            if subtags and recursion == 0:\n                if mode in ('both', 'tree') or (wanted and mode != 'flat'):\n                    info['subtags'] = ListTags(self.session,\n                                               arg=[t.slug for t in subtags],\n                                               data={'_recursion': 1}\n                                               ).run().result['tags']\n\n            result.append(info)\n        result.sort(key=lambda k: (float(k.get('display_order', 0)),\n                                         k.get('slug', 'zzz')))\n        return self._success(_('Listed %d tags') % len(result), {\n            'search': search,\n            'wanted': wanted,\n            'unwanted': unwanted,\n            'tags': result\n        })\n\n\nclass DeleteTag(TagCommand):\n    \"\"\"Delete a tag\"\"\"\n    SYNOPSIS = (None, 'tags/delete', 'tags/delete', '<tag>')\n    ORDER = ('Tagging', 0)\n    HTTP_CALLABLE = ('POST', 'DELETE')\n    HTTP_POST_VARS = {\n        \"tag\" : \"tag(s) to delete\"\n    }\n    COMMAND_SECURITY = security.CC_CHANGE_TAGS\n\n    class CommandResult(TagCommand.CommandResult):\n        def as_text(self):\n            if not self.result:\n                return 'Failed'\n            if not self.result['removed']:\n                return 'Nothing happened'\n            return ('Removed tags: ' +\n                    ', '.join([k['name'] for k in self.result['removed']]))\n\n    def command(self):\n        session, config = self.session, self.session.config\n        clean_session = mailpile.ui.Session(config)\n        clean_session.ui = session.ui\n        clean_session.order = 'all-flat'\n        result = []\n\n        tag_names = []\n        if self.args:\n            tag_names = list(self.args)\n        elif self.data.get('tag', []):\n            tag_names = self.data.get('tag', [])\n\n        for tag_name in tag_names:\n\n            tag = config.get_tag(tag_name)\n\n            if tag:\n                tag_id = tag._key\n\n                # FIXME: Refuse to delete tag if in use by filters\n\n                msgs = set(config.index.TAGS.get(tag_id, set()))\n                rem = config.index.remove_tag(clean_session, tag_id,\n                                              msg_idxs=msgs)\n                if not config.index.TAGS.get(tag_id, set()):\n                    del config.tags[tag_id]\n                    result.append({'name': tag.name, 'tid': tag_id})\n                else:\n                    raise Exception('That failed: %s' % rv)\n            else:\n                self._error('No such tag %s' % tag_name)\n        if result:\n            self._reorder_all_tags()\n            self.finish(save=True)\n        return self._success(_('Deleted %d tags') % len(result),\n                             {'removed': result})\n\n\nclass TagAutomation(Command):\n    \"\"\"Perform automatically scheduled tasks for one or more tags\"\"\"\n    SYNOPSIS = (None, 'tags/auto', 'tags/auto', '[--force|--test] <-all|tags ...>')\n    ORDER = ('Tagging', 9)\n    HTTP_CALLABLE = ('POST', )\n    HTTP_POST_VARS = {}\n\n    def _tags(self, args):\n        tags = self.session.config.tags\n        if '-all' in args:\n            for tag in tags.values():\n                yield tag\n        for tid, tag in tags.iteritems():\n            if tid in args or tag.slug in args:\n                yield tag\n\n    def _perform_auto_action(self, session, tag, dry_run=False):\n        action = tag.auto_action\n        if action == '!delete':\n            if not dry_run:\n                return DeleteMessages(session, arg=['all']).run()\n\n        elif action == '!untag':\n            action = '-%s' % tag._key\n\n        if not dry_run:\n            return Tag(session, arg=action.split() + ['all']).run()\n        else:\n            return {\n                 'tag': tag.slug,\n                 'should': action,\n                 'messages': session.results}\n\n    def command(self):\n        session, config = self.session, self.session.config\n        args = list(self.args)\n\n        force = ('-force' in args) or ('--force' in args)\n        dry_run = ('-test' in args) or ('--test' in args)\n\n        today = time.time() // (24 * 3600)\n        results = []\n        for tag in self._tags(args):\n            if tag.auto_after == 0 or not tag.auto_action:\n                continue\n\n            clean_session = mailpile.ui.Session(config)\n            clean_session.ui = session.ui\n            search_terms = ['in:%s' % tag.slug]\n            if not force:\n                if tag.auto_after < 0:\n                    # If auto_after is negative, use a more conservative\n                    # (and error-prone) approach. This will only act on\n                    # messages that exactly match; if the app is off for\n                    # a day, messages will never get processed at all.\n                    search_terms.append('u:%x' % (today + tag.auto_after))\n                else:\n                    search_terms.extend(['-u:%x' % (today - n)\n                                         for n in range(0, tag.auto_after)])\n\n            Search(clean_session, arg=search_terms).run()\n            if clean_session.results or dry_run:\n                results.append(self._perform_auto_action(clean_session, tag,\n                                                         dry_run=dry_run))\n\n        return self._success(\n            _('Performed automation for %d tags') % len(results),\n            {'results': results})\n\n    @classmethod\n    def run_in_background(cls, session):\n        result = cls(session, arg=['-all']).run()\n        return True\n\n\n_plugins.register_config_variables('prefs', {\n    'tag_automation_interval': [\n        _('Periodically perform tag automation (seconds)'), int, 8*60*60]})\n\n_plugins.register_slow_periodic_job(\n     'tag_automation',\n     'prefs.tag_automation_interval',\n     TagAutomation.run_in_background)\n\n\nclass FilterCommand(Command):\n    def finish(self, save=True):\n        self._background_save(config=True, index=True)\n        return True\n\n\nclass Filter(FilterCommand):\n    \"\"\"Add auto-tag rule for current search or terms\"\"\"\n    SYNOPSIS = (None, 'filter', 'filter', '[new|read] [notag|maketag] [=<mid>] '\n                                          '[<terms>] [+<tag>] [-<tag>] '\n                                          '[<comment>]')\n    ORDER = ('Tagging', 1)\n    HTTP_CALLABLE = ('POST', )\n    HTTP_POST_VARS = {\n        'comment': '...',\n        'terms': '...',\n        'add-tag': 'tag,tag,tag,... or !CREATE',\n        'del-tag': 'tag,tag,tag,... ',\n        'mark-read': 'yes or no',\n        'skip-inbox': 'yes or no',\n        'never-spam': 'yes or no',\n        'create-tag': 'yes or no',\n        'tag-icon': 'icon',\n        'tag-color': 'color',\n        'replace': 'filter ID'\n    }\n    COMMAND_SECURITY = security.CC_CHANGE_FILTERS\n\n    def _truthy(self, var):\n        return truthy(self.data.get(var, ['no'])[0])\n\n    def command(self, save=True):\n        session, config = self.session, self.session.config\n        args = list(self.args)\n\n        flags = []\n        while args and args[0] in ('add', 'set', 'new', 'read',\n                                   'notag', 'maketag'):\n            flags.append(args.pop(0))\n        if self._truthy('create-tag'):\n            flags.append('maketag')\n\n        if args and args[0][:1] == '=':\n            filter_id = args.pop(0)[1:]\n        else:\n            filter_id = self.data.get('replace', [None])[0] or None\n\n        if args and args[0][:1] == '@':\n            filter_type = args.pop(0)[1:]\n        else:\n            filter_type = FILTER_TYPES[0]\n\n        # Convert HTTP variable tag ops...\n        for tag in self.data.get('add-tag', []):\n            args.append('+%s' % tag)\n        for tag in self.data.get('del-tag', []):\n            args.append('-%s' % tag)\n        if self._truthy('mark-read'):\n            args.append('-new')\n        if self._truthy('skip-inbox'):\n            args.append('-inbox')\n        if self._truthy('never-spam'):\n            args.append('-spam')\n\n        auto_tag = False\n        if 'read' in flags:\n            terms = ['@read']\n        elif 'new' in flags:\n            terms = ['*']\n        elif self.data.get('terms', [''])[0]:\n            terms = self.data['terms'][0].strip().split()\n            auto_tag = True\n        elif args and args[0][:1] not in ('-', '+'):\n            terms = []\n            while args and args[0][0] not in ('-', '+'):\n                terms.append(args.pop(0))\n        else:\n            terms = session.searched\n            auto_tag = True\n\n        tag_ops = []\n        while args and args[0][0] in ('-', '+'):\n            tag_ops.append(args.pop(0))\n\n        comment = self.data.get('comment', [None])[0] or ' '.join(args)\n\n        if filter_id:\n            primary_tag = config.filters[filter_id].primary_tag or None\n        else:\n            primary_tag = None\n\n        if primary_tag is None and 'maketag' in flags:\n            if not comment:\n                raise UsageError(_('Need tag name'))\n            result = AddTag(session, arg=[comment]).run(save=False).result\n            primary_tag = result['added'][0]['tid']\n\n        if not terms or (len(tag_ops) < 1):\n            raise UsageError(_('Need flags and search terms or a hook'))\n\n        tags, tids = [], []\n        for tag in tag_ops:\n            if tag[1:] == '!PRIMARY':\n                tid = primary_tag\n                tag = tag[0] + tid\n            else:\n                tid = config.get_tag_id(tag[1:])\n            if tid is not None:\n                tags.append(tag)\n                tids.append(tag[0] + tid)\n            else:\n                raise UsageError(_('No such tag: %s') % tag)\n\n        if not args:\n            args = ['Filter for %s' % ' '.join(tags)]\n\n        if auto_tag and 'notag' not in flags:\n            if not Tag(session, arg=tags + ['all']).run(save=False):\n                raise UsageError()\n\n        filter_dict = {\n            'primary_tag': primary_tag,\n            'comment': comment,\n            'terms': ' '.join(terms),\n            'tags': ' '.join(tids),\n            'type': filter_type\n        }\n        if filter_id:\n            config.filters[filter_id] = filter_dict\n        else:\n            filter_id = config.filters.append(filter_dict)\n\n        if 'maketag' in flags and primary_tag and primary_tag in config.tags:\n            tag_icon = self.data.get('tag-icon', [None])[0]\n            tag_color = self.data.get('tag-color', [None])[0]\n            if tag_icon:\n                config.tags[primary_tag].icon = tag_icon\n            if tag_color:\n                config.tags[primary_tag].label_color = tag_color\n            config.tags[primary_tag].name = comment\n            config.tags[primary_tag].slug = 'saved-search-%s' % filter_id\n\n        self.finish(save=save)\n\n        filter_dict['id'] = filter_id\n        return self._success(_('Added new filter'), result=filter_dict)\n\n\nclass DeleteFilter(FilterCommand):\n    \"\"\"Delete an auto-tagging rule\"\"\"\n    SYNOPSIS = (None, 'filter/delete', None, '<filter-id>')\n    ORDER = ('Tagging', 1)\n    HTTP_CALLABLE = ('POST', 'DELETE')\n    COMMAND_SECURITY = security.CC_CHANGE_FILTERS\n\n    def command(self):\n        session, config = self.session, self.session.config\n        if len(self.args) < 1:\n            raise UsageError('Delete what?')\n\n        args = list(self.args)\n        args.sort(key=lambda fid: int(fid, 36))\n\n        filter_keys = config.get('filters', {}).keys()\n        removed = 0\n        for fid in reversed(args):\n            if fid in filter_keys:\n                self.session.config.filter_delete(fid)\n                removed += 1\n            else:\n                session.ui.warning('Failed to remove %s' % fid)\n        if removed:\n            self.finish()\n\n        return self._success(_('Removed %d filter(s)') % removed)\n\n\nclass ListFilters(Command):\n    \"\"\"List (all) auto-tagging rules\"\"\"\n    SYNOPSIS = (None, 'filter/list', 'filter/list', '[<search>|=<id>|@<type>]')\n    ORDER = ('Tagging', 1)\n    HTTP_CALLABLE = ('GET', 'POST')\n    HTTP_QUERY_VARS = {\n        'search': 'Text to search for',\n        'id': 'Filter ID',\n        'type': 'Filter type'\n    }\n\n    class CommandResult(Command.CommandResult):\n        def as_text(self):\n            if self.result is False:\n                return unicode(self.result)\n            return '\\n'.join([('%3.3s %-10s %-18s %-18s %s'\n                               ) % (r['fid'], r['type'],\n                                    r['terms'], r['human_tags'], r['comment'])\n                              for r in self.result])\n\n    def command(self, want_fid=None):\n        results = []\n        for (fid, trms, tags, cmnt, ftype\n             ) in self.session.config.get_filters(filter_on=None,\n                                                  types=['all']):\n            if want_fid and fid != want_fid:\n                continue\n\n            human_tags = []\n            for tterm in tags.split():\n                tagname = self.session.config.tags.get(\n                    tterm[1:], {}).get('slug', '(None)')\n                human_tags.append('%s%s' % (tterm[0], tagname))\n\n            skip = False\n            args = list(self.args)\n            args.extend([t for t in self.data.get('search', [])])\n            args.extend(['='+t for t in self.data.get('id', [])])\n            args.extend(['@'+t for t in self.data.get('type', [])])\n            if args and not want_fid:\n                for term in args:\n                    term = term.lower()\n                    if term.startswith('='):\n                        if (term[1:] != fid):\n                            skip = True\n                    elif term.startswith('@'):\n                        if (term[1:] != ftype):\n                            skip = True\n                    elif ((term not in ' '.join(human_tags).lower())\n                            and (term not in trms.lower())\n                            and (term not in cmnt.lower())):\n                        skip = True\n            if skip:\n                continue\n\n            results.append({\n                'fid': fid,\n                'terms': trms,\n                'tags': tags,\n                'human_tags': ' '.join(human_tags),\n                'comment': cmnt,\n                'type': ftype\n            })\n        return results\n\n\nclass MoveFilter(ListFilters):\n    \"\"\"Move an auto-tagging rule\"\"\"\n    SYNOPSIS = (None, 'filter/move', None, '<filter-id> <position>')\n    ORDER = ('Tagging', 1)\n    HTTP_CALLABLE = ('POST', 'UPDATE')\n    COMMAND_SECURITY = security.CC_CHANGE_FILTERS\n\n    def command(self):\n        self.session.config.filter_move(self.args[0], self.args[1])\n        self._background_save(config=True)\n        return ListFilters.command(self, want_fid=self.args[1])\n\n\n_plugins.register_commands(Tag, TagAutomation, # TagLater, TagTemporarily,\n                           AddTag, DeleteTag, ListTags,\n                           Filter, DeleteFilter,\n                           MoveFilter, ListFilters)\n"
  },
  {
    "path": "mailpile/plugins/vcard_carddav.py",
    "content": "#coding:utf-8\nimport base64\nimport httplib\nimport sys\nimport re\nimport getopt\nfrom lxml import etree\n\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.vcard import *\nfrom mailpile.util import *\n\n\n_plugins = PluginManager(builtin=__file__)\n\n\nclass DAVClient:\n    def __init__(self, host,\n                 port=None, username=None, password=None, protocol='https'):\n        if not port:\n            if protocol == 'https':\n                port = 443\n            elif protocol == 'http':\n                port = 80\n            else:\n                raise Exception(\"Can't determine port from protocol. \"\n                                \"Please specifiy a port.\")\n        self.cwd = \"/\"\n        self.baseurl = \"%s://%s:%d\" % (protocol, host, port)\n        self.host = host\n        self.port = port\n        self.protocol = protocol\n        self.username = username\n        self.password = password\n        if username and password:\n            self.auth = base64.encodestring('%s:%s' % (username, password)\n                                            ).replace('\\n', '')\n        else:\n            self.auth = None\n\n    def request(self, url, method, headers={}, body=\"\"):\n        if self.protocol == \"https\":\n            req = httplib.HTTPSConnection(self.host, self.port)\n            # FIXME: Verify HTTPS certificate\n        else:\n            req = httplib.HTTPConnection(self.host, self.port)\n\n        req.putrequest(method, url)\n        req.putheader(\"Host\", self.host)\n        req.putheader(\"User-Agent\", \"Mailpile\")\n        if self.auth:\n            req.putheader(\"Authorization\", \"Basic %s\" % self.auth)\n\n        for key, value in headers.iteritems():\n            req.putheader(key, value)\n\n        req.endheaders()\n        req.send(body)\n        res = req.getresponse()\n\n        self.last_status = res.status\n        self.last_statusmessage = res.reason\n        self.last_headers = dict(res.getheaders())\n        self.last_body = res.read()\n\n        if self.last_status >= 300:\n            raise Exception((\"HTTP %d: %s\\n(%s %s)\\n>>>%s<<<\"\n                             ) % (self.last_status, self.last_statusmessage,\n                                  method, url, self.last_body))\n        return (self.last_status, self.last_statusmessage,\n                self.last_headers, self.last_body)\n\n    def options(self, url):\n        status, msg, header, resbody = self.request(url, \"OPTIONS\")\n        return header[\"allow\"].split(\", \")\n\n\nclass CardDAV(DAVClient):\n    def __init__(self, host, url,\n                 port=None, username=None, password=None, protocol='https'):\n        DAVClient.__init__(self, host, port, username, password, protocol)\n        self.url = url\n\n        if not self._check_capability():\n            raise Exception(\"No CardDAV support on server\")\n\n    def cd(self, url):\n        self.url = url\n\n    def _check_capability(self):\n        result = self.options(self.url)\n        return \"addressbook\" in self.last_headers[\"dav\"].split(\", \")\n\n    def get_vcard(self, url):\n        status, msg, header, resbody = self.request(url, \"GET\")\n        card = MailpileVCard()\n        card.load(data=resbody)\n        return card\n\n    def put_vcard(self, url, vcard):\n        raise Exception('Unimplemented')\n\n    def list_vcards(self):\n        stat, msg, hdr, resbody = self.request(self.url, \"PROPFIND\", {}, {})\n        tr = etree.fromstring(resbody)\n        urls = [x.text for x in tr.xpath(\"/d:multistatus/d:response/d:href\",\n                                         namespaces={\"d\": \"DAV:\"})\n                if x.text not in (\"\", None) and x.text[-3:] == \"vcf\"]\n        return urls\n\n\nclass CardDAVImporter(VCardImporter):\n    REQUIRED_PARAMETERS = [\"host\", \"url\"]\n    OPTIONAL_PARAMETERS = [\"port\", \"username\", \"password\", \"protocol\"]\n    FORMAT_NAME = \"CardDAV Server\"\n    FORMAT_DESCRIPTION = \"CardDAV HTTP contact server.\"\n    SHORT_NAME = \"carddav\"\n    CONFIG_RULES = {\n        'host': ('Host name', 'hostname', None),\n        'port': ('Port number', int, None),\n        'url': ('CardDAV URL', 'url', None),\n        'protcol': ('Connection protocol', 'string', 'https'),\n        'password': ('CardDAV URL', 'url', None),\n        'username': ('CardDAV URL', 'url', None)\n    }\n\n    def get_contacts(self):\n        self.carddav = CardDAV(host, url, port, username, password, protocol)\n        results = []\n        cards = self.carddav.list_vcards()\n        for card in cards:\n            results.append(self.carddav.get_vcard(card))\n\n        return results\n\n    def filter_contacts(self, terms):\n        pass\n\n\n_plugins.register_vcard_importers(CardDAVImporter)\n"
  },
  {
    "path": "mailpile/plugins/vcard_gnupg.py",
    "content": "#coding:utf-8\nimport os\n\nimport mailpile.security as security\nfrom mailpile.commands import Command\nfrom mailpile.eventlog import GetThreadEvent\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.crypto.gpgi import GnuPG\nfrom mailpile.vcard import *\n\n\n_plugins = PluginManager(builtin=__file__)\n\n\n# User default GnuPG key file\nDEF_GNUPG_HOME = os.path.expanduser('~/.gnupg')\n\n\nclass GnuPGImporter(VCardImporter):\n    FORMAT_NAME = 'GnuPG'\n    FORMAT_DESCRIPTION = _('Import contacts from GnuPG keyring')\n    SHORT_NAME = 'gpg'\n    CONFIG_RULES = {\n        'active': [_('Enable this importer'), bool, True],\n        'gpg_home': [_('Location of keyring'), 'str', DEF_GNUPG_HOME],\n    }\n    VCL_KEY_FMT = 'data:application/x-pgp-fingerprint,%s'\n\n    # Merge by own identifier, email or key (in that order)\n    MERGE_BY = ['x-gpg-mrgid', 'email']\n\n    # Update index's email->name mapping\n    UPDATE_INDEX = True\n\n    def get_guid(self, vcard):\n        return '%s/%s' % (self.config.guid, vcard.get('x-gpg-mrgid').value)\n\n    def import_vcards(self, session, vcard_store, *args, **kwargs):\n        kwargs['vcards'] = vcard_store\n        return VCardImporter.import_vcards(self, session, vcard_store,\n                                           *args, **kwargs)\n\n    def get_vcards(self,\n                   selectors=None, public=True, secret=True, vcards=None,\n                   event=None):\n        if not self.config.active:\n            return []\n\n        # Event magic\n        event = event or GetThreadEvent()\n\n        # Generate all the nice new cards!\n        new_cards = self.gnupg_keys_as_vcards(GnuPG(self.session.config,\n                                                    event=event),\n                                              selectors=selectors,\n                                              public=public,\n                                              secret=secret)\n\n        # Generate tombstones for keys which are gone from the keyring.\n        if vcards:\n            deleted = set()\n            deleted_names = {}\n            search = ';%s/' % self.config.guid\n            for cardid, vcard in (vcards or {}).iteritems():\n                for vcl in vcard.get_all('clientpidmap'):\n                    if search in vcl.value:\n                        key_id = vcl.value.split(';')[1]\n                        deleted.add(key_id)\n                        deleted_names[key_id] = vcard.fn\n            if selectors:\n                deleted = set([guid for guid in deleted\n                               if guid.split('/')[-1] in selectors])\n            deleted -= set([self.get_guid(card) for card in new_cards])\n            for guid in deleted:\n                mrgid = guid.split('/')[-1]\n                fn = deleted_names[guid]\n                new_cards.append(MailpileVCard(VCardLine(name='fn', value=fn),\n                                               VCardLine(name='x-gpg-mrgid',\n                                                         value=mrgid)))\n\n        return new_cards\n\n    @classmethod\n    def key_is_useless(cls, key):\n        return (key.get(\"disabled\") or key.get(\"expired\") or key.get(\"revoked\") or\n                not key[\"capabilities_map\"].get(\"encrypt\") or\n                not key[\"capabilities_map\"].get(\"sign\"))\n\n    @classmethod\n    def key_vcl(cls, key_id, key):\n        attrs = {}\n        for a in ('keysize', 'creation_date',\n                  'expiration_date', 'keytype_name'):\n            if key.get(a):\n                attrs[a.split('_')[0]] = key[a]\n        return VCardLine(name='KEY',\n                         value=cls.VCL_KEY_FMT % key_id,\n                         **attrs)\n\n    @classmethod\n    def vcards_one_per_uid(cls, keys, vcards, kindhint=None):\n        \"\"\"This creates one VCard per e-mail address found in UIDs\"\"\"\n        new_vcards = []\n        for key_id, key in keys.iteritems():\n            if cls.key_is_useless(key):\n                continue\n            for uid in key.get('uids', []):\n                email = uid.get('email')\n                if email:\n                    vcls = [cls.key_vcl(key_id, key)]\n                    if uid.get('name'):\n                        vcls.append(VCardLine(name='fn', value=uid['name']))\n                    if kindhint:\n                        vcls += [VCardLine(name='x-mailpile-kind-hint',\n                                           value=kindhint)]\n                    card = vcards.get(email)\n                    if card:\n                        card.add(*vcls)\n                    else:\n                        vcls += [VCardLine(name='x-gpg-mrgid', value=email),\n                                 VCardLine(name='email', value=email)]\n                        vcards[email] = card = MailpileVCard(*vcls)\n                        new_vcards.append(card)\n        return new_vcards\n\n    @classmethod\n    def vcards_per_key(cls, keys, vcards):\n        \"\"\"This creates on VCards per key\"\"\"\n        new_vcards = []\n        for key_id, key in keys.iteritems():\n            if cls.key_is_useless(key):\n                continue\n            vcls = [cls.key_vcl(key_id, key)]\n            emails = []\n            for uid in key.get('uids', []):\n                if uid.get('email'):\n                    vcls.append(VCardLine(name='email', value=uid['email']))\n                    emails.append(uid['email'])\n                if uid.get('name'):\n                    name = uid['name']\n                    vcls.append(VCardLine(name='fn', value=name))\n            if emails:\n                # This is us taking care to only create one card for each\n                # set of e-mail addresses.\n                card = MailpileVCard(*vcls)\n                card.add(VCardLine(name='x-gpg-mrgid', value=key_id))\n                for email in emails:\n                    if email not in vcards:\n                        vcards[email] = card\n                new_vcards.append(card)\n        return new_vcards\n\n    @classmethod\n    def vcards_merged(cls, keys, vcards):\n        \"\"\"This creates merged VCards, grouping by uid/e-mail and key\"\"\"\n        new_vcards = []\n        for key_id, key in keys.iteritems():\n            if cls.key_is_useless(key):\n                continue\n            vcls = [cls.key_vcl(key_id, key)]\n            card = None\n            emails = []\n            for uid in key.get('uids', []):\n                if uid.get('email'):\n                    vcls.append(VCardLine(name='email', value=uid['email']))\n                    card = card or vcards.get(uid['email'])\n                    emails.append(uid['email'])\n                if uid.get('name'):\n                    name = uid['name']\n                    vcls.append(VCardLine(name='fn', value=name))\n            if card and emails:\n                card.add(*vcls)\n            elif emails:\n                # This is us taking care to only create one card for each\n                # set of e-mail addresses.\n                card = MailpileVCard(*vcls)\n                card.add(VCardLine(name='x-gpg-mrgid', value=key_id))\n                for email in emails:\n                    vcards[email] = card\n                new_vcards.append(card)\n        return new_vcards\n\n    @classmethod\n    def gnupg_keys_as_vcards(cls, gnupg,\n                             selectors=None, public=True, secret=True):\n        results = []\n        vcards = {}\n\n        # Secret keys first, as they'll probably all show up on the public\n        # list as well and we want to be done handling them here.\n        secret_keys = gnupg.list_secret_keys(selectors=selectors)\n        if secret:\n            results += cls.vcards_one_per_uid(secret_keys, vcards,\n                                              kindhint='profile')\n\n        if public:\n            keys = gnupg.list_keys(selectors=selectors)\n            for key in secret_keys:\n                if key in keys:\n                    del keys[key]\n            results += cls.vcards_per_key(keys, vcards)\n\n        # Set ranking markers on the best/newest key\n        for card in results:\n            keylines = card.get_all('key')\n            if len(keylines) > 1:\n                keylines.sort(key=lambda k: (k.get('keysize', '0000'),\n                                             k.get('creation', '1970-01-01')))\n                for idx, kl in enumerate(keylines):\n                    keylines[idx].set_attr('x-rank', 2*(idx+1))\n\n        return results\n\n\nclass PGPKeysAsVCards(Command):\n    \"\"\"PGP keys as VCards (keychain import logic)\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/gpg/vcards', 'crypto/gpg/vcards', '<selectors>')\n    HTTP_CALLABLE = ('GET', )\n    HTTP_QUERY_VARS = {\n        'q': 'selectors',\n        'no_public': 'omit public keys',\n        'no_secret': 'omit secret keys'\n    }\n\n    class CommandResult(Command.CommandResult):\n        def as_text(self):\n            if self.result:\n                return ('\\n\\n'.join(vc.as_vCard() for vc in self.result)\n                        ).decode('utf-8')\n            else:\n                return _(\"No results\")\n\n    def command(self):\n        selectors = [a for a in self.args if not a.startswith('-')]\n        selectors.extend(self.data.get('q', []))\n\n        public = not (int(self.data.get('no_public', [0])[0]) or\n                      '-no_public' in self.args)\n        secret = not (int(self.data.get('no_secret', [0])[0]) or\n                      '-no_secret' in self.args)\n\n        vcards = GnuPGImporter.gnupg_keys_as_vcards(\n            self._gnupg(),\n            selectors=selectors,\n            public=public,\n            secret=secret)\n\n        return self._success(_('Extracted %d vCards from GPG keychain'\n                               ) % len(vcards), vcards)\n\n\nclass PGPKeysImportAsVCards(Command):\n    \"\"\"Import PGP keys as VCards\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'crypto/gpg/vcardimport',\n                      'crypto/gpg/vcardimport', '<selectors>')\n    HTTP_CALLABLE = ('GET', )\n    HTTP_QUERY_VARS = {\n        'q': 'selectors',\n        'no_public': 'omit public keys',\n        'no_secret': 'omit secret keys'\n    }\n    COMMAND_SECURITY = security.CC_CHANGE_CONTACTS\n\n    def command(self):\n        session, config = self.session, self.session.config\n\n        selectors = [a for a in self.args if not a.startswith('-')]\n        selectors.extend(self.data.get('q', []))\n\n        public = not (int(self.data.get('no_public', [0])[0]) or\n                      '-no_public' in self.args)\n        secret = not (int(self.data.get('no_secret', [0])[0]) or\n                      '-no_secret' in self.args)\n\n        imported = 0\n        for cfg in config.prefs.vcard.importers.gpg:\n            gimp = GnuPGImporter(session, cfg)\n            imported += gimp.import_vcards(session, config.vcards,\n                                           selectors=selectors)\n\n        return self._success(_('Imported %d vCards from GPG keychain'\n                               ) % imported, {'vcards': imported})\n\n\n_plugins.register_commands(PGPKeysAsVCards, PGPKeysImportAsVCards)\n_plugins.register_vcard_importers(GnuPGImporter)\n"
  },
  {
    "path": "mailpile/plugins/vcard_gravatar.py",
    "content": "#coding:utf-8\nimport os\nimport random\nimport time\n\nimport mailpile.util\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.security import secure_urlget\nfrom mailpile.util import *\nfrom mailpile.vcard import *\n\n\n_plugins = PluginManager(builtin=__file__)\n\n\nclass GravatarImporter(VCardImporter):\n    \"\"\"\n    This importer will pull contact details down from a central server,\n    using the Gravatar JSON API and caching thumbnail data locally.\n\n    For details, see https://secure.gravatar.com/site/implement/\n\n    The importer will only pull down a few contacts at a time, to limit\n    the impact on Gravatar's servers and prevent network traffic from\n    stalling the rescan process too much.\n    \"\"\"\n    FORMAT_NAME = 'Gravatar'\n    FORMAT_DESCRIPTION = _('Import contact info from a Gravatar server')\n    SHORT_NAME = 'gravatar'\n    CONFIG_RULES = {\n        'active': [_('Enable this importer'), bool, True],\n        'anonymous': [_('Require anonymity for use'), bool, True],\n        'interval': [_('Minimum days between refreshing'), 'int', 7],\n        'batch': [_('Max batch size per update'), 'int', 30],\n        'default': [_('Default thumbnail style'), str, 'retro'],\n        'rating': [_('Preferred thumbnail rating'),\n                   ['g', 'pg', 'r', 'x'], 'g'],\n        'size': [_('Preferred thumbnail size'), 'int', 80],\n        'url': [_('Gravatar server URL'), 'url', 'https://en.gravatar.com'],\n    }\n    VCARD_TS = 'x-gravatar-ts'\n    VCARD_IMG = ''\n\n    def _want_update(self):\n        def _jittery_time():\n            # This introduces 5 hours of jitter into the time check below,\n            # biased towards extending the delay by an average of 1.5 hours\n            # each time. This is mostly done to spread out the load on the\n            # Gravatar server over time, as to begin with all contacts will\n            # be checked at roughly the same time.\n            return time.time() + random.randrange(-14400, 3600)\n\n        want = []\n        vcards = self.session.config.vcards\n        for vcard in vcards.find_vcards([], kinds=vcards.KINDS_PEOPLE):\n            try:\n                ts = int(vcard.get(self.VCARD_TS).value)\n            except IndexError:\n                ts = 0\n            if ts < _jittery_time() - (self.config.interval * 24 * 3600):\n                want.append(vcard)\n            if len(want) >= self.config.batch:\n                break\n        return want\n\n    def _get(self, url):\n        self.session.ui.mark('Getting: %s' % url)\n        return secure_urlget(self.session, url,\n                             timeout=5, padding=False,\n                             anonymous=self.config.anonymous)\n\n    def check_gravatar(self, vcard, email):\n        img = vcf = json = None\n        for vcl in vcard.get_all('email'):\n            digest = md5_hex(vcl.value.lower())\n            try:\n                if mailpile.util.QUITTING:\n                    return None, None, None, None\n                if not img:\n                    img = self._get(('%s/avatar/%s.jpg?s=%s&r=%s&d=404'\n                                     ) % (self.config.url,\n                                          digest,\n                                          self.config.size,\n                                          self.config.rating))\n\n                # FIXME\n                #if not vcf:\n                #    vcf = self._get('%s/%s.vcf' % (self.config.url, digest))\n\n                # FIXME\n                #if not json:\n                #    json = self._get('%s/%s.json' % (self.config.url, digest))\n\n                if img or vcf or json:\n                    email = vcl.value\n            except IOError:\n                pass\n\n        if (self.config.default != '404') and not img:\n            try:\n                img = self._get(('%s/avatar/%s.jpg?s=%s&d=%s'\n                                 ) % (self.config.url,\n                                      md5_hex(email.lower()),\n                                      self.config.size,\n                                      self.config.default))\n            except IOError:\n                pass\n\n        return email, img, vcf, json\n\n    def get_vcards(self):\n        if not self.config.active:\n            return []\n\n        def _b64(data):\n            return data.encode('base64').replace('\\n', '')\n\n        results = []\n        for contact in self._want_update():\n            email = contact.email\n            if not email:\n                continue\n\n            if mailpile.util.QUITTING:\n                return []\n\n            vcard = MailpileVCard(VCardLine(name=self.VCARD_TS,\n                                            value=int(time.time())))\n            email, img, gcard, gjson = self.check_gravatar(contact, email)\n\n            if gcard:\n                # FIXME: Is this boring?\n                # vcard.load(data=gcard)\n                pass\n\n            if gjson:\n                # FIXME: This is less boring!\n                pass\n\n            if img:\n                vcard.add(VCardLine(\n                    name='photo',\n                    value='data:image/jpeg;base64,%s' % _b64(img),\n                    mediatype='image/jpeg'\n                ))\n\n            if gcard or gjson or img:\n                vcard.add(VCardLine(name='email', value=email))\n                results.append(vcard)\n        return results\n\n\n_plugins.register_vcard_importers(GravatarImporter)\n"
  },
  {
    "path": "mailpile/plugins/vcard_libravatar.py",
    "content": "#coding:utf-8\nimport random\nimport time\n\nimport mailpile.util\nfrom mailpile.i18n import gettext as _\nfrom mailpile.plugins import PluginManager\nfrom mailpile.security import secure_urlget\nfrom mailpile.vcard import VCardImporter, MailpileVCard, VCardLine\n\n\n_plugins = PluginManager(builtin=__file__)\n\n\nclass LibravatarImporter(VCardImporter):\n    \"\"\"\n    This importer will pull contact details down from a central server,\n    using the Libravatar JSON API and caching thumbnail data locally.\n\n    For details, see https://wiki.libravatar.org/api/\n\n    The importer will only pull down a few contacts at a time, to limit\n    the impact on Libravatar's servers and prevent network traffic from\n    stalling the rescan process too much.\n    \"\"\"\n    FORMAT_NAME = 'Libravatar'\n    FORMAT_DESCRIPTION = _('Import contact info from a Libravatar server')\n    SHORT_NAME = 'libravatar'\n    CONFIG_RULES = {\n        'active': [_('Enable this importer'), bool, True],\n        'anonymous': [_('Require anonymity for use'), bool, True],\n        'interval': [_('Minimum days between refreshing'), 'int', 7],\n        'batch': [_('Max batch size per update'), 'int', 30],\n        'default': [_('Default thumbnail style'), str, 'retro'],\n        'rating': [_('Preferred thumbnail rating'),\n                   ['g', 'pg', 'r', 'x'], 'g'],\n        'size': [_('Preferred thumbnail size'), 'int', 80],\n        'url': [_('Libravatar server URL'), 'url',\n                'https://seccdn.libravatar.org'],\n    }\n    VCARD_TS = 'x-libravatar-ts'\n    VCARD_IMG = ''\n\n    def _want_update(self):\n        def _jittery_time():\n            # This introduces 5 hours of jitter into the time check\n            # below, biased towards extending the delay by an average\n            # of 1.5 hours each time. This is mostly done to spread\n            # out the load on the Libravatar server over time, as to\n            # begin with all contacts will be checked at roughly the\n            # same time.\n            return time.time() + random.randrange(-14400, 3600)\n\n        want = []\n        vcards = self.session.config.vcards\n        for vcard in vcards.find_vcards([], kinds=vcards.KINDS_PEOPLE):\n            try:\n                ts = int(vcard.get(self.VCARD_TS).value)\n            except IndexError:\n                ts = 0\n            if ts < _jittery_time() - (self.config.interval * 24 * 3600):\n                want.append(vcard)\n            if len(want) >= self.config.batch:\n                break\n        return want\n\n    def _get(self, url):\n        self.session.ui.mark('Getting: %s' % url)\n        return secure_urlget(self.session, url,\n                             timeout=5,\n                             anonymous=self.config.anonymous)\n\n    def check_libravatar(self, vcard, email):\n        img = vcf = json = None\n        for vcl in vcard.get_all('email'):\n            digest = mailpile.util.md5_hex(vcl.value.lower())\n            try:\n                if mailpile.util.QUITTING:\n                    return None, None, None, None\n                if not img:\n                    img = self._get(('%s/avatar/%s?s=%s&r=%s&d=404'\n                                     ) % (self.config.url,\n                                          digest,\n                                          self.config.size,\n                                          self.config.rating))\n\n                # FIXME\n                #if not vcf:\n                #    vcf = self._get('%s/%s.vcf' % (self.config.url, digest))\n\n                # FIXME\n                #if not json:\n                #    json = self._get('%s/%s.json' % (self.config.url, digest))\n\n                if img or vcf or json:\n                    email = vcl.value\n            except IOError:\n                pass\n\n        if (self.config.default != '404') and not img:\n            try:\n                img = self._get(('%s/avatar/%s?s=%s&d=%s'\n                                 ) % (self.config.url,\n                                      mailpile.util.md5_hex(email.lower()),\n                                      self.config.size,\n                                      self.config.default))\n            except IOError:\n                pass\n\n        return email, img, vcf, json\n\n    def get_vcards(self):\n        if not self.config.active:\n            return []\n\n        def _b64(data):\n            return data.encode('base64').replace('\\n', '')\n\n        results = []\n        for contact in self._want_update():\n            email = contact.email\n            if not email:\n                continue\n\n            if mailpile.util.QUITTING:\n                return []\n\n            vcard = MailpileVCard(VCardLine(name=self.VCARD_TS,\n                                            value=int(time.time())))\n            email, img, gcard, gjson = self.check_libravatar(contact, email)\n\n            if gcard:\n                # FIXME: Is this boring?\n                # vcard.load(data=gcard)\n                pass\n\n            if gjson:\n                # FIXME: This is less boring!\n                pass\n\n            if img:\n                vcard.add(VCardLine(\n                    name='photo',\n                    value='data:image/jpeg;base64,%s' % _b64(img),\n                    mediatype='image/jpeg'\n                ))\n\n            if gcard or gjson or img:\n                vcard.add(VCardLine(name='email', value=email))\n                results.append(vcard)\n        return results\n\n\n_plugins.register_vcard_importers(LibravatarImporter)\n"
  },
  {
    "path": "mailpile/plugins/vcard_mork.py",
    "content": "#!/usr/bin/env python2.7\n#coding:utf-8\nfrom __future__ import print_function\nimport sys\nimport re\nimport getopt\nfrom sys import stdin, stdout, stderr\n\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.vcard import *\n\n\n_plugins = PluginManager(builtin=__file__)\n\n\ndef hexcmp(x, y):\n    try:\n        a = int(x, 16)\n        b = int(y, 16)\n        if a < b:\n            return -1\n        if a > b:\n            return 1\n        return 0\n\n    except:\n        return cmp(x, y)\n\n\nclass MorkImporter(VCardImporter):\n    # Based on Demork by Mike Hoye <mhoye@off.net>\n    # Which is based on Mindy by Kumaran Santhanam <kumaran@alumni.stanford.org>\n    #\n    # To understand the insanity that is Mork, read these:\n    #  http://www-archive.mozilla.org/mailnews/arch/mork/primer.txt\n    #  http://www.jwz.org/blog/2004/03/when-the-database-worms-eat-into-your-brain/\n    #\n    FORMAT_NAME = \"Mork Database\"\n    FORMAT_DESCRPTION = \"Thunderbird contacts database format.\"\n    SHORT_NAME = \"mork\"\n    CONFIG_RULES = {\n        'filename': [_('Location of Mork database'), 'path', \"\"],\n    }\n\n    class Database:\n        def __init__(self):\n            self.cdict = {}\n            self.adict = {}\n            self.tables = {}\n\n    class Table:\n        def __init__(self):\n            self.id = None\n            self.scope = None\n            self.kind = None\n            self.rows = {}\n\n    class Row:\n        def __init__(self):\n            self.id = None\n            self.scope = None\n            self.cells = []\n\n    class Cell:\n        def __init__(self):\n            self.column = None\n            self.atom = None\n\n    def escapeData(self, match):\n        return match.group() \\\n            .replace('\\\\\\\\n', '$0A') \\\n            .replace('\\\\)', '$29') \\\n            .replace('>', '$3E') \\\n            .replace('}', '$7D') \\\n            .replace(']', '$5D')\n\n    pCellText = re.compile(r'\\^(.+?)=(.*)')\n    pCellOid = re.compile(r'\\^(.+?)\\^(.+)')\n    pCellEscape = re.compile(r'((?:\\\\[\\$\\0abtnvfr])|(?:\\$..))')\n    pMindyEscape = re.compile('([\\x00-\\x1f\\x80-\\xff\\\\\\\\])')\n\n    def escapeMindy(self, match):\n        s = match.group()\n        if s == '\\\\':\n            return '\\\\\\\\'\n        if s == '\\0':\n            return '\\\\0'\n        if s == '\\r':\n            return '\\\\r'\n        if s == '\\n':\n            return '\\\\n'\n        return \"\\\\x%02x\" % ord(s)\n\n    def encodeMindyValue(self, value):\n        return self.pMindyEscape.sub(self.escapeMindy, value)\n\n    backslash = {'\\\\\\\\': '\\\\',\n                 '\\\\$': '$',\n                 '\\\\0': chr(0),\n                 '\\\\a': chr(7),\n                 '\\\\b': chr(8),\n                 '\\\\t': chr(9),\n                 '\\\\n': chr(10),\n                 '\\\\v': chr(11),\n                 '\\\\f': chr(12),\n                 '\\\\r': chr(13)}\n\n    def unescapeMork(self, match):\n        s = match.group()\n        if s[0] == '\\\\':\n            return self.backslash[s]\n        else:\n            return chr(int(s[1:], 16))\n\n    def decodeMorkValue(self, value):\n        m = self.pCellEscape.sub(self.unescapeMork, value)\n        m = m.decode(\"utf-8\")\n        return m\n\n    def addToDict(self, dict, cells):\n        for cell in cells:\n            eq = cell.find('=')\n            key = cell[1:eq]\n            val = cell[eq+1:-1]\n            dict[key] = self.decodeMorkValue(val)\n\n    def getRowIdScope(self, rowid, cdict):\n        idx = rowid.find(':')\n        if idx > 0:\n            return (rowid[:idx], cdict[rowid[idx+2:]])\n        else:\n            return (rowid, None)\n\n    def delRow(self, db, table, rowid):\n        (rowid, scope) = self.getRowIdScope(rowid, db.cdict)\n        if scope:\n            rowkey = rowid + \"/\" + scope\n        else:\n            rowkey = rowid + \"/\" + table.scope\n\n        if rowkey in table.rows:\n            del table.rows[rowkey]\n\n    def addRow(self, db, table, rowid, cells):\n        row = self.Row()\n        row.id, row.scope = self.getRowIdScope(rowid, db.cdict)\n\n        for cell in cells:\n            obj = self.Cell()\n            cell = cell[1:-1]\n\n            match = self.pCellText.match(cell)\n            if match:\n                obj.column = db.cdict[match.group(1)]\n                obj.atom = self.decodeMorkValue(match.group(2))\n\n            else:\n                match = self.pCellOid.match(cell)\n                if match:\n                    obj.column = db.cdict[match.group(1)]\n                    obj.atom = db.adict[match.group(2)]\n\n            if obj.column and obj.atom:\n                row.cells.append(obj)\n\n        if row.scope:\n            rowkey = row.id + \"/\" + row.scope\n        else:\n            rowkey = row.id + \"/\" + table.scope\n\n        if rowkey in table.rows:\n            print(\"ERROR: duplicate rowid/scope %s\" % rowkey, file=stderr)\n            print(cells, file=stderr)\n\n        table.rows[rowkey] = row\n\n    def inputMork(self, data):\n        # Remove beginning comment\n        pComment = re.compile('//.*')\n        data = pComment.sub('', data, 1)\n\n        # Remove line continuation backslashes\n        pContinue = re.compile(r'(\\\\(?:\\r|\\n))')\n        data = pContinue.sub('', data)\n\n        # Remove line termination\n        pLine = re.compile(r'(\\n\\s*)|(\\r\\s*)|(\\r\\n\\s*)')\n        data = pLine.sub('', data)\n\n        # Create a database object\n        db = self.Database()\n\n        # Compile the appropriate regular expressions\n        pCell = re.compile(r'(\\(.+?\\))')\n        pSpace = re.compile(r'\\s+')\n        pColumnDict = re.compile(r'<\\s*<\\(a=c\\)>\\s*(?:\\/\\/)?\\s*'\n                                 '(\\(.+?\\))\\s*>')\n        pAtomDict = re.compile(r'<\\s*(\\(.+?\\))\\s*>')\n        pTable = re.compile(r'\\{-?(\\d+):\\^(..)\\s*\\{\\(k\\^(..):c\\)'\n                            '\\(s=9u?\\)\\s*(.*?)\\}\\s*(.+?)\\}')\n        pRow = re.compile(r'(-?)\\s*\\[(.+?)((\\(.+?\\)\\s*)*)\\]')\n\n        pTranBegin = re.compile(r'@\\$\\$\\{.+?\\{\\@')\n        pTranEnd = re.compile(r'@\\$\\$\\}.+?\\}\\@')\n\n        # Escape all '%)>}]' characters within () cells\n        data = pCell.sub(self.escapeData, data)\n\n        # Iterate through the data\n        index = 0\n        length = len(data)\n        match = None\n        tran = 0\n        while True:\n            if match:\n                index += match.span()[1]\n            if index >= length:\n                break\n            sub = data[index:]\n\n            # Skip whitespace\n            match = pSpace.match(sub)\n            if match:\n                index += match.span()[1]\n                continue\n\n            # Parse a column dictionary\n            match = pColumnDict.match(sub)\n            if match:\n                m = pCell.findall(match.group())\n                # Remove extraneous '(f=iso-8859-1)'\n                if len(m) >= 2 and m[1].find('(f=') == 0:\n                    m = m[1:]\n                self.addToDict(db.cdict, m[1:])\n                continue\n\n            # Parse an atom dictionary\n            match = pAtomDict.match(sub)\n            if match:\n                cells = pCell.findall(match.group())\n                self.addToDict(db.adict, cells)\n                continue\n\n            # Parse a table\n            match = pTable.match(sub)\n            if match:\n                id = match.group(1) + ':' + match.group(2)\n\n                try:\n                    table = db.tables[id]\n\n                except KeyError:\n                    table = self.Table()\n                    table.id = match.group(1)\n                    table.scope = db.cdict[match.group(2)]\n                    table.kind = db.cdict[match.group(3)]\n                    db.tables[id] = table\n\n                rows = pRow.findall(match.group())\n                for row in rows:\n                    cells = pCell.findall(row[2])\n                    rowid = row[1]\n                    if tran and rowid[0] == '-':\n                        rowid = rowid[1:]\n                        self.delRow(db, db.tables[id], rowid)\n\n                    if tran and row[0] == '-':\n                        pass\n\n                    else:\n                        self.addRow(db, db.tables[id], rowid, cells)\n                continue\n\n            # Transaction support\n            match = pTranBegin.match(sub)\n            if match:\n                tran = 1\n                continue\n\n            match = pTranEnd.match(sub)\n            if match:\n                tran = 0\n                continue\n\n            match = pRow.match(sub)\n            if match and tran:\n                # print >>stderr, (\"WARNING: using table '1:^80' \"\n                #                  \"for dangling row: %s\") % match.group()\n                rowid = match.group(2)\n                if rowid[0] == '-':\n                    rowid = rowid[1:]\n\n                cells = pCell.findall(match.group(3))\n                self.delRow(db, db.tables['1:80'], rowid)\n                if row[0] != '-':\n                    self.addRow(db, db.tables['1:80'], rowid, cells)\n                continue\n\n            # Syntax error\n            print(\"ERROR: syntax error while parsing MORK file\", file=stderr)\n            print(\"context[%d]: %s\" % (index, sub[:40]), file=stderr)\n            index += 1\n\n        # Return the database\n        self.db = db\n        return db\n\n    def morkToHash(self):\n        results = []\n        columns = self.db.cdict.keys()\n        columns.sort(hexcmp)\n\n        tables = self.db.tables.keys()\n        tables.sort(hexcmp)\n\n        for table in [self.db.tables[k] for k in tables]:\n            rows = table.rows.keys()\n            rows.sort(hexcmp)\n            for row in [table.rows[k] for k in rows]:\n                email = name = \"\"\n                result = {}\n                for cell in row.cells:\n                    result[cell.column] = cell.atom\n                    if cell.column == \"PrimaryEmail\":\n                        result[\"email\"] = cell.atom.lower()\n                    elif cell.column == \"DisplayName\":\n                        result[\"name\"] = cell.atom.strip(\"'\")\n                results.append(result)\n\n        return results\n\n    def load(self):\n        with open(self.config.filename, \"r\") as fh:\n            data = fh.read()\n\n            if data.find(\"<mdb:mork\") < 0:\n                raise ValueError(\"Mork file required\")\n\n            self.inputMork(data)\n\n    def get_vcards(self):\n        self.load()\n        people = self.morkToHash()\n        # print people\n\n        results = []\n        vcards = {}\n        for person in people:\n            card = MailpileVCard()\n            if \"name\" in person:\n                card.add(VCardLine(name=\"FN\", value=person[\"name\"]))\n            if \"email\" in person:\n                card.add(VCardLine(name=\"EMAIL\", value=person[\"email\"]))\n            results.append(card)\n\n        return results\n\n\nif __name__ == \"__main__\":\n    import json\n    filename = sys.argv[1]\n\n    m = MorkImporter(filename=filename)\n    m.load()\n    print(m.get_contacts(data))\nelse:\n    _plugins.register_vcard_importers(MorkImporter)\n"
  },
  {
    "path": "mailpile/plugins/webterminal.py",
    "content": "import json\nimport random\n\nfrom mailpile.app import FriendlyPipeTransform\nfrom mailpile.commands import *\nfrom mailpile.plugins import PluginManager\nfrom mailpile.ui import Session\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.security import CC_WEB_TERMINAL\n\n_plugins = PluginManager(builtin=__file__)\n\nSESSIONS = {}\n\n\nclass TerminalSessionNew(Command):\n    \"\"\"Create a terminal session.\"\"\"\n    SYNOPSIS = ('', '', 'terminal_session_new', '')\n    ABOUT = ('Start a new named session.')\n    HTTP_CALLABLE = ('POST', )\n    CONFIG_REQUIRED = True\n    IS_USER_ACTIVITY = True\n    COMMAND_SECURITY = CC_WEB_TERMINAL\n\n    def command(self):\n        global SESSIONS\n        config = self.session.config\n\n        s = Session(config)\n        s.ui.log_parent = self.session.ui\n        s.ui.render_mode = 'text'\n        sid = \"%08x\" % random.randint(0, 1000000000)\n        SESSIONS[sid] = s\n\n        return self._success('Created a session', result={\"sid\": sid})\n\n\nclass TerminalSessionEnd(Command):\n    \"\"\"End a terminal session.\"\"\"\n    SYNOPSIS = ('', '', 'terminal_session_end', '')\n    ABOUT = ('End a named session.')\n    HTTP_CALLABLE = ('POST', )\n    HTTP_QUERY_VARS = {\n        'sid': 'id of session to use',\n    }\n    CONFIG_REQUIRED = True\n    IS_USER_ACTIVITY = True\n    COMMAND_SECURITY = CC_WEB_TERMINAL\n\n    def command(self):\n        global SESSIONS\n\n        config = self.session.config\n        sid = self.data.get('sid', [''])[0]\n        if sid == '':\n            return self._error('No SID supplied')\n\n        del(SESSIONS[sid])\n\n        return self._success('Ended a session', result={\"sid\": sid})\n\n\nclass TerminalCommand(Command):\n    \"\"\"Execute a terminal command.\"\"\"\n    SYNOPSIS = ('', '', 'terminal_command', '<command> <session>')\n    ABOUT = ('Run a terminal command via the API')\n    CONFIG_REQUIRED = True\n    IS_USER_ACTIVITY = True\n    HTTP_CALLABLE = ('POST', )\n    HTTP_QUERY_VARS = {\n        'sid': 'id of session to use',\n        'width': 'width of terminal in characters',\n        'command': 'command to execute'\n    }\n    TERMINAL_BLACKLIST = [\"eventlog/watch\", \"hacks/pycli\", \"quit\"]\n    COMMAND_SECURITY = CC_WEB_TERMINAL\n\n    def command(self):\n        global SESSIONS\n\n        config = self.session.config\n        sid = self.data.get('sid', [''])[0]\n        if sid == '':\n            return self._error('No session ID supplied')\n        if sid not in SESSIONS:\n            return self._error(\n                'Unknown session ID: %s' % sid, result={'sessions': SESSIONS.keys()})\n\n        wt_session = SESSIONS[sid]\n        max_width = int(float(self.data.get('width', [79])[0]))\n        cmd = self.data.get('command', [''])[0]\n        old_render_mode, cmd = FriendlyPipeTransform(wt_session, cmd)\n        cmd = cmd.split(\" \")\n        command = cmd[0]\n        args = ' '.join(cmd[1:])\n\n        if command in self.TERMINAL_BLACKLIST:\n            return self._error(\n                _('This command is not allowed in the web terminal.'),\n                result={})\n        try:\n            main_ui = wt_session.ui\n            from mailpile.ui import CapturingUserInteraction as CUI\n            wt_session.ui = capture = CUI(self.session.config, log_parent=self.session.ui)\n            wt_session.ui.render_mode = main_ui.render_mode\n            wt_session.ui.term.max_width = max_width\n\n            result = Action(wt_session, command, args)\n            if wt_session.ui.render_mode == 'html':\n                wt_session.ui.render_mode = 'html!content'\n            capture.display_result(result)\n            rendered = [capture.render_mode.split('!')[0], capture.captured]\n\n            # Allow the user to persistently change the render mode\n            main_ui.render_mode = capture.render_mode\n        except Exception as e:\n            result = {\"error\": \"%s\" % e}\n            rendered = [\"text\", \"error: %s\" % e]\n        finally:\n            wt_session.ui = main_ui\n\n        if old_render_mode is not None:\n            wt_session.ui.render_mode = old_render_mode\n        return self._success(_('Ran a command'), result={\n            'result': rendered,\n            'raw_result': result,\n            'sessions': SESSIONS.keys()})\n\n\n_plugins.register_commands(\n    TerminalCommand, TerminalSessionNew, TerminalSessionEnd)\n"
  },
  {
    "path": "mailpile/postinglist.py",
    "content": "from __future__ import print_function\nimport os\nimport sys\nimport random\nimport threading\nimport traceback\nimport time\n\nimport mailpile.util\nfrom mailpile.crypto.streamer import EncryptingStreamer\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.util import *\n\n\nGLOBAL_POSTING_LOCK = PListRLock()\nGLOBAL_OPTIMIZE_LOCK = PListLock()\n\nGLOBAL_GPL_LOCK = PListRLock()\nGLOBAL_GPL = None\n\nPLC_CACHE_LOCK = PListLock()\nPLC_CACHE = {}\n\nTIMERS = {\n    'render': 0,\n    'save': 0,\n    'save_count': 0,\n    'load': 0,\n    'load_count': 0,\n}\n\n# These are special keys in the Global Posting List that never\n# get migrated into the search index itself.\n# Note: To avoid conflicts, these keys should be in all caps.\nGPL_MSGID_TRACKER = '_MAX_MSGID_'\nGPL_NEVER_MIGRATE = (GPL_MSGID_TRACKER, )\n\n\ndef PLC_CACHE_FlushAndClean(session, min_changes=0, keep=5, runtime=None):\n    def save(plc):\n        job_name = _('Save PLC %s') % plc.sig\n        session.ui.mark(job_name)\n        session.config.save_worker.do(session, job_name, plc.save)\n        play_nice_with_threads()\n\n    def remove(ts, plc):\n        with PLC_CACHE_LOCK:\n            if plc.sig in PLC_CACHE and ts == PLC_CACHE[plc.sig][0]:\n                del PLC_CACHE[plc.sig]\n\n    startt = int(time.time())\n    expire = startt - max(30, 300 - len(PLC_CACHE))\n    savets = startt - 15\n\n    def time_up():\n        return (runtime and startt + runtime < time.time())\n\n    with PLC_CACHE_LOCK:\n        plc_cache = sorted(PLC_CACHE.values())\n\n    for ts, plc in plc_cache[:-keep]:\n        if plc.changes:\n            save(plc)\n        remove(ts, plc)\n        if time_up():\n            return\n\n    for ts, plc in plc_cache[-keep:]:\n        if (plc.changes > min_changes) or (plc.changes and ts < savets):\n            save(plc)\n        if ts < expire:\n            remove(ts, plc)\n        if time_up():\n            return\n\n\nclass PostingListContainer(object):\n    \"\"\"A container for posting lists mapping search terms to message IDs.\"\"\"\n\n    MAX_ITEMS = int((60 * 1024) / 5)  # Target size of about 60KB\n    MAX_HASH_LEN = 24\n\n    @classmethod\n    def Load(cls, session, sig, uncached_cb=None):\n        fn, sig = cls._GetFilenameAndSig(session.config, sig)\n        plc = None\n        with PLC_CACHE_LOCK:\n            if sig in PLC_CACHE:\n                PLC_CACHE[sig][0] = int(time.time())\n                plc = PLC_CACHE[sig][1]\n        if plc is None:\n            plc = cls(session, sig)  # Unlocked: stalled loads would deadlock\n            with PLC_CACHE_LOCK:\n                PLC_CACHE[sig] = [int(time.time()), plc]\n            if uncached_cb:\n                uncached_cb()\n        # We return the cached version no matter what, in case a race above\n        # had us loading the same posting-list twice: we want to be sure\n        # calling code gets the shared object.\n        with PLC_CACHE_LOCK:\n            return PLC_CACHE[sig][1]\n\n    def __init__(self, session, sig, fd=None):\n        self.session = session\n        self.config = session.config\n\n        self.lock = PListRLock()\n        self.sig = sig\n        self.fd = fd\n        self.words = {sig: set()}\n\n        self.changes = 0\n        self._load()\n\n    def get(self, sig, default=None):\n        return self.words.get(sig, default)\n\n    def add(self, *args, **kwargs):\n        with self.lock:\n            return self._unlocked_add(*args, **kwargs)\n\n    def remove(self, *args, **kwargs):\n        with self.lock:\n            self.changed = True\n            return self._unlocked_remove(*args, **kwargs)\n\n    def purge_deleted(self, deleted_sig, deleted_set):\n        changes = 0\n        for sig in self.words:\n\t    overlap = (self.words[sig] & deleted_set)\n            if (sig != deleted_sig) and overlap:\n                self.words[sig] -= overlap\n                changes += len(overlap)\n        self.changes += changes\n        return changes\n\n    def save(self, split=True):\n        if not self.changes:\n            return\n\n        if split and len(self.words) > 1:\n            with self.lock:\n                for plc in self._splits():\n                    plc.save(split=False)\n            return\n\n        t = [time.time()]\n        encryption_key = self.config.get_master_key()\n        outfile = self._SaveFile(self.config, self.sig)\n        with self.lock:\n            # Optimizing for fast loads, so deletion only happens on save.\n            output = '\\n'.join('\\t'.join(l) for l\n                               in ([sig] + [str(v) for v in vals]\n                                   for sig, vals in self.words.iteritems())\n                               if len(l) > 1)\n            t.append(time.time())\n\n            if not output:\n                try:\n                    os.remove(outfile)\n                except OSError:\n                    pass\n            elif self.config.prefs.encrypt_index and encryption_key:\n                subj = self.config.mailpile_path(outfile)\n                with EncryptingStreamer(encryption_key,\n                                        delimited=False,\n                                        dir=self.config.tempfile_dir(),\n                                        header_data={'subject': subj},\n                                        name='PLC/%s' % self.sig) as fd:\n                    fd.write(output.encode('utf-8'))\n                    fd.save(outfile)\n            else:\n                with open(outfile, 'wb') as fd:\n                    fd.write(output)\n\n            t.append(time.time())\n            self.changes = 0\n\n        if len(t) == 3:\n            TIMERS['render'] += t[1] - t[0]\n            TIMERS['save'] += t[2] - t[1]\n            TIMERS['save_count'] += 1\n\n    def _splits(self):\n        splits = [self]\n        if len(self.sig) < self.MAX_HASH_LEN:\n            total, sums = 0, {}\n            for sig, values in self.words.iteritems():\n                total += len(values)\n                if len(values) >= (self.MAX_ITEMS / 2):\n                    nsig = sig[:self.MAX_HASH_LEN]\n                else:\n                    nsig = sig[:len(self.sig)+1]\n                if nsig in sums:\n                    sums[nsig] += len(values)\n                else:\n                    sums[nsig] = len(values)\n\n            while total > self.MAX_ITEMS and sums:\n                skeys = sums.keys()\n                skeys.sort(key=lambda k: -sums[k])\n                nsig = skeys[0]\n                total -= sums[nsig]\n                del sums[nsig]\n                try:\n                    fn = self._SaveFile(self.config, nsig)\n                    if not os.path.exists(fn):\n                        open(fn, 'w').close()\n\n                    plc = PostingListContainer(self.session, nsig)\n                    for sig in list(self.words.keys()):\n                        if sig.startswith(nsig):\n                            plc.add(sig, self.words[sig])\n                            del self.words[sig]\n                    splits.append(plc)\n                except (OSError, IOError):\n                    pass\n\n        return splits\n\n    def _load(self):\n        t0 = time.time()\n        if not self.fd:\n            fn, self.sig = self._GetFilenameAndSig(self.config, self.sig)\n            try:\n                self.fd = open(fn, 'rb')\n            except (IOError, OSError):\n                return\n        with self.lock, self.fd:\n            try:\n                decrypt_and_parse_lines(self.fd,\n                                        self._unlocked_parse_lines,\n                                        self.config)\n                self.changes = 0\n            except (ValueError, IOError):\n                self.session.ui.warning('load(%s) %s'\n                                        % (self.sig, sys.exc_info()))\n                if self.config.sys.debug:\n                    traceback.print_exc()\n        self.fd = None\n        TIMERS['load'] += time.time() - t0\n        TIMERS['load_count'] += 1\n\n    def _unlocked_parse_lines(self, lines):\n        for line in lines:\n            words = line.strip().split('\\t')\n            if len(words) > 1:\n                self._unlocked_add(words[0], words[1:])\n\n    def _unlocked_add(self, sig, values):\n        wset = set(values)\n        self.changes += len(wset)\n        if sig in self.words:\n            self.words[sig] |= wset\n        else:\n            self.words[sig] = wset\n\n    def _unlocked_remove(self, sig, values):\n        wset = set(values)\n        self.changes += len(wset)\n        if sig in self.words:\n            self.words[sig] -= wset\n            if not self.words[sig]:\n                del self.words[sig]\n\n    @classmethod\n    def _SaveFile(cls, config, sig):\n        return os.path.join(config.postinglist_dir(sig), sig)\n\n    @classmethod\n    def _GetFilenameAndSig(cls, config, sig):\n        \"\"\"Find and the closest matching posting list container file\"\"\"\n        sig = sig[:cls.MAX_HASH_LEN]\n        while len(sig) > 0:\n            fn = cls._SaveFile(config, sig)\n            try:\n                if os.path.exists(fn):\n                    return (fn, sig)\n            except (IOError, OSError):\n                pass\n\n            if len(sig) > 1:\n                sig = sig[:-1]\n            else:\n                return (fn, sig)\n\n        # Not reached\n        return (None, None)\n\n\nclass PostingList(object):\n    \"\"\"A posting list is a map of search terms to message IDs.\"\"\"\n\n    HASH_LEN = 24\n\n    @classmethod\n    def Append(cls, session, word, values, compact=False, sig=None):\n        sig = sig or cls._WordSig(word, session.config)\n        PostingListContainer.Load(session, sig).add(sig, values)\n\n    @classmethod\n    def Optimize(cls, session, index, lazy=False, quick=False):\n        threshold = (quick or lazy) and 250 or 50\n        PLC_CACHE_FlushAndClean(session, min_changes=threshold)\n\n    def __init__(self, session, word):\n        self.config = session.config\n        self.session = session\n        if word:\n            self.word = word\n            self.sig = self._WordSig(word, self.config)\n            self.plc = PostingListContainer.Load(self.session, self.sig)\n\n    def hits(self):\n        return self.plc.get(self.sig) or set()\n\n    def append(self, *eids):\n        self.plc.add(self.sig, eids)\n        return self\n\n    def remove(self, eids):\n        self.plc.remove(self.sig, eids)\n        return self\n\n    @classmethod\n    def _WordSig(cls, word, config):\n        return strhash(word, cls.HASH_LEN,\n                       obfuscate=((config.prefs.obfuscate_index or\n                                   config.prefs.encrypt_index) and\n                                  config.get_master_key()))\n\n\n##############################################################################\n\nclass OldPostingList(object):\n    \"\"\"A posting list is a map of search terms to message IDs.\"\"\"\n\n    CHARACTERS = 'abcdefghijklmnopqrstuvwxyz0123456789+_'\n\n    MAX_SIZE = 60    # perftest gives: 75% below 500ms, 50% below 100ms\n    HASH_LEN = 24\n\n    @classmethod\n    def _Optimize(cls, session, idx, force=False):\n        return 0  # Disabled, this is incompatible with new posting lists!\n\n    @classmethod\n    def _Append(cls, session, word, mail_ids, compact=True, sig=None):\n        config = session.config\n        sig = sig or cls.WordSig(word, config)\n\n        fd = None\n        while fd is None:\n            fd, fn = cls.GetFile(session, sig, mode='a')\n            fn_path = cls.SaveFile(session, fn)\n            try:\n                # The code below will compact the files and split out hot-spots,\n                # but we only bother \"once in a while\" when the files are \"big\".\n                if compact:\n                    max_size = ((1024 * config.sys.postinglist_kb) -\n                                (cls.HASH_LEN * 6))\n                    if (os.path.getsize(fn_path) > max_size and\n                            random.randint(0, 50) == 1):\n                        break\n                if fd:\n                    with fd:\n                        fd.write('%s\\t%s\\n' % (sig, '\\t'.join(mail_ids)))\n                        return\n            except IOError:\n                print ('RETRY: APPEND(compact=%s, %s, %s) %s'\n                       % (compact, fn_path, fd, sys.exc_info()))\n                time.sleep(0.2)\n                fd = None\n\n        # OK, compactinate!\n        pls = cls(session, word, sig=sig)\n        for mail_id in mail_ids:\n            pls.append(mail_id)\n        pls.save()\n\n    @classmethod\n    def Lock(cls, lock, method, *args, **kwargs):\n        with lock:\n            return method(*args, **kwargs)\n\n    @classmethod\n    def Optimize(cls, *args, **kwargs):\n        return cls.Lock(GLOBAL_OPTIMIZE_LOCK, cls._Optimize, *args, **kwargs)\n\n    @classmethod\n    def Append(cls, *args, **kwargs):\n        return cls.Lock(GLOBAL_POSTING_LOCK, cls._Append, *args, **kwargs)\n\n    @classmethod\n    def WordSig(cls, word, config):\n        return strhash(word, cls.HASH_LEN,\n                       obfuscate=((config.prefs.obfuscate_index or\n                                   config.prefs.encrypt_index) and\n                                  config.get_master_key()))\n\n    @classmethod\n    def SaveFile(cls, session, prefix):\n        return os.path.join(session.config.postinglist_dir(prefix), prefix)\n\n    @classmethod\n    def GetFile(cls, session, sig, mode='r'):\n        sig = sig[:cls.HASH_LEN]\n        while len(sig) > 0:\n            fn = cls.SaveFile(session, sig)\n            try:\n                if os.path.exists(fn):\n                    return (open(fn, mode), sig)\n            except (IOError, OSError):\n                pass\n\n            if len(sig) > 1:\n                sig = sig[:-1]\n            else:\n                if 'r' in mode:\n                    return (None, sig)\n                else:\n                    return (open(fn, mode), sig)\n        # Not reached\n        return (None, None)\n\n    def __init__(self, session, word, sig=None, config=None):\n        self.config = config or session.config\n        self.session = session\n        self.sig = sig or self.WordSig(word, self.config)\n        self.size = 0\n        self.word = word\n        self.WORDS = {self.sig: set()}\n        self.lock = PListRLock()\n        self.load()\n\n    def _parse_lines(self, lines):\n        for line in lines:\n            self.size += len(line)\n            words = line.strip().split('\\t')\n            if len(words) > 1:\n                wset = set(words[1:])\n                if words[0] in self.WORDS:\n                    self.WORDS[words[0]] |= wset\n                else:\n                    self.WORDS[words[0]] = wset\n\n    def load(self):\n        fd, self.filename = self.GetFile(self.session, self.sig)\n        if not fd:\n            return\n        with self.lock, fd:\n            try:\n                self.size = 0\n                decrypt_and_parse_lines(fd, self._parse_lines, self.config)\n            except (ValueError, IOError):\n                self.session.ui.warning('load(%s) %s'\n                                        % (self.filename, sys.exc_info()))\n\n    def _fmt_file(self, prefix):\n        output = []\n        self.session.ui.mark('Formatting prefix %s' % unicode(prefix))\n        for word in self.WORDS.keys():\n            data = self.WORDS.get(word, [])\n            if ((prefix == 'ALL' or word.startswith(prefix))\n                    and len(data) > 0):\n                output.append(('%s\\t%s\\n'\n                               ) % (word, '\\t'.join(['%s' % x for x in data])))\n        play_nice_with_threads(weak=True)\n        return ''.join(output)\n\n    def _compact(self, prefix, output):\n        while ((len(output) > 1024 * self.config.sys.postinglist_kb) and\n               (len(prefix) < self.HASH_LEN)):\n            with self.lock:\n                biggest = self.sig\n                for word in self.WORDS:\n                    if (len(self.WORDS.get(word, []))\n                            > len(self.WORDS.get(biggest, []))):\n                        biggest = word\n                if len(biggest) > len(prefix):\n                    biggest = biggest[:len(prefix) + 1]\n                    self.save(prefix=biggest, mode='ab')\n                    for key in [k for k in self.WORDS\n                                if k.startswith(biggest)]:\n                        del self.WORDS[key]\n                    output = self._fmt_file(prefix)\n        return prefix, output\n\n    def save(self, prefix=None, compact=True, mode='wb'):\n        with self.lock:\n            prefix = prefix or self.filename\n            output = self._fmt_file(prefix)\n            if compact:\n                prefix, output = self._compact(prefix, output)\n            try:\n                outfile = self.SaveFile(self.session, prefix)\n                self.session.ui.mark('Writing %d bytes to %s' % (len(output),\n                                                                 outfile))\n                if output:\n                    if self.config.prefs.encrypt_index:\n                        encryption_key = self.config.get_master_key()\n                        with EncryptingStreamer(encryption_key,\n                                                delimited=True,\n                                                dir=self.config.tempfile_dir(),\n                                                name='PostingList') as efd:\n                            efd.write(output.encode('utf-8'))\n                            efd.save(outfile, mode=mode)\n                    else:\n                        with open(outfile, mode) as fd:\n                            fd.write(output)\n                    return len(output)\n                elif os.path.exists(outfile):\n                    os.remove(outfile)\n            except:\n                self.session.ui.warning('%s=>%s' % (outfile, sys.exc_info(),))\n            return 0\n\n    def hits(self):\n        return self.WORDS[self.sig]\n\n    def append(self, eid):\n        with self.lock:\n            if self.sig not in self.WORDS:\n                self.WORDS[self.sig] = set()\n            self.WORDS[self.sig].add(eid)\n            return self\n\n    def remove(self, eids):\n        with self.lock:\n            for eid in eids:\n                try:\n                    self.WORDS[self.sig].remove(eid)\n                except KeyError:\n                    pass\n            return self\n\n\nclass GlobalPostingList(OldPostingList):\n\n    @classmethod\n    def _Optimize(cls, session, idx,\n                  force=False, lazy=False, quick=False, ratio=1.0, runtime=0):\n\n        # Record the largest known msg_mid here so it doesn't get lost\n        cls.UpdateMaxMsgMid(session, [b36(cls.GetMaxMsgIdxPos())])\n\n        deleted = GlobalPostingList(session, 'deleted:is')\n        deleted_set = deleted.hits()\n        deleted_sig = deleted.sig\n\n        starttime = time.time()\n        count = 0\n        global GLOBAL_GPL\n        if (GLOBAL_GPL and (not lazy or len(GLOBAL_GPL) > 50*1024)):\n            pls = GlobalPostingList(session, '')\n\n            # Why sort? Processing keys in order is more efficient, as it lets\n            # things accumulate in the PLC_CACHE.\n            keys = sorted(GLOBAL_GPL.keys())\n            if not quick and not lazy:\n                keys = sorted(keys + pls.plc_keys())\n\n            if ratio and keys:\n                keyn = int(max(1, len(keys) * min(1.0, ratio)))\n                start = random.randint(0, len(keys))\n                # This lets the selection wrap around to the beginning,\n                # so we don't have a bias against writing out the first\n                # keys compared with the others.\n                keys += keys\n                keys = keys[start:start+keyn]\n\n            for sig in keys:\n                if pls._migrate(sig):\n                    pls._purge_deleted(sig, deleted_sig, deleted_set)\n                    count += 1\n                elif pls._purge_deleted(sig, deleted_sig, deleted_set):\n                    count += 1\n\n                if (count % 97) == 0:\n                    PLC_CACHE_FlushAndClean(session, min_changes=10000)\n                    session.ui.mark(('Updating search index... %d%% (%s)'\n                                     ) % (count * 100 / len(keys), sig))\n\n                if runtime and (starttime + runtime) < time.time():\n                    break\n                if mailpile.util.QUITTING:\n                    break\n\n            PLC_CACHE_FlushAndClean(session)\n            pls.save()\n\n        return count\n\n    @classmethod\n    def SaveFile(cls, session, prefix):\n        return os.path.join(session.config.workdir, 'kw-journal.dat')\n\n    @classmethod\n    def GetFile(cls, session, sig, mode='r'):\n        try:\n            return (open(cls.SaveFile(session, sig), mode),\n                    'kw-journal.dat')\n        except (IOError, OSError):\n            return (None, 'kw-journal.dat')\n\n    @classmethod\n    def _Append(cls, session, word, mail_ids, compact=True):\n        super(GlobalPostingList, cls)._Append(session, word, mail_ids,\n                                              compact=compact)\n        with GLOBAL_GPL_LOCK:\n            global GLOBAL_GPL\n            sig = cls.WordSig(word, session.config)\n            if GLOBAL_GPL is None:\n                GLOBAL_GPL = {}\n            if sig not in GLOBAL_GPL:\n                GLOBAL_GPL[sig] = set()\n            for mail_id in mail_ids:\n                GLOBAL_GPL[sig].add(mail_id)\n\n    @classmethod\n    def GetMaxMsgIdxPos(cls):\n        with GLOBAL_GPL_LOCK:\n            return cls._unlocked_GetMaxMsgIdxPos()\n\n    @classmethod\n    def _unlocked_GetMaxMsgIdxPos(cls):\n        global GLOBAL_GPL\n        try:\n            max_idx = 0\n            for hits in GLOBAL_GPL.values():\n                if hits:\n                    max_idx = max(max_idx, max(int(id, 36) for id in hits))\n            return max_idx\n        except:\n            return 0\n\n    @classmethod\n    def UpdateMaxMsgMid(cls, session, msg_mids):\n        if len(msg_mids) == 0:\n            return\n        try:\n            msg_idx_pos = max(int(id, 36) for id in msg_mids)\n        except:\n            return\n\n        global GLOBAL_GPL\n        with GLOBAL_GPL_LOCK:\n            max_msg_idx_pos = max(\n                int(id, 36)\n                for id in GLOBAL_GPL.get(GPL_MSGID_TRACKER, set(['0'])))\n            if max_msg_idx_pos and msg_idx_pos <= max_msg_idx_pos:\n                return\n\n            if GLOBAL_GPL is None:\n                GLOBAL_GPL = {}\n            GLOBAL_GPL[GPL_MSGID_TRACKER] = [b36(msg_idx_pos)]\n\n        super(GlobalPostingList, cls)._Append(\n            session, '', GLOBAL_GPL[GPL_MSGID_TRACKER],\n            sig=GPL_MSGID_TRACKER, compact=False)\n\n    def __init__(self, *args, **kwargs):\n        with GLOBAL_GPL_LOCK:\n            OldPostingList.__init__(self, *args, **kwargs)\n            self.lock = GLOBAL_GPL_LOCK\n\n    def _fmt_file(self, prefix):\n        return OldPostingList._fmt_file(self, 'ALL')\n\n    def _compact(self, prefix, output, **kwargs):\n        return prefix, output\n\n    def load(self):\n        with self.lock:\n            self.filename = 'kw-journal.dat'\n            global GLOBAL_GPL\n            if GLOBAL_GPL:\n                self.WORDS = GLOBAL_GPL\n            else:\n                OldPostingList.load(self)\n                GLOBAL_GPL = self.WORDS\n\n    def _migrate(self, sig=None, compact=True):\n        with self.lock:\n            sig = sig or self.sig\n            if sig in GPL_NEVER_MIGRATE:\n                return False\n            if sig in self.WORDS and len(self.WORDS[sig]) > 0:\n                PostingList.Append(self.session, sig, self.WORDS[sig],\n                                   sig=sig, compact=compact)\n                del self.WORDS[sig]\n                return True\n        return False\n\n    def _purge_deleted(self, sig, deleted_sig, deleted_set):\n        if sig in GPL_NEVER_MIGRATE:\n            return False\n        with self.lock:\n            plc = PostingListContainer.Load(self.session, sig)\n            return plc.purge_deleted(deleted_sig, deleted_set)\n\n    def remove(self, eids):\n        PostingList(self.session, self.word).remove(eids).save()\n        return OldPostingList.remove(self, eids)\n\n    def hits(self):\n        return (self.WORDS.get(self.sig, set())\n                | PostingList(self.session, self.word).hits())\n\n    def plc_keys(self):\n        keys = []\n        # FIXME: Scan the posting list directory tree for keys as well\n        return list(keys)\n"
  },
  {
    "path": "mailpile/safe_popen.py",
    "content": "from __future__ import print_function\n#\n# This module implements a safer version of Popen and a safe wrapper around\n# os.pipe(), to avoid deadlocks caused by file descriptors being shared\n# between processes and threads.\n#\n# The subprocess.Popen semantics are changed in the following ways:\n#\n#   * close_fds=True is mandatory on all Unix operating systems\n#   * keep_open=[] can be passed to explicitly keep other FDs open\n#   * preexec_fn will call os.setpgrp on all Unix operating systems\n#\n# On Windows, close_fds and preexec_fn are unavailable in Python 2.7, so\n# instead we do the following:\n#\n#   * close_fds=False, most of the time\n#   * creationflags=CREATE_NEW_PROCESS_GROUP is set\n#   * subprocesses hold a global lock for as long as is \"reasonable\"\n#\n# The os.pipe() wrapper simply makes sure pipe file handles are wrapped\n# in Python file objects so Python's garbage collector and intelligent\n# handling of close() are taken advantage of, and adds a couple of\n# convenience functions and properties to make piping code more readable.\n#\nimport os\nimport subprocess\nimport sys\nimport thread\nimport threading\n\nimport mailpile.platforms\n\n\nUnsafe_Popen = subprocess.Popen\nPIPE = subprocess.PIPE\n\nSERIALIZE_POPEN_STRICT = True\nSERIALIZE_POPEN_ALWAYS = False\nSERIALIZE_POPEN_LOCK = threading.Lock()\n\nTHREAD_LOCAL = threading.local()\n\n\ndef PresetSafePopenArgs(**kwargs):\n    \"\"\"\n    Make it possible to preset Popen arguments, for injecting tweaks into\n    third-party code. We do this using thread-local data, so as to avoid\n    the need for yet another lock.\n    \"\"\"\n    if hasattr(THREAD_LOCAL, 'preset_args'):\n        THREAD_LOCAL.preset_args.append(kwargs)\n    else:\n        THREAD_LOCAL.preset_args = [kwargs]\n\n\nclass Safe_Pipe(object):\n    \"\"\"\n    Creates a pipe consisting of two Python file objects.\n\n    This prevents leaks and prevents weird thread bugs cuased by closing\n    the underlying FD more than once, because Python's objects are smart\n    (as opposed to dumb ints).\n    \"\"\"\n    def __init__(self):\n        p = os.pipe()\n        self.read_end = os.fdopen(p[0], 'r')\n        self.write_end = os.fdopen(p[1], 'w')\n\n    def write(self, *args, **kwargs):\n        return self.write_end.write(*args, **kwargs)\n\n    def read(self, *args, **kwargs):\n        return self.read_end.read(*args, **kwargs)\n\n    def close(self):\n        self.read_end.close()\n        self.write_end.close()\n\n\nclass Safe_Popen(Unsafe_Popen):\n    def _preset_args(self):\n        if hasattr(THREAD_LOCAL, 'preset_args') and THREAD_LOCAL.preset_args:\n            return THREAD_LOCAL.preset_args.pop(-1)\n        else:\n            return {}\n\n    def __init__(self, args, bufsize=0,\n                             executable=None,\n                             stdin=None,\n                             stdout=None,\n                             stderr=None,\n                             preexec_fn=None,\n                             close_fds=None,\n                             shell=False,\n                             cwd=None,\n                             env=None,\n                             universal_newlines=False,\n                             startupinfo=None,\n                             creationflags=None,\n                             keep_open=None,\n                             long_running=False):\n\n        self._internal_fds = []\n\n        # Windows-work around: Console Handles can't be inherited, so if no\n        # source is passed, simulate stdin as a closed pipe. Not ideal, but\n        # stops pythonw crashing.\n        #\n        # See: https://bugs.python.org/issue3905\n        #\n        if stdin is None:\n            stdin = open(os.devnull, 'r')\n            self._internal_fds.append(stdin)\n\n        if stdout is None:\n            stdout = open(os.devnull, 'w')\n            self._internal_fds.append(stdout)\n\n        if stderr is None:\n            stderr = open(os.devnull, 'w')\n            self._internal_fds.append(stderr)\n\n        # This lets us inject Popen args into libraries\n        preset = self._preset_args()\n        if preset: print('PRESET[%s]: %s' % (args, preset))\n        cwd = preset.get('cwd', cwd)\n        env = preset.get('env', env)\n        stdin = preset.get('stdin', stdin)\n        stdour = preset.get('stdout', stdout)\n        stderr = preset.get('stderr', stderr)\n        bufsize = preset.get('bufsize', bufsize)\n        close_fds = preset.get('close_fds', close_fds)\n        executable = preset.get('executable', executable)\n        long_running = preset.get('long_running', long_running)\n\n        # Set our default locking strategy\n        self._SAFE_POPEN_hold_lock = SERIALIZE_POPEN_ALWAYS\n\n        # Raise assertions if people try to explicitly use the API in\n        # an unsafe way.  These all have different meanings on differnt\n        # platforms, so we don't allow the programmer to configure them\n        # at all.\n        if SERIALIZE_POPEN_STRICT:\n            if not ((preexec_fn is None) and\n                    (close_fds is None) and\n                    (startupinfo is None) and\n                    (creationflags is None)):\n                raise AssertionError(\"Unsafe use of POpen API!\")\n\n        # The goal of the following sections is to achieve two things:\n        #\n        #    1. Prevent file descriptor leaks from causing deadlocks\n        #    2. Prevent signals from propagating\n        #\n        if mailpile.platforms.WindowsPopenSemantics():\n            startupinfo = subprocess.STARTUPINFO()\n            startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW\n            creationflags = subprocess.CREATE_NEW_PROCESS_GROUP  # 2.\n            if (stdin is not None or\n                    stdout is not None or\n                    stderr is not None or\n                    keep_open):\n                close_fds = False\n#               self._SAFE_POPEN_hold_lock = True  # 1.\n            else:\n                close_fds = True  # 1.\n\n        else:\n            creationflags = 0\n            if keep_open:\n                # Always leave stdin, stdout and stderr alone so we don't\n                # end up with different assumptions from subprocess.Popen.\n                keep_open.extend([0, 1, 2])\n                for i in range(0, len(keep_open)):\n                    if hasattr(keep_open[i], 'fileno'):\n                        keep_open[i] = keep_open[i].fileno()\n                close_fds = False\n            else:\n                close_fds = True  # 1.\n\n            def pre_exec_magic():\n                try:\n                    os.setpgrp()  # 2.\n                except (OSError, NameError):\n                    pass\n                # FIXME: some platforms may give us more FDs...\n                if not close_fds:\n                    for i in set(range(0, 1024)) - set(keep_open):\n                        try:\n                            os.close(i)\n                        except OSError:\n                            pass\n\n            preexec_fn = pre_exec_magic\n\n        if self._SAFE_POPEN_hold_lock:\n            SERIALIZE_POPEN_LOCK.acquire()\n        try:\n            Unsafe_Popen.__init__(self, args,\n                                  bufsize=bufsize,\n                                  executable=executable,\n                                  stdin=stdin,\n                                  stdout=stdout,\n                                  stderr=stderr,\n                                  preexec_fn=preexec_fn,\n                                  close_fds=close_fds,\n                                  shell=shell,\n                                  cwd=cwd,\n                                  env=env,\n                                  universal_newlines=universal_newlines,\n                                  startupinfo=startupinfo,\n                                  creationflags=creationflags)\n        except:\n            self._SAFE_POPEN_unlock()\n            raise\n\n        if long_running:\n            self._SAFE_POPEN_unlock()\n\n    def _SAFE_POPEN_unlock(self):\n        if self._SAFE_POPEN_hold_lock:\n            self._SAFE_POPEN_hold_lock = False\n            try:\n                SERIALIZE_POPEN_LOCK.release()\n            except thread.error:\n                pass\n\n    def communicate(self, *args, **kwargs):\n        rv = Unsafe_Popen.communicate(self, *args, **kwargs)\n        self._SAFE_POPEN_unlock()\n        return rv\n\n    def wait(self, *args, **kwargs):\n        rv = Unsafe_Popen.wait(self, *args, **kwargs)\n        self._SAFE_POPEN_unlock()\n        return rv\n\n    def __del__(self):\n        for handle in self._internal_fds:\n            handle.close()\n        if Unsafe_Popen is not None:\n            Unsafe_Popen.__del__(self)\n        self._SAFE_POPEN_unlock()\n\n\n# This is a vain attempt to monkeypatch, whether it works or not will\n# depend on module load order.\ndef MakePopenUnsafe():\n    subprocess.Popen = Unsafe_Popen\n\n\ndef MakePopenSafe():\n    THREAD_LOCAL.preset_args = []\n    subprocess.Popen = Safe_Popen\n    return Safe_Popen\n\nPopen = MakePopenSafe()\n"
  },
  {
    "path": "mailpile/search.py",
    "content": "from __future__ import print_function\nimport cStringIO\nimport email\nimport random\nimport re\nimport rfc822\nimport time\nimport threading\nimport traceback\nfrom urllib import quote, unquote\n\nimport mailpile.util\nfrom mailpile.crypto.gpgi import GnuPG\nfrom mailpile.crypto.state import CryptoInfo, SignatureInfo, EncryptionInfo\nfrom mailpile.crypto.streamer import EncryptingStreamer\nfrom mailpile.eventlog import GetThreadEvent\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.index.base import BaseIndex\nfrom mailpile.index.search import SearchResultSet, CachedSearchResultSet\nfrom mailpile.plugins import PluginManager\nfrom mailpile.mailutils import FormatMbxId, MBX_ID_LEN, NoSuchMailboxError\nfrom mailpile.mailutils.addresses import AddressHeaderParser\nfrom mailpile.mailutils.emails import ExtractEmails, ExtractEmailAndName\nfrom mailpile.mailutils.emails import Email, ParseMessage, GetTextPayload\nfrom mailpile.mailutils.header import decode_header\nfrom mailpile.mailutils.headerprint import HeaderPrints\nfrom mailpile.mailutils.html import extract_text_from_html\nfrom mailpile.mailutils.safe import *\nfrom mailpile.postinglist import GlobalPostingList\nfrom mailpile.ui import *\nfrom mailpile.util import *\nfrom mailpile.vfs import vfs, FilePath\n\n\n_plugins = PluginManager()\n\n\nclass MailIndex(BaseIndex):\n    \"\"\"This is a lazily parsing object representing a mailpile index.\"\"\"\n\n    # Parameters that set threshold to rewrite complete metadata index file.\n    MIN_ITEMS_PER_INCREMENT = 200   # Limits number of appends.\n    MIN_ITEMS_PER_DUPLICATE = 10    # Limits number of duplicated MSG_MIDs.\n    ITEM_COUNT_OFFSET = 5000        # Puts lower bound on limits.\n    # Appends start with a comment including this so they can be counted.\n    APPEND_MARK = '-----APPENDED SECTION-----'\n\n    MAX_CACHE_ENTRIES = 2500\n    CAPABILITIES = set([\n        BaseIndex.CAN_SEARCH,\n        BaseIndex.CAN_SORT,\n        BaseIndex.HAS_UNREAD,\n        BaseIndex.HAS_ATTS,\n        BaseIndex.HAS_TAGS])\n\n    def __init__(self, config):\n        BaseIndex.__init__(self, config)\n        self.interrupt = None\n        self.loaded_index = False\n        self.INDEX = []\n        self.INDEX_SORT = {}\n        self.INDEX_THR = []\n        self.PTRS = {}\n        self.TAGS = {}\n        self.MSGIDS = {}\n        self.MODIFIED = set()\n        self.EMAILS_SAVED = 0\n        self._scanned = {}\n        self._saved_changes = 0\n        self._saved_lines = 0\n        self._lock = SearchRLock()\n        self._save_lock = SearchRLock()\n        self._prepare_sorting()\n        self._url_re_cache = {}\n\n    @classmethod\n    def l2m(self, line):\n        return line.decode('utf-8').split(u'\\t')\n\n    # A translation table for message parts stored in the index, consists of\n    # a mapping from unicode ordinals to either another unicode ordinal or\n    # None, to remove a character. By default it removes the ASCII control\n    # characters and replaces tabs and newlines with spaces.\n    NORM_TABLE = dict([(i, None) for i in range(0, 0x20)], **{\n        ord(u'\\t'): ord(u' '),\n        ord(u'\\r'): ord(u' '),\n        ord(u'\\n'): ord(u' '),\n        0x7F: None\n    })\n\n    @classmethod\n    def m2l(self, message):\n        # Normalize the message before saving it so we can be sure that we will\n        # be able to read it back later.\n        parts = [unicode(p).translate(self.NORM_TABLE) for p in message]\n        return (u'\\t'.join(parts)).encode('utf-8')\n\n    def load(self, session=None):\n        self.INDEX = []\n        self.CACHE = {}\n        self.PTRS = {}\n        self.MSGIDS = {}\n        self.EMAILS = []\n        self.EMAIL_IDS = {}\n        CachedSearchResultSet.DropCaches()\n        bogus_lines = []\n\n        def process_lines(lines):\n            for line in lines:\n                line = line.strip()\n                if line[:1] in ('#', ''):\n                    if self.APPEND_MARK in line:\n                        self._saved_changes += 1\n                elif line[:1] == '@':\n                    try:\n                        pos, email = line[1:].split('\\t', 1)\n                        pos = int(pos, 36)\n                        while len(self.EMAILS) < pos + 1:\n                            self.EMAILS.append('')\n                        unquoted_email = unquote(email).decode('utf-8')\n                        self.EMAILS[pos] = unquoted_email\n                        self.EMAIL_IDS[unquoted_email.split()[0].lower()] = pos\n                        self._saved_lines += 1\n                    except (ValueError, IndexError, TypeError):\n                        bogus_lines.append(line)\n                else:\n                    bogus = False\n                    words = line.split('\\t')\n\n                    # Migration: converting old metadata into new!\n                    if len(words) != self.MSG_FIELDS_V2:\n\n                        # V1 -> V2 adds MSG_CC and MSG_KB\n                        if len(words) == self.MSG_FIELDS_V1:\n                            words[self.MSG_CC:self.MSG_CC] = ['']\n                            words[self.MSG_KB:self.MSG_KB] = ['0']\n\n                        # Add V2 -> V3 here, etc. etc.\n\n                        if len(words) == self.MSG_FIELDS_V2:\n                            line = '\\t'.join(words)\n                        else:\n                            bogus = True\n\n                    if not bogus:\n                        try:\n                            pos = int(words[self.MSG_MID], 36)\n                            self.set_msg_at_idx_pos(pos, words,\n                                                    original_line=line)\n                            if session and len(self.INDEX) % 107 == 100:\n                                session.ui.mark(\n                                    _('Loading metadata index...') +\n                                    ' %s' % len(self.INDEX))\n                            self._saved_lines += 1\n                        except ValueError:\n                            bogus = True\n\n                    if bogus:\n                        bogus_lines.append(line)\n                        if len(bogus_lines) > max(0.02 * len(self.INDEX), 50):\n                            raise Exception(_('Your metadata index is '\n                                              'either too old, too new '\n                                              'or corrupt!'))\n                        elif session and 1 == len(bogus_lines) % 100:\n                            session.ui.error(_('Corrupt data in metadata '\n                                               'index! Trying to cope...'))\n\n        if session:\n            session.ui.mark(_('Loading metadata index...'))\n        try:\n            import mailpile.mail_source\n            with self._save_lock, self._lock:\n                with open(self.config.mailindex_file(), 'r') as fd:\n                    # We don't raise on errors, in case only some of the chunks\n                    # are corrupt - we want to read the rest of them.\n                    errors = 0\n                    def warn(offset):\n                        if session:\n                            session.ui.error('WARNING: Failed to decrypt '\n                                             'block of index ending at %d'\n                                             % offset)\n                    # FIXME: Differentiate between partial index and no index?\n                    gpgi = GnuPG(self.config, event=GetThreadEvent())\n                    decrypt_and_parse_lines(fd, process_lines, self.config,\n                        newlines=True, decode=False, gpgi=gpgi,\n                        _raise=False, error_cb=warn)\n        except IOError:\n            if session:\n                session.ui.warning(_('Metadata index not found: %s'\n                                     ) % self.config.mailindex_file())\n\n        session.ui.mark(_('Loading global posting list...'))\n        GlobalPostingList(session, '')\n\n        if bogus_lines:\n            bogus_file = (self.config.mailindex_file() +\n                          '.bogus.%x' % time.time())\n            with open(bogus_file, 'w') as bl:\n                bl.write('\\n'.join(bogus_lines))\n            if session:\n                session.ui.warning(_('Recovered! Wrote bad metadata to: %s'\n                                     ) % bogus_file)\n\n        if session:\n            session.ui.mark(_n('Loaded metadata, %d message',\n                               'Loaded metadata, %d messages',\n                               len(self.INDEX)\n                               ) % len(self.INDEX))\n        self.EMAILS_SAVED = len(self.EMAILS)\n\n        # Make sure metadata has entry for every msg_mid in keyword index.\n        max_kw_msg_idx_pos = GlobalPostingList.GetMaxMsgIdxPos()\n        if max_kw_msg_idx_pos and max_kw_msg_idx_pos >= len(self.INDEX):\n            new_max_msg_idx_pos = max_kw_msg_idx_pos + 1\n            if session:\n                session.ui.warning(\n                    _('Fixing %d messages in keyword index not in metadata.'\n                      ) % (new_max_msg_idx_pos - len(self.INDEX)))\n\n            for msg_idx_pos in range(len(self.INDEX), new_max_msg_idx_pos):\n                self.add_new_ghost(b36(msg_idx_pos), trash=True)\n\n        self.loaded_index = True\n\n    def update_msg_tags(self, msg_idx_pos, msg_info):\n        tags = set(self.get_tags(msg_info=msg_info))\n        with self._lock:\n            for tid in (set(self.TAGS.keys()) - tags):\n                self.TAGS[tid] -= set([msg_idx_pos])\n            for tid in tags:\n                if tid not in self.TAGS:\n                    self.TAGS[tid] = set()\n                self.TAGS[tid].add(msg_idx_pos)\n\n    def _maybe_encrypt(self, data):\n        gpgr = self.config.prefs.gpg_recipient\n        tokeys = ([gpgr]\n                  if gpgr not in (None, '', '!CREATE', '!PASSWORD')\n                  else None)\n\n        if self.config.get_master_key():\n            with EncryptingStreamer(self.config.get_master_key(),\n                                    delimited=True) as es:\n                es.write(data)\n                es.finish()\n                return es.save(None)\n\n        elif tokeys:\n            stat, edata = GnuPG(self.config, event=GetThreadEvent()\n                                ).encrypt(data, tokeys=tokeys)\n            if stat == 0:\n                return edata\n\n        return data\n\n    def save_changes(self, session=None):\n        self._save_lock.acquire()\n        try:\n            # In a locked section, check what needs to be done!\n            with self._lock:\n                mods, self.MODIFIED = self.MODIFIED, set()\n                old_emails_saved, total = self.EMAILS_SAVED, len(self.EMAILS)\n                index_items = total + len(self.INDEX)\n\n            if old_emails_saved == total and not mods:\n                # Nothing to do...\n                return\n\n            max_incremental_saves = (index_items + self.ITEM_COUNT_OFFSET\n                                    ) / self.MIN_ITEMS_PER_INCREMENT\n            max_saved_lines = index_items + (\n                                      index_items + self.ITEM_COUNT_OFFSET\n                                    ) / self.MIN_ITEMS_PER_DUPLICATE\n\n            if (not os.path.isfile(self.config.mailindex_file())\n                    or ((self._saved_changes > max_incremental_saves\n                           or self._saved_lines > max_saved_lines)\n                        and not mailpile.util.QUITTING)):\n                # Write a new metadata index file.\n                return self.save(session=session)\n\n            if session:\n                session.ui.mark(_(\"Saving metadata index changes...\"))\n\n            # In a locked section we just prepare our data\n            with self._lock:\n                emails = []\n                for eid in range(old_emails_saved, total):\n                    quoted_email = quote(self.EMAILS[eid].encode('utf-8'))\n                    emails.append('@%s\\t%s\\n' % (b36(eid), quoted_email))\n                self.EMAILS_SAVED = total\n\n            # Unlocked, try to write this out, prepending append mark comment.\n\n            data = self._maybe_encrypt(''.join(\n                ['#' + self.APPEND_MARK + '\\n']\n                + emails\n                + [self.INDEX[pos] + '\\n' for pos in mods]))\n            with open(self.config.mailindex_file(), 'a') as fd:\n                fd.write(data)\n                self._saved_changes += 1\n                self._saved_lines += total - old_emails_saved + len(mods)\n\n            if session:\n                session.ui.mark(_(\"Saved metadata index changes\"))\n        except:\n            # Failed, roll back...\n            self.MODIFIED |= mods\n            self.EMAILS_SAVED = old_emails_saved\n            raise\n        finally:\n            self._save_lock.release()\n\n    def save(self, session=None):\n        try:\n            self._save_lock.acquire()\n            with self._lock:\n                old_mods, self.MODIFIED = self.MODIFIED, set()\n                old_emails_saved = self.EMAILS_SAVED\n\n            if session:\n                session.ui.mark(_(\"Saving metadata index...\"))\n\n            idxfile = self.config.mailindex_file()\n            newfile = '%s.new' % idxfile\n\n            data = [\n                '# This is the mailpile.py index file.\\n',\n                '# We have %d messages!\\n' % len(self.INDEX)\n            ]\n            self.EMAILS_SAVED = email_counter = len(self.EMAILS)\n            for eid in range(0, email_counter):\n                quoted_email = quote(self.EMAILS[eid].encode('utf-8'))\n                data.append('@%s\\t%s\\n' % (b36(eid), quoted_email))\n            index_counter = len(self.INDEX)\n            for i in range(0, index_counter):\n                data.append(self.INDEX[i] + '\\n')\n\n            data = self._maybe_encrypt(''.join(data))\n            with open(newfile, 'w') as fd:\n                fd.write(data)\n\n            # Keep the last 5 index files around... just in case.\n            backup_file(idxfile, backups=5, min_age_delta=10)\n            os.rename(newfile, idxfile)\n\n            self._saved_changes = 0\n            self._saved_lines = email_counter + index_counter\n            if session:\n                session.ui.mark(_(\"Saved metadata index\"))\n        except:\n            # Failed, roll back...\n            with self._lock:\n                self.MODIFIED |= old_mods\n                self.EMAILS_SAVED = old_emails_saved\n            raise\n        finally:\n            self._save_lock.release()\n\n    def update_ptrs_and_msgids(self, session):\n        session.ui.mark(_('Updating high level indexes'))\n        with self._lock:\n            self.PTRS = {}\n            self.MSGIDS = {}\n            for offset in range(0, len(self.INDEX)):\n                message = self.l2m(self.INDEX[offset])\n                if len(message) == self.MSG_FIELDS_V2:\n                    self.MSGIDS[message[self.MSG_ID]] = offset\n                    for msg_ptr in message[self.MSG_PTRS].split(','):\n                        if msg_ptr:\n                            self.PTRS[msg_ptr] = offset\n                else:\n                    session.ui.warning(_('Bogus line: %s') % line)\n\n    def _remove_location(self, session, msg_ptr):\n        msg_idx_pos = self.PTRS[msg_ptr]\n        del self.PTRS[msg_ptr]\n\n        msg_info = self.get_msg_at_idx_pos(msg_idx_pos)\n        msg_ptrs = [p for p in msg_info[self.MSG_PTRS].split(',')\n                    if p and p != msg_ptr]\n\n        msg_info[self.MSG_PTRS] = ','.join(msg_ptrs)\n        self.set_msg_at_idx_pos(msg_idx_pos, msg_info)\n\n    def _update_location(self, session, msg_idx_pos, msg_ptr):\n        if 'rescan' in session.config.sys.debug:\n            session.ui.debug('Duplicate? %s -> %s' % (b36(msg_idx_pos), msg_ptr))\n\n        msg_info = self.get_msg_at_idx_pos(msg_idx_pos)\n        msg_ptrs = msg_info[self.MSG_PTRS].split(',')\n\n        # New location! Some other process will prune obsolete pointers.\n        if msg_ptr:\n            self.PTRS[msg_ptr] = msg_idx_pos\n            msg_ptrs.append(msg_ptr)\n\n        msg_info[self.MSG_PTRS] = ','.join(list(set(msg_ptrs)))\n        self.set_msg_at_idx_pos(msg_idx_pos, msg_info)\n        return msg_info\n\n    def _extract_date_ts(self, session, msg_mid, msg_id, msg, default):\n        \"\"\"Extract a date, sanity checking against the Received: headers.\"\"\"\n        return (safe_message_ts(\n            msg,\n            default=default,\n            msg_mid=msg_mid,\n            msg_id=msg_id,\n            session=session) or int(time.time()-1))\n\n    def _get_scan_progress(self, mailbox_idx, event=None, reset=False):\n        if event:\n            if 'rescans' not in event.data:\n                event.data['rescans'] = []\n            if reset:\n                event.data['rescan'] = {}\n            progress = event.data.get('rescan', {})\n            if not progress.keys():\n                reset = True\n        else:\n            progress = {}\n            reset = True\n        if reset:\n            progress.update({\n                'running': True,\n                'complete': False,\n                'mailbox_id': mailbox_idx,\n                'errors': [],\n                'added': 0,\n                'updated': 0,\n                'total': 0,\n                'batch_size': 0\n            })\n        return progress\n\n    def scan_mailbox(self, session, mailbox_idx, mailbox_fn, mailbox_opener,\n                     process_new=None, apply_tags=None, editable=False,\n                     stop_after=None, deadline=None, reverse=False, lazy=False,\n                     event=None, force=False):\n        mailbox_idx = FormatMbxId(mailbox_idx)\n        progress = self._get_scan_progress(mailbox_idx,\n                                           event=event, reset=True)\n\n        def finito(code, message, **kwargs):\n            if event:\n                event.data['rescans'].append(\n                    (mailbox_idx, code, message, kwargs))\n                progress['running'] = False\n                if 'complete' in kwargs:\n                    progress['complete'] = kwargs['complete']\n            if 'rescan' in session.config.sys.debug:\n                session.ui.debug(message)\n            session.ui.mark(message)\n            return code\n\n        try:\n            mbox = mailbox_opener(session, mailbox_idx)\n            if mbox.editable != editable:\n                return finito(0, _('%s: Skipped: %s'\n                                   ) % (mailbox_idx, mailbox_fn))\n            else:\n                session.ui.mark(_('%s: Checking: %s'\n                                  ) % (mailbox_idx, mailbox_fn))\n                mbox.update_toc()\n        except (IOError, OSError, ValueError, NoSuchMailboxError) as e:\n            if 'rescan' in session.config.sys.debug:\n                session.ui.debug(traceback.format_exc())\n            return finito(-1, _('%s: Error opening: %s (%s)'\n                                ) % (mailbox_idx, mailbox_fn, e),\n                          error=True)\n\n        if len(self.PTRS.keys()) == 0:\n            self.update_ptrs_and_msgids(session)\n\n        messages = sorted(mbox.keys())\n        messages_md5 = md5_hex(str(messages))\n        mbox_version = mbox.last_updated()\n        if (not force) and messages_md5 == self._scanned.get(mailbox_idx, ''):\n            return finito(0, _('%s: No new mail in: %s'\n                               ) % (mailbox_idx, mailbox_fn),\n                          complete=True)\n\n        parse_fmt1 = _('%s: Reading your mail: %d%% (%d/%d message)')\n        parse_fmtn = _('%s: Reading your mail: %d%% (%d/%d messages)')\n        def parse_status(ui):\n            n = len(messages)\n            return ((n == 1) and parse_fmt1 or parse_fmtn\n                    ) % (mailbox_idx, 100 * ui / n, ui, n)\n\n        start_time = time.time()\n        progress.update({\n            'total': len(messages),\n            'batch_size': stop_after or len(messages)\n        })\n\n        added = updated = 0\n        last_date = long(start_time)\n        not_done_yet = 'NOT DONE YET'\n        if reverse:\n            messages.reverse()\n        for ui in range(0, len(messages)):\n            if mailpile.util.QUITTING or self.interrupt:\n                ir, self.interrupt = self.interrupt, None\n                return finito(-1, _('Rescan interrupted: %s') % ir)\n            if stop_after and added >= stop_after:\n                messages_md5 = not_done_yet\n                break\n            elif deadline and time.time() > deadline:\n                messages_md5 = not_done_yet\n                break\n            elif mbox_version != mbox.last_updated():\n                messages_md5 = not_done_yet\n                break\n\n            i = messages[ui]\n            msg_ptr = mbox.get_msg_ptr(mailbox_idx, i)\n            if msg_ptr in self.PTRS:\n                if not lazy:\n                    msg_info = self.get_msg_at_idx_pos(self.PTRS[msg_ptr])\n                    msg_body = msg_info[self.MSG_BODY]\n                if lazy or (msg_body not in self.MSG_BODY_UNSCANNED):\n                    if (ui % 1129) == 0:\n                        session.ui.mark(parse_status(ui))\n                    continue\n\n            session.ui.mark(parse_status(ui))\n            if (ui % 127) == 0 and not force:\n                play_nice_with_threads()\n\n            # Message new or modified, let's parse it.\n            try:\n                last_date, a, u = self.scan_one_message(session,\n                                                        mailbox_idx, mbox, i,\n                                                        wait=True,\n                                                        msg_ptr=msg_ptr,\n                                                        last_date=last_date,\n                                                        process_new=process_new,\n                                                        apply_tags=apply_tags,\n                                                        stop_after=stop_after,\n                                                        editable=editable,\n                                                        event=event,\n                                                        progress=progress,\n                                                        lazy=lazy)\n            except TypeError:\n                a = u = 0\n\n            added += a\n            updated += u\n\n        if not lazy:\n            # Figure out which messages exist at all, and remove stale pointers.\n            # This happens last and in a locked section, because other threads may\n            # have added messages while we were busy with other things.\n            with self._lock:\n                messages = sorted(mbox.keys())  # Update this, in case it changed\n                existing_ptrs = set()\n                for ui in range(0, len(messages)):\n                    msg_ptr = mbox.get_msg_ptr(mailbox_idx, messages[ui])\n                    existing_ptrs.add(msg_ptr)\n                for msg_ptr in self.PTRS.keys():\n                    if (msg_ptr[:MBX_ID_LEN] == mailbox_idx and\n                            msg_ptr not in existing_ptrs):\n                        self._remove_location(session, msg_ptr)\n                        updated += 1\n        if not force:\n            play_nice_with_threads()\n\n        progress.update({\n            'added': added,\n            'updated': updated})\n\n        self._scanned[mailbox_idx] = messages_md5\n        short_fn = '/'.join(mailbox_fn.split('/')[-2:])\n        return finito(added,\n                      _('%s: Indexed mailbox: ...%s (%d new, %d updated)'\n                        ) % (mailbox_idx, short_fn, added, updated),\n                      new=added,\n                      updated=updated,\n                      complete=(messages_md5 != not_done_yet))\n\n    def scan_one_message(self, session, mailbox_idx, mbox, msg_mbox_key,\n                         wait=False, **kwargs):\n        args = [session, mailbox_idx, mbox, msg_mbox_key]\n        task = 'scan:%s/%s' % (mailbox_idx, msg_mbox_key)\n        if wait:\n            return session.config.scan_worker.do(\n                session, task, lambda: self._real_scan_one(*args, **kwargs))\n        else:\n            session.config.scan_worker.add_task(\n                session, task, lambda: self._real_scan_one(*args, **kwargs))\n            return 0, 0, 0\n\n    def _real_scan_one(self, session,\n                       mailbox_idx, mbox, msg_mbox_idx,\n                       msg_ptr=None, msg_data=None, msg_metadata_kws=None,\n                       last_date=None,\n                       process_new=None, apply_tags=None, stop_after=None,\n                       editable=False, event=None, progress=None,\n                       lazy=False):\n        with self._lock:\n            # This is not actually a critical section, this is more of a belt and\n            # suspenders thing to encourage serialization of scanning processes.\n            msg_ptr = msg_ptr or mbox.get_msg_ptr(mailbox_idx, msg_mbox_idx)\n\n        added = updated = 0\n        last_date = last_date or long(time.time())\n        progress = progress or self._get_scan_progress(mailbox_idx,\n                                                       event=event)\n\n        if 'rescan' in session.config.sys.debug:\n            session.ui.debug('Reading message %s/%s'\n                             % (mailbox_idx, msg_mbox_idx))\n        try:\n            if msg_data:\n                msg_fd = cStringIO.StringIO(msg_data)\n                msg_metadata_kws = msg_metadata_kws or []\n            elif lazy:\n                msg_data = mbox.get_bytes(msg_mbox_idx, 10240)\n                msg_data = msg_data.split('\\r\\n\\r\\n')[0].split('\\n\\n')[0]\n                msg_fd = cStringIO.StringIO(msg_data)\n                msg_bytes = mbox.get_msg_size(msg_mbox_idx)\n                msg_metadata_kws = mbox.get_metadata_keywords(msg_mbox_idx)\n            else:\n                msg_fd = mbox.get_file(msg_mbox_idx)\n                msg_metadata_kws = mbox.get_metadata_keywords(msg_mbox_idx)\n\n            msg = ParseMessage(msg_fd,\n                pgpmime=(session.config.prefs.index_encrypted and 'all'),\n                config=session.config)\n            if not lazy:\n                msg_bytes = msg_fd.tell()\n\n        except (IOError, OSError, ValueError, IndexError, KeyError):\n            if session.config.sys.debug:\n                traceback.print_exc()\n            progress['errors'].append(msg_mbox_idx)\n            session.ui.warning(('Reading message %s/%s FAILED, skipping'\n                                ) % (mailbox_idx, msg_mbox_idx))\n            return last_date, added, updated\n\n        msg_snippet = msg_info = None\n        msg_id = self.get_msg_id(msg, msg_ptr)\n        if msg_id in self.MSGIDS:\n            with self._lock:\n                msg_info = self._update_location(session,\n                                                 self.MSGIDS[msg_id],\n                                                 msg_ptr)\n                msg_snippet = msg_info[self.MSG_BODY]\n                updated += 1\n\n        rescan_body = (not lazy) and msg_snippet in self.MSG_BODY_UNSCANNED\n        if rescan_body or msg_id not in self.MSGIDS:\n            lazy_body = self.MSG_BODY_LAZY if lazy else None\n            msg_info = self._index_incoming_message(\n                session, msg_id, msg_ptr, msg_bytes,\n                msg, msg_metadata_kws,\n                last_date + 1, mailbox_idx, process_new, apply_tags,\n                lazy_body, msg_info)\n            last_date = long(msg_info[self.MSG_DATE], 36)\n            added += 1\n\n        progress['added'] = progress.get('added', 0) + added\n        progress['updated'] = progress.get('updated', 0) + updated\n        return last_date, added, updated\n\n    def edit_msg_info(self, msg_info,\n                      msg_mid=None, raw_msg_id=None, msg_id=None, msg_ts=None,\n                      msg_from=None, msg_subject=None, msg_body=None,\n                      msg_to=None, msg_cc=None, msg_size=None,\n                      msg_tags=None, msg_replies=None,\n                      msg_parent_mid=None, msg_thread_mid=None):\n        if msg_mid is not None:\n            msg_info[self.MSG_MID] = msg_mid\n        if raw_msg_id is not None:\n            msg_info[self.MSG_ID] = self._encode_msg_id(raw_msg_id)\n        if msg_id is not None:\n            msg_info[self.MSG_ID] = msg_id\n        if msg_ts is not None:\n            msg_info[self.MSG_DATE] = b36(msg_ts)\n        if msg_from is not None:\n            msg_info[self.MSG_FROM] = msg_from\n        if msg_subject is not None:\n            msg_info[self.MSG_SUBJECT] = msg_subject\n        if msg_body is not None:\n            msg_info[self.MSG_BODY] = msg_body\n        if msg_size is not None:\n            msg_info[self.MSG_KB] = b36(int(msg_size) // 1024)\n        if msg_to is not None:\n            msg_info[self.MSG_TO] = self.compact_to_list(msg_to or [])\n        if msg_cc is not None:\n            msg_info[self.MSG_CC] = self.compact_to_list(msg_cc or [])\n        if msg_tags is not None:\n            msg_info[self.MSG_TAGS] = ','.join(msg_tags or [])\n        if msg_replies is not None:\n            if len(msg_replies) > 1:\n                msg_info[self.MSG_REPLIES] = ','.join(msg_replies) + ','\n            else:\n                msg_info[self.MSG_REPLIES] = ''\n\n        if msg_parent_mid is not None or msg_thread_mid is not None:\n            if msg_thread_mid is None:\n                msg_thread_mid = msg_info[self.MSG_THREAD_MID].split('/')[0]\n            if msg_parent_mid is None:\n                msg_parent_mid = msg_info[self.MSG_THREAD_MID].split('/')[-1]\n            if msg_thread_mid != msg_parent_mid:\n                msg_info[self.MSG_THREAD_MID] = (\n                    '%s/%s' % (msg_thread_mid, msg_parent_mid))\n            else:\n                msg_info[self.MSG_THREAD_MID] = msg_thread_mid\n\n        return msg_info\n\n    def _extract_header_info(self, msg):\n        # FIXME: this stuff is actually pretty weak!\n        msg_to = AddressHeaderParser(msg.get('to', ''))\n        msg_cc = AddressHeaderParser(msg.get('cc', ''))\n        msg_cc += AddressHeaderParser(msg.get('bcc', ''))  # Usually a noop\n        msg_subj = safe_decode_hdr(msg, 'subject')\n        return msg_to, msg_cc, msg_subj\n\n    # FIXME: Finish merging this function with the one below it...\n    def _extract_info_and_index(self, session, mailbox_idx,\n                                msg_mid, msg_id,\n                                msg_size, msg, msg_metadata_kws,\n                                default_date,\n                                **index_kwargs):\n\n        msg_ts = self._extract_date_ts(session, msg_mid, msg_id, msg,\n                                       default_date)\n\n        msg_to, msg_cc, msg_subj = self._extract_header_info(msg)\n\n        filters = _plugins.get_filter_hooks([self.filter_keywords])\n        kw, bi = self.index_message(session, msg_mid, msg_id,\n                                    msg, msg_metadata_kws, msg_size, msg_ts,\n                                    mailbox=mailbox_idx,\n                                    compact=False,\n                                    filter_hooks=filters,\n                                    **index_kwargs)\n\n        snippet_max = session.config.sys.snippet_max\n        self.truncate_body_snippet(bi, max(0, snippet_max - len(msg_subj)))\n        msg_body = self.encode_body(bi)\n\n        tags = [k.split(':')[0] for k in kw\n                if k.endswith(':in') or k.endswith(':tag')]\n\n        return (msg_ts, msg_to, msg_cc, msg_subj, msg_body, tags)\n\n    def _index_incoming_message(self, session,\n                                msg_id, msg_ptr, msg_size,\n                                msg, msg_metadata_kws, default_date,\n                                mailbox_idx, process_new, apply_tags,\n                                lazy_body, msg_info):\n        if lazy_body:\n            msg_ts = self._extract_date_ts(session, 'new', msg_id, msg,\n                                           default_date)\n            msg_to, msg_cc, msg_subj = self._extract_header_info(msg)\n            msg_idx_pos, msg_info = self.add_new_msg(\n                msg_ptr, msg_id, msg_ts,\n                safe_decode_hdr(msg, 'from'), msg_to, msg_cc, msg_size,\n                msg_subj, lazy_body, [])\n\n        else:\n            # If necessary, add the message to the index so we can index\n            # terms to the right MID.\n            if msg_info:\n                msg_mid = msg_info[self.MSG_MID]\n                msg_idx_pos = int(msg_mid, 36)\n            else:\n                msg_idx_pos, msg_info = self.add_new_msg(\n                    msg_ptr, msg_id, default_date,\n                    '', [], [], 0, _('(processing message ...)'),\n                    self.MSG_BODY_GHOST, [])\n                msg_mid = b36(msg_idx_pos)\n\n            # Parse and index\n            (msg_ts, msg_to, msg_cc, msg_subj, msg_body, tags\n             ) = self._extract_info_and_index(session, mailbox_idx,\n                                              msg_mid, msg_id, msg_size,\n                                              msg, msg_metadata_kws,\n                                              default_date,\n                                              process_new=process_new,\n                                              apply_tags=apply_tags,\n                                              incoming=True)\n\n            # Finally, update the metadata index with whatever we learned\n            self.edit_msg_info(msg_info,\n                               msg_from=safe_decode_hdr(msg, 'from'),\n                               msg_ts=msg_ts,\n                               msg_to=msg_to,\n                               msg_cc=msg_cc,\n                               msg_subject=msg_subj,\n                               msg_body=msg_body,\n                               msg_size=msg_size,\n                               msg_tags=tags)\n            self.set_msg_at_idx_pos(msg_idx_pos, msg_info)\n\n        self.set_conversation_ids(msg_info[self.MSG_MID], msg)\n        return msg_info\n\n    def index_email(self, session, email):\n        # Extract info from the email object...\n        msg = email.get_msg(\n            pgpmime=(session.config.prefs.index_encrypted and 'all'),\n            crypto_state_feedback=False)\n        msg_mid = email.msg_mid()\n        msg_info = email.get_msg_info()\n        msg_size = email.get_msg_size()\n        msg_metadata_kws = email.get_metadata_kws()\n        msg_id = msg_info[self.MSG_ID]\n        mailbox_idx = msg_info[self.MSG_PTRS].split(',')[0][:MBX_ID_LEN]\n        default_date = long(msg_info[self.MSG_DATE], 36)\n\n        (msg_ts, msg_to, msg_cc, msg_subj, msg_body, tags\n         ) = self._extract_info_and_index(session, mailbox_idx,\n                                          msg_mid, msg_id, msg_size,\n                                          msg, msg_metadata_kws,\n                                          default_date,\n                                          incoming=False)\n        self.edit_msg_info(msg_info,\n                           msg_ts=msg_ts,\n                           msg_from=safe_decode_hdr(msg, 'from'),\n                           msg_to=msg_to,\n                           msg_cc=msg_cc,\n                           msg_subject=msg_subj,\n                           msg_body=msg_body)\n\n        self.set_msg_at_idx_pos(email.msg_idx_pos, msg_info)\n\n        # Reset the internal tags on this message\n        for tag_id in self.get_tags(msg_info=msg_info):\n            tag = session.config.get_tag(tag_id)\n            if tag and tag.slug.startswith('mp_'):\n                self.remove_tag(session, tag_id, msg_idxs=[email.msg_idx_pos])\n\n        # Update conversation threading\n        self.set_conversation_ids(msg_info[self.MSG_MID], msg,\n                                  subject_threading=False)\n\n        # Add normal tags implied by a rescan\n        for tag_id in tags:\n            self.add_tag(session, tag_id, msg_idxs=[email.msg_idx_pos])\n\n    def set_conversation_ids(self, msg_mid, msg, subject_threading=True):\n        \"\"\"\n        This method will calculate/update the thread-ID and parent-ID of a\n        given message. Mailpile will group all messages in a thread\n        together as \"replies\" to the root message; but it will also keep track\n        of what is the immediate parent of any given mail.\n\n        Note that the \"root\" message may not be the actual root of the thread;\n        it's just whatever message fit the bill at the time.\n\n        See https://www.jwz.org/doc/threading.html for a very nice discussion\n        and threading algorithm which we're not using, but wish we could.\n        \"\"\"\n        # We are looking for two things: an immediate parent, and the\n        # conversation we're linked to. Since messages may arrive out of\n        # order, our strategy is as follows:\n        #\n        # 1. To determine immediate parent, look at in-reply-to or the\n        #    last entry in references.\n        #\n        # 2. If any messages in References (or In-Reply-To) don't exist,\n        #    create a ghost for the closest missing ancestor.\n        #\n        # 3. To determine converstation, look at In-Reply-To and References,\n        #    merge all conversations we find into one, including us. Note\n        #    that if a message in References has been \"unthreaded\", we should\n        #    ignore all previous ones.\n        #\n        # Step two is a compromise; for the best possible threading we would\n        # create a ghost for each missing reference; however that would leave\n        # us vulnerable to a DOS where hostile messages contain hundreds of\n        # bogus references. So we never add more than one...\n\n        # Initial state of ignorance\n        parent_mid = None\n        parent_idx_pos = None\n        msg_thr_mid = None\n\n        # These are the headers we're examining\n        in_reply_to = safe_decode_hdr(msg, 'in-reply-to')\n        refs = safe_decode_hdr(msg, 'references'\n                               ).replace(',', ' ').strip().split()\n\n        # Part 1: Figure out parent stuff.\n        if in_reply_to:\n            if '<' in in_reply_to:\n                irt_ref = '<%s>' % in_reply_to.split('<')[1].split('>')[0]\n                while irt_ref in refs:\n                    refs.remove(irt_ref)\n                refs.append(irt_ref)\n        # According to the RFCs, the last entry in the References header\n        # should be our immediate parent. We have made sure In-Reply-To has\n        # precedence if it exists...\n        if refs:\n            parent_idx_pos = self.MSGIDS.get(self._encode_msg_id(refs[-1]))\n            if parent_idx_pos is not None:\n                parent_mid = b36(parent_idx_pos)\n\n        # Part 2: Add a ghost for at most 1 missing ancestor\n        enc_refs = [self._encode_msg_id(r) for r in refs]\n        ref_idxs = [self.MSGIDS.get(er) for er in enc_refs]\n        last_missing = None\n        for i, r in enumerate(ref_idxs):\n            if r is None:\n                last_missing = i\n        if last_missing is not None:\n            g_idx_pos, g_info = self.add_new_ghost(enc_refs[last_missing])\n            ref_idxs[last_missing] = g_idx_pos\n\n        # Part 3: Discover and merge conversations\n        ref_idxs = [r for r in ref_idxs if r is not None]\n        conversations = set([])\n        for ref_idx_pos in (r for r in reversed(ref_idxs) if r is not None):\n            try:\n                ref_info = self.get_msg_at_idx_pos(ref_idx_pos)\n                if ref_info[self.MSG_REPLIES]:\n                    conversations.add(b36(ref_idx_pos))\n                msg_thr_mid = ref_info[self.MSG_THREAD_MID].split('/')[0]\n                conversations.add(msg_thr_mid)\n                if ref_info[self.MSG_THREAD_MID].endswith('/-'):\n                    break\n            except (KeyError, ValueError, IndexError):\n                pass\n\n        root = None\n        replies = []\n        if msg_thr_mid:\n            root = self.get_msg_at_idx_pos(int(msg_thr_mid, 36))\n            replies = [r for r in root[self.MSG_REPLIES][:-1].split(',') if r]\n            reparent = []\n            conversations.add(msg_mid)\n            for t_mid in (c for c in conversations if c != msg_thr_mid):\n                t_msg_info = self.get_msg_at_idx_pos(int(t_mid, 36))\n                reparent.extend(t_msg_info[self.MSG_REPLIES][:-1].split(','))\n                reparent.append(t_mid)\n            for m_mid in set([r for r in reparent if r]):\n                m_msg_idx = int(m_mid, 36)\n                m_msg_info = self.get_msg_at_idx_pos(m_msg_idx)\n                otp = m_msg_info[self.MSG_THREAD_MID].split('/')\n                otp[0] = msg_thr_mid\n                m_msg_info[self.MSG_THREAD_MID] = '/'.join(otp)\n                m_msg_info[self.MSG_REPLIES] = ''\n                self.set_msg_at_idx_pos(m_msg_idx, m_msg_info)\n                replies.append(m_mid)\n\n        # FIXME: If nothing was found, do a subject-based search of recent\n        #        messages, for subject-based grouping.\n\n        # OK, finally we add ourselves and our references the conversation, yay!\n        if msg_thr_mid and root:\n            replies.append(msg_mid)\n            for ref_idx_pos in (r for r in ref_idxs if r is not None):\n                ref_mid = b36(ref_idx_pos)\n                replies.append(ref_mid)\n            root[self.MSG_REPLIES] = ','.join(sorted(list(set(replies)))) + ','\n            self.set_msg_at_idx_pos(int(msg_thr_mid, 36), root)\n\n        msg_idx_pos = int(msg_mid, 36)\n        msg_info = self.get_msg_at_idx_pos(msg_idx_pos)\n        msg_replies = msg_info[self.MSG_REPLIES][:-1].split(',')\n\n        if subject_threading and not (msg_thr_mid or refs or msg_replies):\n            # Can we do plain GMail style subject-based threading?\n            subj = msg_info[self.MSG_SUBJECT].lower()\n            subj = subj.replace('re: ', '')  # FIXME: i18n?\n            date = long(msg_info[self.MSG_DATE], 36)\n            if subj.strip() != '':\n                # FIXME: Is this too aggressive? Make configurable?\n                for midx in reversed(range(max(0, msg_idx_pos - 150),\n                                           msg_idx_pos)):\n                    try:\n                        m_info = self.get_msg_at_idx_pos(midx)\n                        m_date = long(m_info[self.MSG_DATE], 36)\n                        m_subj = m_info[self.MSG_SUBJECT]\n                        if ((m_date < date) and  # FIXME: i18n?\n                                (m_subj.lower().replace('re: ', '') == subj)):\n                            msg_thr_mid = m_info[self.MSG_THREAD_MID]\n                            msg_thr_mid = msg_thr_mid.split('/')[0]\n                            parent = self.get_msg_at_idx_pos(int(msg_thr_mid,\n                                                                 36))\n                            replies = parent[self.MSG_REPLIES][:-1].split(',')\n                            if len(replies) < 100:\n                                if msg_mid not in replies:\n                                    replies.append(msg_mid)\n                                parent[self.MSG_REPLIES] = (','.join(replies)\n                                                            + ',')\n                                self.set_msg_at_idx_pos(int(msg_thr_mid, 36),\n                                                        parent)\n                                break\n                            else:\n                                msg_thr_mid = None\n                        if date - m_date > 5 * 24 * 3600:\n                            break\n                    except (KeyError, ValueError, IndexError):\n                        pass\n\n        if not msg_thr_mid:\n            # OK, we are our own conversation root.\n            msg_thr_mid = msg_mid\n\n        if parent_mid and parent_mid != msg_thr_mid:\n            msg_info[self.MSG_THREAD_MID] = '/'.join([msg_thr_mid, parent_mid])\n        else:\n            msg_info[self.MSG_THREAD_MID] = msg_thr_mid\n\n        self.set_msg_at_idx_pos(msg_idx_pos, msg_info)\n\n    def unthread_message(self, msg_mid, new_subject=None):\n        msg_idx_pos = int(msg_mid, 36)\n        msg_info = self.get_msg_at_idx_pos(msg_idx_pos)\n\n        par_mid = msg_info[self.MSG_THREAD_MID].split('/')[-1]\n        thr_mid = msg_info[self.MSG_THREAD_MID].split('/')[0]\n        thr_idx_pos = int(thr_mid, 36)\n        thr_info = self.get_msg_at_idx_pos(thr_idx_pos)\n\n        thread = [t for t in thr_info[self.MSG_REPLIES][:-1].split(',') if t]\n\n        # Collect parent/children relationships\n        has_kids = {}\n        for t_mid in thread:\n            t_info = self.get_msg_at_idx_pos(int(t_mid, 36))\n            t_pmid = t_info[self.MSG_THREAD_MID].split('/')[-1]\n            has_kids[t_pmid] = has_kids.get(t_pmid, []) + [t_mid]\n\n        # Gather all descendants of a message\n        def _family(p_mid, hk):\n            family = [p_mid]\n            if p_mid in hk:\n                gather = copy.copy(hk[p_mid])\n                while gather:\n                    r_mid = gather.pop(0)\n                    if r_mid not in family:\n                        family.append(r_mid)\n                    if r_mid in hk:\n                        gather.extend([\n                            m for m in hk[r_mid]\n                            if m not in family and m not in gather])\n            return family\n\n        # Reparent all descendants of a message\n        def _reparent(p_mid, hk, new_subj=None):\n            new_thread = _family(p_mid, hk)\n            old_tmids = set([])\n            orphans = set([])\n\n            # Set up the new thread structure\n            for t_mid in new_thread:\n                t_idx_pos = int(t_mid, 36)\n                t_info = self.get_msg_at_idx_pos(t_idx_pos)\n\n                old_tmids.add(t_info[self.MSG_THREAD_MID].split('/')[0])\n                orphaning = [\n                    o for o in t_info[self.MSG_REPLIES].split(',')\n                    if o and o not in new_thread]\n                if orphaning:\n                    orphans |= set(orphaning)\n\n                if t_mid == p_mid:\n                    self.edit_msg_info(t_info,\n                        msg_subject=new_subj,\n                        msg_replies=new_thread,\n                        msg_parent_mid=p_mid,\n                        msg_thread_mid=p_mid)\n                else:\n                    self.edit_msg_info(t_info,\n                        msg_subject=new_subj,\n                        msg_replies=[],\n                        msg_thread_mid=p_mid)\n                self.set_msg_at_idx_pos(t_idx_pos, t_info)\n\n            # If we've left any messages without a thread marker, choose\n            # a new one and reparent.\n            orphans = sorted(list(orphans))\n            for o_mid in orphans:\n                o_idx_pos = int(o_mid, 36)\n                o_info = self.get_msg_at_idx_pos(o_idx_pos)\n                if o_mid == orphans[0]:\n                    self.edit_msg_info(o_info,\n                        msg_replies=orphans,\n                        msg_thread_mid=o_mid)\n                else:\n                    self.edit_msg_info(o_info, msg_thread_mid=orphans[0])\n                self.set_msg_at_idx_pos(o_idx_pos, o_info)\n\n            # If we've left an existing thread, clean its reply list.\n            old_tmids -= set(new_thread)\n            for ot_mid in old_tmids:\n                ot_idx_pos = int(ot_mid, 36)\n                ot_info = self.get_msg_at_idx_pos(ot_idx_pos)\n                self.edit_msg_info(ot_info, msg_replies=[\n                    rmid for rmid in ot_info[self.MSG_REPLIES].split(',')\n                    if rmid and (rmid not in new_thread)])\n                self.set_msg_at_idx_pos(ot_idx_pos, ot_info)\n\n        if par_mid == msg_mid:\n            # If we're reparenting the root of a thread, this actually means\n            # cut all our kids loose to their own threads.\n            for k_mid in has_kids.get(msg_mid, []):\n                _reparent(k_mid, has_kids)\n            msg_info = self.get_msg_at_idx_pos(msg_idx_pos)\n            self.edit_msg_info(msg_info,\n                msg_subject=new_subject,\n                msg_thread_mid=msg_mid,\n                msg_parent_mid=msg_mid)\n            self.set_msg_at_idx_pos(msg_idx_pos, msg_info)\n        else:\n            # Otherwise, cut ourselves and our kids loose.\n            _reparent(msg_mid, has_kids, new_subj=new_subject)\n\n    def add_new_msg(self, msg_ptr, msg_id, msg_ts, msg_from,\n                    msg_to, msg_cc, msg_bytes, msg_subject, msg_body,\n                    tags):\n        with self._lock:\n            msg_idx_pos = len(self.INDEX)\n            msg_mid = b36(msg_idx_pos)\n            # FIXME: Refactor this to use edit_msg_info.\n            msg_info = [\n                msg_mid,                             # Index ID\n                msg_ptr,                             # Location on disk\n                msg_id,                              # Message ID\n                b36(msg_ts),                         # Date as UTC timstamp\n                msg_from,                            # From:\n                self.compact_to_list(msg_to or []),  # To:\n                self.compact_to_list(msg_cc or []),  # Cc:\n                b36(msg_bytes // 1024),              # KB\n                msg_subject,                         # Subject:\n                msg_body,                            # Snippet etc.\n                ','.join(tags),                      # Initial tags\n                '',                                  # No replies for now\n                msg_mid]                             # Conversation ID\n\n            if msg_from:\n                ahp = AddressHeaderParser(msg_from)\n                if ahp:\n                    fn = ahp[0].fn\n                    email = ahp[0].address\n                else:\n                    email, fn = ExtractEmailAndName(msg_from)\n            else:\n                email = fn = None\n            if email and fn:\n                self.update_email(email, name=fn)\n            self.set_msg_at_idx_pos(msg_idx_pos, msg_info)\n            return msg_idx_pos, msg_info\n\n    def add_new_ghost(self, msg_id, trash=False, subject=None):\n        tags = []\n        if trash:\n            tags = [t._key for t in self.config.get_tags(type='trash')]\n        return self.add_new_msg(\n            '',  # msg_ptr\n            msg_id,\n            1,   # msg_ts\n            '',  # from\n            [],  # msg_to\n            [],  # msg_cc\n            0,\n            subject or _('(missing message)'),\n            self.MSG_BODY_GHOST,\n            tags)\n\n    def filter_keywords(self, session, msg_mid, msg, keywords, incoming=True):\n        keywordmap = {}\n        msg_idx_list = [msg_mid]\n        for kw in keywords:\n            keywordmap[unicode(kw)] = msg_idx_list\n\n        import mailpile.plugins.tags\n        ftypes = set(mailpile.plugins.tags.FILTER_TYPES)\n        if not incoming:\n            ftypes -= set(['incoming'])\n\n        for (fid, terms, tags, comment, ftype\n             ) in session.config.get_filters(types=ftypes):\n            if (terms == '*' or\n                    len(self.search(None, terms.split(),\n                                    keywords=keywordmap)) > 0):\n                for t in tags.split():\n                    for fmt in ('%s:in', '%s:tag'):\n                        kw = unicode(fmt % t[1:])\n                        if kw in keywordmap:\n                            del keywordmap[kw]\n                    if t[0] != '-':\n                        keywordmap[unicode('%s:in' % t[1:])] = msg_idx_list\n\n        return set(keywordmap.keys())\n\n    def apply_filters(self, session, filter_on, msg_mids=None, msg_idxs=None):\n        if msg_idxs is None:\n            msg_idxs = [int(mid, 36) for mid in msg_mids]\n        if not msg_idxs:\n            return\n        for fid, trms, tags, c, t in session.config.get_filters(\n                filter_on=filter_on):\n            for t in tags.split():\n                tag_id = t[1:].split(':')[0]\n                if t[0] == '-':\n                    self.remove_tag(session, tag_id, msg_idxs=set(msg_idxs))\n                else:\n                    self.add_tag(session, tag_id, msg_idxs=set(msg_idxs))\n\n    def _list_header_keywords(self, hdr, val_lower, body_info):\n        \"\"\"Extracts IDs and such from <...> in list-headers.\"\"\"\n        words = []\n        for word in val_lower.replace(',', ' ').split():\n            if not word:\n                continue\n            elif word[:5] == '<http':\n                continue  # We just ignore web URLs for now\n            elif (len(word) > 65) and ('+' in word) and ('@' in word):\n                continue  # Ignore very long plussed addresses\n            elif word[-1:] == '>':\n                if word[:8] == '<mailto:':\n                    word = word[8:-1]\n                    if '?' in word:\n                        word = word.split('?')[0]\n                elif word[:1] == '<':\n                    word = word[1:-1]\n                else:\n                    continue\n                if ((hdr == 'list-post') or\n                        (hdr == 'list-id' and 'list' not in body_info)):\n                    body_info['list'] = word\n                words.append(word)\n                words.extend(re.findall(WORD_REGEXP, word))\n        return set(words)\n\n    def read_message(self, session,\n                     msg_mid, msg_id, msg, msg_size, msg_ts,\n                     mailbox=None):\n        keywords = []\n        snippet_text = snippet_html = ''\n        body_info = {}\n        payload = [None]\n        textparts = 0\n        parts = []\n        urls = []\n        for part in msg.walk():\n            textpart = payload[0] = None\n            ctype = part.get_content_type()\n            pinfo = ''\n            charset = part.get_content_charset() or 'utf-8'\n\n            def _loader(p):\n                if payload[0] is None:\n                    payload[0] = try_decode(GetTextPayload(p), charset)\n                return payload[0]\n\n            if ctype == 'text/plain':\n                textpart = _loader(part)\n                if textpart[:3] in ('<di', '<ht', '<p>', '<p '):\n                    ctype = 'text/html'\n                else:\n                    # FIXME: Search for URLs in the text part, add to urls list.\n                    textparts += 1\n                    pinfo = '%x::T' % len(payload[0])\n\n            if ctype == 'text/html':\n                _loader(part)\n                pinfo = '%x::H' % len(payload[0])\n                if len(payload[0]) > 3:\n                    try:\n                        textpart = extract_text_from_html(\n                            payload[0],\n                            url_callback=lambda u, t: urls.append((u, t)))\n                    except:\n                        session.ui.warning(_('=%s/%s has bogus HTML.'\n                                             ) % (msg_mid, msg_id))\n                        textpart = payload[0]\n                else:\n                    textpart = payload[0]\n\n            if ctype == 'message/delivery-status':\n                keywords.append('dsn:has')\n            elif ctype == 'message/disposition-notification':\n                keywords.append('mdn:has')\n\n            if 'pgp' in part.get_content_type().lower():\n                keywords.append('pgp:has')\n                keywords.append('crypto:has')\n\n            att = part.get_filename()\n            if att:\n                att = try_decode(att, charset)\n                keywords.append('attachment:has')\n                keywords.extend([t + ':att' for t\n                                 in re.findall(WORD_REGEXP, att.lower())])\n                att_kws = []\n                for kw, ext_list in ATT_EXTS.iteritems():\n                    ext = att.lower().rsplit('.', 1)[-1]\n                    if ext in ext_list:\n                        keywords.append('%s:has' % kw)\n                        att_kws.append(kw)\n\n                pmore = squish_mimetype(ctype)\n                pdata = part.get_payload(None, True) or ''\n                if 'image' in att_kws:\n                    try:\n                        if pdata:\n                            # We disallow use of C libraries here, because of\n                            # the massive attack surface, it's just not safe\n                            # to use on all incoming e-mail.\n                            size = image_size(pdata, pure_python=True)\n                            if size is not None:\n                                pmore = '%dx%d' % size\n                    except:\n                        traceback.print_exc()\n                        pass\n\n                pinfo = '%x:%s:%s' % (len(pdata), pmore, att)\n                textpart = (textpart or '') + ' ' + att\n\n            if textpart:\n                # FIXME: Does this lowercase non-ASCII characters correctly?\n                lines = [l for l in textpart.splitlines(True)\n                         if not l.startswith('>')\n                         and l[:4] not in ('----', '====', '____')]\n                keywords.extend(re.findall(WORD_REGEXP,\n                                           ''.join(lines).lower()))\n\n                # NOTE: As a side effect here, the cryptostate plugin will\n                #       add a 'crypto:has' keyword which we check for below\n                #       before performing further processing.\n                for kwe in _plugins.get_text_kw_extractors():\n                    try:\n                        keywords.extend(kwe(self, msg, ctype, textpart,\n                                            body_info=body_info))\n                    except:\n                        if session.config.sys.debug:\n                            traceback.print_exc()\n\n                if ctype == 'text/plain':\n                    snippet_text += ''.join(lines).strip() + '\\n'\n                else:\n                    snippet_html += textpart.strip() + '\\n'\n\n            for extract in _plugins.get_data_kw_extractors():\n                try:\n                    keywords.extend(extract(self, msg, ctype, att, part,\n                                            lambda: _loader(part),\n                                            body_info=body_info))\n                except:\n                    if session.config.sys.debug:\n                        traceback.print_exc()\n\n            if not ctype.startswith('multipart/'):\n                parts.append(pinfo)\n\n        if urls:\n            att_urls = []\n            for (full_url, txt) in set(urls):\n                url = full_url.lower().split('/', 3)\n                if len(url) == 4 and url[0] in ('http:', 'https:'):\n                    keywords.append('%s:url' % url[2])\n                    for raw_re in session.config.prefs.attachment_urls:\n                        url_re = self._url_re_cache.get(raw_re)\n                        if url_re is None:\n                            url_re = re.compile(raw_re)\n                            self._url_re_cache[raw_re] = url_re\n                        if url_re.search(full_url):\n                            att_urls.append((full_url, txt))\n                            break\n            if att_urls:\n                body_info['att_urls'] = att_urls\n                keywords.append('attachment_url:has')\n                keywords.append('attachment:has')\n\n        if len(parts) > 1:\n            body_info['parts'] = parts\n\n        if textparts == 0:\n            keywords.append('text:missing')\n\n        if 'crypto:has' in keywords:\n            e = Email(self, -1,\n                      msg_parsed=msg,\n                      msg_parsed_pgpmime=('all', msg),\n                      msg_info=self.BOGUS_METADATA[:])\n            tree = e.get_message_tree(want=(e.WANT_MSG_TREE_PGP +\n                                            ('text_parts', )))\n\n            # Look for inline PGP parts, update our status if found\n            e.evaluate_pgp(tree, decrypt=session.config.prefs.index_encrypted,\n                                 crypto_state_feedback=False)\n            msg.signature_info = tree['crypto']['signature']\n            msg.encryption_info = tree['crypto']['encryption']\n\n            # Index the contents, if configured to do so\n            if session.config.prefs.index_encrypted:\n                for text in [t['data'] for t in tree['text_parts']]:\n                    keywords.extend(re.findall(WORD_REGEXP, text.lower()))\n                    for kwe in _plugins.get_text_kw_extractors():\n                        try:\n                            keywords.extend(kwe(self, msg, 'text/plain', text,\n                                                body_info=body_info))\n                        except:\n                            if session.config.sys.debug:\n                                traceback.print_exc()\n\n        keywords.append('%s:id' % msg_id)\n        keywords.extend(re.findall(WORD_REGEXP,\n                                   safe_decode_hdr(msg, 'subject').lower()))\n        keywords.extend(re.findall(WORD_REGEXP,\n                                   safe_decode_hdr(msg, 'from').lower()))\n        if mailbox:\n            keywords.append('%s:mailbox' % FormatMbxId(mailbox).lower())\n\n        headerprints = HeaderPrints(msg)\n        # This is a signal for the bayesian filters to discriminate by MUA.\n        keywords.append('%s:hpt' % headerprints['tools'])\n        # This is used to detect forgeries and phishing, it includes info\n        # about how the message was delivered (DKIM, Received, ...)\n        keywords.append('%s:hps' % headerprints['sender'])\n        # If we think we know what MUA that was, make it searchable\n        if headerprints.get('mua'):\n            keywords.append('%s:mua' % headerprints['mua'].split()[0].lower())\n\n        is_list = False\n        for key in msg.keys():\n            key_lower = key.lower()\n            if key_lower.startswith('list-'):\n                is_list = True\n            if key_lower not in BORING_HEADERS and key_lower[:2] != 'x-':\n                val_lower = safe_decode_hdr(msg, key).lower()\n                if key_lower[:5] == 'list-':\n                    words = self._list_header_keywords(key_lower, val_lower,\n                                                       body_info)\n                    emails = []\n                    key_lower = 'list'\n                else:\n                    words = set(re.findall(WORD_REGEXP, val_lower))\n                    emails = ExtractEmails(val_lower)\n\n                # Strip some common crap off; stop-words and robotic emails.\n                words -= STOPLIST\n                emails = [e for e in emails if\n                          (len(e) < 40) or ('+' not in e and '/' not in e)]\n\n                domains = [e.split('@')[-1] for e in emails]\n\n                keywords.extend(['%s:%s' % (t, key_lower) for t in words])\n                keywords.extend(['%s:%s' % (e, key_lower) for e in emails])\n                keywords.extend(['%s:%s' % (d, key_lower) for d in domains])\n                keywords.extend(['%s:email' % e for e in emails])\n\n        # Personal mail: not from lists or common robots?\n        msg_from = msg.get('from', '').lower()\n        reply_to = msg.get('reply-to', '').lower()\n        if not (is_list\n                or 'robot@' in msg_from or 'notifications@' in msg_from\n                or 'noreply' in msg_from.replace('-', '')\n                or 'noreply' in reply_to.replace('-', '')\n                or 'billing@' in msg_from or 'itinerary@' in msg_from\n                or 'root@' in msg_from or 'mailer-daemon@' in msg_from\n                or 'cron@' in msg_from or 'postmaster@' in msg_from\n                or 'logwatch@' in msg_from\n                or 'feedback-id' in msg):\n            keywords.extend(['personal:is'])\n\n            # This generates a unique group:X keyword identifying the\n            # participants in this conversation. This will facilitate\n            # more people-focused UI work down the line.\n            emails = []\n            for hdr in ('from', 'to', 'cc'):\n                hdr = msg.get(hdr)\n                if hdr:\n                    ahp = AddressHeaderParser(hdr)\n                    emails.extend([a.address.lower() for a in ahp])\n            emails = sorted(list(set(emails)))\n            if len(emails) > 1:\n                keywords.append('%s:group' % md5_hex(', '.join(emails)))\n\n        for key in EXPECTED_HEADERS:\n            # This is a useful signal for spam classification\n            if not msg[key]:\n                keywords.append('%s:missing' % key)\n\n        for extract in _plugins.get_meta_kw_extractors():\n            try:\n                keywords.extend(extract(self, msg_mid, msg, msg_size, msg_ts,\n                                        body_info=body_info))\n            except:\n                if session.config.sys.debug:\n                    traceback.print_exc()\n\n        # FIXME: If we have a good snippet from the HTML part, it is likely\n        #        to be more relevant due to the unfortunate habit of some\n        #        senders to put all content in HTML and useless crap in text.\n        if snippet_text.strip() != '':\n            body_info['snippet'] = self.clean_snippet(snippet_text[:1024])\n        else:\n            body_info['snippet'] = self.clean_snippet(snippet_html[:1024])\n\n        return (set(keywords) - STOPLIST), body_info\n\n    # FIXME: Here it would be nice to recognize more boilerplate junk in\n    #        more languages!\n    SNIPPET_JUNK_RE = re.compile(\n        '(\\n[^\\s]+ [^\\n]+(@[^\\n]+|(wrote|crit|schreib)):\\s+>[^\\n]+'\n                                                          # On .. X wrote:\n        '|\\n>[^\\n]*'                                      # Quoted content\n        '|\\n--[^\\n]+BEGIN PGP[^\\n]+--\\s+(\\S+:[^\\n]+\\n)*'  # PGP header\n        ')+')\n    SNIPPET_SPACE_RE = re.compile('\\s+')\n\n    @classmethod\n    def clean_snippet(self, snippet):\n        # FIXME: Can we do better than this? Probably!\n        return (re.sub(self.SNIPPET_SPACE_RE, ' ',\n                       re.sub(self.SNIPPET_JUNK_RE, '',\n                              '\\n' + snippet.replace('\\r', '')\n                              ).split('\\n--')[0])\n                ).strip()\n\n    def index_message(self, session, msg_mid, msg_id,\n                      msg, msg_metadata_kws, msg_size, msg_ts,\n                      mailbox=None, compact=True, filter_hooks=None,\n                      process_new=None, apply_tags=None, incoming=False):\n        keywords, snippet = self.read_message(session,\n                                              msg_mid, msg_id, msg,\n                                              msg_size, msg_ts,\n                                              mailbox=mailbox)\n\n        # Apply the defaults for this mail source / mailbox.\n        if apply_tags:\n            keywords |= set(['%s:in' % tid for tid in apply_tags])\n        if process_new:\n            process_new(msg, msg_metadata_kws, msg_ts, keywords, snippet)\n        elif incoming:\n            # This is the default behavior if the above are undefined.\n            if process_new is None:\n                from mailpile.mail_source import ProcessNew\n                ProcessNew(session, msg, msg_metadata_kws, msg_ts,\n                           keywords, snippet)\n            if apply_tags is None:\n                keywords |= set(['%s:in' % tag._key for tag in\n                                 self.config.get_tags(type='inbox')])\n\n        # Mark as updated (modified/touched) today and on msg_ts\n        keywords.add('%x:u' % (time.time() / (24 * 3600)))\n        if msg_ts:\n            keywords.add('%x:u' % (msg_ts / (24 * 3600)))\n\n        for hook in filter_hooks or []:\n            keywords = hook(session, msg_mid, msg, keywords,\n                            incoming=incoming)\n\n        if 'keywords' in self.config.sys.debug:\n            print('KEYWORDS: %s' % keywords)\n\n        for word in keywords:\n            if (word.startswith('__') or\n                    # Tags are now handled outside the posting lists\n                    word.endswith(':tag') or word.endswith(':in')):\n                continue\n            try:\n                GlobalPostingList.Append(session, word, [msg_mid],\n                                         compact=compact)\n            except UnicodeDecodeError:\n                # FIXME: we just ignore garbage\n                pass\n\n        self.config.command_cache.mark_dirty(set([u'mail:all']) | keywords)\n        return keywords, snippet\n\n    def get_msg_at_idx_pos_uncached(self, msg_idx):\n        rv = self.l2m(self.INDEX[msg_idx])\n        if len(rv) != self.MSG_FIELDS_V2:\n            raise ValueError()\n        return rv\n\n    def delete_msg_at_idx_pos(self, session, msg_idx, keep_msgid=False):\n        info = self.get_msg_at_idx_pos(msg_idx)\n\n        # Remove from PTR index\n        for ptr in (p for p in info[self.MSG_PTRS].split(',') if p):\n            if ptr in self.PTRS:\n                del self.PTRS[ptr]\n\n        # Most of the information just gets nuked.\n        info[self.MSG_PTRS] = ''\n        info[self.MSG_FROM] = ''\n        info[self.MSG_TO] = ''\n        info[self.MSG_CC] = ''\n        info[self.MSG_KB] = 0\n        info[self.MSG_SUBJECT] = ''\n        info[self.MSG_BODY] = self.MSG_BODY_DELETED\n\n        # The timestamp we keep partially intact, to not completely break\n        # ordering within theads. This may not really be necessary.\n        ts = long(info[self.MSG_DATE], 36)\n        info[self.MSG_DATE] = b36(ts - (ts % (3600 * 24)))\n\n        # FIXME: Remove from threads? This may break threading. :(\n\n        if not keep_msgid:\n            # If we don't keep the msgid, the message may reappear later\n            # if it wasn't deleted from all source mailboxes. The caller\n            # may request this if deletion is known to be incomplete.\n            if info[self.MSG_ID] in self.MSGIDS:\n                del self.MSGIDS[info[self.MSG_ID]]\n            info[self.MSG_ID] = self._encode_msg_id('%s' % msg_idx)\n\n        # Save changes...\n        self.set_msg_at_idx_pos(msg_idx, info)\n\n        # Remove all tags\n        for tag in self.get_tags(msg_info=info):\n            self.remove_tag(session, tag, msg_idxs=[msg_idx])\n\n        # Record that these messages were deleted\n        GlobalPostingList.Append(session, 'deleted:is', [b36(msg_idx)])\n\n    def update_msg_sorting(self, msg_idx, msg_info):\n        for order, sorter in self.SORT_ORDERS.iteritems():\n            self.INDEX_SORT[order][msg_idx] = sorter(self, msg_info)\n\n    def set_msg_at_idx_pos(self, msg_idx, msg_info, original_line=None):\n        with self._lock:\n            while len(self.INDEX) <= msg_idx:\n                self.INDEX.append('')\n                self.INDEX_THR.append(-1)\n                for order in self.INDEX_SORT:\n                    self.INDEX_SORT[order].append(0)\n\n        msg_thr_mid = msg_info[self.MSG_THREAD_MID].split('/')[0]\n        self.INDEX[msg_idx] = original_line or self.m2l(msg_info)\n        self.INDEX_THR[msg_idx] = int(msg_thr_mid, 36)\n        self.MSGIDS[msg_info[self.MSG_ID]] = msg_idx\n        for msg_ptr in msg_info[self.MSG_PTRS].split(','):\n            self.PTRS[msg_ptr] = msg_idx\n        self.update_msg_sorting(msg_idx, msg_info)\n        self.update_msg_tags(msg_idx, msg_info)\n\n        if not original_line:\n            dirty_tags = [u'%s:in' % self.config.tags[t].slug for t in\n                          self.get_tags(msg_info=msg_info)]\n            self.config.command_cache.mark_dirty(\n                [u'mail:all', u'%s:msg' % msg_idx,\n                 u'%s:thread' % int(msg_thr_mid, 36)] + dirty_tags)\n            CachedSearchResultSet.DropCaches(msg_idxs=[msg_idx])\n            self.MODIFIED.add(msg_idx)\n            try:\n                del self.CACHE[msg_idx]\n            except KeyError:\n                pass\n\n    def get_conversation(self, msg_info=None, msg_idx=None, ghosts=False):\n        if not msg_info:\n            msg_info = self.get_msg_at_idx_pos(msg_idx)\n        conv_mid = msg_info[self.MSG_THREAD_MID].split('/')[0]\n        if conv_mid:\n            conv_mid_idx = int(conv_mid, 36)\n            replies = self.get_replies(msg_idx=conv_mid_idx)\n\n            # In case of buggy data, ensure both the conversation head and\n            # the message itself are included in the results.\n            reply_mids = [r[self.MSG_MID] for r in replies]\n            if conv_mid not in reply_mids:\n                replies = [self.get_msg_at_idx_pos(conv_mid_idx)] + replies\n                reply_mids.append(conv_mid)\n            if msg_info[self.MSG_MID] not in reply_mids:\n                replies += [msg_info]\n\n            if ghosts:\n                return replies\n            else:\n                return [r for r in replies\n                        if r[self.MSG_BODY] not in self.MSG_BODY_MAGIC]\n        else:\n            return [msg_info]\n\n    def get_replies(self, msg_info=None, msg_idx=None):\n        if not msg_info:\n            msg_info = self.get_msg_at_idx_pos(msg_idx)\n        return [self.get_msg_at_idx_pos(int(r, 36)) for r\n                in set(msg_info[self.MSG_REPLIES].split(',')) if r]\n\n    def get_tags(self, msg_info=None, msg_idx=None):\n        if not msg_info:\n            msg_info = self.get_msg_at_idx_pos(msg_idx)\n        taglist = [r for r in msg_info[self.MSG_TAGS].split(',') if r]\n        if not 'tags' in self.config:\n            return taglist\n        return [r for r in taglist if r in self.config.tags]\n\n    def add_tag(self, session, tag_id,\n                msg_info=None, msg_idxs=None,\n                conversation=False, allow_message_id_clearing=False):\n        if msg_info and msg_idxs is None:\n            msg_idxs = set([int(msg_info[self.MSG_MID], 36)])\n        else:\n            msg_idxs = set(msg_idxs)\n        if not msg_idxs:\n            return set()\n\n        CachedSearchResultSet.DropCaches()\n\n        if conversation:\n            session.ui.mark(_n('Tagging %d conversation (%s)',\n                           'Tagging %d conversations (%s)',\n                           len(msg_idxs)\n                           ) % (len(msg_idxs), tag_id))\n            for msg_idx in list(msg_idxs):\n                for reply in self.get_conversation(msg_idx=msg_idx,\n                                                   ghosts=True):\n                    if reply[self.MSG_MID]:\n                        msg_idxs.add(int(reply[self.MSG_MID], 36))\n        else:\n            session.ui.mark(_n('Tagging %d message (%s)',\n                           'Tagging %d messages (%s)',\n                           len(msg_idxs)\n                           ) % (len(msg_idxs), tag_id))\n\n        clear_message_id = False\n        if allow_message_id_clearing:\n            if session.config.tags[tag_id].type == 'trash':\n                 clear_message_id = True\n\n        eids = set()\n        added = set()\n        threads = set()\n        for msg_idx in msg_idxs:\n            if msg_idx >= 0 and msg_idx < len(self.INDEX):\n                modified = False\n                msg_info = self.get_msg_at_idx_pos(msg_idx)\n                tags = set([r for r in msg_info[self.MSG_TAGS].split(',')\n                            if r and r in session.config.tags])\n                if tag_id not in tags:\n                    tags.add(tag_id)\n                    msg_info[self.MSG_TAGS] = ','.join(list(tags))\n                    added.add(msg_idx)\n                    threads.add(msg_info[self.MSG_THREAD_MID].split('/')[0])\n                    modified = True\n                if clear_message_id:\n                    old_msgid = msg_info[self.MSG_ID]\n                    if old_msgid in self.MSGIDS:\n                        del self.MSGIDS[old_msgid]\n                    msg_info[self.MSG_ID] = self._encode_msg_id('%s' % msg_idx)\n                    self.MSGIDS[msg_info[self.MSG_ID]] = msg_idx\n                    modified = True\n                if modified:\n                    self.INDEX[msg_idx] = self.m2l(msg_info)\n                    self.MODIFIED.add(msg_idx)\n                    self.update_msg_sorting(msg_idx, msg_info)\n                    if msg_idx in self.CACHE:\n                        del self.CACHE[msg_idx]\n                eids.add(msg_idx)\n\n        with self._lock:\n            if tag_id in self.TAGS:\n                self.TAGS[tag_id] |= eids\n            elif eids:\n                self.TAGS[tag_id] = eids\n\n        # Record that these messages were touched in some way\n        GlobalPostingList.Append(session,\n                                 '%x:u' % (time.time() // (24 * 3600)),\n                                 [b36(e) for e in eids])\n\n        try:\n            self.config.command_cache.mark_dirty(\n                [u'mail:all', u'%s:in' % self.config.tags[tag_id].slug] +\n                [u'%s:msg' % e_idx for e_idx in added] +\n                [u'%s:thread' % int(mid, 36) for mid in threads])\n        except:\n            pass\n        return added\n\n    def remove_tag(self, session, tag_id,\n                   msg_info=None, msg_idxs=None, conversation=False):\n        if msg_info and msg_idxs is None:\n            msg_idxs = set([int(msg_info[self.MSG_MID], 36)])\n        else:\n            msg_idxs = set(msg_idxs)\n        if not msg_idxs:\n            return set()\n\n        session.ui.mark(_n('Untagging conversation (%s)',\n                           'Untagging conversations (%s)',\n                           len(msg_idxs)\n                           ) % (tag_id, ))\n        CachedSearchResultSet.DropCaches()\n        for msg_idx in list(msg_idxs):\n            if conversation:\n                for reply in self.get_conversation(msg_idx=msg_idx,\n                                                   ghosts=True):\n                    if reply[self.MSG_MID]:\n                        msg_idxs.add(int(reply[self.MSG_MID], 36))\n\n        session.ui.mark(_n('Untagging %d message (%s)',\n                           'Untagging %d messages (%s)',\n                           len(msg_idxs)\n                           ) % (len(msg_idxs), tag_id))\n        eids = set()\n        removed = set()\n        threads = set()\n        for msg_idx in msg_idxs:\n            if msg_idx >= 0 and msg_idx < len(self.INDEX):\n                msg_info = self.get_msg_at_idx_pos(msg_idx)\n                tags = set([r for r in msg_info[self.MSG_TAGS].split(',')\n                            if r and r in session.config.tags])\n                if tag_id in tags:\n                    tags.remove(tag_id)\n                    msg_info[self.MSG_TAGS] = ','.join(list(tags))\n                    self.INDEX[msg_idx] = self.m2l(msg_info)\n                    self.MODIFIED.add(msg_idx)\n                    self.update_msg_sorting(msg_idx, msg_info)\n                    if msg_idx in self.CACHE:\n                        del self.CACHE[msg_idx]\n                    removed.add(msg_idx)\n                    threads.add(msg_info[self.MSG_THREAD_MID].split('/')[0])\n                eids.add(msg_idx)\n        with self._lock:\n            if tag_id in self.TAGS:\n                self.TAGS[tag_id] -= eids\n\n        # Record that these messages were touched in some way\n        GlobalPostingList.Append(session,\n                                 '%x:u' % (time.time() // (24 * 3600)),\n                                 [b36(e) for e in eids])\n\n        try:\n            self.config.command_cache.mark_dirty(\n                [u'%s:in' % self.config.tags[tag_id].slug] +\n                [u'%s:msg' % e_idx for e_idx in removed] +\n                [u'%s:thread' % int(mid, 36) for mid in threads])\n        except:\n            pass\n        return removed\n\n    def search_tag(self, session, term, hits, recursion=0):\n        t = term.split(':', 1)\n        tag_id, tag = t[1], self.config.get_tag(t[1])\n        results = []\n        if tag:\n            tag_id = tag._key\n            for subtag in self.config.get_tags(parent=tag_id):\n                results.extend(hits('%s:in' % subtag._key))\n            if tag.magic_terms and recursion < 5:\n                results.extend(self.search(session, [tag.magic_terms],\n                                           recursion=recursion+1).as_set())\n        results.extend(hits('%s:in' % tag_id))\n        return results, tag\n\n    def search(self, session, searchterms,\n               keywords=None, order=None, recursion=0, context=None):\n        # Stash the raw search terms\n        raw_terms = searchterms[:]\n\n        # Choose how we are going to search\n        if keywords is not None:\n            # Searching within pre-defined keywords\n            def hits(term):\n                return [int(h, 36) for h in keywords.get(term, [])]\n        else:\n            # Normal search\n            def hits(term):\n                if term.endswith(':in'):\n                    return self.TAGS.get(term.rsplit(':', 1)[0], [])\n                else:\n                    session.ui.mark(_('Searching for %s') % term)\n                    gpl_hits = GlobalPostingList(session, term).hits()\n                    try:\n                        return [int(h, 36) for h in gpl_hits]\n                    except ValueError:\n                        b36re = re.compile('^[a-zA-Z0-9]{1,8}$')\n                        print('FIXME! BAD HITS: %s => %s' % (term, [\n                            h for h in gpl_hits if not b36re.match(h)]))\n                        return [int(h, 36) for h in gpl_hits if b36re.match(h)]\n\n        # Replace some GMail-compatible terms with what we really use\n        if 'tags' in self.config:\n            for p in ('', '+', '-'):\n                while p + 'is:unread' in searchterms:\n                    where = searchterms.index(p + 'is:unread')\n                    new = session.config.get_tags(type='unread')\n                    if new:\n                        searchterms[where] = p + 'in:%s' % new[0].slug\n                for t in [term for term in searchterms\n                          if term.startswith(p + 'tag:')]:\n                    where = searchterms.index(t)\n                    searchterms[where] = p + 'in:' + t.split(':', 1)[1]\n\n        # If first term is a negative search, prepend an all:mail\n        if searchterms and searchterms[0] and searchterms[0][0] == '-':\n            searchterms[:0] = ['all:mail']\n\n        if context:\n            r = [(None, set(context))]\n        else:\n            r = []\n\n        searched_invisible = False\n        searched_mailbox = False\n        searched_deleted = False\n\n        for term in searchterms:\n            if term in STOPLIST:\n                if session:\n                    session.ui.warning(_('Ignoring common word: %s') % term)\n                continue\n\n            if term[0] in ('-', '+'):\n                op = term[0]\n                term = term[1:]\n            else:\n                op = None\n\n            r.append((op, []))\n            rt = r[-1][1]\n            term = term.lower()\n\n            if ':' in term:\n                if term.startswith('in:'):\n                    results, tag = self.search_tag(session, term, hits,\n                                                   recursion=recursion)\n                    rt.extend(results)\n                    if tag:\n                        if tag.flag_hides:\n                            searched_invisible = True\n                        if tag.type == 'mailbox':\n                            searched_mailbox = True\n\n                elif term.startswith('mid:'):\n                    rt.extend([int(t, 36) for t in\n                               term[4:].replace('=', '').split(',')])\n                elif term.startswith('body:'):\n                    rt.extend(hits(term[5:]))\n                elif term == 'all:mail':\n                    rt.extend(range(0, len(self.INDEX)))\n                elif term in ('to:me', 'cc:me', 'from:me'):\n                    vcards = self.config.vcards\n                    emails = []\n                    for vc in vcards.find_vcards([], kinds=['profile']):\n                        emails += [vcl.value for vcl in vc.get_all('email')]\n                    for email in set(emails):\n                        if email:\n                            rt.extend(hits('%s:%s' % (email,\n                                                      term.split(':')[0])))\n                elif term == 'is:encrypted':\n                    for status in EncryptionInfo.STATUSES:\n                        if status in CryptoInfo.STATUSES:\n                            continue\n                        rt.extend(self.search_tag(\n                            session, 'in:mp_enc-%s' % status, hits,\n                            recursion=recursion)[0])\n                elif term == 'is:signed':\n                    for status in SignatureInfo.STATUSES:\n                        if status in CryptoInfo.STATUSES:\n                            continue\n                        rt.extend(self.search_tag(\n                            session, 'in:mp_sig-%s' % status, hits,\n                            recursion=recursion)[0])\n                else:\n                    if term == 'is:deleted':\n                        searched_deleted = True\n                    t = term.split(':', 1)\n                    fnc = _plugins.get_search_term(t[0])\n                    if fnc:\n                        rt.extend(fnc(self.config, self, term, hits))\n                    else:\n                        rt.extend(hits('%s:%s' % (t[1], t[0])))\n            else:\n                rt.extend(hits(term))\n\n        if r:\n            results = set(r[0][1])\n            for (op, rt) in r[1:]:\n                if op == '+':\n                    results |= set(rt)\n                elif op == '-':\n                    results -= set(rt)\n                else:\n                    results &= set(rt)\n            # Sometimes the scan gets aborted...\n            if keywords is None:\n                results -= set([len(self.INDEX)])\n        else:\n            results = set()\n\n        # Unless we are searching for invisible things, remove them from\n        # results by default.\n        exclude = []\n        order = order or (session and session.order) or 'flat-index'\n        if (results and (keywords is None) and\n                (not searched_invisible) and\n                (not searched_mailbox) and\n                (not searched_deleted) and\n                ('tags' in self.config) and\n                (not session or 'all' not in order)):\n            invisible = self.config.get_tags(flag_hides=True)\n            exclude_terms = (['is:deleted'] +\n                             ['in:%s' % i._key for i in invisible])\n            if len(exclude_terms) > 1:\n                exclude_terms = ([exclude_terms[0]] +\n                                 ['+%s' % e for e in exclude_terms[1:]])\n            # Recursing to pull the excluded terms from cache as well\n            exclude = self.search(session, exclude_terms).as_set()\n\n        # Decide if this is cached or not\n        if keywords is None:\n            srs = CachedSearchResultSet(self, raw_terms)\n            if len(srs) > 0:\n                return srs\n        else:\n            srs = SearchResultSet(self, raw_terms, [], [])\n\n        srs.set_results(results, exclude)\n        if session:\n            session.ui.mark(_n('Found %d result ',\n                               'Found %d results ',\n                               len(results)) % (len(results), ) +\n                            _n('%d suppressed',\n                               '%d suppressed',\n                               len(srs.excluded())\n                               ) % (len(srs.excluded()), ))\n        return srs\n\n    def _freshness_sorter(self, msg_info):\n        ts = long(msg_info[self.MSG_DATE], 36)\n        for tid in self.get_tags(msg_info=msg_info):\n            if tid in self._sort_freshness_tags:\n                return ts + self.FRESHNESS_SORT_BOOST\n        return ts\n\n    FRESHNESS_SORT_BOOST = (5 * 24 * 3600)\n    SORT_ORDERS = {\n        'freshness': _freshness_sorter,\n        'date': lambda s, mi: long(mi[s.MSG_DATE], 36),\n# FIXME: The following are disabled for now for being memory hogs\n#       'from': lambda s, mi: s.mi[s.MSG_FROM]),\n#       'subject': lambda s, mi: s.mi[s.MSG_SUBJECT]),\n    }\n\n    def _prepare_sorting(self):\n        self._sort_freshness_tags = [tag._key for tag in\n                                     self.config.get_tags(type='unread')]\n        self.INDEX_SORT = {}\n        for order, sorter in self.SORT_ORDERS.iteritems():\n            self.INDEX_SORT[order] = []\n\n    def sort_results(self, session, results, how):\n        if not results:\n            return\n\n        count = len(results)\n        how = how or 'flat-unsorted'\n        session.ui.mark(_n('Sorting %d message by %s...',\n                           'Sorting %d messages by %s...',\n                           count\n                           ) % (count, _(how)))\n        try:\n            if how.endswith('unsorted'):\n                pass\n            elif how.endswith('index'):\n                results.sort()\n            elif how.endswith('random'):\n                now = time.time()\n                results.sort(key=lambda k: sha1b64('%s%s' % (now, k)))\n            else:\n                did_sort = False\n                for order in self.INDEX_SORT:\n                    if how.endswith(order):\n                        try:\n                            results.sort(\n                                key=self.INDEX_SORT[order].__getitem__)\n                        except IndexError:\n                            say = session.ui.error\n                            if session.config.sys.debug:\n                                traceback.print_exc()\n                            for result in results:\n                                if result >= len(self.INDEX) or result < 0:\n                                    say(('Bogus message index: %s'\n                                         ) % result)\n                            say(_('Recovering from bogus sort, '\n                                  'corrupt index?'))\n                            say(_('Please tell team@mailpile.is !'))\n                            clean_results = [r for r in results\n                                             if r >= 0 and r < len(self.INDEX)]\n                            clean_results.sort(\n                                key=self.INDEX_SORT[order].__getitem__)\n                            results[:] = clean_results\n                        did_sort = True\n                        break\n                if not did_sort:\n                    session.ui.warning(_('Unknown sort order: %s') % how)\n                    return False\n        except:\n            if session.config.sys.debug:\n                traceback.print_exc()\n            session.ui.warning(_('Sort failed, sorting badly. Partial index?'))\n            results.sort()\n\n        if how.startswith('rev'):\n            results.reverse()\n\n        if 'flat' not in how:\n            all_new = set()\n            if 'freshness' in how:\n                # FIXME: This calculation appears very cachable!\n                new_tags = session.config.get_tags(type='unread')\n                for tag in new_tags:\n                    all_new |= self.TAGS.get(tag._key, set([]))\n\n            # This filters away all but the first (or oldst unread) result in\n            # each conversation.\n            session.ui.mark(_('Collapsing conversations...'))\n            seen, pi = {}, 0\n            for ri in results:\n                ti = self.INDEX_THR[ri]\n                if ti in seen:\n                    if ti in all_new:\n                        results[seen[ti]] = ri\n                else:\n                    results[pi] = ri\n                    seen[ti] = pi\n                    pi += 1\n            results[pi:] = []\n            session.ui.mark(_n('Sorted %d message by %s',\n                               'Sorted %d messages by %s',\n                               count\n                               ) % (count, how) +\n                            ', ' +\n                            _n('%d conversation',\n                               '%d conversations',\n                               len(results)\n                               ) % (len(results), ))\n        else:\n            session.ui.mark(_n('Sorted %d message by %s',\n                               'Sorted %d messages by %s',\n                               count\n                               ) % (count, _(how)))\n\n        return True\n\n\nif __name__ == '__main__':\n    import doctest\n    import sys\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                              extraglobs={})\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/search_history.py",
    "content": "import time\nimport zlib\n\nimport mailpile.util\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.util import *\n\n\nSEARCH_HISTORY_LOCK = UiRLock()\n\n\nclass SearchHistory(object):\n    #\n    # This is an in-memory cache of search results, which can be used to\n    # give a \"search context\" to various commands. The actual results are\n    # preserved, so adding/removing/retagging messages won't change the\n    # context and meaning of \"next\" or \"message number five\".\n    #\n    DEFAULT_TTL = 5 * 24 * 3600  # This is a LRU cache, we evict after 5 days\n    RAW_RESULT_TTL = 600         # Compress results after 10 minutes or so\n\n    PICKLE_NAME = 'search-history.dat'\n\n    @classmethod\n    def Load(cls, config, merge=None):\n        with SEARCH_HISTORY_LOCK:\n            try:\n                sh = config.load_pickle(cls.PICKLE_NAME)\n            except (IOError, EOFError):\n                sh = SearchHistory()\n            if merge is not None:\n                sh.cache.update(merge.cache)\n        return sh\n\n    def __init__(self):\n        self.changed = False\n        self.cache = {}\n\n    def save(self, config):\n        with SEARCH_HISTORY_LOCK:\n            self.expire()\n            if self.changed:\n                self.changed = False\n                config.save_pickle(self, self.PICKLE_NAME)\n\n    def _compress(self, results, order):\n        # This generates a compact but complete representation of the search,\n        # compact enough that we COULD embed in API responses if we wanted to\n        # do away with the server-side persistence.\n        # TODO: Explore if this is a better format for posting lists!\n        return zlib.compress(':'.join([intlist_to_bitmask(results),\n                                       str(order)]))\n\n    def _decompress(self, compressed_bitmask):\n        bitmask, order = zlib.decompress(compressed_bitmask).rsplit(':', 1)\n        return bitmask_to_intlist(bitmask), order\n\n    def add(self, terms, results, order):\n        now = int(time.time())\n        data = {\n            'terms': terms[:],\n            'results': results[:],\n            'order': order,\n            't': now\n        }\n        with SEARCH_HISTORY_LOCK:\n            fprint = md5_hex(str(terms), str(results), str(order))\n            self.cache[fprint] = data\n            self.changed = True\n            return fprint\n\n    def get(self, session, fprint):\n        with SEARCH_HISTORY_LOCK:\n            search = self.cache[fprint]\n            self.cache[fprint]['t'] = int(time.time())\n            if 'results' not in search and 'c' in search:\n                results, order = self._decompress(search['c'])\n                session.config.index.sort_results(session, results, order)\n                search['results'] = results\n                search['order'] = order\n            return tuple(search[t] for t in ('terms', 'results', 'order'))\n\n    def expire(self, ttl=None, compact=None):\n        expired = time.time() - (ttl or self.DEFAULT_TTL)\n        compact = time.time() - (compact or self.RAW_RESULT_TTL)\n        with SEARCH_HISTORY_LOCK:\n            for fp in [f for f in self.cache\n                       if expired <= self.cache[f]['t'] < compact]:\n                search = self.cache[fp]\n                if 'results' not in search:\n                    continue\n                if 'c' not in search:\n                    try:\n                        search['c'] = self._compress(search['results'],\n                                                     search['order'])\n                    except TypeError:\n                        pass\n                if 'c' in search:\n                    del search['results']\n                    del search['order']\n                # Note: do not set self.changed, as the actual data being\n                # cached is still the same - we just changed the format.\n\n            expire = [f for f in self.cache if self.cache[f]['t'] < expired]\n            for fp in expire:\n                del self.cache[fp]\n                self.changed = True\n"
  },
  {
    "path": "mailpile/security.py",
    "content": "\"\"\"\nGlobal Mailpile crypto/privacy/security policy\n\nThis module attempts to collect in one place all of the different\nsecurity related decisions made by the app, in order to facilitate\nreview and testing.\n\n\"\"\"\nfrom __future__ import print_function\nimport copy\nimport hashlib\nimport json\nimport ssl\nimport time\n\n# Note: Do NOT import mailpile.conn_broker, as our monkey patching\n#       of ssl depends on things happening in the right order. :-/\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.util import *\nimport mailpile.platforms\n\nDISABLE_LOCKDOWN = False\n\n\n##[ This is a secret only user owning Mailpile could know ]###################\n\ndef GetUserSecret(workdir):\n    \"\"\"Return a secret that only this Unix user could know.\"\"\"\n    secret_file = os.path.join(workdir, 'mailpile.sec')\n    try:\n        return open(secret_file).read().strip()\n    except (OSError, IOError):\n        pass\n\n    # FIXME: Does this work reasonably on Windows? Does chmod do anything?\n    random_secret = okay_random(64, __file__)\n    with open(secret_file, 'w') as fd:\n        fd.write(random_secret)\n    mailpile.platforms.RestrictReadAccess(secret_file)\n    return random_secret\n\n\n##[ These are the sys.lockdown restrictions ]#################################\n\ndef _lockdown(config):\n    if DISABLE_LOCKDOWN: return False\n    if config.detected_memory_corruption: return 99  # FIXME: Breaks demos?\n    lockdown = config.sys.lockdown or 0\n    try:\n        return int(lockdown)\n    except ValueError:\n        pass\n    lockdown = lockdown.lower()\n    if lockdown == 'false': return 0\n    if lockdown == 'true': return 1\n    if lockdown == 'demo': return -1\n    if lockdown == 'strict': return 2\n    return 1\n\n\ndef in_disk_lockdown(config):\n    if DISABLE_LOCKDOWN: return False\n    # If we've dropped below 50% of our target free space, we stop\n    # almost all operations and go into lockdown. This makes the\n    # Mailpile effectively read-only, which should be safe without\n    # being totally useless.\n    if config.need_more_disk_space(ratio=0.5):\n        return _('Insufficient free disk space')\n    return False\n\n\ndef _lockdown_minimal(config):\n    if DISABLE_LOCKDOWN: return False\n    if _lockdown(config) != 0:\n        return _('In lockdown, doing nothing.')\n    return False\n\n\ndef _lockdown_config(config):\n    if DISABLE_LOCKDOWN: return False\n    # This is just like lockdown_basic, except we allow the user to\n    # change the config so they can adjust the minimum free disk space\n    # requirement if it was accidentally made too strict.\n    if _lockdown(config) > 0:\n        return _('In lockdown, doing nothing.')\n    return False\n\n\ndef _lockdown_quit(config):\n    if DISABLE_LOCKDOWN: return False\n    # The user is always allowed to quit, except in demo mode.\n    if _lockdown(config) < 0:\n        return _('In lockdown, doing nothing.')\n    return False\n\n\ndef _lockdown_basic(config):\n    if DISABLE_LOCKDOWN: return False\n    return _lockdown_config(config) or in_disk_lockdown(config)\n\n\ndef _lockdown_strict(config):\n    if DISABLE_LOCKDOWN: return False\n    if _lockdown(config) > 1:\n        return _('In lockdown, doing nothing.')\n    return in_disk_lockdown(config)\n\n\nCC_ACCESS_FILESYSTEM  = [_lockdown_minimal]\nCC_BROWSE_FILESYSTEM  = [_lockdown_basic]\nCC_CHANGE_CONFIG      = [_lockdown_config]\nCC_CHANGE_CONTACTS    = [_lockdown_basic]\nCC_CHANGE_GNUPG       = [_lockdown_basic]\nCC_CHANGE_FILTERS     = [_lockdown_strict]\nCC_CHANGE_SECURITY    = [_lockdown_minimal]\nCC_CHANGE_TAGS        = [_lockdown_strict]\nCC_COMPOSE_EMAIL      = [_lockdown_strict]\nCC_CPU_INTENSIVE      = [_lockdown_basic]\nCC_LIST_PRIVATE_DATA  = [_lockdown_minimal]\nCC_TAG_EMAIL          = [_lockdown_strict]\nCC_QUIT               = [_lockdown_quit]\nCC_WEB_TERMINAL       = [_lockdown_config]\n\nCC_CONFIG_MAP = {\n    # These are security critical\n    'homedir': CC_CHANGE_SECURITY,\n    'master_key': CC_CHANGE_SECURITY,\n    'sys': CC_CHANGE_SECURITY,\n    'prefs.gpg_use_agent': CC_CHANGE_SECURITY,\n    'prefs.gpg_recipient': CC_CHANGE_SECURITY,\n    'prefs.encrypt_mail': CC_CHANGE_SECURITY,\n    'prefs.encrypt_index': CC_CHANGE_SECURITY,\n    'prefs.encrypt_vcards': CC_CHANGE_SECURITY,\n    'prefs.encrypt_events': CC_CHANGE_SECURITY,\n    'prefs.encrypt_misc': CC_CHANGE_SECURITY,\n\n    # These access the filesystem and local OS\n    'prefs.open_in_browser': CC_ACCESS_FILESYSTEM,\n    'prefs.rescan_command': CC_ACCESS_FILESYSTEM,\n    '*.command': CC_ACCESS_FILESYSTEM,\n\n    # These have their own CC\n    'tags': CC_CHANGE_TAGS,\n    'filters': CC_CHANGE_FILTERS,\n}\n\n\ndef forbid_command(command_obj, cc_list=None, config=None):\n    \"\"\"\n    Determine whether to block a command or not.\n    \"\"\"\n    if cc_list is None:\n        cc_list = command_obj.COMMAND_SECURITY\n    if cc_list:\n        for cc in cc_list:\n            forbid = cc(config or command_obj.session.config)\n            if forbid:\n                return forbid\n    return False\n\n\ndef forbid_config_change(config, config_key):\n    parts = config_key.split('.')\n    cc_list = []\n    while parts:\n        cc_list += CC_CONFIG_MAP.get('.'.join(parts), [])\n        cc_list += CC_CONFIG_MAP.get('*.' + parts.pop(-1), [])\n    if not cc_list:\n        cc_list = CC_CHANGE_CONFIG\n    return forbid_command(None, cc_list=cc_list, config=config)\n\n\n##[ Securely download content from the web ]#################################\n\ndef secure_urlget(session, url,\n                  data=None, timeout=30, anonymous=False, maxbytes=None,\n                  padding=True):\n    from mailpile.conn_brokers import Master as ConnBroker\n    from urllib2 import build_opener\n\n    if session.config.prefs.web_content not in (\"on\", \"anon\"):\n        raise IOError(\"Web content is disabled by policy\")\n\n    if url[:5].lower() not in ('http:', 'https'):\n        raise IOError('Non-HTTP URLs are forbidden: %s' % url)\n\n    # User Agent forging and padding...\n    ffrv = int(time.time() / (7 * 24 * 3600 * 4)) - 649 + 60\n    headers = [\n        ('User-Agent', (\n            'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:%s.0) Gecko/20100101 Firefox/%s.0'\n            % (ffrv, ffrv)))]\n    if padding:\n        headers.append(('X-Pad', 'PA%sD' % ('A' * (160 - len(url) % 160))))\n\n    if url[:6].lower() == 'https:':\n        conn_need, conn_reject = [ConnBroker.OUTGOING_HTTPS], []\n    else:\n        conn_need, conn_reject = [ConnBroker.OUTGOING_HTTP], []\n\n    if session.config.prefs.web_content == \"anon\" or anonymous:\n        conn_reject += [ConnBroker.OUTGOING_TRACKABLE]\n\n    with ConnBroker.context(need=conn_need, reject=conn_reject) as ctx:\n        url_opener = build_opener()\n        url_opener.addheaders = headers\n        # Flagged #nosec, because the URL scheme is constrained above\n        fd = url_opener.open(url, None, timeout=timeout)  # nosec\n\n    return fd.read(maxbytes)\n\n\n##[ Common web-server security code ]########################################\n\nCSRF_VALIDITY = 48 * 3600  # How long a CSRF token remains valid\n\ndef http_content_security_policy(http_server):\n    \"\"\"\n    Calculate the default Content Security Policy string.\n\n    This provides an important line of defense against malicious\n    Javascript being injected into our web user-interface.\n    \"\"\"\n    # FIXME: Allow deviations in config, for integration purposes\n    # FIXME: Clean up Javascript and then make this more strict\n    return (\"default-src 'self' 'unsafe-inline' 'unsafe-eval'; \"\n            \"img-src 'self' data:\")\n\n\ndef make_csrf_token(secret, session_id, ts=None):\n    \"\"\"\n    Generate a hashed token from the current timestamp, session ID and\n    the server secret, to avoid CSRF attacks.\n    \"\"\"\n    ts = '%x' % (ts if (ts is not None) else time.time())\n    payload = [secret, session_id, ts]\n    return '%s-%s' % (ts, b64w(sha512b64('-'.join(payload))))\n\n\ndef valid_csrf_token(secret, session_id, csrf_token):\n    \"\"\"\n    Check the validity of a CSRF token.\n    \"\"\"\n    try:\n        when = int(csrf_token.split('-')[0], 16)\n        return ((when > time.time() - CSRF_VALIDITY) and\n                (csrf_token == make_csrf_token(secret, session_id, ts=when)))\n    except (ValueError, IndexError):\n        return False\n\n\n##[ Secure-ish handling of passphrases ]#####################################\n\nScrypt = PBKDF2HMAC = None\ntry:\n    # Depending on whether Cryptography is installed (and which version),\n    # this may all fail, all succeed or succeed in part.\n    import cryptography.hazmat.backends\n    import cryptography.hazmat.primitives.hashes\n    from cryptography.exceptions import UnsupportedAlgorithm\n    from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC\n    from cryptography.hazmat.primitives.kdf.scrypt import Scrypt\nexcept ImportError:\n    pass\n\n\ndef stretch_with_pbkdf2(password, salt, params):\n    return b64w(PBKDF2HMAC(\n        backend=cryptography.hazmat.backends.default_backend(),\n        algorithm=cryptography.hazmat.primitives.hashes.SHA256(),\n        salt=salt,\n        iterations=int(params['iterations']),\n        length=32).derive(password).encode('base64'))\n\n\ndef stretch_with_scrypt(password, salt, params):\n    return b64w(Scrypt(\n        backend=cryptography.hazmat.backends.default_backend(),\n        salt=salt,\n        n=int(params['n']),\n        r=int(params['r']),\n        p=int(params['p']),\n        length=32).derive(password).encode('base64'))\n\n\n# These are our defaults, based on recommendations found on The Internet.\n# The parameters actually used should be stored along with the output so\n# we can change them later if they're found to be too weak or flawed in\n# some other way.\nKDF_PARAMS = {\n    'pbkdf2': {\n        'iterations': 400000\n    },\n    'scrypt': {\n        'n': 2**17,\n        'r': 8,\n        'p': 1\n    }\n}\n\n\nclass SecurePassphraseStorage(object):\n    \"\"\"\n    This is slightly obfuscated in-memory storage of passphrases.\n\n    The data is currently stored as an array of integers, which takes\n    advantage of Python's internal shared storage for small numbers.\n    This is not secure against a determined adversary, but at least the\n    passphrase won't be written in the clear to core dumps or swap.\n\n    >>> sps = SecurePassphraseStorage(passphrase='ABC')\n    >>> sps.data\n    [65, 66, 67]\n\n    To copy a passphrase:\n\n    >>> sps2 = SecurePassphraseStorage().copy(sps)\n    >>> sps2.data\n    [65, 66, 67]\n\n    To check passphrases for validity, use compare():\n\n    >>> sps.compare('CBA')\n    False\n    >>> sps.compare('ABC')\n    True\n\n    To extract the passphrase, use the get_reader() method to get a\n    file-like object that will return the characters of the passphrase\n    one byte at a time.\n\n    >>> rdr = sps.get_reader()\n    >>> rdr.seek(1)\n    >>> [rdr.read(5), rdr.read(), rdr.read(), rdr.read()]\n    ['B', 'C', '', '']\n\n    If an expiration time is set, trying to access the passphrase will\n    make it evaporate.\n\n    >>> sps.expiration = time.time() - 5\n    >>> sps.get_reader() is None\n    True\n    >>> sps.data is None\n    True\n    \"\"\"\n    # FIXME: Replace this with a memlocked ctype buffer, whenever possible\n\n    def __init__(self, passphrase=None, stretched=False):\n        self.generation = 0\n        self.expiration = -1\n        self.is_stretched = stretched\n        self.stretch_cache = {}\n        if passphrase is not None:\n            self.set_passphrase(passphrase)\n        else:\n            self.data = None\n\n    def copy(self, src):\n        self.data = src.data\n        self.expiration = src.expiration\n        self.generation += 1\n        return self\n\n    def is_set(self):\n        return (self.data is not None)\n\n    def stretches(self, salt, params=None):\n        if self.is_stretched:\n            yield (self.is_stretched, self)\n            return\n\n        if params is None:\n            params = KDF_PARAMS\n\n        for which, name, stretch in (\n                (Scrypt, 'scrypt', stretch_with_scrypt),\n                (PBKDF2HMAC, 'pbkdf2', stretch_with_pbkdf2), ):\n            if which:\n                try:\n                    how = params[name]\n                    name += ' ' + json.dumps(how, sort_keys=True)\n                    sc_key = '%s/%s' % (name, salt)\n                    if sc_key not in self.stretch_cache:\n                        pf = intlist_to_string(self.data).encode('utf-8')\n                        self.stretch_cache[sc_key] = SecurePassphraseStorage(\n                            stretch(pf, salt, how), stretched=name)\n                    yield (name, self.stretch_cache[sc_key])\n                except (KeyError, AttributeError, UnsupportedAlgorithm):\n                    pass\n\n        yield ('clear', self)\n\n    def stretched(self, salt, params=None):\n        for name, stretch in self.stretches(salt, params=params):\n            return stretch\n\n    def set_passphrase(self, passphrase):\n        # This stores the passphrase as a list of integers, which is a\n        # primitive in-memory obfuscation relying on how Python represents\n        # small integers as globally shared objects. Better Than Nothing!\n        self.data = string_to_intlist(passphrase)\n        self.stretch_cache = {}\n        self.generation += 1\n\n    def compare(self, passphrase):\n        if (self.expiration > 0) and (time.time() > self.expiration):\n            self.data = None\n            return False\n        return (self.data is not None and\n                self.data == string_to_intlist(passphrase))\n\n    def read_byte_at(self, offset):\n        if self.data is None or offset >= len(self.data):\n            return ''\n        return chr(self.data[offset])\n\n    def get_passphrase(self):\n        if self.data is None:\n            return ''\n        return intlist_to_string(self.data)\n\n    def get_reader(self):\n        class SecurePassphraseReader(object):\n            def __init__(self, sps):\n                self.storage = sps\n                self.offset = 0\n\n            def seek(self, offset, whence=0):\n                safe_assert(whence == 0)\n                self.offset = offset\n\n            def read(self, ignored_bytecount=None):\n                one_byte = self.storage.read_byte_at(self.offset)\n                self.offset += 1\n\n                return one_byte\n\n            def close(self):\n                pass\n\n        if (self.expiration > 0) and (time.time() > self.expiration):\n            self.data = None\n            return None\n        elif self.data is not None:\n            return SecurePassphraseReader(self)\n        else:\n            return None\n\n\n##[ TLS/SSL security code ]##################################################\n#\n# We monkey-patch ssl.wrap_socket and ssl.SSLContext.wrap_socket so we can\n# implement and enforce our own policies here.\n#\nKNOWN_TLS_HOSTS = {}\nMAX_TLS_CERTS = 5\n\n\ndef tls_sock_cert_sha256(sock=None, cert=None):\n    if cert is None:\n        try:\n            peer_cert = sock.getpeercert(binary_form=True)\n        except ValueError:\n            return None\n    else:\n        peer_cert = cert\n\n    if peer_cert:\n        return unicode(\n            hashlib.sha256(peer_cert).digest().encode('base64').strip())\n    else:\n        return None\n\n\ndef tls_configure(sock, context, args, kwargs):\n    # FIXME: We should convert positional arguments to named ones, to\n    #        make sure everything Just Works.\n    #\n    # Pop off any positional arguments that just want defaults\n    args = list(args)\n    while args and args[-1] is None:\n        args.pop(-1)\n\n    kwargs = copy.copy(kwargs)\n    if (not hasattr(ssl, 'OP_NO_SSLv3')) and not context:\n        # This build/version of Python is insecure!\n        # Force the protocol version to TLSv1.\n        kwargs['ssl_version'] = kwargs.get('ssl_version', ssl.PROTOCOL_TLSv1)\n\n    # Per-site configuration, SNI and TOFU!\n    hostname = None\n    accept_certs = []\n    if 'server_hostname' in kwargs:\n        try:\n            hostname = '%s:%s' % (kwargs['server_hostname'], sock.getpeername()[1])\n        except TypeError:\n            # sock.getpeername() may fail\n            hostname = '%s' % (kwargs.get('server_hostname'),)\n\n        if hostname:\n            tls_settings = KNOWN_TLS_HOSTS.get(md5_hex(hostname))\n        else:\n            tls_settings = None\n\n        # These defaults allow us to do certificate TOFU\n        if tls_settings is not None:\n            accept_certs = [c for c in tls_settings.accept_certs]\n        kwargs['cert_reqs'] = ssl.CERT_NONE\n        if context:\n            context.check_hostname = False\n            context.verify_mode = ssl.CERT_NONE\n\n        # Attempt to configure for Certificate Authorities\n        use_web_ca = kwargs.get('use_web_ca',\n            tls_settings is None or tls_settings.use_web_ca)\n        if use_web_ca:\n            try:\n                if context:\n                    context.load_default_certs()\n                    context.verify_mode = ssl.CERT_REQUIRED\n                    context.check_hostname = True\n                    accept_certs = None\n                elif 'ca_certs' in kwargs:\n                    kwargs['cert_reqs'] = ssl.CERT_REQUIRED\n                    accept_certs = None\n                elif hasattr(ssl, 'get_default_verify_paths'):\n                    kwargs['cert_reqs'] = ssl.CERT_REQUIRED\n                    kwargs['ca_certs'] = ssl.get_default_verify_paths().cafile\n                    accept_certs = None\n                else:\n                    # Fall back to TOFU.\n                    pass\n            except (NameError, AttributeError):\n                # Old Python: Fall back to TOFU\n                pass\n\n        if context:\n            del kwargs['cert_reqs']\n        else:\n            # The context-less ssl.wrap_socket() doesn't understand this\n            # argument, so get rid of it.\n            del kwargs['server_hostname']\n\n    if 'use_web_ca' in kwargs:\n        del kwargs['use_web_ca']\n\n    return tuple(args), kwargs, hostname, accept_certs\n\n\ndef tls_new_context():\n    if hasattr(ssl, 'OP_NO_SSLv3'):\n        return ssl.SSLContext(ssl.PROTOCOL_SSLv23)\n    else:\n        return ssl.SSLContext(ssl.PROTOCOL_TLSv1)\n\n\ndef tls_cert_tofu(wrapped, accept_certs, sname):\n    global KNOWN_TLS_HOSTS\n    cert = tls_sock_cert_sha256(wrapped)\n    if accept_certs and cert not in accept_certs:\n        raise ssl.CertificateError('Unrecognized certificate: %s' % cert)\n\n    skey = md5_hex(sname)\n    if skey not in KNOWN_TLS_HOSTS:\n        KNOWN_TLS_HOSTS[skey] = {'server': sname}\n        KNOWN_TLS_HOSTS[skey].use_web_ca = (accept_certs is None)\n    if cert not in KNOWN_TLS_HOSTS[skey].accept_certs:\n        KNOWN_TLS_HOSTS[skey].accept_certs.append(cert)\n        while len(KNOWN_TLS_HOSTS[skey].accept_certs) > MAX_TLS_CERTS:\n            del KNOWN_TLS_HOSTS[skey].accept_certs[0]\n\ndef tls_context_wrap_socket(org_wrap, context, sock, *args, **kwargs):\n    args, kwargs, sname, accept_certs = tls_configure(sock, context, args, kwargs)\n    tofu = kwargs.get('tofu', True)\n    if 'tofu' in kwargs: del kwargs['tofu']\n    wrapped = org_wrap(context, sock, *args, **kwargs)\n    if tofu:\n        tls_cert_tofu(wrapped, accept_certs, sname)\n    return wrapped\n\n\ndef tls_wrap_socket(org_wrap, sock, *args, **kwargs):\n    args, kwargs, sname, accept_certs = tls_configure(sock, None, args, kwargs)\n    tofu = kwargs.get('tofu', True)\n    if 'tofu' in kwargs: del kwargs['tofu']\n    wrapped = org_wrap(sock, *args, **kwargs)\n    if tofu:\n        tls_cert_tofu(wrapped, accept_certs, sname)\n    return wrapped\n\n\n##[ Key Trust ]#############################################################\n\ndef evaluate_sender_trust(config, email, tree):\n    \"\"\"\n    This uses historic data from the search engine to refine and expand\n    upon the states we get back from GnuPG and attempt to detect forgeries.\n\n    The new potential signature states are:\n\n      unsigned  We expected a signature from this sender but found none\n      changed   The signature was made with a key we've rarely seen before\n      signed    The signature was made with a key we've often seen before\n\n    The first state depends on the user's ratio of signed to unsigned\n    messages, the second two depend on how frequently we've seen a given\n    key used for signatures vs. the total number of signatures.\n\n    These states will supercede the states we get from GnuPG like so:\n\n      * `none` becomes `unsigned`\n      * `unknown` or `unverified` may become `changed`\n      * `unverified` may become `signed`\n\n    The constants used in this algorithm can be found and tweaked in the\n    `prefs.key_trust` section of the configuration file.\n    \"\"\"\n    sender = email.get_sender()\n    if not sender:\n        return\n\n    tree['trust'] = {}\n    trust = tree[\"trust\"]\n\n    # If this mail didn't come from outside, skip all this.\n    # We don't vet ourselves for forgeries.\n    # FIXME: THIS IS INSECURE. We need to fix this mechanism globally.\n    message = email.get_msg()\n    if 'x-mp-internal-sender' in message:\n        trust[\"status\"] = _(\"We trust ourselves\")\n        return tree\n\n    # Calculate the default window we search for information. Don't include\n    # the same day as the message was received, to not be fooled by other\n    # junk that arrived the same day.\n    days = config.prefs.key_trust.window_days\n    msgts = long(email.get_msg_info(config.index.MSG_DATE), 36)\n    end = msgts - (24 * 3600)\n    begin = end - (days * 24 * 3600)\n    scope = ['dates:%d..%d' % (begin, end), 'from:%s' % sender]\n\n    messages_per_key = {}\n    trust['counts'] = messages_per_key\n    def count(name, terms):\n        if name not in messages_per_key:\n            # Note: using .as_set() will exclude spam and trash, which\n            #       is almsot certainly a good thing.\n            msgs = config.index.search(config.background, scope + terms)\n            messages_per_key[name] = len(msgs.as_set())\n        return messages_per_key[name]\n\n    total = lambda: count('total', [])\n    if total() < min(5, config.prefs.key_trust.threshold):\n        # If we have too few messages within our desired window, try\n        # expanding the window...\n        scope[1] = 'dates:1970..%d' % end\n        del messages_per_key['total']\n\n        # Still too few? Abort.\n        if total() < min(5, config.prefs.key_trust.threshold):\n            trust[\"trust_unknown\"] = True\n            trust[\"warning\"] = _(\"This sender's reputation is unknown\")\n            return tree\n\n    signed = lambda: count('signed', ['has:signature'])\n    swr = config.prefs.key_trust.sig_warn_pct / 100.0\n    ktr = config.prefs.key_trust.key_trust_pct / 100.0\n    knr = config.prefs.key_trust.key_new_pct / 100.0\n\n    def update_siginfo(si, trust):\n        stat = si[\"status\"]\n        keyid = si.get('keyinfo', '')[-16:].lower()\n\n        # Unsigned message: if the ratio of total signed messages is\n        # above config.prefs.sig_warn_pct percent, we EXPECT signatures\n        # and warn the user if they're not present.\n        if (stat == 'none') and (signed() > swr * total()):\n            si[\"status\"] = 'unsigned'\n            trust[\"missing_signature\"] = True\n\n        # Compare email timestamp with the signature timestamp.\n        # If they differ by a great deal, treat the signature as\n        # invalid. This makes it much harder to copy old signed\n        # content (undetected) into new messages.\n        elif abs(msgts - si.get(\"timestamp\", msgts)) > 7 * 24 * 3600:\n            si[\"status\"] = 'invalid'\n            trust[\"invalid_signature\"] = True\n\n        # Signed by unverified key: Signal that we trust this key if\n        # this is the key we've seen most of the time for this user.\n        # This is TOFU-ish.\n        elif (keyid and\n                ('unverified' in stat) and\n                (count(keyid, ['sig:%s' % keyid]) > ktr * signed())):\n            si[\"status\"] = stat.replace('unverified', 'signed')\n            trust[\"signed\"] = True\n\n        # Signed by a key we have seen very rarely for this user. Gently\n        # warn the user that something unsual is going on.\n        elif (keyid and\n                ('unverified' in stat or 'unknown' in stat) and\n                (count(keyid, ['sig:%s' % keyid]) < knr * signed())):\n            changed = \"mixed-changed\" if (\"mixed\" in stat) else \"changed\"\n            si[\"status\"] = changed\n            trust[\"key_changed\"] = True\n\n        else:\n            trust[\"%s_signature\" % si[\"status\"].replace(\"mixed-\", \"\")] = True\n\n    if signed() >= config.prefs.key_trust.threshold:\n        if 'crypto' in tree:\n            update_siginfo(tree['crypto']['signature'], tree[\"trust\"])\n\n        for skey in ('text_parts', 'html_parts', 'attachments'):\n            for i, part in enumerate(tree[skey]):\n                if 'crypto' in part:\n                    update_siginfo(part['crypto']['signature'], {})\n\n    if 'received' in message:\n        headerprints = email.get_headerprints()\n        term = 'hps:%s' % headerprints['sender']\n        hps = count(term, [term])\n        if hps < 2:\n            trust[\"mua_or_mta_changed\"] = True\n\n    # Translate accumulated state into a \"problem\" if applicable\n    problem = \"problem\" if (total() > 20) else \"warning\"\n    if trust.get(\"invalid_signature\") or trust.get(\"revoked_signature\"):\n        trust[problem] = _(\"The digital signature is invalid\")\n    elif trust.get(\"missing_signature\"):\n        trust[problem] = _(\"This person usually signs their mail\")\n    elif trust.get(\"key_changed\"):\n        trust[problem] = _(\"This was signed by an unexpected key\")\n    elif trust.get(\"expired_signature\"):\n        trust[problem] = _(\"This was signed by an expired key\")\n    elif trust.get(\"verified_signature\") or trust.get(\"signed\"):\n        trust[\"status\"] = _(\"Good signature, we are happy\")\n    elif trust.get(\"mua_or_mta_changed\"):\n        trust[\"warning\"] = _(\"This came from an unexpected source\")\n    else:\n        trust[\"status\"] = _(\"No problems detected.\")\n\n    return tree\n\n\n##[ Setup ]#################################################################\n\nif __name__ != \"__main__\":\n    if hasattr(ssl, 'SSLContext'):\n        ssl.SSLContext.wrap_socket = monkey_patch(\n            ssl.SSLContext.wrap_socket, tls_context_wrap_socket)\n        def add_tls_context(unused_org_wrap, sock, *args, **kwargs):\n            try:\n                return tls_new_context().wrap_socket(sock, *args, **kwargs)\n            except:\n                raise\n        ssl.wrap_socket = monkey_patch(ssl.wrap_socket, add_tls_context)\n    else:\n        ssl.wrap_socket = monkey_patch(ssl.wrap_socket, tls_wrap_socket)\n\n\n##[ Tests ]##################################################################\n\nif __name__ == \"__main__\":\n    import doctest\n    import sys\n    result = doctest.testmod(optionflags=doctest.ELLIPSIS)\n    print('%s' % (result, ))\n    if result.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/smtp_client.py",
    "content": "# -*- coding: utf-8 -*-\nimport random\nimport hashlib\nimport smtplib\nimport socket\nimport ssl\nimport sys\nimport time\n\nimport mailpile.util\nfrom mailpile.auth import IndirectPassword\nfrom mailpile.conn_brokers import Master as ConnBroker\nfrom mailpile.eventlog import Event\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.config.detect import ssl, socks\nfrom mailpile.mailutils import InsecureSmtpError\nfrom mailpile.mailutils.emails import CleanMessage, MessageAsString\nfrom mailpile.safe_popen import Popen, PIPE\nfrom mailpile.util import *\nfrom mailpile.vcard import VCardLine\n\n\ndef sha512_512k(data):\n    #\n    # This abuse of sha512 forces it to work with at least 512kB of data,\n    # no matter what it started with. On each iteration, we add one\n    # hexdigest to the front of the string (to prevent reuse of state).\n    # Each hexdigest is 128 bytes, so that gives:\n    #\n    # Total == 128 * (0 + 1 + 2 + ... + 90) + 128 == 128 * 4096 == 524288\n    #\n    # Max memory use is sadly only 10KB or so - hardly memory-hard. :-)\n    # Oh well!  I'm no cryptographer, and yes, we should probably just\n    # be using scrypt.\n    #\n    sha512 = hashlib.sha512\n    for i in range(0, 91):\n        data = sha512(data).hexdigest() + data\n    return sha512(data).hexdigest()\n\n\ndef sha512_512kCheck(challenge, bits, solution):\n    hexchars = bits // 4\n    wanted = '0' * hexchars\n    digest = sha512_512k('-'.join([solution, challenge]))\n    return (digest[:hexchars] == wanted)\n\n\ndef sha512_512kCollide(challenge, bits, callback1k=None):\n    hexchars = bits // 4\n    wanted = '0' * hexchars\n    for i in xrange(1, 0x10000):\n        if callback1k is not None:\n            callback1k(i)\n        challenge_i = '-'.join([str(i), challenge])\n        for j in xrange(0, 1024):\n            collision = '-'.join([str(j), challenge_i])\n            if sha512_512k(collision)[:hexchars] == wanted:\n                return '-'.join(collision.split('-')[:2])\n    return None\n\n\nSMTORP_HASHCASH_RCODE = 450\nSMTORP_HASHCASH_PREFIX = 'Please collide'\nSMTORP_HASHCASH_FORMAT = (SMTORP_HASHCASH_PREFIX +\n                          ' %(bits)d,%(challenge)s or retry. See: %(url)s')\n\n\ndef SMTorP_HashCash(rcpt, msg, callback1k=None):\n    bits_challenge_etc = msg[len(SMTORP_HASHCASH_PREFIX):].strip()\n    bits, challenge = bits_challenge_etc.split()[0].split(',', 1)\n\n    def cb(*args, **kwargs):\n        play_nice_with_threads()\n        if callback1k:\n            callback1k(*args, **kwargs)\n\n    return '%s##%s' % (rcpt, sha512_512kCollide(challenge, int(bits),\n                                                callback1k=cb))\n\n\nclass SMTP(smtplib.SMTP):\n    pass\n\nif ssl is not None:\n    class SMTP_SSL(smtplib.SMTP_SSL):\n        pass\nelse:\n    SMTP_SSL = SMTP\n\n\nclass SendMailError(IOError):\n    def __init__(self, msg, details=None):\n        IOError.__init__(self, msg)\n        self.error_info = details or {}\n\n\ndef _RouteTuples(session, from_to_msg_ev_tuples, test_route=None):\n    tuples = []\n    for frm, to, msg, events in from_to_msg_ev_tuples:\n        rcpts = {}\n        routes = {}\n        for recipient in to:\n            # If any of the events thinks this message has been delivered,\n            # then don't try to send it again.\n            frm_to = '>'.join([frm, recipient])\n            for ev in (events or []):\n                if ev.private_data.get(frm_to, False):\n                    recipient = None\n                    break\n            if recipient:\n                route = {\"protocol\": \"\",\n                         \"username\": \"\",\n                         \"password\": \"\",\n                         \"auth_type\": \"\",\n                         \"command\": \"\",\n                         \"host\": \"\",\n                         \"port\": 25}\n\n                if test_route:\n                    route.update(test_route)\n                else:\n                    route.update(session.config.get_route(frm, [recipient]))\n\n                # Group together recipients that use the same route\n                rid = '/'.join(sorted(['%s' % (k, )\n                                       for k in route.iteritems()]))\n                routes[rid] = route\n                rcpts[rid] = rcpts.get(rid, [])\n                rcpts[rid].append(recipient)\n        for rid in rcpts:\n            tuples.append((frm, routes[rid], rcpts[rid], msg, events))\n    return tuples\n\n\ndef SendMail(session, msg_mid, from_to_msg_ev_tuples,\n             test_only=False, test_route=None):\n    routes = _RouteTuples(session, from_to_msg_ev_tuples,\n                          test_route=test_route)\n\n    # Randomize order of routes, so we don't always try the broken\n    # one first. Any failure will bail out, but we do keep track of\n    # our successes via. the event, so eventually everything sendable\n    # should get sent.\n    routes.sort(key=lambda k: random.randint(0, 10))\n\n    # Update initial event state before we go through and start\n    # trying to deliver stuff.\n    for frm, route, to, msg, events in routes:\n        for ev in (events or []):\n            for rcpt in to:\n                ev.private_data['>'.join([frm, rcpt])] = False\n\n    for frm, route, to, msg, events in routes:\n        for ev in events:\n            ev.data['recipients'] = len(ev.private_data.keys())\n            ev.data['delivered'] = len([k for k in ev.private_data\n                                        if ev.private_data[k]])\n\n    def mark(msg, events, log=True, clear_errors=False):\n        for ev in events:\n            ev.flags = Event.RUNNING\n            ev.message = msg\n            if clear_errors:\n                if 'last_error' in ev.data:\n                    del ev.data['last_error']\n                if 'last_error_details' in ev.data:\n                    del ev.data['last_error_details']\n            if log:\n                session.config.event_log.log_event(ev)\n        session.ui.mark(msg)\n\n    def fail(msg, events, details=None, exception=SendMailError):\n        mark(msg, events, log=True)\n        for ev in events:\n            ev.data['last_error'] = msg\n            if details:\n                ev.data['last_error_details'] = details\n        raise exception(msg, details=details)\n\n    def smtp_do_or_die(msg, events, method, *args, **kwargs):\n        rc, msg = method(*args, **kwargs)\n        if rc != 250:\n            fail(msg + ' (%s %s)' % (rc, msg), events,\n                 details={'smtp_error': '%s: %s' % (rc, msg)})\n\n    # Do the actual delivering...\n    for frm, route, to, msg, events in routes:\n        route_description = route['command'] or route['host']\n\n        frm_vcard = session.config.vcards.get_vcard(frm)\n        update_to_vcards = msg and msg[\"x-mp-internal-pubkeys-attached\"]\n\n        if 'sendmail' in session.config.sys.debug:\n            sys.stderr.write(_('Sendmail: From %s (%s), to %s via %s\\n')\n                             % (frm, frm_vcard and frm_vcard.random_uid or '',\n                                to, route_description))\n        sm_write = sm_close = lambda: True\n\n        mark(_('Sending via %s') % route_description, events, clear_errors=True)\n\n        if route['command']:\n            # Note: The .strip().split() here converts our cmd into a list,\n            #       which should ensure that Popen does not spawn a shell\n            #       with potentially exploitable arguments.\n            cmd = (route['command'] % {\"rcpt\": \",\".join(to)}).strip().split()\n            if cmd[0][:1] == '|':\n                cmd[0] = cmd[0][1:]\n            proc = Popen(cmd, stdin=PIPE, long_running=True)\n            sm_startup = None\n            sm_write = proc.stdin.write\n\n            def sm_close():\n                proc.stdin.close()\n                rv = proc.wait()\n                if rv != 0:\n                    fail(_('%s failed with exit code %d') % (cmd, rv), events,\n                         details={'failed_command': cmd,\n                                  'exit_code': rv})\n\n            sm_cleanup = lambda: [proc.stdin.close(), proc.wait()]\n            # FIXME: Update session UI with progress info\n            for ev in events:\n                ev.data['proto'] = 'subprocess'\n                ev.data['command'] = cmd[0]\n\n        elif route['protocol'] in ('smtp', 'smtorp', 'smtpssl', 'smtptls'):\n            proto = route['protocol']\n            host, port = route['host'], route['port']\n            user = route['username']\n            pwd = IndirectPassword(session.config, route['password'])\n            auth_type = route['auth_type'] or ''\n            smtp_ssl = proto in ('smtpssl', )  # FIXME: 'smtorp'\n\n            for ev in events:\n                ev.data['proto'] = proto\n                ev.data['host'] = host\n                ev.data['auth'] = bool(user and pwd)\n\n            if 'sendmail' in session.config.sys.debug:\n                sys.stderr.write(_('SMTP connection to: %s:%s as %s\\n'\n                                   ) % (host, port, user or '(anon)'))\n\n            serverbox = [None]\n            def sm_connect_server():\n                server = (smtp_ssl and SMTP_SSL or SMTP\n                          )(local_hostname='mailpile.local', timeout=120)\n                if 'sendmail' in session.config.sys.debug:\n                    server.set_debuglevel(1)\n                if smtp_ssl or proto in ('smtorp', 'smtptls'):\n                    conn_needs = [ConnBroker.OUTGOING_ENCRYPTED]\n                else:\n                    conn_needs = [ConnBroker.OUTGOING_SMTP]\n                try:\n                    with ConnBroker.context(need=conn_needs) as ctx:\n                        server.connect(host, int(port))\n                    server.sock.settimeout(120)\n                    server.ehlo_or_helo_if_needed()\n                except (IOError, OSError, smtplib.SMTPServerDisconnected):\n                    fail(_('Failed to connect to %s') % host, events,\n                         details={'connection_error': True})\n\n                return server\n\n            def sm_startup():\n                try:\n                    server = sm_connect_server()\n                    if not smtp_ssl:\n                        # We always try to enable TLS, even if the user just\n                        # requested plain-text smtp.  But we only throw errors\n                        # if the user asked for encryption.\n                        try:\n                            server.starttls()\n                            server.ehlo_or_helo_if_needed()\n                        except:\n                            if proto == 'smtptls':\n                                raise\n                            else:\n                                server = sm_connect_server()\n                except (ssl.CertificateError, ssl.SSLError):\n                    fail(_('Failed to make a secure TLS connection'),\n                         events,\n                         details={\n                             'tls_error': True,\n                             'server': '%s:%d' % (host, port)},\n                         exception=InsecureSmtpError)\n\n                serverbox[0] = server\n\n                if user:\n                    try:\n                        if auth_type.lower() == 'oauth2':\n                            from mailpile.plugins.oauth import OAuth2\n                            tok_info = OAuth2.GetFreshTokenInfo(session, user)\n                            if not (user and tok_info and tok_info.access_token):\n                                fail(_('Access denied by mail server'),\n                                     events,\n                                     details={'oauth_error': True,\n                                              'username': user})\n                            authstr = (OAuth2.XOAuth2Response(user, tok_info)\n                                       ).encode('base64').replace('\\n', '')\n                            server.docmd('AUTH', 'XOAUTH2 ' + authstr)\n                        elif auth_type:\n                            server.login(user.encode('utf-8'),\n                                         (pwd or '').encode('utf-8'))\n                    except UnicodeDecodeError:\n                        fail(_('Bad character in username or password'),\n                             events,\n                             details={'authentication_error': True,\n                                      'username': user})\n                    except smtplib.SMTPAuthenticationError:\n                        fail(_('Invalid username or password'), events,\n                             details={'authentication_error': True,\n                                      'username': user})\n                    except smtplib.SMTPException:\n                        # If the server does not support authentication, assume\n                        # it's passwordless and try to carry one anyway.\n                        pass\n\n                smtp_do_or_die(_('Sender rejected by SMTP server'),\n                               events, server.mail, frm)\n                for rcpt in to:\n                    rc, msg = server.rcpt(rcpt)\n                    if (rc == SMTORP_HASHCASH_RCODE and\n                            msg.startswith(SMTORP_HASHCASH_PREFIX)):\n                        rc, msg = server.rcpt(SMTorP_HashCash(rcpt, msg))\n                    if rc != 250:\n                        fail(_('Server rejected recipient: %s') % rcpt, events)\n                rcode, rmsg = server.docmd('DATA')\n                if rcode != 354:\n                    fail(_('Server rejected DATA: %s %s') % (rcode, rmsg))\n\n            def sm_write(data):\n                server = serverbox[0]\n                for line in data.splitlines(True):\n                    if line.startswith('.'):\n                        server.send('.')\n                    server.send(line)\n\n            def sm_close():\n                server = serverbox[0]\n                server.send('\\r\\n.\\r\\n')\n                smtp_do_or_die(_('Error spooling mail'),\n                               events, server.getreply)\n\n            def sm_cleanup():\n                server = serverbox[0]\n                if hasattr(server, 'sock'):\n                    server.close()\n        else:\n            fail(_('Invalid route: %s') % route, events)\n\n        try:\n            # Run the entire connect/login sequence in a single timer, but\n            # give it plenty of time in case the network is lame.\n            if sm_startup:\n                RunTimed(300, sm_startup, unique_thread='smtp-client')\n\n            if test_only:\n                return True\n\n            mark(_('Preparing message…'), events)\n\n            msg_string = MessageAsString(CleanMessage(session.config, msg))\n            total = len(msg_string)\n            while msg_string:\n                if mailpile.util.QUITTING:\n                    raise TimedOut(_('Quitting'))\n                mark(('Sending message... (%d%%)'\n                      ) % (100 * (total-len(msg_string))/total), events,\n                     log=False)\n                sm_write(msg_string[:20480])\n                msg_string = msg_string[20480:]\n            sm_close()\n\n            mark(_n('Message sent, %d byte',\n                    'Message sent, %d bytes',\n                    total\n                    ) % total, events)\n\n            for ev in events:\n                for rcpt in to:\n                    vcard = session.config.vcards.get_vcard(rcpt)\n                    if vcard:\n                        with vcard:\n                            vcard.record_history('send', time.time(), msg_mid)\n                            if frm_vcard:\n                                vcard.prefer_sender(rcpt, frm_vcard)\n                            if update_to_vcards:\n                                vcard.pgp_key_shared = int(time.time())\n                            vcard.save()\n                    ev.private_data['>'.join([frm, rcpt])] = True\n                ev.data['bytes'] = total\n                ev.data['delivered'] = len([k for k in ev.private_data\n                                            if ev.private_data[k]])\n        finally:\n            sm_cleanup()\n    return True\n"
  },
  {
    "path": "mailpile/spambayes/LICENSE.txt",
    "content": "Copyright (C) 2002-2007 Python Software Foundation; All Rights Reserved\n\nThe Python Software Foundation (PSF) holds copyright on all material\nin this project.  You may use it under the terms of the PSF license:\n\nPSF LICENSE AGREEMENT FOR THE SPAMBAYES PROJECT\n-----------------------------------------------\n\n1. This LICENSE AGREEMENT is between the Python Software Foundation\n(\"PSF\"), and the Individual or Organization (\"Licensee\") accessing and\notherwise using the spambayes software (\"Software\") in source or binary\nform and its associated documentation.\n\n2. Subject to the terms and conditions of this License Agreement, PSF\nhereby grants Licensee a nonexclusive, royalty-free, world-wide\nlicense to reproduce, analyze, test, perform and/or display publicly,\nprepare derivative works, distribute, and otherwise use the Software\nalone or in any derivative version, provided, however, that PSF's\nLicense Agreement and PSF's notice of copyright, i.e., \"Copyright (c)\n2002-2004 Python Software Foundation; All Rights Reserved\" are retained\nthe Software alone or in any derivative version prepared by Licensee.\n\n3. In the event Licensee prepares a derivative work that is based on\nor incorporates the Software or any part thereof, and wants to make\nthe derivative work available to others as provided herein, then\nLicensee hereby agrees to include in any such work a brief summary of\nthe changes made to the Software.\n\n4. PSF is making the Software available to Licensee on an \"AS IS\"\nbasis.  PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR\nIMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND\nDISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS\nFOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT\nINFRINGE ANY THIRD PARTY RIGHTS.\n\n5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE\nSOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS\nA RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING THE SOFTWARE,\nOR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.\n\n6. This License Agreement will automatically terminate upon a material\nbreach of its terms and conditions.\n\n7. Nothing in this License Agreement shall be deemed to create any\nrelationship of agency, partnership, or joint venture between PSF and\nLicensee.  This License Agreement does not grant permission to use PSF\ntrademarks or trade name in a trademark sense to endorse or promote\nproducts or services of Licensee, or any third party.\n\n8. By copying, installing or otherwise using the Software, Licensee\nagrees to be bound by the terms and conditions of this License\nAgreement.\n"
  },
  {
    "path": "mailpile/spambayes/Options.py",
    "content": "\"\"\"Options\n\nAbstract:\n\nOptions.options is a globally shared options object.\nThis object is initialised when the module is loaded: the envar\nBAYESCUSTOMIZE is checked for a list of names, if nothing is found\nthen the local directory and the home directory are checked for a\nfile called bayescustomize.ini or .spambayesrc (respectively) and\nthe initial values are loaded from this.\n\nThe Option class is defined in OptionsClass.py - this module\nis responsible only for instantiating and loading the globally\nshared instance.\n\nTo Do:\n o Suggestions?\n\"\"\"\n\nfrom __future__ import print_function\nimport sys, os\n\ntry:\n    _\nexcept NameError:\n    _ = lambda arg: arg\n\n__all__ = ['options', '_']\n\n# Grab the stuff from the core options class.\nfrom mailpile.spambayes.OptionsClass import *\n\n# A little magic.  We'd like to use ZODB as the default storage,\n# because we've had so many problems with bsddb, and we'd like to swap\n# to new ZODB problems <wink>.  However, apart from this, we only need\n# a standard Python install - if the default was ZODB then we would\n# need ZODB to be installed as well (which it will br for binary users,\n# but might not be for source users).  So what we do is check whether\n# ZODB is importable and if it is, default to that, and if not, default\n# to dbm.  If ZODB is sometimes importable and sometimes not (e.g. you\n# muck around with the PYTHONPATH), then this may not work well - the\n# best idea would be to explicitly put the type in your configuration\n# file.\ntry:\n    import ZODB\nexcept ImportError:\n    DB_TYPE = \"dbm\", \"hammie.db\", \"spambayes.messageinfo.db\"\nelse:\n    del ZODB\n    DB_TYPE = \"zodb\", \"hammie.fs\", \"messageinfo.fs\"\n\n# Format:\n# defaults is a dictionary, where the keys are the section names\n# each key maps to a tuple consisting of:\n#   option name, display name, default,\n#   doc string, possible values, restore on restore-to-defaults\n# The display name and doc string should be enclosed in _() to allow\n# i18n.  In a few cases, then possible values should also be enclosed\n# in _().\n\ndefaults = {\n  \"Tokenizer\" : (\n    (\"basic_header_tokenize\", _(\"Basic header tokenising\"), False,\n     _(\"\"\"If true, tokenizer.Tokenizer.tokenize_headers() will tokenize the\n     contents of each header field just like the text of the message\n     body, using the name of the header as a tag.  Tokens look like\n     \"header:word\".  The basic approach is simple and effective, but also\n     very sensitive to biases in the ham and spam collections.  For\n     example, if the ham and spam were collected at different times,\n     several headers with date/time information will become the best\n     discriminators.  (Not just Date, but Received and X-From_.)\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"basic_header_tokenize_only\", _(\"Only basic header tokenising\"), False,\n     _(\"\"\"If true and basic_header_tokenize is also true, then\n     basic_header_tokenize is the only action performed.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"basic_header_skip\", _(\"Basic headers to skip\"), (\"received date x-.*\",),\n     _(\"\"\"If basic_header_tokenize is true, then basic_header_skip is a set\n     of headers that should be skipped.\"\"\"),\n     HEADER_NAME, RESTORE),\n\n    (\"check_octets\", _(\"Check application/octet-stream sections\"), False,\n     _(\"\"\"If true, the first few characters of application/octet-stream\n     sections are used, undecoded.  What 'few' means is decided by\n     octet_prefix_size.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"octet_prefix_size\", _(\"Number of characters of octet stream to process\"), 5,\n     _(\"\"\"The number of characters of the application/octet-stream sections\n     to use, if check_octets is set to true.\"\"\"),\n     INTEGER, RESTORE),\n\n    (\"x-short_runs\", _(\"Count runs of short 'words'\"), False,\n     _(\"\"\"(EXPERIMENTAL) If true, generate tokens based on max number of\n     short word runs. Short words are anything of length < the\n     skip_max_word_size option.  Normally they are skipped, but one common\n     spam technique spells words like 'V I A G RA'.\n     \"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"x-lookup_ip\", _(\"Generate IP address tokens from hostnames\"), False,\n     _(\"\"\"(EXPERIMENTAL) Generate IP address tokens from hostnames.\n     Requires PyDNS (http://pydns.sourceforge.net/).\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"lookup_ip_cache\", _(\"x-lookup_ip cache file location\"), \"\",\n     _(\"\"\"Tell SpamBayes where to cache IP address lookup information.\n     Only comes into play if lookup_ip is enabled. The default\n     (empty string) disables the file cache.  When caching is enabled,\n     the cache file is stored using the same database type as the main\n     token store (only dbm and zodb supported so far, zodb has problems,\n     dbm is untested, hence the default).\"\"\"),\n     PATH, RESTORE),\n\n    (\"image_size\", _(\"Generate image size tokens\"), False,\n     _(\"\"\"If true, generate tokens based on the sizes of\n     embedded images.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"crack_images\", _(\"Look inside images for text\"), False,\n     _(\"\"\"If true, generate tokens based on the\n     (hopefully) text content contained in any images in each message.\n     The current support is minimal, relies on the installation of\n     an OCR 'engine' (see ocr_engine.)\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"ocr_engine\", _(\"OCR engine to use\"), \"\",\n     _(\"\"\"The name of the OCR engine to use.  If empty, all\n     supported engines will be checked to see if they are installed.\n     Engines currently supported include ocrad\n     (http://www.gnu.org/software/ocrad/ocrad.html) and gocr\n     (http://jocr.sourceforge.net/download.html) and they require the\n     appropriate executable be installed in either your PATH, or in the\n     main spambayes directory.\"\"\"),\n     HEADER_VALUE, RESTORE),\n\n    (\"crack_image_cache\", _(\"Cache to speed up ocr.\"), \"\",\n     _(\"\"\"If non-empty, names a file from which to read cached ocr info\n     at start and to which to save that info at exit.\"\"\"),\n     PATH, RESTORE),\n\n    (\"ocrad_scale\", _(\"Scale factor to use with ocrad.\"), 2,\n     _(\"\"\"Specifies the scale factor to apply when running ocrad.  While\n     you can specify a negative scale it probably won't help.  Scaling up\n     by a factor of 2 or 3 seems to work well for the sort of spam images\n     encountered by SpamBayes.\"\"\"),\n     INTEGER, RESTORE),\n\n    (\"ocrad_charset\", _(\"Charset to apply with ocrad.\"), \"ascii\",\n     _(\"\"\"Specifies the charset to use when running ocrad.  Valid values\n     are 'ascii', 'iso-8859-9' and 'iso-8859-15'.\"\"\"),\n     OCRAD_CHARSET, RESTORE),\n\n    (\"max_image_size\", _(\"Max image size to try OCR-ing\"), 100000,\n     _(\"\"\"When crack_images is enabled, this specifies the largest\n     image to try OCR on.\"\"\"),\n     INTEGER, RESTORE),\n\n    (\"count_all_header_lines\", _(\"Count all header lines\"), False,\n     _(\"\"\"Generate tokens just counting the number of instances of each kind\n     of header line, in a case-sensitive way.\n\n     Depending on data collection, some headers are not safe to count.\n     For example, if ham is collected from a mailing list but spam from\n     your regular inbox traffic, the presence of a header like List-Info\n     will be a very strong ham clue, but a bogus one.  In that case, set\n     count_all_header_lines to False, and adjust safe_headers instead.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"record_header_absence\", _(\"Record header absence\"), False,\n     _(\"\"\"When True, generate a \"noheader:HEADERNAME\" token for each header\n     in safe_headers (below) that *doesn't* appear in the headers.  This\n     helped in various of Tim's python.org tests, but appeared to hurt a\n     little in Anthony Baxter's tests.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"safe_headers\", _(\"Safe headers\"), (\"abuse-reports-to\", \"date\", \"errors-to\",\n                                      \"from\", \"importance\", \"in-reply-to\",\n                                      \"message-id\", \"mime-version\",\n                                      \"organization\", \"received\",\n                                      \"reply-to\", \"return-path\", \"subject\",\n                                      \"to\", \"user-agent\", \"x-abuse-info\",\n                                      \"x-complaints-to\", \"x-face\"),\n     _(\"\"\"Like count_all_header_lines, but restricted to headers in this list.\n     safe_headers is ignored when count_all_header_lines is true, unless\n     record_header_absence is also true.\"\"\"),\n     HEADER_NAME, RESTORE),\n\n    (\"mine_received_headers\", _(\"Mine the received headers\"), False,\n     _(\"\"\"A lot of clues can be gotten from IP addresses and names in\n     Received: headers.  This can give spectacular results for bogus\n     reasons if your corpora are from different sources.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"x-mine_nntp_headers\", _(\"Mine NNTP-Posting-Host headers\"), False,\n     _(\"\"\"Usenet is host to a lot of spam.  Usenet/Mailing list gateways\n     can let it leak across.  Similar to mining received headers, we pick\n     apart the IP address or host name in this header for clues.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"address_headers\", _(\"Address headers to mine\"), (\"from\", \"to\", \"cc\",\n                                                       \"sender\", \"reply-to\"),\n     _(\"\"\"Mine the following address headers. If you have mixed source\n     corpuses (as opposed to a mixed sauce walrus, which is delicious!)\n     then you probably don't want to use 'to' or 'cc') Address headers will\n     be decoded, and will generate charset tokens as well as the real\n     address.  Others to consider: errors-to, ...\"\"\"),\n     HEADER_NAME, RESTORE),\n\n    (\"generate_long_skips\", _(\"Generate long skips\"), True,\n     _(\"\"\"If legitimate mail contains things that look like text to the\n     tokenizer and turning turning off this option helps (perhaps binary\n     attachments get 'defanged' by something upstream from this operation\n     and thus look like text), this may help, and should be an alert that\n     perhaps the tokenizer is broken.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"summarize_email_prefixes\", _(\"Summarise email prefixes\"), False,\n     _(\"\"\"Try to capitalize on mail sent to multiple similar addresses.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"summarize_email_suffixes\", _(\"Summarise email suffixes\"), False,\n     _(\"\"\"Try to capitalize on mail sent to multiple similar addresses.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"skip_max_word_size\", _(\"Long skip trigger length\"), 12,\n     _(\"\"\"Length of words that triggers 'long skips'. Longer than this\n     triggers a skip.\"\"\"),\n     INTEGER, RESTORE),\n\n    (\"x-pick_apart_urls\", _(\"Extract clues about url structure\"), False,\n     _(\"\"\"(EXPERIMENTAL) Note whether url contains non-standard port or\n     user/password elements.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"x-fancy_url_recognition\", _(\"Extract URLs without http:// prefix\"), False,\n     _(\"\"\"(EXPERIMENTAL) Recognize 'www.python.org' or ftp.python.org as URLs\n     instead of just long words.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"replace_nonascii_chars\", _(\"Replace non-ascii characters\"), False,\n     _(\"\"\"If true, replace high-bit characters (ord(c) >= 128) and control\n     characters with question marks.  This allows non-ASCII character\n     strings to be identified with little training and small database\n     burden.  It's appropriate only if your ham is plain 7-bit ASCII, or\n     nearly so, so that the mere presence of non-ASCII character strings is\n     known in advance to be a strong spam indicator.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"x-search_for_habeas_headers\", _(\"Search for Habeas Headers\"), False,\n     _(\"\"\"(EXPERIMENTAL) If true, search for the habeas headers (see\n     http://www.habeas.com). If they are present and correct, this should\n     be a strong ham sign, if they are present and incorrect, this should\n     be a strong spam sign.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"x-reduce_habeas_headers\", _(\"Reduce Habeas Header Tokens to Single\"), False,\n     _(\"\"\"(EXPERIMENTAL) If SpamBayes is set to search for the Habeas\n     headers, nine tokens are generated for messages with habeas headers.\n     This should be fine, since messages with the headers should either be\n     ham, or result in FN so that we can send them to habeas so they can\n     be sued.  However, to reduce the strength of habeas headers, we offer\n     the ability to reduce the nine tokens to one. (This option has no\n     effect if 'Search for Habeas Headers' is False)\"\"\"),\n     BOOLEAN, RESTORE),\n  ),\n\n  # These options control how a message is categorized\n  \"Categorization\" : (\n    # spam_cutoff and ham_cutoff are used in Python slice sense:\n    #    A msg is considered    ham if its score is in 0:ham_cutoff\n    #    A msg is considered unsure if its score is in ham_cutoff:spam_cutoff\n    #    A msg is considered   spam if its score is in spam_cutoff:\n    #\n    # So it's unsure iff  ham_cutoff <= score < spam_cutoff.\n    # For a binary classifier, make ham_cutoff == spam_cutoff.\n    # ham_cutoff > spam_cutoff doesn't make sense.\n    #\n    # The defaults here (.2 and .9) may be appropriate for the default chi-\n    # combining scheme.  Cutoffs for chi-combining typically aren't touchy,\n    # provided you're willing to settle for \"really good\" instead of \"optimal\".\n    # Tim found that .3 and .8 worked very well for well-trained systems on\n    # his personal email, and his large comp.lang.python test.  If just\n    # beginning training, or extremely fearful of mistakes, 0.05 and 0.95 may\n    # be more appropriate for you.\n    #\n    # Picking good values for gary-combining is much harder, and appears to be\n    # corpus-dependent, and within a single corpus dependent on how much\n    # training has been done.  Values from 0.50 thru the low 0.60's have been\n    # reported to work best by various testers on their data.\n    (\"ham_cutoff\", _(\"Ham cutoff\"), 0.20,\n     _(\"\"\"Spambayes gives each email message a spam probability between\n     0 and 1. Emails below the Ham Cutoff probability are classified\n     as Ham. Larger values will result in more messages being\n     classified as ham, but with less certainty that all of them\n     actually are ham. This value should be between 0 and 1,\n     and should be smaller than the Spam Cutoff.\"\"\"),\n     REAL, RESTORE),\n\n    (\"spam_cutoff\", _(\"Spam cutoff\"), 0.90,\n     _(\"\"\"Emails with a spam probability above the Spam Cutoff are\n     classified as Spam - just like the Ham Cutoff but at the other\n     end of the scale.  Messages that fall between the two values\n     are classified as Unsure.\"\"\"),\n     REAL, RESTORE),\n  ),\n\n  # These control various displays in class TestDriver.Driver, and\n  # Tester.Test.\n  \"TestDriver\" : (\n    (\"nbuckets\", _(\"Number of buckets\"), 200,\n     _(\"\"\"Number of buckets in histograms.\"\"\"),\n     INTEGER, RESTORE),\n\n    (\"show_histograms\", _(\"Show histograms\"), True,\n     _(\"\"\"\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"compute_best_cutoffs_from_histograms\", _(\"Compute best cutoffs from histograms\"), True,\n     _(\"\"\"After the display of a ham+spam histogram pair, you can get a\n     listing of all the cutoff values (coinciding with histogram bucket\n     boundaries) that minimize:\n         best_cutoff_fp_weight * (# false positives) +\n         best_cutoff_fn_weight * (# false negatives) +\n         best_cutoff_unsure_weight * (# unsure msgs)\n\n     This displays two cutoffs:  hamc and spamc, where\n        0.0 <= hamc <= spamc <= 1.0\n\n     The idea is that if something scores < hamc, it's called ham; if\n     something scores >= spamc, it's called spam; and everything else is\n     called 'I am not sure' -- the middle ground.\n\n     Note:  You may wish to increase nbuckets, to give this scheme more cutoff\n     values to analyze.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"best_cutoff_fp_weight\", _(\"Best cutoff false positive weight\"), 10.00,\n     _(\"\"\"\"\"\"),\n     REAL, RESTORE),\n\n    (\"best_cutoff_fn_weight\", _(\"Best cutoff false negative weight\"), 1.00,\n     _(\"\"\"\"\"\"),\n     REAL, RESTORE),\n\n    (\"best_cutoff_unsure_weight\", _(\"Best cutoff unsure weight\"), 0.20,\n     _(\"\"\"\"\"\"),\n     REAL, RESTORE),\n\n    (\"percentiles\", _(\"Percentiles\"), (5, 25, 75, 95),\n     _(\"\"\"Histogram analysis also displays percentiles.  For each percentile\n     p in the list, the score S such that p% of all scores are <= S is\n     given. Note that percentile 50 is the median, and is displayed (along\n     with the min score and max score) independent of this option.\"\"\"),\n     INTEGER, RESTORE),\n\n    (\"show_spam_lo\", _(\"\"), 1.0,\n     _(\"\"\"Display spam when show_spam_lo <= spamprob <= show_spam_hi and\n     likewise for ham.  The defaults here do not show anything.\"\"\"),\n     REAL, RESTORE),\n\n    (\"show_spam_hi\", _(\"\"), 0.0,\n     _(\"\"\"Display spam when show_spam_lo <= spamprob <= show_spam_hi and\n     likewise for ham.  The defaults here do not show anything.\"\"\"),\n     REAL, RESTORE),\n\n    (\"show_ham_lo\", _(\"\"), 1.0,\n     _(\"\"\"Display spam when show_spam_lo <= spamprob <= show_spam_hi and\n     likewise for ham.  The defaults here do not show anything.\"\"\"),\n     REAL, RESTORE),\n\n    (\"show_ham_hi\", _(\"\"), 0.0,\n     _(\"\"\"Display spam when show_spam_lo <= spamprob <= show_spam_hi and\n     likewise for ham.  The defaults here do not show anything.\"\"\"),\n     REAL, RESTORE),\n\n    (\"show_false_positives\", _(\"Show false positives\"), True,\n     _(\"\"\"\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"show_false_negatives\", _(\"Show false negatives\"), False,\n     _(\"\"\"\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"show_unsure\", _(\"Show unsure\"), False,\n     _(\"\"\"\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"show_charlimit\", _(\"Show character limit\"), 3000,\n     _(\"\"\"The maximum # of characters to display for a msg displayed due to\n     the show_xyz options above.\"\"\"),\n     INTEGER, RESTORE),\n\n    (\"save_trained_pickles\", _(\"Save trained pickles\"), False,\n     _(\"\"\"If save_trained_pickles is true, Driver.train() saves a binary\n     pickle of the classifier after training.  The file basename is given\n     by pickle_basename, the extension is .pik, and increasing integers are\n     appended to pickle_basename.  By default (if save_trained_pickles is\n     true), the filenames are class1.pik, class2.pik, ...  If a file of\n     that name already exists, it is overwritten.  pickle_basename is\n     ignored when save_trained_pickles is false.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"pickle_basename\", _(\"Pickle basename\"), \"class\",\n     _(\"\"\"\"\"\"),\n     r\"[\\w]+\", RESTORE),\n\n    (\"save_histogram_pickles\", _(\"Save histogram pickles\"), False,\n     _(\"\"\"If save_histogram_pickles is true, Driver.train() saves a binary\n     pickle of the spam and ham histogram for \"all test runs\". The file\n     basename is given by pickle_basename, the suffix _spamhist.pik\n     or _hamhist.pik is appended  to the basename.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"spam_directories\", _(\"Spam directories\"), \"Data/Spam/Set%d\",\n     _(\"\"\"default locations for timcv and timtest - these get the set number\n     interpolated.\"\"\"),\n     VARIABLE_PATH, RESTORE),\n\n    (\"ham_directories\", _(\"Ham directories\"), \"Data/Ham/Set%d\",\n     _(\"\"\"default locations for timcv and timtest - these get the set number\n     interpolated.\"\"\"),\n     VARIABLE_PATH, RESTORE),\n  ),\n\n  \"CV Driver\": (\n    (\"build_each_classifier_from_scratch\", _(\"Build each classifier from scratch\"), False,\n     _(\"\"\"A cross-validation driver takes N ham+spam sets, and builds N\n     classifiers, training each on N-1 sets, and the predicting against the\n     set not trained on.  By default, it does this in a clever way,\n     learning *and* unlearning sets as it goes along, so that it never\n     needs to train on N-1 sets in one gulp after the first time.  Setting\n     this option true forces ''one gulp from-scratch'' training every time.\n     There used to be a set of combining schemes that needed this, but now\n     it is just in case you are paranoid <wink>.\"\"\"),\n     BOOLEAN, RESTORE),\n  ),\n\n  \"Classifier\": (\n    (\"max_discriminators\", _(\"Maximum number of extreme words\"), 150,\n     _(\"\"\"The maximum number of extreme words to look at in a message, where\n     \"extreme\" means with spam probability farthest away from 0.5.  150\n     appears to work well across all corpora tested.\"\"\"),\n     INTEGER, RESTORE),\n\n    (\"unknown_word_prob\", _(\"Unknown word probability\"), 0.5,\n     _(\"\"\"These two control the prior assumption about word probabilities.\n     unknown_word_prob is essentially the probability given to a word that\n     has never been seen before.  Nobody has reported an improvement via\n     moving it away from 1/2, although Tim has measured a mean spamprob of\n     a bit over 0.5 (0.51-0.55) in 3 well-trained classifiers.\"\"\"),\n     REAL, RESTORE),\n\n    (\"unknown_word_strength\", _(\"Unknown word strength\"), 0.45,\n     _(\"\"\"This adjusts how much weight to give the prior\n     assumption relative to the probabilities estimated by counting.  At 0,\n     the counting estimates are believed 100%, even to the extent of\n     assigning certainty (0 or 1) to a word that has appeared in only ham\n     or only spam.  This is a disaster.\n\n     As unknown_word_strength tends toward infinity, all probabilities\n     tend toward unknown_word_prob.  All reports were that a value near 0.4\n     worked best, so this does not seem to be corpus-dependent.\"\"\"),\n     REAL, RESTORE),\n\n    (\"minimum_prob_strength\", _(\"Minimum probability strength\"), 0.1,\n     _(\"\"\"When scoring a message, ignore all words with\n     abs(word.spamprob - 0.5) < minimum_prob_strength.\n     This may be a hack, but it has proved to reduce error rates in many\n     tests.  0.1 appeared to work well across all corpora.\"\"\"),\n     REAL, RESTORE),\n\n    (\"use_chi_squared_combining\", _(\"Use chi-squared combining\"), True,\n     _(\"\"\"For vectors of random, uniformly distributed probabilities,\n     -2*sum(ln(p_i)) follows the chi-squared distribution with 2*n degrees\n     of freedom.  This is the \"provably most-sensitive\" test the original\n     scheme was monotonic with.  Getting closer to the theoretical basis\n     appears to give an excellent combining method, usually very extreme in\n     its judgment, yet finding a tiny (in # of msgs, spread across a huge\n     range of scores) middle ground where lots of the mistakes live.  This\n     is the best method so far. One systematic benefit is is immunity to\n     \"cancellation disease\". One systematic drawback is sensitivity to\n     *any* deviation from a uniform distribution, regardless of whether\n     actually evidence of ham or spam. Rob Hooft alleviated that by\n     combining the final S and H measures via (S-H+1)/2 instead of via\n     S/(S+H)). In practice, it appears that setting ham_cutoff=0.05, and\n     spam_cutoff=0.95, does well across test sets; while these cutoffs are\n     rarely optimal, they get close to optimal.  With more training data,\n     Tim has had good luck with ham_cutoff=0.30 and spam_cutoff=0.80 across\n     three test data sets (original c.l.p data, his own email, and newer\n     general python.org traffic).\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"use_bigrams\", _(\"Use mixed uni/bi-grams scheme\"), False,\n     _(\"\"\"Generate both unigrams (words) and bigrams (pairs of\n     words). However, extending an idea originally from Gary Robinson, the\n     message is 'tiled' into non-overlapping unigrams and bigrams,\n     approximating the strongest outcome over all possible tilings.\n\n     Note that to really test this option you need to retrain with it on,\n     so that your database includes the bigrams - if you subsequently turn\n     it off, these tokens will have no effect.  This option will at least\n     double your database size given the same training data, and will\n     probably at least triple it.\n\n     You may also wish to increase the max_discriminators (maximum number\n     of extreme words) option if you enable this option, perhaps doubling or\n     quadrupling it.  It's not yet clear.  Bigrams create many more hapaxes,\n     and that seems to increase the brittleness of minimalist training\n     regimes; increasing max_discriminators may help to soften that effect.\n     OTOH, max_discriminators defaults to 150 in part because that makes it\n     easy to prove that the chi-squared math is immune from numeric\n     problems.  Increase it too much, and insane results will eventually\n     result (including fatal floating-point exceptions on some boxes).\n\n     This option is experimental, and may be removed in a future release.\n     We would appreciate feedback about it if you use it - email\n     spambayes@python.org with your comments and results.\n     \"\"\"),\n     BOOLEAN, RESTORE),\n  ),\n\n  \"Hammie\": (\n    (\"train_on_filter\", _(\"Train when filtering\"), False,\n     _(\"\"\"Train when filtering?  After filtering a message, hammie can then\n     train itself on the judgement (ham or spam).  This can speed things up\n     with a procmail-based solution.  If you do enable this, please make\n     sure to retrain any mistakes.  Otherwise, your word database will\n     slowly become useless.  Note that this option is only used by\n     sb_filter, and will have no effect on sb_server's POP3 proxy, or\n     the IMAP filter.\"\"\"),\n     BOOLEAN, RESTORE),\n  ),\n\n  # These options control where Spambayes data will be stored, and in\n  # what form.  They are used by many Spambayes applications (including\n  # pop3proxy, smtpproxy, imapfilter and hammie), and mean that data\n  # (such as the message database) is shared between the applications.\n  # If this is not the desired behaviour, you must have a different\n  # value for each of these options in a configuration file that gets\n  # loaded by the appropriate application only.\n  \"Storage\" : (\n    (\"persistent_use_database\", _(\"Database backend\"), DB_TYPE[0],\n     _(\"\"\"SpamBayes can use either a ZODB or dbm database (quick to score\n     one message) or a pickle (quick to train on huge amounts of messages).\n     There is also (experimental) ability to use a mySQL or PostgresSQL\n     database.\"\"\"),\n     (\"zeo\", \"zodb\", \"cdb\", \"mysql\", \"pgsql\", \"dbm\", \"pickle\"), RESTORE),\n\n    (\"persistent_storage_file\", _(\"Storage file name\"), DB_TYPE[1],\n     _(\"\"\"Spambayes builds a database of information that it gathers\n     from incoming emails and from you, the user, to get better and\n     better at classifying your email.  This option specifies the\n     name of the database file.  If you don't give a full pathname,\n     the name will be taken to be relative to the location of the\n     most recent configuration file loaded.\"\"\"),\n     FILE_WITH_PATH, DO_NOT_RESTORE),\n\n    (\"messageinfo_storage_file\", _(\"Message information file name\"), DB_TYPE[2],\n     _(\"\"\"Spambayes builds a database of information about messages\n     that it has already seen and trained or classified.  This\n     database is used to ensure that these messages are not retrained\n     or reclassified (unless specifically requested to).  This option\n     specifies the name of the database file.  If you don't give a\n     full pathname, the name will be taken to be relative to the location\n     of the most recent configuration file loaded.\"\"\"),\n     FILE_WITH_PATH, DO_NOT_RESTORE),\n\n    (\"cache_use_gzip\", _(\"Use gzip\"), False,\n     _(\"\"\"Use gzip to compress the cache.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"cache_expiry_days\", _(\"Days before cached messages expire\"), 7,\n     _(\"\"\"Messages will be expired from the cache after this many days.\n     After this time, you will no longer be able to train on these messages\n     (note this does not affect the copy of the message that you have in\n     your mail client).\"\"\"),\n     INTEGER, RESTORE),\n\n    (\"spam_cache\", _(\"Spam cache directory\"), \"pop3proxy-spam-cache\",\n     _(\"\"\"Directory that SpamBayes should cache spam in.  If this does\n     not exist, it will be created.\"\"\"),\n     PATH, DO_NOT_RESTORE),\n\n    (\"ham_cache\", _(\"Ham cache directory\"), \"pop3proxy-ham-cache\",\n     _(\"\"\"Directory that SpamBayes should cache ham in.  If this does\n     not exist, it will be created.\"\"\"),\n     PATH, DO_NOT_RESTORE),\n\n    (\"unknown_cache\", _(\"Unknown cache directory\"), \"pop3proxy-unknown-cache\",\n     _(\"\"\"Directory that SpamBayes should cache unclassified messages in.\n     If this does not exist, it will be created.\"\"\"),\n     PATH, DO_NOT_RESTORE),\n\n    (\"core_spam_cache\", _(\"Spam cache directory\"), \"core-spam-cache\",\n     _(\"\"\"Directory that SpamBayes should cache spam in.  If this does\n     not exist, it will be created.\"\"\"),\n     PATH, DO_NOT_RESTORE),\n\n    (\"core_ham_cache\", _(\"Ham cache directory\"), \"core-ham-cache\",\n     _(\"\"\"Directory that SpamBayes should cache ham in.  If this does\n     not exist, it will be created.\"\"\"),\n     PATH, DO_NOT_RESTORE),\n\n    (\"core_unknown_cache\", _(\"Unknown cache directory\"), \"core-unknown-cache\",\n     _(\"\"\"Directory that SpamBayes should cache unclassified messages in.\n     If this does not exist, it will be created.\"\"\"),\n     PATH, DO_NOT_RESTORE),\n\n    (\"cache_messages\", _(\"Cache messages\"), True,\n     _(\"\"\"You can disable the pop3proxy caching of messages.  This\n     will make the proxy a bit faster, and make it use less space\n     on your hard drive.  The proxy uses its cache for reviewing\n     and training of messages, so if you disable caching you won't\n     be able to do further training unless you re-enable it.\n     Thus, you should only turn caching off when you are satisfied\n     with the filtering that Spambayes is doing for you.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"no_cache_bulk_ham\", _(\"Suppress caching of bulk ham\"), False,\n     _(\"\"\"Where message caching is enabled, this option suppresses caching\n     of messages which are classified as ham and marked as\n     'Precedence: bulk' or 'Precedence: list'.  If you subscribe to a\n     high-volume mailing list then your 'Review messages' page can be\n     overwhelmed with list messages, making training a pain.  Once you've\n     trained Spambayes on enough list traffic, you can use this option\n     to prevent that traffic showing up in 'Review messages'.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"no_cache_large_messages\", _(\"Maximum size of cached messages\"), 0,\n     _(\"\"\"Where message caching is enabled, this option suppresses caching\n     of messages which are larger than this value (measured in bytes).\n     If you receive a lot of messages that include large attachments\n     (and are correctly classified), you may not wish to cache these.\n     If you set this to zero (0), then this option will have no effect.\"\"\"),\n     INTEGER, RESTORE),\n  ),\n\n  # These options control the various headers that some Spambayes\n  # applications add to incoming mail, including imapfilter, pop3proxy,\n  # and hammie.\n  \"Headers\" : (\n    # The name of the header that hammie, pop3proxy, and any other spambayes\n    # software, adds to emails in filter mode.  This will definately contain\n    # the \"classification\" of the mail, and may also (i.e. with hammie)\n    # contain the score\n    (\"classification_header_name\", _(\"Classification header name\"), \"X-Spambayes-Classification\",\n     _(\"\"\"Spambayes classifies each message by inserting a new header into\n     the message.  This header can then be used by your email client\n     (provided your client supports filtering) to move spam into a\n     separate folder (recommended), delete it (not recommended), etc.\n     This option specifies the name of the header that Spambayes inserts.\n     The default value should work just fine, but you may change it to\n     anything that you wish.\"\"\"),\n     HEADER_NAME, RESTORE),\n\n    # The three disposition names are added to the header as the following\n    # three words:\n    (\"header_spam_string\", _(\"Spam disposition name\"), _(\"spam\"),\n     _(\"\"\"The header that Spambayes inserts into each email has a name,\n     (Classification header name, above), and a value.  If the classifier\n     determines that this email is probably spam, it places a header named\n     as above with a value as specified by this string.  The default\n     value should work just fine, but you may change it to anything\n     that you wish.\"\"\"),\n     HEADER_VALUE, RESTORE),\n\n    (\"header_ham_string\", _(\"Ham disposition name\"), _(\"ham\"),\n     _(\"\"\"As for Spam Designation, but for emails classified as Ham.\"\"\"),\n     HEADER_VALUE, RESTORE),\n\n    (\"header_unsure_string\", _(\"Unsure disposition name\"), _(\"unsure\"),\n     _(\"\"\"As for Spam/Ham Designation, but for emails which the\n     classifer wasn't sure about (ie. the spam probability fell between\n     the Ham and Spam Cutoffs).  Emails that have this classification\n     should always be the subject of training.\"\"\"),\n     HEADER_VALUE, RESTORE),\n\n    (\"header_score_digits\", _(\"Accuracy of reported score\"), 2,\n     _(\"\"\"Accuracy of the score in the header in decimal digits.\"\"\"),\n     INTEGER, RESTORE),\n\n    (\"header_score_logarithm\", _(\"Augment score with logarithm\"), False,\n     _(\"\"\"Set this option to augment scores of 1.00 or 0.00 by a\n     logarithmic \"one-ness\" or \"zero-ness\" score (basically it shows the\n     \"number of zeros\" or \"number of nines\" next to the score value).\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"include_score\", _(\"Add probability (score) header\"), False,\n     _(\"\"\"You can have Spambayes insert a header with the calculated spam\n     probability into each mail.  If you can view headers with your\n     mailer, then you can see this information, which can be interesting\n     and even instructive if you're a serious SpamBayes junkie.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"score_header_name\", _(\"Probability (score) header name\"), \"X-Spambayes-Spam-Probability\",\n     _(\"\"\"\"\"\"),\n     HEADER_NAME, RESTORE),\n\n    (\"include_thermostat\", _(\"Add level header\"), False,\n     _(\"\"\"You can have spambayes insert a header with the calculated spam\n     probability, expressed as a number of '*'s, into each mail (the more\n     '*'s, the higher the probability it is spam). If your mailer\n     supports it, you can use this information to fine tune your\n     classification of ham/spam, ignoring the classification given.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"thermostat_header_name\", _(\"Level header name\"), \"X-Spambayes-Level\",\n     _(\"\"\"\"\"\"),\n     HEADER_NAME, RESTORE),\n\n    (\"include_evidence\", _(\"Add evidence header\"), False,\n     _(\"\"\"You can have spambayes insert a header into mail, with the\n     evidence that it used to classify that message (a collection of\n     words with ham and spam probabilities).  If you can view headers\n     with your mailer, then this may give you some insight as to why\n     a particular message was scored in a particular way.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"evidence_header_name\", _(\"Evidence header name\"), \"X-Spambayes-Evidence\",\n     _(\"\"\"\"\"\"),\n     HEADER_NAME, RESTORE),\n\n    (\"mailid_header_name\", _(\"Spambayes id header name\"), \"X-Spambayes-MailId\",\n     _(\"\"\"\"\"\"),\n     HEADER_NAME, RESTORE),\n\n    (\"include_trained\", _(\"Add trained header\"), True,\n     _(\"\"\"sb_mboxtrain.py and sb_filter.py can add a header that details\n     how a message was trained, which lets you keep track of it, and\n     appropriately re-train messages.  However, if you would rather\n     mboxtrain/sb_filter didn't rewrite the message files, you can disable\n     this option.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"trained_header_name\", _(\"Trained header name\"), \"X-Spambayes-Trained\",\n     _(\"\"\"When training on a message, the name of the header to add with how\n     it was trained\"\"\"),\n     HEADER_NAME, RESTORE),\n\n    (\"clue_mailheader_cutoff\", _(\"Debug header cutoff\"), 0.5,\n     _(\"\"\"The range of clues that are added to the \"debug\" header in the\n     E-mail. All clues that have their probability smaller than this number,\n     or larger than one minus this number are added to the header such that\n     you can see why spambayes thinks this is ham/spam or why it is unsure.\n     The default is to show all clues, but you can reduce that by setting\n     showclue to a lower value, such as 0.1\"\"\"),\n     REAL, RESTORE),\n\n    (\"add_unique_id\", _(\"Add unique spambayes id\"), True,\n     _(\"\"\"If you wish to be able to find a specific message (via the 'find'\n     box on the home page), or use the SMTP proxy to train using cached\n     messages, you will need to know the unique id of each message.  This\n     option adds this information to a header added to each message.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"notate_to\", _(\"Notate to\"), (),\n     _(\"\"\"Some email clients (Outlook Express, for example) can only set up\n     filtering rules on a limited set of headers.  These clients cannot\n     test for the existence/value of an arbitrary header and filter mail\n     based on that information.  To accommodate these kind of mail clients,\n     you can add \"spam\", \"ham\", or \"unsure\" to the recipient list.  A\n     filter rule can then use this to see if one of these words (followed\n     by a comma) is in the recipient list, and route the mail to an\n     appropriate folder, or take whatever other action is supported and\n     appropriate for the mail classification.\n\n     As it interferes with replying, you may only wish to do this for\n     spam messages; simply tick the boxes of the classifications take\n     should be identified in this fashion.\"\"\"),\n     ((), _(\"ham\"), _(\"spam\"), _(\"unsure\")), RESTORE),\n\n    (\"notate_subject\", _(\"Classify in subject: header\"), (),\n     _(\"\"\"This option will add the same information as 'Notate To',\n     but to the start of the mail subject line.\"\"\"),\n     ((), _(\"ham\"), _(\"spam\"), _(\"unsure\")), RESTORE),\n  ),\n\n  # pop3proxy settings: The only mandatory option is pop3proxy_servers, eg.\n  # \"pop3.my-isp.com:110\", or a comma-separated list of those.  The \":110\"\n  # is optional.  If you specify more than one server in pop3proxy_servers,\n  # you must specify the same number of ports in pop3proxy_ports.\n  \"pop3proxy\" : (\n    (\"remote_servers\", _(\"Remote Servers\"), (),\n     _(\"\"\"\\\n     The SpamBayes POP3 proxy intercepts incoming email and classifies it\n     before sending it on to your email client.  You need to specify which\n     POP3 server(s) and port(s) you wish it to connect to - a POP3 server\n     address typically looks like 'pop3.myisp.net:110' where\n     'pop3.myisp.net' is the name of the computer where the POP3 server runs\n     and '110' is the port on which the POP3 server listens.  The other port\n     you might find is '995', which is used for secure POP3.  If you use\n     more than one server, simply separate their names with commas.  For\n     example:  'pop3.myisp.net:110,pop.gmail.com:995'.  You can get\n     these server names and port numbers from your existing email\n     configuration, or from your ISP or system administrator.  If you are\n     using Web-based email, you can't use the SpamBayes POP3 proxy (sorry!).\n     In your email client's configuration, where you would normally put your\n     POP3 server address, you should now put the address of the machine\n     running SpamBayes.\n\"\"\"),\n     SERVER, DO_NOT_RESTORE),\n\n    (\"listen_ports\", _(\"SpamBayes Ports\"), (),\n     _(\"\"\"\\\n     Each monitored POP3 server must be assigned to a different port in the\n     SpamBayes POP3 proxy.  You need to configure your email client to\n     connect to this port instead of the actual remote POP3 server.  If you\n     don't know what port to use, try 8110 and go up from there.  If you\n     have two servers, your list of listen ports might then be '8110,8111'.\n\"\"\"),\n     SERVER, DO_NOT_RESTORE),\n\n    (\"allow_remote_connections\", _(\"Allowed remote POP3 connections\"), \"localhost\",\n     _(\"\"\"Enter a list of trusted IPs, separated by commas. Remote POP\n     connections from any of them will be allowed. You can trust any\n     IP using a single '*' as field value. You can also trust ranges of\n     IPs using the '*' character as a wildcard (for instance 192.168.0.*).\n     The localhost IP will always be trusted. Type 'localhost' in the\n     field to trust this only address.\"\"\"),\n     IP_LIST, RESTORE),\n\n    (\"retrieval_timeout\", _(\"Retrieval timeout\"), 30,\n     _(\"\"\"When proxying messages, time out after this length of time if\n     all the headers have been received.  The rest of the mesasge will\n     proxy straight through.  Some clients have a short timeout period,\n     and will give up on waiting for the message if this is too long.\n     Note that the shorter this is, the less of long messages will be\n     used for classifications (i.e. results may be effected).\"\"\"),\n     REAL, RESTORE),\n\n    (\"use_ssl\", \"Connect via a secure socket layer\", False,\n     \"\"\"Use SSL to connect to the server. This allows spambayes to connect\n     without sending data in plain text.\n\n     Note that this does not check the server certificate at this point in\n     time.\"\"\",\n     (False, True, \"automatic\"), DO_NOT_RESTORE),\n  ),\n\n  \"smtpproxy\" : (\n    (\"remote_servers\", _(\"Remote Servers\"), (),\n     _(\"\"\"Use of the SMTP proxy is optional - if you would rather just train\n     via the web interface, or the pop3dnd or mboxtrain scripts, then you\n     can safely leave this option blank.  The Spambayes SMTP proxy\n     intercepts outgoing email - if you forward mail to one of the\n     addresses below, it is examined for an id and the message\n     corresponding to that id is trained as ham/spam.  All other mail is\n     sent along to your outgoing mail server.  You need to specify which\n     SMTP server(s) you wish it to intercept - a SMTP server address\n     typically looks like \"smtp.myisp.net\".  If you use more than one\n     server, simply separate their names with commas.  You can get these\n     server names from your existing email configuration, or from your ISP\n     or system administrator.  If you are using Web-based email, you can't\n     use the Spambayes SMTP proxy (sorry!).  In your email client's\n     configuration, where you would normally put your SMTP server address,\n     you should now put the address of the machine running SpamBayes.\"\"\"),\n     SERVER, DO_NOT_RESTORE),\n\n    (\"listen_ports\", _(\"SpamBayes Ports\"), (),\n     _(\"\"\"Each SMTP server that is being monitored must be assigned to a\n     'port' in the Spambayes SMTP proxy.  This port must be different for\n     each monitored server, and there must be a port for\n     each monitored server.  Again, you need to configure your email\n     client to use this port.  If there are multiple servers, you must\n     specify the same number of ports as servers, separated by commas.\"\"\"),\n     SERVER, DO_NOT_RESTORE),\n\n    (\"allow_remote_connections\", _(\"Allowed remote SMTP connections\"), \"localhost\",\n     _(\"\"\"Enter a list of trusted IPs, separated by commas. Remote SMTP\n     connections from any of them will be allowed. You can trust any\n     IP using a single '*' as field value. You can also trust ranges of\n     IPs using the '*' character as a wildcard (for instance 192.168.0.*).\n     The localhost IP will always be trusted. Type 'localhost' in the\n     field to trust this only address.  Note that you can unwittingly\n     turn a SMTP server into an open proxy if you open this up, as\n     connections to the server will appear to be from your machine, even\n     if they are from a remote machine *through* your machine, to the\n     server.  We do not recommend opening this up fully (i.e. using '*').\n     \"\"\"),\n     IP_LIST, RESTORE),\n\n    (\"ham_address\", _(\"Train as ham address\"), \"spambayes_ham@localhost\",\n     _(\"\"\"When a message is received that you wish to train on (for example,\n     one that was incorrectly classified), you need to forward or bounce\n     it to one of two special addresses so that the SMTP proxy can identify\n     it.  If you wish to train it as ham, forward or bounce it to this\n     address.  You will want to use an address that is not\n     a valid email address, like ham@nowhere.nothing.\"\"\"),\n     EMAIL_ADDRESS, RESTORE),\n\n    (\"spam_address\", _(\"Train as spam address\"), \"spambayes_spam@localhost\",\n     _(\"\"\"As with Ham Address above, but the address that you need to forward\n     or bounce mail that you wish to train as spam.  You will want to use\n     an address that is not a valid email address, like\n     spam@nowhere.nothing.\"\"\"),\n     EMAIL_ADDRESS, RESTORE),\n\n    (\"use_cached_message\", _(\"Lookup message in cache\"), False,\n     _(\"\"\"If this option is set, then the smtpproxy will attempt to\n     look up the messages sent to it (for training) in the POP3 proxy cache\n     or IMAP filter folders, and use that message as the training data.\n     This avoids any problems where your mail client might change the\n     message when forwarding, contaminating your training data.  If you can\n     be sure that this won't occur, then the id-lookup can be avoided.\n\n     Note that Outlook Express users cannot use the lookup option (because\n     of the way messages are forwarded), and so if they wish to use the\n     SMTP proxy they must enable this option (but as messages are altered,\n     may not get the best results, and this is not recommended).\"\"\"),\n     BOOLEAN, RESTORE),\n  ),\n\n  # imap4proxy settings: The only mandatory option is imap4proxy_servers, eg.\n  # \"imap4.my-isp.com:143\", or a comma-separated list of those.  The \":143\"\n  # is optional.  If you specify more than one server in imap4proxy_servers,\n  # you must specify the same number of ports in imap4proxy_ports.\n  \"imap4proxy\" : (\n    (\"remote_servers\", _(\"Remote Servers\"), (),\n     _(\"\"\"The SpamBayes IMAP4 proxy intercepts incoming email and classifies\n     it before sending it on to your email client.  You need to specify\n     which IMAP4 server(s) you wish it to intercept - a IMAP4 server\n     address typically looks like \"mail.myisp.net\".  If you use more than\n     one server, simply separate their names with commas.  You can get\n     these server names from your existing email configuration, or from\n     your ISP or system administrator.  If you are using Web-based email,\n     you can't use the SpamBayes IMAP4 proxy (sorry!).  In your email\n     client's configuration, where you would normally put your IMAP4 server\n     address, you should now put the address of the machine running\n     SpamBayes.\"\"\"),\n     SERVER, DO_NOT_RESTORE),\n\n    (\"listen_ports\", _(\"SpamBayes Ports\"), (),\n     _(\"\"\"Each IMAP4 server that is being monitored must be assigned to a\n     'port' in the SpamBayes IMAP4 proxy.  This port must be different for\n     each monitored server, and there must be a port for each monitored\n     server.  Again, you need to configure your email client to use this\n     port.  If there are multiple servers, you must specify the same number\n     of ports as servers, separated by commas. If you don't know what to\n     use here, and you only have one server, try 143, or if that doesn't\n     work, try 8143.\"\"\"),\n     SERVER, DO_NOT_RESTORE),\n\n    (\"allow_remote_connections\", _(\"Allowed remote IMAP4 connections\"), \"localhost\",\n     _(\"\"\"Enter a list of trusted IPs, separated by commas. Remote IMAP\n     connections from any of them will be allowed. You can trust any\n     IP using a single '*' as field value. You can also trust ranges of\n     IPs using the '*' character as a wildcard (for instance 192.168.0.*).\n     The localhost IP will always be trusted. Type 'localhost' in the\n     field to trust this only address.\"\"\"),\n     IP_LIST, RESTORE),\n\n    (\"use_ssl\", \"Connect via a secure socket layer\", False,\n     \"\"\"Use SSL to connect to the server. This allows spambayes to connect\n     without sending data in plain text.\n\n     Note that this does not check the server certificate at this point in\n     time.\"\"\",\n     (False, True, \"automatic\"), DO_NOT_RESTORE),\n  ),\n\n  \"html_ui\" : (\n    (\"port\", _(\"Port\"), 8880,\n     _(\"\"\"\"\"\"),\n     PORT, RESTORE),\n\n    (\"launch_browser\", _(\"Launch browser\"), False,\n     _(\"\"\"If this option is set, then whenever sb_server or sb_imapfilter is\n     started the default web browser will be opened to the main web\n     interface page.  Use of the -b switch when starting from the command\n     line overrides this option.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"allow_remote_connections\", _(\"Allowed remote UI connections\"), \"localhost\",\n     _(\"\"\"Enter a list of trusted IPs, separated by commas. Remote\n     connections from any of them will be allowed. You can trust any\n     IP using a single '*' as field value. You can also trust ranges of\n     IPs using the '*' character as a wildcard (for instance 192.168.0.*).\n     The localhost IP will always be trusted. Type 'localhost' in the\n     field to trust this only address.\"\"\"),\n     IP_LIST, RESTORE),\n\n    (\"display_headers\", _(\"Headers to display in message review\"), (\"Subject\", \"From\"),\n     _(\"\"\"When reviewing messages via the web user interface, you are\n     presented with various information about the message.  By default, you\n     are shown the subject and who the message is from.  You can add other\n     message headers to display, however, such as the address the message\n     is to, or the date that the message was sent.\"\"\"),\n     HEADER_NAME, RESTORE),\n\n    (\"display_received_time\", _(\"Display date received in message review\"), False,\n     _(\"\"\"When reviewing messages via the web user interface, you are\n     presented with various information about the message.  If you set\n     this option, you will be shown the date that the message was received.\n     \"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"display_score\", _(\"Display score in message review\"), False,\n     _(\"\"\"When reviewing messages via the web user interface, you are\n     presented with various information about the message.  If you\n     set this option, this information will include the score that\n     the message received when it was classified.  You might wish to\n     see this purely out of curiousity, or you might wish to only\n     train on messages that score towards the boundaries of the\n     classification areas.  Note that in order to use this option,\n     you must also enable the option to include the score in the\n     message headers.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"display_adv_find\", _(\"Display the advanced find query\"), False,\n     _(\"\"\"Present advanced options in the 'Word Query' box on the front page,\n     including wildcard and regular expression searching.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"default_ham_action\", _(\"Default training for ham\"), _(\"discard\"),\n     _(\"\"\"When presented with the review list in the web interface,\n     which button would you like checked by default when the message\n     is classified as ham?\"\"\"),\n     (_(\"ham\"), _(\"spam\"), _(\"discard\"), _(\"defer\")), RESTORE),\n\n    (\"default_spam_action\", _(\"Default training for spam\"), _(\"discard\"),\n     _(\"\"\"When presented with the review list in the web interface,\n     which button would you like checked by default when the message\n     is classified as spam?\"\"\"),\n     (_(\"ham\"), _(\"spam\"), _(\"discard\"), _(\"defer\")), RESTORE),\n\n    (\"default_unsure_action\", _(\"Default training for unsure\"), _(\"defer\"),\n     _(\"\"\"When presented with the review list in the web interface,\n     which button would you like checked by default when the message\n     is classified as unsure?\"\"\"),\n     (_(\"ham\"), _(\"spam\"), _(\"discard\"), _(\"defer\")), RESTORE),\n\n    (\"ham_discard_level\", _(\"Ham Discard Level\"), 0.0,\n     _(\"\"\"Hams scoring less than this percentage will default to being\n     discarded in the training interface (they won't be trained). You'll\n     need to turn off the 'Train when filtering' option, above, for this\n     to have any effect\"\"\"),\n     REAL, RESTORE),\n\n    (\"spam_discard_level\", _(\"Spam Discard Level\"), 100.0,\n     _(\"\"\"Spams scoring more than this percentage will default to being\n     discarded in the training interface (they won't be trained). You'll\n     need to turn off the 'Train when filtering' option, above, for this\n     to have any effect\"\"\"),\n     REAL, RESTORE),\n\n    (\"http_authentication\", _(\"HTTP Authentication\"), \"None\",\n     _(\"\"\"This option lets you choose the security level of the web interface.\n     When selecting Basic or Digest, the user will be prompted a login and a\n     password to access the web interface. The Basic option is faster, but\n     transmits the password in clear on the network. The Digest option\n     encrypts the password before transmission.\"\"\"),\n     (\"None\", \"Basic\", \"Digest\"), RESTORE),\n\n    (\"http_user_name\", _(\"User name\"), \"admin\",\n     _(\"\"\"If you activated the HTTP authentication option, you can modify the\n     authorized user name here.\"\"\"),\n     r\"[\\w]+\", RESTORE),\n\n    (\"http_password\", _(\"Password\"), \"admin\",\n     _(\"\"\"If you activated the HTTP authentication option, you can modify the\n     authorized user password here.\"\"\"),\n     r\"[\\w]+\", RESTORE),\n\n    (\"rows_per_section\", _(\"Rows per section\"), 10000,\n     _(\"\"\"Number of rows to display per ham/spam/unsure section.\"\"\"),\n     INTEGER, RESTORE),\n  ),\n\n  \"imap\" : (\n    (\"server\", _(\"Server\"), (),\n     _(\"\"\"These are the names and ports of the imap servers that store your\n     mail, and which the imap filter will connect to - for example:\n     mail.example.com or imap.example.com:143.  The default IMAP port is\n     143 (or 993 if using SSL); if you connect via one of those ports, you\n     can leave this blank. If you use more than one server, use a comma\n     delimited list of the server:port values.\"\"\"),\n     SERVER, DO_NOT_RESTORE),\n\n    (\"username\", _(\"Username\"), (),\n     _(\"\"\"This is the id that you use to log into your imap server.  If your\n     address is funkyguy@example.com, then your username is probably\n     funkyguy.\"\"\"),\n     IMAP_ASTRING, DO_NOT_RESTORE),\n\n    (\"password\", _(\"Password\"), (),\n     _(\"\"\"That is that password that you use to log into your imap server.\n     This will be stored in plain text in your configuration file, and if\n     you have set the web user interface to allow remote connections, then\n     it will be available for the whole world to see in plain text.  If\n     I've just freaked you out, don't panic <wink>.  You can leave this\n     blank and use the -p command line option to imapfilter.py and you will\n     be prompted for your password.\"\"\"),\n     IMAP_ASTRING, DO_NOT_RESTORE),\n\n    (\"expunge\", _(\"Purge//Expunge\"), False,\n     _(\"\"\"Permanently remove *all* messages flagged with //Deleted on logout.\n     If you do not know what this means, then please leave this as\n     False.\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"use_ssl\", _(\"Connect via a secure socket layer\"), False,\n     _(\"\"\"Use SSL to connect to the server. This allows spambayes to connect\n     without sending the password in plain text.\n\n     Note that this does not check the server certificate at this point in\n     time.\"\"\"),\n     BOOLEAN, DO_NOT_RESTORE),\n\n    (\"filter_folders\", _(\"Folders to filter\"), (\"INBOX\",),\n     _(\"\"\"Comma delimited list of folders to be filtered\"\"\"),\n     IMAP_FOLDER, DO_NOT_RESTORE),\n\n    (\"unsure_folder\", _(\"Folder for unsure messages\"), \"\",\n     _(\"\"\"\"\"\"),\n     IMAP_FOLDER, DO_NOT_RESTORE),\n\n    (\"spam_folder\", _(\"Folder for suspected spam\"), \"\",\n     _(\"\"\"\"\"\"),\n     IMAP_FOLDER, DO_NOT_RESTORE),\n\n    (\"ham_folder\", _(\"Folder for ham messages\"), \"\",\n     _(\"\"\"If you leave this option blank, messages classified as ham will not\n     be moved.  However, if you wish to have ham messages moved, you can\n     select a folder here.\"\"\"),\n     IMAP_FOLDER, DO_NOT_RESTORE),\n    \n    (\"ham_train_folders\", _(\"Folders with mail to be trained as ham\"), (),\n     _(\"\"\"Comma delimited list of folders that will be examined for messages\n     to train as ham.\"\"\"),\n     IMAP_FOLDER, DO_NOT_RESTORE),\n\n    (\"spam_train_folders\", _(\"Folders with mail to be trained as spam\"), (),\n     _(\"\"\"Comma delimited list of folders that will be examined for messages\n     to train as spam.\"\"\"),\n     IMAP_FOLDER, DO_NOT_RESTORE),\n\n    (\"move_trained_spam_to_folder\", _(\"Folder to move trained spam to\"), \"\",\n     _(\"\"\"When training, all messages in the spam training folder(s) (above)\n     are examined - if they are new, they are used to train, if not, they\n     are ignored.  This examination does take time, however, so if speed\n     is an issue for you, you may wish to move messages out of this folder\n     once they have been trained (either to delete them or to a storage\n     folder).  If a folder name is specified here, this will happen\n     automatically.  Note that the filter is not yet clever enough to\n     move the mail to different folders depending on which folder it\n     was originally in - *all* messages will be moved to the same\n     folder.\"\"\"),\n     IMAP_FOLDER, DO_NOT_RESTORE),\n\n    (\"move_trained_ham_to_folder\", _(\"Folder to move trained ham to\"), \"\",\n     _(\"\"\"When training, all messages in the ham training folder(s) (above)\n     are examined - if they are new, they are used to train, if not, they\n     are ignored.  This examination does take time, however, so if speed\n     is an issue for you, you may wish to move messages out of this folder\n     once they have been trained (either to delete them or to a storage\n     folder).  If a folder name is specified here, this will happen\n     automatically.  Note that the filter is not yet clever enough to\n     move the mail to different folders depending on which folder it\n     was originally in - *all* messages will be moved to the same\n     folder.\"\"\"),\n     IMAP_FOLDER, DO_NOT_RESTORE),\n  ),\n\n  \"ZODB\" : (\n    (\"zeo_addr\", _(\"\"), \"\",\n     _(\"\"\"\"\"\"),\n     IMAP_ASTRING, DO_NOT_RESTORE),\n\n    (\"event_log_file\", _(\"\"), \"\",\n     _(\"\"\"\"\"\"),\n     IMAP_ASTRING, RESTORE),\n\n    (\"folder_dir\", _(\"\"), \"\",\n     _(\"\"\"\"\"\"),\n     PATH, DO_NOT_RESTORE),\n\n    (\"ham_folders\", _(\"\"), \"\",\n     _(\"\"\"\"\"\"),\n     PATH, DO_NOT_RESTORE),\n\n    (\"spam_folders\", _(\"\"), \"\",\n     _(\"\"\"\"\"\"),\n     PATH, DO_NOT_RESTORE),\n\n    (\"event_log_severity\", _(\"\"), 0,\n     _(\"\"\"\"\"\"),\n     INTEGER, RESTORE),\n\n    (\"cache_size\", _(\"\"), 2000,\n     _(\"\"\"\"\"\"),\n     INTEGER, RESTORE),\n  ),\n\n  \"imapserver\" : (\n    (\"username\", _(\"Username\"), \"\",\n     _(\"\"\"The username to use when logging into the SpamBayes IMAP server.\"\"\"),\n     IMAP_ASTRING, DO_NOT_RESTORE),\n\n    (\"password\", _(\"Password\"), \"\",\n     _(\"\"\"The password to use when logging into the SpamBayes IMAP server.\"\"\"),\n     IMAP_ASTRING, DO_NOT_RESTORE),\n\n    (\"port\", _(\"IMAP Listen Port\"), 143,\n     _(\"\"\"The port to serve the SpamBayes IMAP server on.\"\"\"),\n     PORT, RESTORE),\n  ),\n\n  \"globals\" : (\n    (\"verbose\", _(\"Verbose\"), False,\n     _(\"\"\"\"\"\"),\n     BOOLEAN, RESTORE),\n\n    (\"dbm_type\", _(\"Database storage type\"), \"best\",\n     _(\"\"\"What DBM storage type should we use?  Must be best, db3hash,\n     dbhash or gdbm.  Windows folk should steer clear of dbhash.  Default\n     is \"best\", which will pick the best DBM type available on your\n     platform.\"\"\"),\n     (\"best\", \"db3hash\", \"dbhash\", \"gdbm\"), RESTORE),\n\n    (\"proxy_username\", _(\"HTTP Proxy Username\"), \"\",\n     _(\"\"\"The username to give to the HTTP proxy when required.  If a\n     username is not necessary, simply leave blank.\"\"\"),\n     r\"[\\w]+\", DO_NOT_RESTORE),\n    (\"proxy_password\", _(\"HTTP Proxy Password\"), \"\",\n     _(\"\"\"The password to give to the HTTP proxy when required.  This is\n     stored in clear text in your configuration file, so if that bothers\n     you then don't do this.  You'll need to use a proxy that doesn't need\n     authentication, or do without any SpamBayes HTTP activity.\"\"\"),\n     r\"[\\w]+\", DO_NOT_RESTORE),\n    (\"proxy_server\", _(\"HTTP Proxy Server\"), \"\",\n     _(\"\"\"If a spambayes application needs to use HTTP, it will try to do so\n     through this proxy server.  The port defaults to 8080, or can be\n     entered with the server:port form.\"\"\"),\n     SERVER, DO_NOT_RESTORE),\n\n    (\"language\", _(\"User Interface Language\"), (\"en_US\",),\n     _(\"\"\"If possible, the user interface should use a language from this\n     list (in order of preference).\"\"\"),\n     r\"\\w\\w(?:_\\w\\w)?\", RESTORE),\n  ),\n  \"Plugin\": (\n    (\"xmlrpc_path\", _(\"XML-RPC path\"), \"/sbrpc\",\n     _(\"\"\"The path to respond to.\"\"\"),\n     r\"[\\w]+\", RESTORE),\n    (\"xmlrpc_host\", _(\"XML-RPC host\"), \"localhost\",\n     _(\"\"\"The host to listen on.\"\"\"),\n     SERVER, RESTORE),\n    (\"xmlrpc_port\", _(\"XML-RPC port\"), 8001,\n     _(\"\"\"The port to listen on.\"\"\"),\n     r\"[\\d]+\", RESTORE),\n    ),\n}\n\n# `optionsPathname` is the pathname of the last ini file in the list.\n# This is where the web-based configuration page will write its changes.\n# If no ini files are found, it defaults to bayescustomize.ini in the\n# current working directory.\noptionsPathname = None\n\n# The global options object - created by load_options\noptions = None\n\ndef load_options():\n    global optionsPathname, options\n    options = OptionsClass()\n    options.load_defaults(defaults)\n\n    # Maybe we are reloading.\n    if optionsPathname:\n        options.merge_file(optionsPathname)\n\n    alternate = None\n    if hasattr(os, 'getenv'):\n        alternate = os.getenv('BAYESCUSTOMIZE')\n    if alternate:\n        filenames = alternate.split(os.pathsep)\n        options.merge_files(filenames)\n        optionsPathname = os.path.abspath(filenames[-1])\n    else:\n        alts = []\n        for path in ['bayescustomize.ini', '~/.spambayesrc']:\n            epath = os.path.expanduser(path)\n            if os.path.exists(epath):\n                alts.append(epath)\n        if alts:\n            options.merge_files(alts)\n            optionsPathname = os.path.abspath(alts[-1])\n\n    if not optionsPathname:\n        optionsPathname = os.path.abspath('bayescustomize.ini')\n        if sys.platform.startswith(\"win\") and \\\n           not os.path.isfile(optionsPathname):\n            # If we are on Windows and still don't have an INI, default to the\n            # 'per-user' directory.\n            try:\n                from win32com.shell import shell, shellcon\n            except ImportError:\n                # We are on Windows, with no BAYESCUSTOMIZE set, no ini file\n                # in the current directory, and no win32 extensions installed\n                # to locate the \"user\" directory - seeing things are so lamely\n                # setup, it is worth printing a warning\n                print(\"NOTE: We can not locate an INI file \" \\\n                      \"for SpamBayes, and the Python for Windows extensions \" \\\n                      \"are not installed, meaning we can't locate your \" \\\n                      \"'user' directory.  An empty configuration file at \" \\\n                      \"'%s' will be used.\" % optionsPathname.encode('mbcs'),\n                      file=sys.stderr)\n            else:\n                windowsUserDirectory = os.path.join(\n                        shell.SHGetFolderPath(0,shellcon.CSIDL_APPDATA,0,0),\n                        \"SpamBayes\", \"Proxy\")\n                try:\n                    if not os.path.isdir(windowsUserDirectory):\n                        os.makedirs(windowsUserDirectory)\n                except os.error:\n                    # unable to make the directory - stick to default.\n                    pass\n                else:\n                    optionsPathname = os.path.join(windowsUserDirectory,\n                                                   'bayescustomize.ini')\n                    # Not everyone is unicode aware - keep it a string.\n                    optionsPathname = optionsPathname.encode(\"mbcs\")\n                    # If the file exists, then load it.\n                    if os.path.exists(optionsPathname):\n                        options.merge_file(optionsPathname)\n\n\ndef get_pathname_option(section, option):\n    \"\"\"Return the option relative to the path specified in the\n    gloabl optionsPathname, unless it is already an absolute path.\"\"\"\n    filename = os.path.expanduser(options.get(section, option))\n    if os.path.isabs(filename):\n        return filename\n    return os.path.join(os.path.dirname(optionsPathname), filename)\n\n# Ideally, we should not create the objects at import time - but we have\n# done it this way forever!\n# We avoid having the options loading code at the module level, as then\n# the only way to re-read is to reload this module, and as at 2.3, that\n# doesn't work in a .zip file.\nload_options()\n"
  },
  {
    "path": "mailpile/spambayes/OptionsClass.py",
    "content": "\"\"\"OptionsClass\n\nClasses:\n    Option - Holds information about an option\n    OptionsClass - A collection of options\n\nAbstract:\n\nThis module is used to manage \"options\" managed in user editable files.\nThis is the implementation of the Options.options globally shared options\nobject for the SpamBayes project, but is also able to be used to manage\nother options required by each application.\n\nThe Option class holds information about an option - the name of the\noption, a nice name (to display), documentation, default value,\npossible values (a tuple or a regex pattern), whether multiple values\nare allowed, and whether the option should be reset when restoring to\ndefaults (options like server names should *not* be).\n\nThe OptionsClass class provides facility for a collection of Options.\nIt is expected that manipulation of the options will be carried out\nvia an instance of this class.\n\nExperimental or deprecated options are prefixed with 'x-', borrowing the\npractice from RFC-822 mail.  If the user sets an option like:\n\n    [Tokenizer]\n    x-transmogrify: True\n\nand an 'x-transmogrify' or 'transmogrify' option exists, it is set silently\nto the value given by the user.  If the user sets an option like:\n\n    [Tokenizer]\n    transmogrify: True\n\nand no 'transmogrify' option exists, but an 'x-transmogrify' option does,\nthe latter is set to the value given by the users and a deprecation message\nis printed to standard error.\n\nTo Do:\n o Stop allowing invalid options in configuration files\n o Find a regex expert to come up with *good* patterns for domains,\n   email addresses, and so forth.\n o str(Option) should really call Option.unconvert since this is what\n   it does.  Try putting that in and running all the tests.\n o [See also the __issues__ string.]\n o Suggestions?\n\n\"\"\"\n\nfrom __future__ import print_function\n\n# This module is part of the spambayes project, which is Copyright 2002-2007\n# The Python Software Foundation and is covered by the Python Software\n# Foundation license.\n\n__credits__ = \"All the Spambayes folk.\"\n# blame for the new format: Tony Meyer <ta-meyer@ihug.co.nz>\n\n__issues__ = \"\"\"Things that should be considered further and by\nother people:\n\nWe are very generous in checking validity when multiple values are\nallowed and the check is a regex (rather than a tuple).  Any sequence\nthat does not match the regex may be used to delimit the values.\nFor example, if the regex was simply r\"[\\d]*\" then these would all\nbe considered valid:\n\"123a234\" -> 123, 234\n\"123abced234\" -> 123, 234\n\"123XST234xas\" -> 123, 234\n\"123 234\" -> 123, 234\n\"123~!@$%^&@234!\" -> 123, 234\n\nIf this is a problem, my recommendation would be to change the\nmultiple_values_allowed attribute from a boolean to a regex/None\ni.e. if multiple is None, then only one value is allowed.  Otherwise\nmultiple is used in a re.split() to separate the input.\n\"\"\"\n\nimport sys\nimport os\nimport shutil\nfrom tempfile import TemporaryFile\n\nfrom six.moves import cStringIO as StringIO\nfrom six.moves import range\n\nimport re\nimport locale\nfrom textwrap import wrap\n\n__all__ = ['OptionsClass',\n           'HEADER_NAME', 'HEADER_VALUE',\n           'INTEGER', 'REAL', 'BOOLEAN',\n           'SERVER', 'PORT', 'EMAIL_ADDRESS',\n           'PATH', 'VARIABLE_PATH', 'FILE', 'FILE_WITH_PATH',\n           'IMAP_FOLDER', 'IMAP_ASTRING',\n           'RESTORE', 'DO_NOT_RESTORE', 'IP_LIST',\n           'OCRAD_CHARSET',\n          ]\n\nMultiContainerTypes = (tuple, list)\ntry:\n    import types\n    types.StringTypes\nexcept AttributeError:\n    # This is python3\n\n    class TypesWrapper(object):\n        StringType = str\n        FloatType = float\n        IntType = int\n        BooleanType = bool\n        TupleType = (tuple,)\n        StringTypes = (str,)\n\n    types = TypesWrapper()\n\n\n\nclass Option(object):\n    def __init__(self, name, nice_name=\"\", default=None,\n                 help_text=\"\", allowed=None, restore=True):\n        self.name = name\n        self.nice_name = nice_name\n        self.default_value = default\n        self.explanation_text = help_text\n        self.allowed_values = allowed\n        self.restore = restore\n        self.delimiter = None\n        # start with default value\n        self.set(default)\n\n    def display_name(self):\n        '''A name for the option suitable for display to a user.'''\n        return self.nice_name\n    def default(self):\n        '''The default value for the option.'''\n        return self.default_value\n    def doc(self):\n        '''Documentation for the option.'''\n        return self.explanation_text\n    def valid_input(self):\n        '''Valid values for the option.'''\n        return self.allowed_values\n    def no_restore(self):\n        '''Do not restore this option when restoring to defaults.'''\n        return not self.restore\n    def set(self, val):\n        '''Set option to value.'''\n        self.value = val\n    def get(self):\n        '''Get option value.'''\n        return self.value\n    def multiple_values_allowed(self):\n        '''Multiple values are allowed for this option.'''\n        return type(self.default_value) in MultiContainerTypes\n\n    def is_valid(self, value):\n        '''Check if this is a valid value for this option.'''\n        if self.allowed_values is None:\n            return False\n\n        if self.multiple_values_allowed():\n            return self.is_valid_multiple(value)\n        else:\n            return self.is_valid_single(value)\n\n    def is_valid_multiple(self, value):\n        '''Return True iff value is a valid value for this option.\n        Use if multiple values are allowed.'''\n        if type(value) in MultiContainerTypes:\n            for val in value:\n                if not self.is_valid_single(val):\n                    return False\n            return True\n        return self.is_valid_single(value)\n\n    def is_valid_single(self, value):\n        '''Return True iff value is a valid value for this option.\n        Use when multiple values are not allowed.'''\n        if type(self.allowed_values) == types.TupleType:\n            if value in self.allowed_values:\n                return True\n            else:\n                return False\n        else:\n            # special handling for booleans, thanks to Python 2.2\n            if self.is_boolean and (value == True or value == False):\n                return True\n            if type(value) != type(self.value) and \\\n               type(self.value) not in MultiContainerTypes:\n                # This is very strict!  If the value is meant to be\n                # a real number and an integer is passed in, it will fail.\n                # (So pass 1. instead of 1, for example)\n                return False\n            if value == \"\":\n                # A blank string is always ok.\n                return True\n            avals = self._split_values(value)\n            # in this case, allowed_values must be a regex, and\n            # _split_values must match once and only once\n            if len(avals) == 1:\n                return True\n            else:\n                # either no match or too many matches\n                return False\n\n    def _split_values(self, value):\n        # do the regex mojo here\n        if not self.allowed_values:\n            return ('',)\n        try:\n            r = re.compile(self.allowed_values)\n        except:\n            print(self.allowed_values, file=sys.stderr)\n            raise\n        s = str(value)\n        i = 0\n        vals = []\n        while True:\n            m = r.search(s[i:])\n            if m is None:\n                break\n            vals.append(m.group())\n            delimiter = s[i:i + m.start()]\n            if self.delimiter is None and delimiter != \"\":\n                self.delimiter = delimiter\n            i += m.end()\n        return tuple(vals)\n\n    def as_nice_string(self, section=None):\n        '''Summarise the option in a user-readable format.'''\n        if section is None:\n            strval = \"\"\n        else:\n            strval = \"[%s] \" % (section)\n        strval += \"%s - \\\"%s\\\"\\nDefault: %s\\nDo not restore: %s\\n\" \\\n                 % (self.name, self.display_name(),\n                    str(self.default()), str(self.no_restore()))\n        strval += \"Valid values: %s\\nMultiple values allowed: %s\\n\" \\\n                  % (str(self.valid_input()),\n                     str(self.multiple_values_allowed()))\n        strval += \"\\\"%s\\\"\\n\\n\" % (str(self.doc()))\n        return strval\n\n    def as_documentation_string(self, section=None):\n        '''Summarise the option in a format suitable for unmodified\n        insertion in HTML documentation.'''\n        strval = [\"<tr>\"]\n        if section is not None:\n            strval.append(\"\\t<td>[%s]</td>\" % (section,))\n        strval.append(\"\\t<td>%s</td>\" % (self.name,))\n        strval.append(\"\\t<td>%s</td>\" % \\\n                      \", \".join([str(s) for s in self.valid_input()]))\n        default = self.default()\n        if isinstance(default, types.TupleType):\n            default = \", \".join([str(s) for s in default])\n        else:\n            default = str(default)\n        strval.append(\"\\t<td>%s</td>\" % (default,))\n        strval.append(\"\\t<td><strong>%s</strong>: %s</td>\" \\\n                      % (self.display_name(), self.doc()))\n        strval.append(\"</tr>\\n\")\n        return \"\\n\".join(strval)\n\n    def write_config(self, file):\n        '''Output value in configuration file format.'''\n        file.write(self.name)\n        file.write(': ')\n        file.write(self.unconvert())\n        file.write('\\n')\n\n    def convert(self, value):\n        '''Convert value from a string to the appropriate type.'''\n        svt = type(self.value)\n        if svt == type(value):\n            # already the correct type\n            return value\n        if type(self.allowed_values) == types.TupleType and \\\n           value in self.allowed_values:\n            # already correct type\n            return value\n        if self.is_boolean():\n            if str(value) == \"True\" or value == 1:\n                return True\n            elif str(value) == \"False\" or value == 0:\n                return False\n            raise TypeError(self.name + \" must be True or False\")\n        if self.multiple_values_allowed():\n            # This will fall apart if the allowed_value is a tuple,\n            # but not a homogenous one...\n            if isinstance(self.allowed_values, types.StringTypes):\n                vals = list(self._split_values(value))\n            else:\n                if isinstance(value, types.TupleType):\n                    vals = list(value)\n                else:\n                    vals = value.split()\n            if len(self.default_value) > 0:\n                to_type = type(self.default_value[0])\n            else:\n                to_type = types.StringType\n            for i in range(0, len(vals)):\n                vals[i] = self._convert(vals[i], to_type)\n            return tuple(vals)\n        else:\n            return self._convert(value, svt)\n        raise TypeError(self.name + \" has an invalid type.\")\n\n    def _convert(self, value, to_type):\n        '''Convert an int, float or string to the specified type.'''\n        if to_type == type(value):\n            # already the correct type\n            return value\n        if to_type == types.IntType:\n            return locale.atoi(value)\n        if to_type == types.FloatType:\n            return locale.atof(value)\n        if to_type in types.StringTypes:\n            return str(value)\n        raise TypeError(\"Invalid type.\")\n\n    def unconvert(self):\n        '''Convert value from the appropriate type to a string.'''\n        if type(self.value) in types.StringTypes:\n            # nothing to do\n            return self.value\n        if self.is_boolean():\n            # A wee bit extra for Python 2.2\n            if self.value == True:\n                return \"True\"\n            else:\n                return \"False\"\n        if type(self.value) == types.TupleType:\n            if len(self.value) == 0:\n                return \"\"\n            if len(self.value) == 1:\n                v = self.value[0]\n                if type(v) == types.FloatType:\n                    return locale.str(self.value[0])\n                return str(v)\n            # We need to separate out the items\n            strval = \"\"\n            # We use a character that is invalid as the separator\n            # so that it will reparse correctly.  We could try all\n            # characters, but we make do with this set of commonly\n            # used ones - note that the first one that works will\n            # be used.  Perhaps a nicer solution than this would be\n            # to specifiy a valid delimiter for all options that\n            # can have multiple values.  Note that we have None at\n            # the end so that this will crash and die if none of\n            # the separators works <wink>.\n            if self.delimiter is None:\n                if type(self.allowed_values) == types.TupleType:\n                    self.delimiter = ' '\n                else:\n                    v0 = self.value[0]\n                    v1 = self.value[1]\n                    for sep in [' ', ',', ':', ';', '/', '\\\\', None]:\n                        # we know at this point that len(self.value) is at\n                        # least two, because len==0 and len==1 were dealt\n                        # with as special cases\n                        test_str = str(v0) + sep + str(v1)\n                        test_tuple = self._split_values(test_str)\n                        if test_tuple[0] == str(v0) and \\\n                           test_tuple[1] == str(v1) and \\\n                           len(test_tuple) == 2:\n                            break\n                    # cache this so we don't always need to do the above\n                    self.delimiter = sep\n            for v in self.value:\n                if type(v) == types.FloatType:\n                    v = locale.str(v)\n                else:\n                    v = str(v)\n                strval += v + self.delimiter\n            strval = strval[:-len(self.delimiter)] # trailing seperator\n        else:\n            # Otherwise, we just hope str() will do the job\n            strval = str(self.value)\n        return strval\n\n    def is_boolean(self):\n        '''Return True iff the option is a boolean value.'''\n        # This is necessary because of the Python 2.2 True=1, False=0\n        # cheat.  The valid values are returned as 0 and 1, even if\n        # they are actually False and True - but 0 and 1 are not\n        # considered valid input (and 0 and 1 don't look as nice)\n        # So, just for the 2.2 people, we have this helper function\n        try:\n            if type(self.allowed_values) == types.TupleType and \\\n               len(self.allowed_values) > 0 and \\\n               type(self.allowed_values[0]) == types.BooleanType:\n                return True\n            return False\n        except AttributeError:\n            # If the user has Python 2.2 and an option has valid values\n            # of (0, 1) - i.e. integers, then this function will return\n            # the wrong value.  I don't know what to do about that without\n            # explicitly stating which options are boolean\n            if self.allowed_values == (False, True):\n                return True\n            return False\n\n\nclass OptionsClass(object):\n    def __init__(self):\n        self.verbose = None\n        self._options = {}\n        self.restore_point = {}\n        self.conversion_table = {} # set by creator if they need it.\n    #\n    # Regular expressions for parsing section headers and options.\n    # Lifted straight from ConfigParser\n    #\n    SECTCRE = re.compile(\n        r'\\['                                 # [\n        r'(?P<header>[^]]+)'                  # very permissive!\n        r'\\]'                                 # ]\n        )\n    OPTCRE = re.compile(\n        r'(?P<option>[^:=\\s][^:=]*)'          # very permissive!\n        r'\\s*(?P<vi>[:=])\\s*'                 # any number of space/tab,\n                                              # followed by separator\n                                              # (either : or =), followed\n                                              # by any # space/tab\n        r'(?P<value>.*)$'                     # everything up to EOL\n        )\n\n    def update_file(self, filename):\n        '''Update the specified configuration file.'''\n        sectname = None\n        optname = None\n        out = TemporaryFile()\n        if os.path.exists(filename):\n            f = file(filename, \"r\")\n        else:\n            # doesn't exist, so create it - all the changed options will\n            # be added to it\n            if self.verbose:\n                print(\"Creating new configuration file\", file=sys.stderr)\n                print(filename, file=sys.stderr)\n            f = file(filename, \"w\")\n            f.close()\n            f = file(filename, \"r\")\n        written = []\n        vi = \": \" # default; uses the one from the file where possible\n        while True:\n            line = f.readline()\n            if not line:\n                break\n            # comment or blank line?\n            if line.strip() == '' or line[0] in '#;':\n                out.write(line)\n                continue\n            if line.split(None, 1)[0].lower() == 'rem' and line[0] in \"rR\":\n                # no leading whitespace\n                out.write(line)\n                continue\n            # continuation line?\n            if line[0].isspace() and sectname is not None and optname:\n                continue\n            # a section header or option header?\n            else:\n                # is it a section header?\n                mo = self.SECTCRE.match(line)\n                if mo:\n                    # Add any missing from the previous section\n                    if sectname is not None:\n                        self._add_missing(out, written, sectname, vi, False)\n                    sectname = mo.group('header')\n                    # So sections can't start with a continuation line\n                    optname = None\n                    if sectname in self.sections():\n                        out.write(line)\n                # an option line?\n                else:\n                    mo = self.OPTCRE.match(line)\n                    if mo:\n                        optname, vi, optval = mo.group('option', 'vi', 'value')\n                        if vi in ('=', ':') and ';' in optval:\n                            # ';' is a comment delimiter only if it follows\n                            # a spacing character\n                            pos = optval.find(';')\n                            if pos != -1 and optval[pos-1].isspace():\n                                optval = optval[:pos]\n                        optval = optval.strip()\n                        # allow empty values\n                        if optval == '\"\"':\n                            optval = ''\n                        optname = optname.rstrip().lower()\n                        if (sectname, optname) in self._options:\n                            out.write(optname)\n                            out.write(vi)\n                            newval = self.unconvert(sectname, optname)\n                            out.write(newval.replace(\"\\n\", \"\\n\\t\"))\n                            out.write('\\n')\n                            written.append((sectname, optname))\n        for sect in self.sections():\n            self._add_missing(out, written, sect, vi)\n        f.close()\n        out.flush()\n        if self.verbose:\n            # save a backup of the old file\n            shutil.copyfile(filename, filename + \".bak\")\n        # copy the new file across\n        f = file(filename, \"w\")\n        out.seek(0)\n        shutil.copyfileobj(out, f)\n        out.close()\n        f.close()\n\n    def _add_missing(self, out, written, sect, vi, label=True):\n        # add any missing ones, where the value does not equal the default\n        for opt in self.options_in_section(sect):\n            if not (sect, opt) in written and \\\n               self.get(sect, opt) != self.default(sect, opt):\n                if label:\n                    out.write('[')\n                    out.write(sect)\n                    out.write(\"]\\n\")\n                    label = False\n                out.write(opt)\n                out.write(vi)\n                newval = self.unconvert(sect, opt)\n                out.write(newval.replace(\"\\n\", \"\\n\\t\"))\n                out.write('\\n')\n                written.append((sect, opt))\n\n    def load_defaults(self, defaults):\n        '''Load default values (stored in Options.py).'''\n        for section, opts in defaults.items():\n            for opt in opts:\n                # If first item of the tuple is a sub-class of Option, then\n                # instantiate that (with the rest as args).  Otherwise,\n                # assume standard Options class.\n                klass = Option\n                args = opt\n                try:\n                    if issubclass(opt[0], Option):\n                        klass = opt[0]\n                        args = opt[1:]\n                except TypeError: # opt[0] not a class\n                    pass\n\n                o = klass(*args)\n                self._options[section, o.name] = o\n\n    def set_restore_point(self):\n        '''Remember what the option values are right now, to\n        be able to go back to them, via revert_to_restore_point().\n\n        Any existing restore point is wiped.  Restore points do\n        not persist over sessions.\n        '''\n        self.restore_point = {}\n        for key, opt_obj in self._options.iteritems():\n            self.restore_point[key] = opt_obj.get()\n\n    def revert_to_restore_point(self):\n        '''Restore option values to their values when set_restore_point()\n        was last called.\n\n        If set_restore_point() has not been called, then this has no\n        effect.  If new options have been added since set_restore_point,\n        their values are not effected.\n        '''\n        for key, value in self.restore_point.iteritems():\n            self._options[key].set(value)\n\n    def merge_files(self, file_list):\n        for f in file_list:\n            self.merge_file(f)\n\n    def convert_and_set(self, section, option, value):\n        value = self.convert(section, option, value)\n        self.set(section, option, value)\n\n    def merge_file(self, filename):\n        import ConfigParser\n        c = ConfigParser.ConfigParser()\n        c.read(filename)\n        for sect in c.sections():\n            for opt in c.options(sect):\n                value = c.get(sect, opt)\n                section = sect\n                option = opt\n                if (section, option) not in self._options:\n                    if option.startswith('x-'):\n                        # try setting option without the x- prefix\n                        option = option[2:]\n                        if (section, option) in self._options:\n                            self.convert_and_set(section, option, value)\n                        # not an error if an X- option is missing\n                    else:\n                        option = 'x-' + option\n                        # going the other way, if the option has been\n                        # deprecated, set its x-prefixed version and\n                        # emit a warning\n                        if (section, option) in self._options:\n                            self.convert_and_set(section, option, value)\n                            self._report_deprecated_error(section, opt)\n                        else:\n                            print((\n                                \"warning: Invalid option %s in\"\n                                \" section %s in file %s\") %\n                                (opt, sect, filename),\n                                file=sys.stderr)\n                else:\n                    self.convert_and_set(section, option, value)\n\n    # not strictly necessary, but convenient shortcuts to self._options\n    def display_name(self, sect, opt):\n        '''A name for the option suitable for display to a user.'''\n        return self._options[sect, opt.lower()].display_name()\n    def default(self, sect, opt):\n        '''The default value for the option.'''\n        return self._options[sect, opt.lower()].default()\n    def doc(self, sect, opt):\n        '''Documentation for the option.'''\n        return self._options[sect, opt.lower()].doc()\n    def valid_input(self, sect, opt):\n        '''Valid values for the option.'''\n        return self._options[sect, opt.lower()].valid_input()\n    def no_restore(self, sect, opt):\n        '''Do not restore this option when restoring to defaults.'''\n        return self._options[sect, opt.lower()].no_restore()\n    def is_valid(self, sect, opt, value):\n        '''Check if this is a valid value for this option.'''\n        return self._options[sect, opt.lower()].is_valid(value)\n    def multiple_values_allowed(self, sect, opt):\n        '''Multiple values are allowed for this option.'''\n        return self._options[sect, opt.lower()].multiple_values_allowed()\n\n    def is_boolean(self, sect, opt):\n        '''The option is a boolean value. (Support for Python 2.2).'''\n        return self._options[sect, opt.lower()].is_boolean()\n\n    def convert(self, sect, opt, value):\n        '''Convert value from a string to the appropriate type.'''\n        return self._options[sect, opt.lower()].convert(value)\n\n    def unconvert(self, sect, opt):\n        '''Convert value from the appropriate type to a string.'''\n        return self._options[sect, opt.lower()].unconvert()\n\n    def get_option(self, sect, opt):\n        '''Get an option.'''\n        if (sect, opt) in self.conversion_table:\n            sect, opt = self.conversion_table[sect, opt]\n        return self._options[sect, opt.lower()]\n\n    def get(self, sect, opt):\n        '''Get an option value.'''\n        if (sect, opt.lower()) in self.conversion_table:\n            sect, opt = self.conversion_table[sect, opt.lower()]\n        return self.get_option(sect, opt.lower()).get()\n\n    def __getitem__(self, key):\n        return self.get(key[0], key[1])\n\n    def set(self, sect, opt, val=None):\n        '''Set an option.'''\n        if (sect, opt.lower()) in self.conversion_table:\n            sect, opt = self.conversion_table[sect, opt.lower()]\n            \n        # Annoyingly, we have a special case.  The notate_to and\n        # notate_subject allowed values have to be set to the same\n        # values as the header_x_ options, but this can't be done\n        # (AFAIK) dynmaically. If this isn't the case, then if the\n        # header_x_string values are changed, the notate_ options don't\n        # work.  Outlook Express users like both of these options...so\n        # we fix it here. See also sf #944109.\n        # This code was originally in Options.py, after loading in the\n        # options.  But that doesn't work, because if we are setting\n        # both in a config file, we need it done immediately.\n        # We now need the hack here, *and* in UserInterface.py\n        # For the moment, this will do.  Use a real mail client, for\n        # goodness sake!\n        if sect == \"Headers\" and opt in (\"notate_to\", \"notate_subject\"):\n            self._options[sect, opt.lower()].set(val)\n            return\n        if self.is_valid(sect, opt, val):\n            self._options[sect, opt.lower()].set(val)\n        else:\n            print((\"Attempted to set [%s] %s with \"\n                   \"invalid value %s (%s)\" %\n                    (sect, opt.lower(), val, type(val))),\n                    file=sys.stderr)\n\n    def set_from_cmdline(self, arg, stream=None):\n        \"\"\"Set option from colon-separated sect:opt:val string.\n\n        If optional stream arg is not None, error messages will be displayed\n        on stream, otherwise KeyErrors will be propagated up the call chain.\n        \"\"\"\n        sect, opt, val = arg.split(':', 2)\n        opt = opt.lower()\n        try:\n            val = self.convert(sect, opt, val)\n        except (KeyError, TypeError) as msg:\n            if stream is not None:\n                self._report_option_error(sect, opt, val, stream, msg)\n            else:\n                raise\n        else:\n            self.set(sect, opt, val)\n\n    def _report_deprecated_error(self, sect, opt):\n        print((\n            \"Warning: option %s in section %s is deprecated\" %\n            (opt, sect)),\n            file=sys.stderr)\n\n    def _report_option_error(self, sect, opt, val, stream, msg):\n        if sect in self.sections():\n            vopts = self.options(True)\n            vopts = [v.split(']', 1)[1] for v in vopts\n                       if v.startswith('[%s]'%sect)]\n            if opt not in vopts:\n                print(\"Invalid option: %s\" % opt, file=stream)\n                print(\"Valid options for %s are: %s\" % sect, file=stream)\n                vopts = ', '.join(vopts)\n                vopts = wrap(vopts)\n                for line in vopts:\n                    print('  %s' % line, file=stream)\n            else:\n                print(\"Invalid value: %s\" % msg, file=stream)\n        else:\n            print(\"Invalid section: %s\" % sect, file=stream)\n            print(\"Valid sections are:\", file=stream)\n            vsects = ', '.join(self.sections())\n            vsects = wrap(vsects)\n            for line in vsects:\n                print('  %s' % line, file=stream)\n\n    def __setitem__(self, key, value):\n        self.set(key[0], key[1], value)\n\n    def sections(self):\n        '''Return an alphabetical list of all the sections.'''\n        all = []\n        for sect, opt in self._options.keys():\n            if sect not in all:\n                all.append(sect)\n        all.sort()\n        return all\n\n    def options_in_section(self, section):\n        '''Return an alphabetical list of all the options in this section.'''\n        all = []\n        for sect, opt in self._options.keys():\n            if sect == section:\n                all.append(opt)\n        all.sort()\n        return all\n\n    def options(self, prepend_section_name=False):\n        '''Return an alphabetical list of all the options, optionally\n        prefixed with [section_name]'''\n        all = []\n        for sect, opt in self._options.keys():\n            if prepend_section_name:\n                all.append('[' + sect + ']' + opt)\n            else:\n                all.append(opt)\n        all.sort()\n        return all\n\n    def display(self, add_comments=False):\n        '''Display options in a config file form.'''\n        output = StringIO()\n        keys = self._options.keys()\n        keys.sort()\n        currentSection = None\n        for sect, opt in keys:\n            if sect != currentSection:\n                if currentSection is not None:\n                    output.write('\\n')\n                output.write('[')\n                output.write(sect)\n                output.write(\"]\\n\")\n                currentSection = sect\n            if add_comments:\n                doc = self._options[sect, opt].doc()\n                if not doc:\n                    doc = \"No information available, sorry.\"\n                doc = re.sub(r\"\\s+\", \" \", doc)\n                output.write(\"\\n# %s\\n\" % (\"\\n# \".join(wrap(doc)),))\n            self._options[sect, opt].write_config(output)\n        return output.getvalue()\n\n    def _display_nice(self, section, option, formatter):\n        '''Display a nice output of the options'''\n        # Given that the Options class is no longer as nice looking\n        # as it once was, this returns all the information, i.e.\n        # the doc, default values, and so on\n        output = StringIO()\n\n        # when section and option are both specified, this\n        # is nothing more than a call to as_nice_string\n        if section is not None and option is not None:\n            opt = self._options[section, option.lower()]\n            output.write(getattr(opt, formatter)(section))\n            return output.getvalue()\n\n        all = self._options.keys()\n        all.sort()\n        for sect, opt in all:\n            if section is not None and sect != section:\n                continue\n            opt = self._options[sect, opt.lower()]\n            output.write(getattr(opt, formatter)(sect))\n        return output.getvalue()\n\n    def display_full(self, section=None, option=None):\n        '''Display options including all information.'''\n        return self._display_nice(section, option, 'as_nice_string')\n        \n    def output_for_docs(self, section=None, option=None):\n        '''Return output suitable for inserting into documentation for\n        the available options.'''\n        return self._display_nice(section, option, 'as_documentation_string')\n\n# These are handy references to commonly used regex/tuples defining\n# permitted values. Although the majority of options use one of these,\n# you may use any regex or tuple you wish.\nHEADER_NAME = r\"[\\w\\.\\-\\*]+\"\nHEADER_VALUE = r\".+\"\nINTEGER = r\"[\\d]+\"              # actually, a *positive* integer\nREAL = r\"[\\d]+[\\.]?[\\d]*\"       # likewise, a *positive* real\nBOOLEAN = (False, True)\nSERVER = r\"([\\w\\.\\-]+(:[\\d]+)?)\"  # in the form server:port\nPORT = r\"[\\d]+\"\nEMAIL_ADDRESS = r\"[\\w\\-\\.]+@[\\w\\-\\.]+\"\nPATH = r\"[\\w \\$\\.\\-~:\\\\/\\*\\@\\=]+\"\nVARIABLE_PATH = PATH + r\"%\"\nFILE = r\"[\\S]+\"\nFILE_WITH_PATH = PATH\nIP_LIST = r\"\\*|localhost|((\\*|[01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.(\\*|[01]?\\d\" \\\n          r\"\\d?|2[0-4]\\d|25[0-5])\\.(\\*|[01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.(\\*\" \\\n          r\"|[01]?\\d\\d?|2[0-4]\\d|25[0-5]),?)+\"\n# IMAP seems to allow any character at all in a folder name,\n# but we want to use the comma as a delimiter for lists, so\n# we don't allow this.  If anyone has folders with commas in the\n# names, please let us know and we'll figure out something else.\n# ImapUI.py prints out a warning if this is the case.\nIMAP_FOLDER = r\"[^,]+\"\n\n# IMAP's astring should also be valid in the form:\n#   \"{\" number \"}\" CRLF *CHAR8\n#   where number represents the number of CHAR8 octets\n# but this is too complex for us at the moment.\nIMAP_ASTRING = []\nfor _i in xrange(1, 128):\n    if chr(_i) not in ['\"', '\\\\', '\\n', '\\r']:\n        IMAP_ASTRING.append(chr(_i))\ndel _i\nIMAP_ASTRING = r\"\\\"?[\" + re.escape(''.join(IMAP_ASTRING)) + r\"]+\\\"?\"\n\n# Similarly, each option must specify whether it should be reset to\n# this value on a \"reset to defaults\" command.  Most should, but with some\n# like a server name that defaults to \"\", this would be pointless.\n# Again, for ease of reading, we define these here:\nRESTORE = True\nDO_NOT_RESTORE = False\n\nOCRAD_CHARSET = r\"ascii|iso-8859-9|iso-8859-15\"\n"
  },
  {
    "path": "mailpile/spambayes/Tester.py",
    "content": "from mailpile.spambayes.Options import options\n\nclass Test:\n    # Pass a classifier instance (an instance of Bayes).\n    # Loop:\n    #     # Train the classifer with new ham and spam.\n    #     train(ham, spam) # this implies reset_test_results\n    #     Loop:\n    #         Optional:\n    #             # Possibly fiddle the classifier.\n    #             set_classifier()\n    #             # Forget smessages the classifier was trained on.\n    #             untrain(ham, spam) # this implies reset_test_results\n    #         Optional:\n    #             reset_test_results()\n    #         # Predict against (presumably new) examples.\n    #         predict(ham, spam)\n    #         Optional:\n    #             suck out the results, via instance vrbls and\n    #             false_negative_rate(), false_positive_rate(),\n    #             false_negatives(), and false_positives()\n\n    def __init__(self):\n        self.reset_test_results()\n\n    # Tell the tester which classifier to use.\n    def set_classifier(self, classifier):\n        self.classifier = classifier\n\n    def reset_test_results(self):\n        # The number of ham and spam instances tested.\n        self.nham_tested = self.nspam_tested = 0\n\n        # The number of test instances correctly and incorrectly classified.\n        self.nham_right = 0\n        self.nham_wrong = 0\n        self.nham_unsure = 0\n        self.nspam_right = 0\n        self.nspam_wrong = 0\n        self.nspam_unsure = 0\n\n        # Lists of bad predictions.\n        self.ham_wrong_examples = []    # False positives:  ham called spam.\n        self.spam_wrong_examples = []   # False negatives:  spam called ham.\n        self.unsure_examples = []       # ham and spam in middle ground\n\n    # Train the classifier on streams of ham and spam.  Updates probabilities\n    # before returning, and resets test results.\n    def train(self, hamstream=None, spamstream=None):\n        self.reset_test_results()\n        learn = self.classifier.learn\n        if hamstream is not None:\n            for example in hamstream:\n                learn(example, False)\n        if spamstream is not None:\n            for example in spamstream:\n                learn(example, True)\n\n    # Untrain the classifier on streams of ham and spam.  Updates\n    # probabilities before returning, and resets test results.\n    def untrain(self, hamstream=None, spamstream=None):\n        self.reset_test_results()\n        unlearn = self.classifier.unlearn\n        if hamstream is not None:\n            for example in hamstream:\n                unlearn(example, False)\n        if spamstream is not None:\n            for example in spamstream:\n                unlearn(example, True)\n\n    # Run prediction on each sample in stream.  You're swearing that stream\n    # is entirely composed of spam (is_spam True), or of ham (is_spam False).\n    # Note that mispredictions are saved, and can be retrieved later via\n    # false_negatives (spam mistakenly called ham) and false_positives (ham\n    # mistakenly called spam).  For this reason, you may wish to wrap examples\n    # in a little class that identifies the example in a useful way, and whose\n    # __iter__ produces a token stream for the classifier.\n    #\n    # If specified, callback(msg, spam_probability) is called for each\n    # msg in the stream, after the spam probability is computed.\n    def predict(self, stream, is_spam, callback=None):\n        guess = self.classifier.spamprob\n        for example in stream:\n            prob = guess(example)\n            if callback:\n                callback(example, prob)\n            is_ham_guessed  = prob <  options[\"Categorization\", \"ham_cutoff\"]\n            is_spam_guessed = prob >= options[\"Categorization\", \"spam_cutoff\"]\n            if is_spam:\n                self.nspam_tested += 1\n                if is_spam_guessed:\n                    self.nspam_right += 1\n                elif is_ham_guessed:\n                    self.nspam_wrong += 1\n                    self.spam_wrong_examples.append(example)\n                else:\n                    self.nspam_unsure += 1\n                    self.unsure_examples.append(example)\n            else:\n                self.nham_tested += 1\n                if is_ham_guessed:\n                    self.nham_right += 1\n                elif is_spam_guessed:\n                    self.nham_wrong += 1\n                    self.ham_wrong_examples.append(example)\n                else:\n                    self.nham_unsure += 1\n                    self.unsure_examples.append(example)\n\n        assert (self.nham_right + self.nham_wrong + self.nham_unsure ==\n                self.nham_tested)\n        assert (self.nspam_right + self.nspam_wrong + self.nspam_unsure ==\n                self.nspam_tested)\n\n    def false_positive_rate(self):\n        \"\"\"Percentage of ham mistakenly identified as spam, in 0.0..100.0.\"\"\"\n        return self.nham_wrong * 1e2 / (self.nham_tested or 1)\n\n    def false_negative_rate(self):\n        \"\"\"Percentage of spam mistakenly identified as ham, in 0.0..100.0.\"\"\"\n        return self.nspam_wrong * 1e2 / (self.nspam_tested or 1)\n\n    def unsure_rate(self):\n        return ((self.nham_unsure + self.nspam_unsure) * 1e2 /\n                ((self.nham_tested + self.nspam_tested) or 1))\n\n    def false_positives(self):\n        return self.ham_wrong_examples\n\n    def false_negatives(self):\n        return self.spam_wrong_examples\n\n    def unsures(self):\n        return self.unsure_examples\n\nclass _Example:\n    def __init__(self, name, words):\n        self.name = name\n        self.words = words\n    def __iter__(self):\n        return iter(self.words)\n\n_easy_test = \"\"\"\n    >>> from mailpile.spambayes.classifier import Bayes\n    >>> from mailpile.spambayes.Options import options\n    >>> options[\"Categorization\", \"ham_cutoff\"] = options[\"Categorization\", \"spam_cutoff\"] = 0.5\n\n    >>> good1 = _Example('', ['a', 'b', 'c'])\n    >>> good2 = _Example('', ['a', 'b'])\n    >>> bad1 = _Example('', ['c', 'd'])\n\n    >>> t = Test()\n    >>> t.set_classifier(Bayes())\n    >>> t.train([good1, good2], [bad1])\n    >>> t.predict([_Example('goodham', ['a', 'b']),\n    ...            _Example('badham', ['d'])    # FP\n    ...           ], False)\n    >>> t.predict([_Example('goodspam', ['d']),\n    ...            _Example('badspam1', ['a']), # FN\n    ...            _Example('badspam2', ['a', 'b']),    # FN\n    ...            _Example('badspam3', ['d', 'a', 'b'])    # FN\n    ...           ], True)\n\n    >>> t.nham_tested\n    2\n    >>> t.nham_right, t.nham_wrong\n    (1, 1)\n    >>> t.false_positive_rate()\n    50.0\n    >>> [e.name for e in t.false_positives()]\n    ['badham']\n\n    >>> t.nspam_tested\n    4\n    >>> t.nspam_right, t.nspam_wrong\n    (1, 3)\n    >>> t.false_negative_rate()\n    75.0\n    >>> [e.name for e in t.false_negatives()]\n    ['badspam1', 'badspam2', 'badspam3']\n\n    >>> [e.name for e in t.unsures()]\n    []\n    >>> t.unsure_rate()\n    0.0\n\"\"\"\n\n__test__ = {'easy': _easy_test}\n\nif __name__ == '__main__':\n    import doctest\n    doctest.testmod()\n"
  },
  {
    "path": "mailpile/spambayes/__init__.py",
    "content": "# package marker.\n\n__version__ = \"1.1b3\"\n__date__ = \"Nov 23, 2017\"\n\nfrom classifier import Classifier\n"
  },
  {
    "path": "mailpile/spambayes/chi2.py",
    "content": "from __future__ import print_function\nimport math as _math\nimport random\n\ndef chi2Q(x2, v, exp=_math.exp, min=min):\n    \"\"\"Return prob(chisq >= x2, with v degrees of freedom).\n\n    v must be even.\n    \"\"\"\n    assert v & 1 == 0\n    # XXX If x2 is very large, exp(-m) will underflow to 0.\n    m = x2 / 2.0\n    sum = term = exp(-m)\n    for i in range(1, v//2):\n        term *= m / i\n        sum += term\n    # With small x2 and large v, accumulated roundoff error, plus error in\n    # the platform exp(), can cause this to spill a few ULP above 1.0.  For\n    # example, chi2Q(100, 300) on my box has sum == 1.0 + 2.0**-52 at this\n    # point.  Returning a value even a teensy bit over 1.0 is no good.\n    return min(sum, 1.0)\n\ndef normZ(z, sqrt2pi=_math.sqrt(2.0*_math.pi), exp=_math.exp):\n    \"Return value of the unit Gaussian at z.\"\n    return exp(-z*z/2.0) / sqrt2pi\n\ndef normP(z):\n    \"\"\"Return area under the unit Gaussian from -inf to z.\n\n    This is the probability that a zscore is <= z.\n    \"\"\"\n\n    # This is very accurate in a fixed-point sense.  For negative z of\n    # large magnitude (<= -8.3), it returns 0.0, essentially because\n    # P(-z) is, to machine precision, indistiguishable from 1.0 then.\n\n    # sum <- area from 0 to abs(z).\n    a = abs(float(z))\n    if a >= 8.3:\n        sum = 0.5\n    else:\n        sum2 = term = a * normZ(a)\n        z2 = a*a\n        sum = 0.0\n        i = 1.0\n        while sum != sum2:\n            sum = sum2\n            i += 2.0\n            term *= z2 / i\n            sum2 += term\n\n    if z >= 0:\n        result = 0.5 + sum\n    else:\n        result = 0.5 - sum\n\n    return result\n\ndef normIQ(p, sqrt=_math.sqrt, ln=_math.log):\n    \"\"\"Return z such that the area under the unit Gaussian from z to +inf is p.\n\n    Must have 0.0 <= p <= 1.0.\n    \"\"\"\n\n    assert 0.0 <= p <= 1.0\n    # This is a low-accuracy rational approximation from Abramowitz & Stegun.\n    # The absolute error is bounded by 3e-3.\n\n    flipped = False\n    if p > 0.5:\n        flipped = True\n        p = 1.0 - p\n\n    if p == 0.0:\n        z = 8.3\n    else:\n        t = sqrt(-2.0 * ln(p))\n        z = t - (2.30753 + .27061*t) / (1. + .99229*t + .04481*t**2)\n\n    if flipped:\n        z = -z\n    return z\n\ndef normIP(p):\n    \"\"\"Return z such that the area under the unit Gaussian from -inf to z is p.\n\n    Must have 0.0 <= p <= 1.0.\n    \"\"\"\n    z = normIQ(1.0 - p)\n    # One Newton step should double the # of good digits.\n    return z + (p - normP(z)) / normZ(z)\n\ndef main():\n    from spambayes.Histogram import Hist\n    import sys\n\n    class WrappedRandom:\n        # There's no way W-H is equidistributed in 50 dimensions, so use\n        # Marsaglia-wrapping to shuffle it more.\n\n        def __init__(self, baserandom=random.random, tabsize=513):\n            self.baserandom = baserandom\n            self.n = tabsize\n            self.tab = [baserandom() for _i in range(tabsize)]\n            self.next = baserandom()\n\n        def random(self):\n            result = self.next\n            i = int(result * self.n)\n            self.next = self.tab[i]\n            self.tab[i] = self.baserandom()\n            return result\n\n    random = WrappedRandom().random\n    #from uni import uni as random\n    #print random\n\n    def judge(ps, ln=_math.log, ln2=_math.log(2), frexp=_math.frexp):\n        H = S = 1.0\n        Hexp = Sexp = 0\n        for p in ps:\n            S *= 1.0 - p\n            H *= p\n            if S < 1e-200:\n                S, e = frexp(S)\n                Sexp += e\n            if H < 1e-200:\n                H, e = frexp(H)\n                Hexp += e\n        S = ln(S) + Sexp * ln2\n        H = ln(H) + Hexp * ln2\n        n = len(ps)\n        S = 1.0 - chi2Q(-2.0 * S, 2*n)\n        H = 1.0 - chi2Q(-2.0 * H, 2*n)\n        return S, H, (S-H + 1.0) / 2.0\n\n    warp = 0\n    bias = 0.99\n    if len(sys.argv) > 1:\n        warp = int(sys.argv[1])\n    if len(sys.argv) > 2:\n        bias = float(sys.argv[2])\n\n    h = Hist(20, lo=0.0, hi=1.0)\n    s = Hist(20, lo=0.0, hi=1.0)\n    score = Hist(20, lo=0.0, hi=1.0)\n\n    for _i in xrange(5000):\n        ps = [random() for _j in xrange(50)]\n        s1, h1, score1 = judge(ps + [bias] * warp)\n        s.add(s1)\n        h.add(h1)\n        score.add(score1)\n\n    print(\"Result for random vectors of 50 probs, + %s forced to %s\" % ( warp, bias))\n\n    # Should be uniformly distributed on all-random data.\n    print()\n    print('H %s' % h.display())\n\n    # Should be uniformly distributed on all-random data.\n    print()\n    print('S %s' % s.display())\n\n    # Distribution doesn't really matter.\n    print()\n    print('(S-H+1)/2 %s' % score.display())\n\ndef showscore(ps, ln=_math.log, ln2=_math.log(2), frexp=_math.frexp):\n    H = S = 1.0\n    Hexp = Sexp = 0\n    for p in ps:\n        S *= 1.0 - p\n        H *= p\n        if S < 1e-200:\n            S, e = frexp(S)\n            Sexp += e\n        if H < 1e-200:\n            H, e = frexp(H)\n            Hexp += e\n    S = ln(S) + Sexp * ln2\n    H = ln(H) + Hexp * ln2\n\n    n = len(ps)\n    probS = chi2Q(-2*S, 2*n)\n    probH = chi2Q(-2*H, 2*n)\n    print(\"P(chisq >= %10g | v=%3d) = %10g\" % (-2*S, 2*n, probS))\n    print(\"P(chisq >= %10g | v=%3d) = %10g\" % (-2*H, 2*n, probH))\n\n    S = 1.0 - probS\n    H = 1.0 - probH\n    score = (S-H + 1.0) / 2.0\n    print(\"spam prob %s\" % S)\n    print(\" ham prob %s\" % H)\n    print(\"(S-H+1)/2 %s\" % score)\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "mailpile/spambayes/classifier.py",
    "content": "#! /usr/bin/env python\n\nfrom __future__ import generators\nfrom __future__ import print_function\n\n# An implementation of a Bayes-like spam classifier.\n#\n# Paul Graham's original description:\n#\n#     http://www.paulgraham.com/spam.html\n#\n# A highly fiddled version of that can be retrieved from our CVS repository,\n# via tag Last-Graham.  This made many demonstrated improvements in error\n# rates over Paul's original description.\n#\n# This code implements Gary Robinson's suggestions, the core of which are\n# well explained on his webpage:\n#\n#    http://radio.weblogs.com/0101454/stories/2002/09/16/spamDetection.html\n#\n# This is theoretically cleaner, and in testing has performed at least as\n# well as our highly tuned Graham scheme did, often slightly better, and\n# sometimes much better.  It also has \"a middle ground\", which people like:\n# the scores under Paul's scheme were almost always very near 0 or very near\n# 1, whether or not the classification was correct.  The false positives\n# and false negatives under Gary's basic scheme (use_gary_combining) generally\n# score in a narrow range around the corpus's best spam_cutoff value.\n# However, it doesn't appear possible to guess the best spam_cutoff value in\n# advance, and it's touchy.\n#\n# The last version of the Gary-combining scheme can be retrieved from our\n# CVS repository via tag Last-Gary.\n#\n# The chi-combining scheme used by default here gets closer to the theoretical\n# basis of Gary's combining scheme, and does give extreme scores, but also\n# has a very useful middle ground (small # of msgs spread across a large range\n# of scores, and good cutoff values aren't touchy).\n#\n# This implementation is due to Tim Peters et alia.\n\nimport math\n\nfrom mailpile.spambayes.Options import options\nfrom mailpile.spambayes.chi2 import chi2Q\nfrom mailpile.spambayes.safepickle import pickle_read, pickle_write\n\nLN2 = math.log(2)       # used frequently by chi-combining\n\nPICKLE_VERSION = 5\n\nclass WordInfo(object):\n    # A WordInfo is created for each distinct word.  spamcount is the\n    # number of trained spam msgs in which the word appears, and hamcount\n    # the number of trained ham msgs.\n    #\n    # Invariant:  For use in a classifier database, at least one of\n    # spamcount and hamcount must be non-zero.\n    #\n    # Important:  This is a tiny object.  Use of __slots__ is essential\n    # to conserve memory.\n    __slots__ = 'spamcount', 'hamcount'\n\n    def __init__(self):\n        self.__setstate__((0, 0))\n\n    def __repr__(self):\n        return \"WordInfo\" + repr((self.spamcount, self.hamcount))\n\n    def __getstate__(self):\n        return self.spamcount, self.hamcount\n\n    def __setstate__(self, t):\n        self.spamcount, self.hamcount = t\n\n\nclass Classifier:\n    # Defining __slots__ here made Jeremy's life needlessly difficult when\n    # trying to hook this all up to ZODB as a persistent object.  There's\n    # no space benefit worth getting from slots in this class; slots were\n    # used solely to help catch errors earlier, when this code was changing\n    # rapidly.\n\n    #__slots__ = ('wordinfo',  # map word to WordInfo record\n    #             'nspam',     # number of spam messages learn() has seen\n    #             'nham',      # number of non-spam messages learn() has seen\n    #            )\n\n    # allow a subclass to use a different class for WordInfo\n    WordInfoClass = WordInfo\n\n    def __init__(self):\n        self.wordinfo = {}\n        self.probcache = {}\n        self.nspam = self.nham = 0\n\n    def __getstate__(self):\n        return (PICKLE_VERSION, self.wordinfo, self.nspam, self.nham)\n\n    def __setstate__(self, t):\n        if t[0] != PICKLE_VERSION:\n            raise ValueError(\"Can't unpickle -- version %s unknown\" % t[0])\n        (self.wordinfo, self.nspam, self.nham) = t[1:]\n        self.probcache = {}\n\n    # spamprob() implementations.  One of the following is aliased to\n    # spamprob, depending on option settings.\n    # Currently only chi-squared is available, but maybe there will be\n    # an alternative again someday.\n\n    # Across vectors of length n, containing random uniformly-distributed\n    # probabilities, -2*sum(ln(p_i)) follows the chi-squared distribution\n    # with 2*n degrees of freedom.  This has been proven (in some\n    # appropriate sense) to be the most sensitive possible test for\n    # rejecting the hypothesis that a vector of probabilities is uniformly\n    # distributed.  Gary Robinson's original scheme was monotonic *with*\n    # this test, but skipped the details.  Turns out that getting closer\n    # to the theoretical roots gives a much sharper classification, with\n    # a very small (in # of msgs), but also very broad (in range of scores),\n    # \"middle ground\", where most of the mistakes live.  In particular,\n    # this scheme seems immune to all forms of \"cancellation disease\":  if\n    # there are many strong ham *and* spam clues, this reliably scores\n    # close to 0.5.  Most other schemes are extremely certain then -- and\n    # often wrong.\n    def chi2_spamprob(self, wordstream, evidence=False):\n        \"\"\"Return best-guess probability that wordstream is spam.\n\n        wordstream is an iterable object producing words.\n        The return value is a float in [0.0, 1.0].\n\n        If optional arg evidence is True, the return value is a pair\n            probability, evidence\n        where evidence is a list of (word, probability) pairs.\n        \"\"\"\n\n        from math import frexp, log as ln\n\n        # We compute two chi-squared statistics, one for ham and one for\n        # spam.  The sum-of-the-logs business is more sensitive to probs\n        # near 0 than to probs near 1, so the spam measure uses 1-p (so\n        # that high-spamprob words have greatest effect), and the ham\n        # measure uses p directly (so that lo-spamprob words have greatest\n        # effect).\n        #\n        # For optimization, sum-of-logs == log-of-product, and f.p.\n        # multiplication is a lot cheaper than calling ln().  It's easy\n        # to underflow to 0.0, though, so we simulate unbounded dynamic\n        # range via frexp.  The real product H = this H * 2**Hexp, and\n        # likewise the real product S = this S * 2**Sexp.\n        H = S = 1.0\n        Hexp = Sexp = 0\n\n        clues = self._getclues(wordstream)\n        for prob, word, record in clues:\n            S *= 1.0 - prob\n            H *= prob\n            if S < 1e-200:  # prevent underflow\n                S, e = frexp(S)\n                Sexp += e\n            if H < 1e-200:  # prevent underflow\n                H, e = frexp(H)\n                Hexp += e\n\n        # Compute the natural log of the product = sum of the logs:\n        # ln(x * 2**i) = ln(x) + i * ln(2).\n        S = ln(S) + Sexp * LN2\n        H = ln(H) + Hexp * LN2\n\n        n = len(clues)\n        if n:\n            S = 1.0 - chi2Q(-2.0 * S, 2*n)\n            H = 1.0 - chi2Q(-2.0 * H, 2*n)\n\n            # How to combine these into a single spam score?  We originally\n            # used (S-H)/(S+H) scaled into [0., 1.], which equals S/(S+H).  A\n            # systematic problem is that we could end up being near-certain\n            # a thing was (for example) spam, even if S was small, provided\n            # that H was much smaller.\n            # Rob Hooft stared at these problems and invented the measure\n            # we use now, the simpler S-H, scaled into [0., 1.].\n            prob = (S-H + 1.0) / 2.0\n        else:\n            prob = 0.5\n\n        if evidence:\n            clues = [(w, p) for p, w, _r in clues]\n            clues.sort(lambda a, b: cmp(a[1], b[1]))\n            clues.insert(0, ('*S*', S))\n            clues.insert(0, ('*H*', H))\n            return prob, clues\n        else:\n            return prob\n\n    if options[\"Classifier\", \"use_chi_squared_combining\"]:\n        spamprob = chi2_spamprob\n\n    def learn(self, wordstream, is_spam):\n        \"\"\"Teach the classifier by example.\n\n        wordstream is a word stream representing a message.  If is_spam is\n        True, you're telling the classifier this message is definitely spam,\n        else that it's definitely not spam.\n        \"\"\"\n        if options[\"Classifier\", \"use_bigrams\"]:\n            wordstream = self._enhance_wordstream(wordstream)\n        self._add_msg(wordstream, is_spam)\n\n    def unlearn(self, wordstream, is_spam):\n        \"\"\"In case of pilot error, call unlearn ASAP after screwing up.\n\n        Pass the same arguments you passed to learn().\n        \"\"\"\n        if options[\"Classifier\", \"use_bigrams\"]:\n            wordstream = self._enhance_wordstream(wordstream)\n        self._remove_msg(wordstream, is_spam)\n\n    def probability(self, record):\n        \"\"\"Compute, store, and return prob(msg is spam | msg contains word).\n\n        This is the Graham calculation, but stripped of biases, and\n        stripped of clamping into 0.01 thru 0.99.  The Bayesian\n        adjustment following keeps them in a sane range, and one\n        that naturally grows the more evidence there is to back up\n        a probability.\n        \"\"\"\n\n        spamcount = record.spamcount\n        hamcount = record.hamcount\n\n        # Try the cache first\n        try:\n            return self.probcache[spamcount][hamcount]\n        except KeyError:\n            pass\n\n        nham = float(self.nham or 1)\n        nspam = float(self.nspam or 1)\n\n        assert hamcount <= nham, \"Token seen in more ham than ham trained.\"\n        hamratio = hamcount / nham\n\n        assert spamcount <= nspam, \"Token seen in more spam than spam trained.\"\n        spamratio = spamcount / nspam\n\n        prob = spamratio / (hamratio + spamratio)\n\n        S = options[\"Classifier\", \"unknown_word_strength\"]\n        StimesX = S * options[\"Classifier\", \"unknown_word_prob\"]\n\n\n        # Now do Robinson's Bayesian adjustment.\n        #\n        #         s*x + n*p(w)\n        # f(w) = --------------\n        #           s + n\n        #\n        # I find this easier to reason about like so (equivalent when\n        # s != 0):\n        #\n        #        x - p\n        #  p +  -------\n        #       1 + n/s\n        #\n        # IOW, it moves p a fraction of the distance from p to x, and\n        # less so the larger n is, or the smaller s is.\n\n        n = hamcount + spamcount\n        prob = (StimesX + n * prob) / (S + n)\n\n        # Update the cache\n        try:\n            self.probcache[spamcount][hamcount] = prob\n        except KeyError:\n            self.probcache[spamcount] = {hamcount: prob}\n\n        return prob\n\n    # NOTE:  Graham's scheme had a strange asymmetry:  when a word appeared\n    # n>1 times in a single message, training added n to the word's hamcount\n    # or spamcount, but predicting scored words only once.  Tests showed\n    # that adding only 1 in training, or scoring more than once when\n    # predicting, hurt under the Graham scheme.\n    # This isn't so under Robinson's scheme, though:  results improve\n    # if training also counts a word only once.  The mean ham score decreases\n    # significantly and consistently, ham score variance decreases likewise,\n    # mean spam score decreases (but less than mean ham score, so the spread\n    # increases), and spam score variance increases.\n    # I (Tim) speculate that adding n times under the Graham scheme helped\n    # because it acted against the various ham biases, giving frequently\n    # repeated spam words (like \"Viagra\") a quick ramp-up in spamprob; else,\n    # adding only once in training, a word like that was simply ignored until\n    # it appeared in 5 distinct training spams.  Without the ham-favoring\n    # biases, though, and never ignoring words, counting n times introduces\n    # a subtle and unhelpful bias.\n    # There does appear to be some useful info in how many times a word\n    # appears in a msg, but distorting spamprob doesn't appear a correct way\n    # to exploit it.\n    def _add_msg(self, wordstream, is_spam):\n        self.probcache = {}    # nuke the prob cache\n        if is_spam:\n            self.nspam += 1\n        else:\n            self.nham += 1\n\n        for word in set(wordstream):\n            record = self._wordinfoget(word)\n            if record is None:\n                record = self.WordInfoClass()\n\n            if is_spam:\n                record.spamcount += 1\n            else:\n                record.hamcount += 1\n\n            self._wordinfoset(word, record)\n\n        self._post_training()\n\n    def _remove_msg(self, wordstream, is_spam):\n        self.probcache = {}    # nuke the prob cache\n        if is_spam:\n            if self.nspam <= 0:\n                raise ValueError(\"spam count would go negative!\")\n            self.nspam -= 1\n        else:\n            if self.nham <= 0:\n                raise ValueError(\"non-spam count would go negative!\")\n            self.nham -= 1\n\n        for word in set(wordstream):\n            record = self._wordinfoget(word)\n            if record is not None:\n                if is_spam:\n                    if record.spamcount > 0:\n                        record.spamcount -= 1\n                else:\n                    if record.hamcount > 0:\n                        record.hamcount -= 1\n                if record.hamcount == 0 == record.spamcount:\n                    self._wordinfodel(word)\n                else:\n                    self._wordinfoset(word, record)\n\n        self._post_training()\n\n    def _post_training(self):\n        \"\"\"This is called after training on a wordstream.  Subclasses might\n        want to ensure that their databases are in a consistent state at\n        this point.  Introduced to fix bug #797890.\"\"\"\n        pass\n\n    # Return list of (prob, word, record) triples, sorted by increasing\n    # prob.  \"word\" is a token from wordstream; \"prob\" is its spamprob (a\n    # float in 0.0 through 1.0); and \"record\" is word's associated\n    # WordInfo record if word is in the training database, or None if it's\n    # not.  No more than max_discriminators items are returned, and have\n    # the strongest (farthest from 0.5) spamprobs of all tokens in wordstream.\n    # Tokens with spamprobs less than minimum_prob_strength away from 0.5\n    # aren't returned.\n    def _getclues(self, wordstream):\n        mindist = options[\"Classifier\", \"minimum_prob_strength\"]\n\n        if options[\"Classifier\", \"use_bigrams\"]:\n            # This scheme mixes single tokens with pairs of adjacent tokens.\n            # wordstream is \"tiled\" into non-overlapping unigrams and\n            # bigrams.  Non-overlap is important to prevent a single original\n            # token from contributing to more than one spamprob returned\n            # (systematic correlation probably isn't a good thing).\n\n            # First fill list raw with\n            #     (distance, prob, word, record), indices\n            # pairs, one for each unigram and bigram in wordstream.\n            # indices is a tuple containing the indices (0-based relative to\n            # the start of wordstream) of the tokens that went into word.\n            # indices is a 1-tuple for an original token, and a 2-tuple for\n            # a synthesized bigram token.  The indices are needed to detect\n            # overlap later.\n            raw = []\n            push = raw.append\n            pair = None\n            # Keep track of which tokens we've already seen.\n            # Don't use a set here!  This is an innermost loop, so speed is\n            # important here (direct dict fiddling is much quicker than\n            # invoking Python-level set methods; in Python 2.4 that will\n            # change).\n            seen = {pair: 1} # so the bigram token is skipped on 1st loop trip\n            for i, token in enumerate(wordstream):\n                if i:   # not the 1st loop trip, so there is a preceding token\n                    # This string interpolation must match the one in\n                    # _enhance_wordstream().\n                    pair = \"bi:%s %s\" % (last_token, token)\n                last_token = token\n                for clue, indices in (token, (i,)), (pair, (i-1, i)):\n                    if clue not in seen:    # as always, skip duplicates\n                        seen[clue] = 1\n                        tup = self._worddistanceget(clue)\n                        if tup[0] >= mindist:\n                            push((tup, indices))\n\n            # Sort raw, strongest to weakest spamprob.\n            raw.sort()\n            raw.reverse()\n            # Fill clues with the strongest non-overlapping clues.\n            clues = []\n            push = clues.append\n            # Keep track of which indices have already contributed to a\n            # clue in clues.\n            seen = {}\n            for tup, indices in raw:\n                overlap = [i for i in indices if i in seen]\n                if not overlap: # no overlap with anything already in clues\n                    for i in indices:\n                        seen[i] = 1\n                    push(tup)\n            # Leave sorted from smallest to largest spamprob.\n            clues.reverse()\n\n        else:\n            # The all-unigram scheme just scores the tokens as-is.  A set()\n            # is used to weed out duplicates at high speed.\n            clues = []\n            push = clues.append\n            for word in set(wordstream):\n                tup = self._worddistanceget(word)\n                if tup[0] >= mindist:\n                    push(tup)\n            clues.sort()\n\n        if len(clues) > options[\"Classifier\", \"max_discriminators\"]:\n            del clues[0 : -options[\"Classifier\", \"max_discriminators\"]]\n        # Return (prob, word, record).\n        return [t[1:] for t in clues]\n\n    def _worddistanceget(self, word):\n        record = self._wordinfoget(word)\n        if record is None:\n            prob = options[\"Classifier\", \"unknown_word_prob\"]\n        else:\n            prob = self.probability(record)\n        distance = abs(prob - 0.5)\n        return distance, prob, word, record\n\n    def _wordinfoget(self, word):\n        return self.wordinfo.get(word)\n\n    def _wordinfoset(self, word, record):\n        self.wordinfo[word] = record\n\n    def _wordinfodel(self, word):\n        del self.wordinfo[word]\n\n    def _enhance_wordstream(self, wordstream):\n        \"\"\"Add bigrams to the wordstream.\n\n        For example, a b c -> a b \"a b\" c \"b c\"\n\n        Note that these are *token* bigrams, and not *word* bigrams - i.e.\n        'synthetic' tokens get bigram'ed, too.\n\n        The bigram token is simply \"bi:unigram1 unigram2\" - a space should\n        be sufficient as a separator, since spaces aren't in any other\n        tokens, apart from 'synthetic' ones.  The \"bi:\" prefix is added\n        to avoid conflict with tokens we generate (like \"subject: word\",\n        which could be \"word\" in a subject, or a bigram of \"subject:\" and\n        \"word\").\n\n        If the \"Classifier\":\"use_bigrams\" option is removed, this function\n        can be removed, too.\n        \"\"\"\n\n        last = None\n        for token in wordstream:\n            yield token\n            if last:\n                # This string interpolation must match the one in\n                # _getclues().\n                yield \"bi:%s %s\" % (last, token)\n            last = token\n\n    def _wordinfokeys(self):\n        return self.wordinfo.keys()\n\n\nBayes = Classifier\n"
  },
  {
    "path": "mailpile/spambayes/safepickle.py",
    "content": "\"\"\"Lock pickle files for reading and writing.\"\"\"\n\nfrom __future__ import print_function\nimport sys\nimport os\nfrom six.moves import cPickle as pickle\n\nimport fasteners\n\nfrom mailpile.spambayes.Options import options\n\ndef pickle_read(filename):\n    \"\"\"Read pickle file contents with a lock.\"\"\"\n    lock = fastener.InterProcessLock(filename)\n    lock.acquire(timeout=20)\n    try:\n        return pickle.load(open(filename, 'rb'))\n    finally:\n        lock.release()\n\ndef pickle_write(filename, value, protocol=0):\n    '''Store value as a pickle without creating corruption'''\n\n    lock = fastener.InterProcessLock(filename)\n    lock.acquire(timeout=20)\n\n    try:\n        # Be as defensive as possible.  Always keep a safe copy.\n        tmp = filename + '.tmp'\n        fp = None\n        try: \n            fp = open(tmp, 'wb') \n            pickle.dump(value, fp, protocol) \n            fp.close() \n        except IOError as e:\n            if options[\"globals\", \"verbose\"]: \n                print('Failed update: %s' % str(e), file=sys.stderr)\n            if fp is not None: \n                os.remove(tmp) \n            raise\n        try:\n            # With *nix we can just rename, and (as long as permissions\n            # are correct) the old file will vanish.  With win32, this\n            # won't work - the Python help says that there may not be\n            # a way to do an atomic replace, so we rename the old one,\n            # put the new one there, and then delete the old one.  If\n            # something goes wrong, there is at least a copy of the old\n            # one.\n            os.rename(tmp, filename)\n        except OSError:\n            os.rename(filename, filename + '.bak')\n            os.rename(tmp, filename)\n            os.remove(filename + '.bak')\n    finally:\n        lock.release()\n\n"
  },
  {
    "path": "mailpile/tests/TESTS",
    "content": "Tests that need to be written:\n\n  - Search tests\n\t- Performance test [DONE?]\n\t- from:, to:, etc..\n\t- Keywords\n  - Indexing tests\n\t- Make sure indexing works [DONE]\n\t- Feed in badly formatted mail\n\t- Different text encodings\n  - Tagging tests\n\t- Add tag\n\t- Tag mail\n\t- Untag mail\n  - Filtering tests\n\t- Add\n\t- Filter\n\t- Delete\n  - HTTP server\n\t- ...\n  - ...\n"
  },
  {
    "path": "mailpile/tests/__init__.py",
    "content": "import contextlib\nimport os\nimport random\nimport shutil\nimport stat\nimport sys\nimport unittest\nfrom cStringIO import StringIO\n\n# Mailpile core\nimport mailpile\nimport mailpile.util\nimport mailpile.config.manager\nimport mailpile.config.defaults\nfrom mailpile.plugins.tags import AddTag, Filter\nfrom mailpile.crypto.gpgi import GNUPG_HOMEDIR\nfrom mailpile.ui import SilentInteraction\nfrom mailpile.vcard import AddressInfo\n\n# Pull in all the standard plugins, plus the demos.\nfrom mailpile.mailboxes import *\nfrom mailpile.plugins import *\n\nMP = None\nMY_FROM = 'team+testing@mailpile.is'\nMY_NAME = 'Mailpile Team'\n\n\ndef get_mailpile_root():\n    return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))\n\nTAGS = {\n    'New': {\n        'type': 'unread',\n        'label': False,\n        'display': 'invisible'\n    },\n    'Inbox': {\n        'type': 'inbox',\n        'display': 'priority',\n        'display_order': 2,\n    }\n}\n\n\ndef _initialize_mailpile_for_testing(workdir, test_data):\n    config = mailpile.config.manager.ConfigManager(\n        workdir=workdir,\n        rules=mailpile.config.defaults.CONFIG_RULES)\n    session = mailpile.ui.Session(config)\n    session.config.load(session)\n    session.main = True\n    ui = session.ui = SilentInteraction(config)\n\n    mailpile.util.TESTING = True\n    config.sys.http_port = random.randint(33500, 34000)\n#   config.sys.debug = 'log'\n\n    mp = mailpile.Mailpile(session=session)\n    session.config.plugins.load('demos')\n    mp.set('prefs.index_encrypted=true')\n\n    # Add some mail, scan it.\n    # Create local mailboxes\n    session.config.open_local_mailbox(session)\n    for t in TAGS:\n        AddTag(session, arg=[t]).run(save=False)\n        session.config.get_tag(t).update(TAGS[t])\n\n    mp.add(test_data)\n    mp.rescan('mailboxes')\n    mp.profiles_add(MY_FROM, '=', MY_NAME)\n\n    return mp, session, config, ui\n\n\ndef get_shared_mailpile():\n    global MP\n    if MP is not None:\n        return MP\n\n    sys.stderr.write('Preparing shared Mailpile test environment, '\n                     'please wait. 8-)\\n')\n\n    rootdir = get_mailpile_root()\n    datadir = os.path.join(rootdir, 'mailpile', 'tests', 'data')\n    gpgdir = os.path.join(datadir, 'gpg-keyring')\n    tmpdir = os.path.join(datadir, 'tmp')\n    test_data = os.path.join(datadir, 'Maildir')\n\n    # force usage of test keyring whenever the test mailpile instance is used\n    os.chmod(gpgdir, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR)\n    global GNUPG_HOMEDIR\n    GNUPG_HOMEDIR = gpgdir\n\n    if os.path.exists(tmpdir):\n        shutil.rmtree(tmpdir)\n    if not os.path.exists(os.path.join(test_data, \"new\")):\n        os.mkdir(os.path.join(test_data, \"new\"))\n\n    MP = _initialize_mailpile_for_testing(tmpdir, test_data)\n    return MP\n\n\n@contextlib.contextmanager\ndef capture():\n    oldout, olderr = sys.stdout, sys.stderr\n    try:\n        out = [StringIO(), StringIO()]\n        sys.stdout, sys.stderr = out\n        yield out\n    finally:\n        sys.stdout, sys.stderr = oldout, olderr\n        out[0] = out[0].getvalue()\n        out[1] = out[1].getvalue()\n\n\nclass MailPileUnittest(unittest.TestCase):\n    def __init__(self, *args, **kwargs):\n        unittest.TestCase.__init__(self, *args, **kwargs)\n\n    @classmethod\n    def setUpClass(cls):\n        (cls.mp, cls.session, cls.config, cls.ui) = get_shared_mailpile()\n"
  },
  {
    "path": "mailpile/tests/data/Maildir/cur/.gitkeep",
    "content": ""
  },
  {
    "path": "mailpile/tests/data/Maildir/cur/1379857166.25979_1.hottie,2,S",
    "content": "Return-path: <b0964ab49cbbre=example.com@bounce.twitter.com>\nEnvelope-to: bre@localhost\nDelivery-date: Mon, 16 Sep 2013 14:55:12 +0000\nReceived: from localhost ([::1] helo=hottie)\n\tby hottie with esmtp (Exim 4.80)\n\t(envelope-from <b0964ab49cbbre=example.com@bounce.twitter.com>)\n\tid 1VLaCl-0000WS-KA\n\tfor bre@localhost; Mon, 16 Sep 2013 14:55:11 +0000\nDelivered-To: fake@example.com\nReceived: from gmail-pop.l.google.com [173.194.71.109]\n\tby hottie with POP3 (fetchmail-6.3.21)\n\tfor <bre@localhost> (single-drop); Mon, 16 Sep 2013 14:55:11 +0000 (GMT)\nReceived: by 10.58.13.104 with SMTP id g8csp68965vec;\n        Sun, 15 Sep 2013 20:01:58 -0700 (PDT)\nX-Received: by 10.58.77.65 with SMTP id q1mr6601188vew.8.1379300516868;\n        Sun, 15 Sep 2013 20:01:56 -0700 (PDT)\nDomainKey-Status: good\nReceived-SPF: pass (google.com: domain of b0964ab49cbbre=example.com@bounce.twitter.com designates 199.59.150.84 as permitted sender) client-ip=199.59.150.84;\nReceived: by 10.220.239.138 with POP3 id kw10mf4205774vcb.22;\n        Sun, 15 Sep 2013 20:01:56 -0700 (PDT)\nX-Gmail-Fetch-Info: fake@example.com 1 example.com 110 bre\nReceived: from spruce-goose-ao.twitter.com (spruce-goose-ao.twitter.com [199.59.150.84])\n\tby example.com (8.12.11.20060308/8.12.11) with ESMTP id r8G2JRqt022101\n\tfor <fake@example.com>; Mon, 16 Sep 2013 02:19:28 GMT\nDKIM-Signature: v=1; a=rsa-sha1; d=twitter.com; s=dkim-201303; c=relaxed/relaxed;\n\tq=dns/txt; i=@twitter.com; t=1379297966;\n\th=From:Subject:Date:To;\n\tbh=faruDww5fzZr12D2Xxf371QQxhk=;\n\tb=e7IphNWuRGTsZk0MSsNTcvFOxm2yEJC2jI7xtPhImrYsvrC4cb4v9DF0POiNod7V\n\tTee4u1zETKQPw+EZuyHhDeLEuPqsBkWZIVSYSa3wb9a3yZg1FHsLHAF2gJ356zWA\n\tNhA/QqxoKVPiWci+N+WStCbnxYaBOQ43UwlKIfvI7n0=;\nX-MSFBL: YnJlQGtsYWtpLm5ldEBzbWYxLWJmbi0yNC1zcjEtRXZlcnl0aGluZy4xODRARXZl\n\tcnl0aGluZ0A=\nDate: Mon, 16 Sep 2013 02:19:26 +0000\nFrom: =?iso-8859-1?Q?_Test=E9_?= <redacted�_redacted@example.com>,\nTo: =?iso-8859-1?Q?_Test=E9_?= <redacted@example.com>, <redacted�_redacted@example.com>,\nSubject: Bjarni R. Einarsson, you have new followers on Twitter!\nMIME-Version: 1.0\nContent-Type: multipart/alternative; \n\tboundary=\"----=_Part_20901379_212901501.1379297966645\"\nPrecedence: Bulk\nMessage-ID: <C8.E0.59218.EAA66325@spruce-goose.twitter.com>\nX-BRE-Whitelisted: procmail (info@twitter.com)\nContent-Length: 30265\nLines: 702\n\n------=_Part_20901379_212901501.1379297966645\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 7bit\n\nBjarni R. Einarsson,\nYou have new followers on Twitter!\n\n------------------------\n\nMikael Strandlund @MikaelMploy\nWorking @Spotify\nhttps://twitter.com/MikaelMploy\n\nReport for spam: https://twitter.com/i/redirect?url=https%3A%2F%2Ftwitter.com%2Fuser_spam_reports%2FHerraBRE%2Freport%2FMikaelMploy%3Ft%3D1%26sig%3D1e04a87c05b76c9b33635df28b34ef4e159a4426%26iid%3D5508071c-6e78-4544-8537-381ca8ef1e19%26uid%3D796789%26accused%3DMikaelMploy%26nid%3D10%2B465%2B20130916&sig=e80882fe1099de10eaf7c9de6a8fd9a164cc94da&uid=796789&iid=5508071c-6e78-4544-8537-381ca8ef1e19&nid=10+465+20130916&t=1\n\n\nFrancisco Gama @fcogama\nHive mind at @SocialSynaptics\nhttps://twitter.com/fcogama\n\nReport for spam: https://twitter.com/i/redirect?url=https%3A%2F%2Ftwitter.com%2Fuser_spam_reports%2FHerraBRE%2Freport%2Ffcogama%3Ft%3D1%26sig%3Dfbf7271d133c7a46dbc7c756123372db297ab168%26iid%3D5508071c-6e78-4544-8537-381ca8ef1e19%26uid%3D796789%26accused%3Dfcogama%26nid%3D10%2B466%2B20130916&sig=c290664f8bceae9809ac319b3a59137d7455d950&uid=796789&iid=5508071c-6e78-4544-8537-381ca8ef1e19&nid=10+466+20130916&t=1\n\n\nSee all your followers:\nhttps://twitter.com/HerraBRE/followers\n\n-- \n\nForgot your Twitter password? Get instructions on how to reset it:\nhttps://twitter.com/account/resend_password\n\nYou can also unsubscribe from these emails or change your notification settings:\nhttps://twitter.com/i/u?t=1&sig=f9556f720643c6906a7850bf363f6a158f4e2cd9&iid=5508071c-6e78-4544-8537-381ca8ef1e19&uid=796789&nid=10+26+20130916\nhttps://twitter.com/settings/notifications\n\nNeed help?\nhttps://support.twitter.com\n\nIf you received this message in error and did not sign up for a Twitter account, click on the url below:\nhttps://twitter.com/account/not_my_account/HerraBRE/BEGF4-BA35A-137929\n------=_Part_20901379_212901501.1379297966645\nContent-Type: text/html; charset=UTF-8\nContent-Transfer-Encoding: quoted-printable\n\n<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/htm=\nl4/strict.dtd\">\n<html>\n<head>\n<meta http-equiv=3D\"Content-Type\" content=3D\"text/html; charset=3Dutf-8\" />\n<meta name=3D\"viewport\" content=3D\"width=3Ddevice-width, minimum-scale=3D1.=\n0, maximum-scale=3D1.0, user-scalable=3D0\" />\n<meta name=3D\"apple-mobile-web-app-capable\" content=3D\"yes\" />\n<style type=3D\"text/css\">\n@media only screen and (max-device-width: 480px) {\ntable[class=3Douter] .global-width-670-to-320 {\nwidth: 320px !important;\n}\ntable[class=3Douter] .global-width-520-to-320 {\nwidth: 320px !important;\n}\ntable[class=3Douter] .global-width-500-to-300 {\nwidth: 300px !important;\n}\ntable[class=3Douter] .global-separator-padding {\nheight: 8px !important;\n}\ntable[class=3Douter] .global-shrinking-to-0 {\nheight: 0 !important;\n}\ntable[class=3Douter] .global-shrinking-to-10 {\nheight: 10px !important;\n}\ntable[class=3Douter] .global-h1 {\nfont-size: 14px !important;\n}\n\ntable[class=3Douter] .cut {\nwidth: 0 !important;\npadding: 0 !important;\n}\ntable[class=3Douter] .vcut {\nheight: 0 !important;\npadding: 0 !important;\n}\ntable[class=3Douter] img.cut {\ndisplay: none !important;\nwidth: 0 !important;\nheight: 0 !important;\n}\ntable[class=3Douter] .cut span, table[class=3Douter] .cut a {\ndisplay: none !important;\n}\ntable[class=3Douter] .frame {\nwidth: 320px !important;\nborder-left: 0 !important;\nborder-right: 0 !important;\n}\ntable[class=3Douter] .main_header.media_header {\nwidth: 300px !important;\nheight: 55px !important;\n}\ntable[class=3Douter] .header_left {\nheight: 55px !important;\n}\ntable[class=3Douter] .logo_header {\nheight: 62px !important;\n}\ntable[class=3Douter] .main_name {\nfont-size: 12px !important;\n}\ntable[class=3Douter] .subtitle {\nfont-size: 12px !important;\n}\ntable[class=3Douter] .media_main {\nwidth: 300px !important;\n}\ntable[class=3Douter] .media_main .intro {\nwidth: 260px !important;\nfont-size: 14px !important;\n}\ntable[class=3Douter] .media_main .intro2 {\nfont-size: 14px !important;\n}\ntable[class=3Douter] .media_main .suggestions {\nfont-size: 14px !important;\n}\ntable[class=3Douter] .media_more {\nwidth: 300px !important;\nborder-radius: 0 !important;\nbackground: #fff !important;\nborder-top: 1px solid #e8e8e8 !important;\n}\ntable[class=3Douter] .media_button {\nwidth: 300px !important;\nbackground-color: #209ae4 !important;\npadding-top: 3px !important;\npadding-bottom: 3px !important;\n}\ntable[class=3Douter] .media_footer {\nfont-size: 11px !important;\nline-height: 14px !important;\npadding: 0 10px !important;\n}\ntable[class=3Douter] .address {\nfont-size: 11px !important;\nline-height: 14px !important;\ndisplay: block !important;\n}\ntable[class=3Douter] td[class=3Dfooter-padding-top] {\nheight: 12px !important;\n}\ntable[class=3Douter] td[class=3Dfooter-padding-bottom] {\nheight: 17px !important;\n}\ntable[class=3Douter] .media_footer br {\ndisplay: none !important;\n}\ntable[class=3Douter] .spacer.ios {\ndisplay: none !important;\n}\ntable[class=3Douter] .reset {\ndisplay: block !important;\npadding-bottom: 4px !important;\n}\ntable[class=3Douter] .media_logo_div {\ndisplay: block !important;\nposition: absolute !important;\nleft: 274px !important;\ntop: 0 !important;\nbackground-image: url('https://ea.twimg.com/email/t1/ribbon.png') !importan=\nt;\nbackground-size: 100% 100% !important;\nwidth: 36px !important;\nheight: 68px !important;\nz-index: 1 !important;\n}\ntable[class=3Demployee-only-padding-top-bottom] {\nwidth: 300px !important;\n}\ntable[class=3Demployee-only] {\nwidth: 300px !important;\npadding: 10px 0;\n}\ntd[class=3Dheader_padding] {\nheight: 55px !important;\n}\n}\n\n@media only screen and (min-device-width: 768px) and (max-device-width: 102=\n4px) {\ntable[class=3Douter] .media_button {\npadding-top: 3px !important;\npadding-bottom: 3px !important;\nbackground-color: #209ae4 !important;\n}\ntable[class=3Douter] .media_button td.cut {\nwidth: 0 !important;\npadding: 0 !important;\n}\ntable[class=3Douter] .media_button {\npadding-left: 10px !important;\npadding-right: 10px !important;\n}\n}\n</style>\n</head>\n<body style=3D\"margin: 0; padding: 0; background: #fff;-webkit-text-size-ad=\njust:100%;\">\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" width=3D\"100%\" clas=\ns=3D\"outer\" style=3D\"position:relative;background:#ddd;\">\n<tbody>\n<tr>\n<td>\n<table class=3D\"inner frame\" align=3D\"center\" cellpadding=3D\"0\" cellspacing=\n=3D\"0\" border=3D\"0\" width=3D\"670\" style=3D\"background:#fff;position:relativ=\ne;border:0;border-left:1px solid #ccc;border-right:1px solid #ccc;\">\n<tbody>\n<tr>\n<td> <a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.=\ncom%3Frefsrc%3Demail&amp;sig=3D76add118f9dfd33f63864ce34d9372e483cfba4a&amp=\n;uid=3D796789&amp;iid=3D5508071c-6e78-4544-8537-381ca8ef1e19&amp;nid=3D10+2=\n1+20130916&amp;t=3D1\" style=3D\"border:none;color:#0084b4;text-decoration:no=\nne;\"><span class=3D\"media_logo_div\"></span></a>\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" width=3D\"670\" class=\n=3D\"header frame\" style=3D\"background:#f2f2f2;table-layout:fixed;\">\n<tbody>\n<tr>\n<td class=3D\"header_left cut\" style=3D\"width:19px;height:77px;\"> &nbsp; </t=\nd>\n<td height=3D\"94\" width=3D\"46\" valign=3D\"top\" rowspan=3D\"2\" class=3D\"logo_h=\neader cut\" style=3D\"background:#fff;line-height:100%;\"><a href=3D\"https://t=\nwitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%3Frefsrc%3Demail&amp;=\nsig=3D76add118f9dfd33f63864ce34d9372e483cfba4a&amp;uid=3D796789&amp;iid=3D5=\n508071c-6e78-4544-8537-381ca8ef1e19&amp;nid=3D10+21+20130916&amp;t=3D1\" sty=\nle=3D\"border:none;color:#0084b4;text-decoration:none;\"><img class=3D\"logo c=\nut\" src=3D\"https://ea.twimg.com/email/t1/ribbon.png\" width=3D\"46\" height=3D=\n\"94\" style=3D\"border:0;line-height:100%;border:0;\" /></a></td>\n<td class=3D\"cut\" width=3D\"9\"> &nbsp; </td>\n<td width=3D\"10\" height=3D\"77\" class=3D\"header_padding\"> &nbsp; </td>\n<td width=3D\"458\" height=3D\"77\" class=3D\"main_header media_header\" style=3D=\n\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;color:#333;\">\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\">\n<tbody>\n<tr>\n<td class=3D\"main_name\" style=3D\"font-size:14px;font-weight:bold;color:#000=\n;\"> <span dir=3D\"ltr\">Bjarni R. Einarsson,</span> </td>\n</tr>\n<tr>\n<td class=3D\"subtitle\" style=3D\"font-size:14px;color:#666;\"> You have new f=\nollowers on Twitter! </td>\n</tr>\n</tbody>\n</table> </td>\n<td width=3D\"10\" height=3D\"77\" class=3D\"header_padding\"> &nbsp; </td>\n<td class=3D\"main_avatar cut\" width=3D\"32\" style=3D\"text-align:right;\"> <a =\nhref=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2FHe=\nrraBRE%3Frefsrc%3Demail&amp;sig=3De63e99778c0859b62c6cc0df2d3f9b7187011e39&=\namp;uid=3D796789&amp;iid=3D5508071c-6e78-4544-8537-381ca8ef1e19&amp;nid=3D1=\n0+22+20130916&amp;t=3D1\" style=3D\"border:none;color:#0084b4;text-decoration=\n:none;\"><img class=3D\"cut\" src=3D\"https://si0.twimg.com/profile_images/1260=\n879678/Bjarni-tie-done_reasonably_small.jpg\" width=3D\"32\" height=3D\"32\" alt=\n=3D\"Bjarni R. Einarsson\" style=3D\"background:#fff;border-radius:5px;border:=\n0;\" /></a> </td>\n<td class=3D\"col cut\" style=3D\"width:85px;\"></td>\n</tr>\n<tr>\n<td class=3D\"main header_drop cut\" style=3D\"background:#fff;border-top:1px =\nsolid #ddd;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\">&nb=\nsp;</td>\n<td class=3D\"main header_drop cut\" style=3D\"background:#fff;border-top:1px =\nsolid #ddd;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\">&nb=\nsp;</td>\n<td class=3D\"main header_drop media_header\" height=3D\"17\" style=3D\"backgrou=\nnd:#fff;border-top:1px solid #ddd;font-family:'Helvetica Neue', Helvetica, =\nArial, sans-serif;\"><img width=3D\"1\" height=3D\"1\" style=3D\"display: block;b=\norder:0;\" src=3D\"https://twitter.com/scribe/ibis?uid=3D796789&amp;iid=3D550=\n8071c-6e78-4544-8537-381ca8ef1e19&amp;nid=3D10+20+20130916&amp;t=3D1\" /></t=\nd>\n<td class=3D\"main header_drop cut\" height=3D\"17\" colspan=3D\"4\" style=3D\"bac=\nkground:#fff;border-top:1px solid #ddd;font-family:'Helvetica Neue', Helvet=\nica, Arial, sans-serif;\">&nbsp;</td>\n</tr>\n</tbody>\n</table> </td>\n<td rowspan=3D\"3\"></td>\n</tr>\n<tr>\n<td class=3D\"content\" style=3D\"background:#fff;\">  <style type=3D\"text/css\"=\n>\n@media only screen and (max-device-width: 480px) {\ntable[class=3Douter] .fd_avatar img {\nwidth: 48px !important;\nheight: 48px !important;\n}\ntable[class=3Douter] .fd_avatar {\nwidth: 58px !important;\n}\ntable[class=3Douter] .fd_button, table[class=3Douter] .following {\npadding-top: 3px !important;\npadding-bottom: 3px !important\n}\n}\n\n@media only screen and (min-device-width: 768px) and (max-device-width: 102=\n4px) {\ntable[class=3Douter] .fd_button, table[class=3Douter] .following {\npadding-top: 3px !important;\npadding-bottom: 3px !important;\n}\n}\n</style>\n<table width=3D\"670\" border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" class=\n=3D\"main frame\" style=3D\"background:#fff;font-family:'Helvetica Neue', Helv=\netica, Arial, sans-serif;\">\n<tbody>\n<tr>\n<td class=3D\"col cut\" style=3D\"width:85px;\"></td>\n<td class=3D\"media_main\" colspan=3D\"2\">\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\">\n<tbody>\n<tr class=3D\"padder\">\n<td class=3D\"vcut\" height=3D\"5px;\"></td>\n</tr>\n<tr>\n<td class=3D\"mid top_border mid_more\" style=3D\"padding:10px;font-family:'He=\nlvetica Neue', Helvetica, Arial, sans-serif;color:#333;border-top:1px solid=\n #e8e8e8;padding-top:15px;padding-bottom:15px;border-top:none;\">\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" class=3D\"media_main=\n entry\" width=3D\"500\" style=3D\"table-layout:fixed;\">\n<tbody>\n<tr>\n<td rowspan=3D\"4\" width=3D\"138\" class=3D\"fd_avatar\" valign=3D\"top\" style=3D=\n\"padding-right:10px;line-height:0;\"><a href=3D\"https://twitter.com/i/redire=\nct?url=3Dhttps%3A%2F%2Ftwitter.com%2FMikaelMploy%3Frefsrc%3Demail&amp;sig=\n=3Dc8c212bf3e820119a15dee69c6173e12889fad53&amp;uid=3D796789&amp;iid=3D5508=\n071c-6e78-4544-8537-381ca8ef1e19&amp;nid=3D10+1100+20130916&amp;t=3D1\" styl=\ne=3D\"border:none;color:#0084b4;text-decoration:none;\"><img src=3D\"https://s=\ni0.twimg.com/profile_images/378800000325657217/0486dd833f7c32bb8fe602b87454=\n5eb3_reasonably_small.jpeg\" width=3D\"128\" height=3D\"128\" style=3D\"border:0;=\nbackground-color:#f2f2f2;border-radius:5px;\" /></a></td>\n<td colspan=3D\"2\" valign=3D\"top\" class=3D\"name_handler\" dir=3D\"ltr\" style=\n=3D\"line-height:12px;\"><a href=3D\"https://twitter.com/i/redirect?url=3Dhttp=\ns%3A%2F%2Ftwitter.com%2FMikaelMploy%3Frefsrc%3Demail&amp;sig=3Dc8c212bf3e82=\n0119a15dee69c6173e12889fad53&amp;uid=3D796789&amp;iid=3D5508071c-6e78-4544-=\n8537-381ca8ef1e19&amp;nid=3D10+1120+20130916&amp;t=3D1\" style=3D\"border:non=\ne;color:#0084b4;text-decoration:none;\"><span class=3D\"name\" style=3D\"color:=\n#333333;font-size:14px;font-weight:bold;line-height:100%;\">Mikael Strandlun=\nd</span></a> <a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2F=\ntwitter.com%2FMikaelMploy%3Frefsrc%3Demail&amp;sig=3Dc8c212bf3e820119a15dee=\n69c6173e12889fad53&amp;uid=3D796789&amp;iid=3D5508071c-6e78-4544-8537-381ca=\n8ef1e19&amp;nid=3D10+1110+20130916&amp;t=3D1\" style=3D\"border:none;color:#0=\n084b4;text-decoration:none;\"><span class=3D\"handle screen-name\" style=3D\"di=\nrection:ltr;unicode-bidi:embed;font-size:12px;color:#777777;\">@MikaelMploy<=\n/span></a></td>\n</tr>\n<tr>\n<td colspan=3D\"2\" class=3D\"bio\" dir=3D\"ltr\" style=3D\"padding-top:2px;paddin=\ng-bottom:2px;font-size:14px;line-height:18px;font-style:italic;font-family:=\n'Georgia', 'Helvetica Neue', Helvetica, Arial, sans-serif;color:#777;\">Work=\ning <a class=3D\"screen-name\" href=3D\"https://twitter.com/i/redirect?url=3Dh=\nttp%3A%2F%2Ftwitter.com%2FSpotify%3Frefsrc%3Demail&amp;sig=3D20f40fe5efb557=\n5f7154690bf32d8886ecb36fb9&amp;uid=3D796789&amp;iid=3D5508071c-6e78-4544-85=\n37-381ca8ef1e19&amp;nid=3D10+925+20130916&amp;t=3D1\" style=3D\"direction:ltr=\n;unicode-bidi:embed;border:none;color:#0084b4;text-decoration:none;color:#7=\n77;\">@Spotify</a></td>\n</tr>\n<tr>\n<td colspan=3D\"2\" class=3D\"followed_by\" valign=3D\"top\" style=3D\"font-size:1=\n2px;line-height:18px;color:#333333;font-family:'Helvetica Neue', Helvetica,=\n Arial, sans-serif;\"> Followed by <a href=3D\"https://twitter.com/i/redirect=\n?url=3Dhttps%3A%2F%2Ftwitter.com%2FLundberg_J%3Frefsrc%3Demail&amp;sig=3D8b=\n38926b86e0783f2bbcbf20d1e8f5127b122af0&amp;uid=3D796789&amp;iid=3D5508071c-=\n6e78-4544-8537-381ca8ef1e19&amp;nid=3D10+1009+20130916&amp;t=3D1\" style=3D\"=\nborder:none;color:#0084b4;text-decoration:none;\">Johan Lundberg</a>.<br /> =\nFollowing: <a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftw=\nitter.com%2FMikaelMploy%2Ffollowing%3Frefsrc%3Demail&amp;sig=3D0466956f4aca=\n0b8e86d259c927a1416564ed2da5&amp;uid=3D796789&amp;iid=3D5508071c-6e78-4544-=\n8537-381ca8ef1e19&amp;nid=3D10+1150+20130916&amp;t=3D1\" style=3D\"border:non=\ne;color:#0084b4;text-decoration:none;\">1403</a> &middot; Followers: <a href=\n=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2FMikael=\nMploy%2Ffollowers%3Frefsrc%3Demail&amp;sig=3D74dad0af817f3e09428e746d99c979=\n85db1b9890&amp;uid=3D796789&amp;iid=3D5508071c-6e78-4544-8537-381ca8ef1e19&=\namp;nid=3D10+1140+20130916&amp;t=3D1\" style=3D\"border:none;color:#0084b4;te=\nxt-decoration:none;\">699</a> </td>\n</tr>\n<tr>\n<td class=3D\"fd_follow\" valign=3D\"bottom\" style=3D\"padding-top:10px;\">\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\">\n<tbody>\n<tr>\n<td>\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" background=3D\"https=\n://ea.twimg.com/email/t1/grey_background.png\" class=3D\"fd_button\" style=3D\"=\nbackground-color:#eeeeee;border-radius:5px;border:1px solid #cccccc;padding=\n:5px 0 5px 0;text-align:center;height:28px;width:1px;\">\n<tbody>\n<tr>\n<td valign=3D\"middle\" class=3D\"button_icon\" style=3D\"padding-right:4px;padd=\ning-left:10px;line-height:0;color:#333333;font-size:13px;font-weight:bold;f=\nont-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\"><a class=3D\"but=\nton_link\" href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitte=\nr.com%2Fintent%2Ffollow%3Fea_u%3D796789%26ea_e%3D1380507566%26screen_name%3=\nDMikaelMploy%26ea_s%3Dbb9afc41820d9693083691c58525ab0d384129cb%26refsrc%3De=\nmail&amp;sig=3D66cd8038bdaae8e03e70fab2d608e007e970bcf8&amp;uid=3D796789&am=\np;iid=3D5508071c-6e78-4544-8537-381ca8ef1e19&amp;nid=3D10+489+20130916&amp;=\nt=3D1\" style=3D\"border:none;color:#0084b4;text-decoration:none;color:#33333=\n3;font-size:13px;font-weight:bold;font-family:'Helvetica Neue', Helvetica, =\nArial, sans-serif;\"><img width=3D\"18\" height=3D\"14\" src=3D\"https://ea.twimg=\n.com/email/t1/blue_bird.png\" style=3D\"border:0;\" /></a></td>\n<td class=3D\"follow_button\" style=3D\"color:#333333;font-size:13px;font-weig=\nht:bold;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;padding-=\nright:10px;white-space:nowrap;\"><a class=3D\"button_link\" href=3D\"https://tw=\nitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2Fintent%2Ffollow%3Fea=\n_u%3D796789%26ea_e%3D1380507566%26screen_name%3DMikaelMploy%26ea_s%3Dbb9afc=\n41820d9693083691c58525ab0d384129cb%26refsrc%3Demail&amp;sig=3D66cd8038bdaae=\n8e03e70fab2d608e007e970bcf8&amp;uid=3D796789&amp;iid=3D5508071c-6e78-4544-8=\n537-381ca8ef1e19&amp;nid=3D10+489+20130916&amp;t=3D1\" style=3D\"border:none;=\ncolor:#0084b4;text-decoration:none;color:#333333;font-size:13px;font-weight=\n:bold;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\">Follow</=\na></td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n<td valign=3D\"bottom\" align=3D\"right\" width=3D\"100\"> <a class=3D\"report_spa=\nm\" href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2=\nFuser_spam_reports%2FHerraBRE%2Freport%2FMikaelMploy%3Ft%3D1%26sig%3D1e04a8=\n7c05b76c9b33635df28b34ef4e159a4426%26iid%3D5508071c-6e78-4544-8537-381ca8ef=\n1e19%26uid%3D796789%26accused%3DMikaelMploy%26nid%3D10%2B465%2B20130916&amp=\n;sig=3De80882fe1099de10eaf7c9de6a8fd9a164cc94da&amp;uid=3D796789&amp;iid=3D=\n5508071c-6e78-4544-8537-381ca8ef1e19&amp;nid=3D10+465+20130916&amp;t=3D1\" s=\ntyle=3D\"border:none;color:#0084b4;text-decoration:none;font-size:11px;color=\n:#999999;\">Report for spam</a> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n<tr>\n<td class=3D\"mid top_border mid_more\" style=3D\"padding:10px;font-family:'He=\nlvetica Neue', Helvetica, Arial, sans-serif;color:#333;border-top:1px solid=\n #e8e8e8;padding-top:15px;padding-bottom:15px;\">\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" class=3D\"media_main=\n entry\" width=3D\"500\" style=3D\"table-layout:fixed;\">\n<tbody>\n<tr>\n<td rowspan=3D\"4\" width=3D\"138\" class=3D\"fd_avatar\" valign=3D\"top\" style=3D=\n\"padding-right:10px;line-height:0;\"><a href=3D\"https://twitter.com/i/redire=\nct?url=3Dhttps%3A%2F%2Ftwitter.com%2Ffcogama%3Frefsrc%3Demail&amp;sig=3D327=\n50760b024b2d474b19bf54c77447c8d344902&amp;uid=3D796789&amp;iid=3D5508071c-6=\ne78-4544-8537-381ca8ef1e19&amp;nid=3D10+1101+20130916&amp;t=3D1\" style=3D\"b=\norder:none;color:#0084b4;text-decoration:none;\"><img src=3D\"https://si0.twi=\nmg.com/profile_images/378800000073376139/469afc3d115bcc3f0569a2951f69d41c_r=\neasonably_small.jpeg\" width=3D\"128\" height=3D\"128\" style=3D\"border:0;backgr=\nound-color:#f2f2f2;border-radius:5px;\" /></a></td>\n<td colspan=3D\"2\" valign=3D\"top\" class=3D\"name_handler\" dir=3D\"ltr\" style=\n=3D\"line-height:12px;\"><a href=3D\"https://twitter.com/i/redirect?url=3Dhttp=\ns%3A%2F%2Ftwitter.com%2Ffcogama%3Frefsrc%3Demail&amp;sig=3D32750760b024b2d4=\n74b19bf54c77447c8d344902&amp;uid=3D796789&amp;iid=3D5508071c-6e78-4544-8537=\n-381ca8ef1e19&amp;nid=3D10+1121+20130916&amp;t=3D1\" style=3D\"border:none;co=\nlor:#0084b4;text-decoration:none;\"><span class=3D\"name\" style=3D\"color:#333=\n333;font-size:14px;font-weight:bold;line-height:100%;\">Francisco Gama</span=\n></a> <a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter=\n.com%2Ffcogama%3Frefsrc%3Demail&amp;sig=3D32750760b024b2d474b19bf54c77447c8=\nd344902&amp;uid=3D796789&amp;iid=3D5508071c-6e78-4544-8537-381ca8ef1e19&amp=\n;nid=3D10+1111+20130916&amp;t=3D1\" style=3D\"border:none;color:#0084b4;text-=\ndecoration:none;\"><span class=3D\"handle screen-name\" style=3D\"direction:ltr=\n;unicode-bidi:embed;font-size:12px;color:#777777;\">@fcogama</span></a></td>\n</tr>\n<tr>\n<td colspan=3D\"2\" class=3D\"bio\" dir=3D\"ltr\" style=3D\"padding-top:2px;paddin=\ng-bottom:2px;font-size:14px;line-height:18px;font-style:italic;font-family:=\n'Georgia', 'Helvetica Neue', Helvetica, Arial, sans-serif;color:#777;\">Hive=\n mind at <a class=3D\"screen-name\" href=3D\"https://twitter.com/i/redirect?ur=\nl=3Dhttp%3A%2F%2Ftwitter.com%2FSocialSynaptics%3Frefsrc%3Demail&amp;sig=3D9=\nb99d5940f55a0c9e60404760bbed56cb834b985&amp;uid=3D796789&amp;iid=3D5508071c=\n-6e78-4544-8537-381ca8ef1e19&amp;nid=3D10+925+20130916&amp;t=3D1\" style=3D\"=\ndirection:ltr;unicode-bidi:embed;border:none;color:#0084b4;text-decoration:=\nnone;color:#777;\">@SocialSynaptics</a></td>\n</tr>\n<tr>\n<td colspan=3D\"2\" class=3D\"followed_by\" valign=3D\"top\" style=3D\"font-size:1=\n2px;line-height:18px;color:#333333;font-family:'Helvetica Neue', Helvetica,=\n Arial, sans-serif;\"> Following: <a href=3D\"https://twitter.com/i/redirect?=\nurl=3Dhttps%3A%2F%2Ftwitter.com%2Ffcogama%2Ffollowing%3Frefsrc%3Demail&amp;=\nsig=3D56b7a39b1c03a5b8435a2f52d67c04cdaac99862&amp;uid=3D796789&amp;iid=3D5=\n508071c-6e78-4544-8537-381ca8ef1e19&amp;nid=3D10+1151+20130916&amp;t=3D1\" s=\ntyle=3D\"border:none;color:#0084b4;text-decoration:none;\">799</a> &middot; F=\nollowers: <a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwi=\ntter.com%2Ffcogama%2Ffollowers%3Frefsrc%3Demail&amp;sig=3D3e3d2d8ea3a335865=\n9da86a6cc82194b233e829c&amp;uid=3D796789&amp;iid=3D5508071c-6e78-4544-8537-=\n381ca8ef1e19&amp;nid=3D10+1141+20130916&amp;t=3D1\" style=3D\"border:none;col=\nor:#0084b4;text-decoration:none;\">462</a> </td>\n</tr>\n<tr>\n<td class=3D\"fd_follow\" valign=3D\"bottom\" style=3D\"padding-top:10px;\">\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\">\n<tbody>\n<tr>\n<td>\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" background=3D\"https=\n://ea.twimg.com/email/t1/grey_background.png\" class=3D\"fd_button\" style=3D\"=\nbackground-color:#eeeeee;border-radius:5px;border:1px solid #cccccc;padding=\n:5px 0 5px 0;text-align:center;height:28px;width:1px;\">\n<tbody>\n<tr>\n<td valign=3D\"middle\" class=3D\"button_icon\" style=3D\"padding-right:4px;padd=\ning-left:10px;line-height:0;color:#333333;font-size:13px;font-weight:bold;f=\nont-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\"><a class=3D\"but=\nton_link\" href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitte=\nr.com%2Fintent%2Ffollow%3Fea_u%3D796789%26ea_e%3D1380507566%26screen_name%3=\nDfcogama%26ea_s%3D7d6ca8e93a6e2ff7199932bc381cca702683a8dd%26refsrc%3Demail=\n&amp;sig=3D2e9fbe824ff314977bc34ec965f934222a922263&amp;uid=3D796789&amp;ii=\nd=3D5508071c-6e78-4544-8537-381ca8ef1e19&amp;nid=3D10+490+20130916&amp;t=3D=\n1\" style=3D\"border:none;color:#0084b4;text-decoration:none;color:#333333;fo=\nnt-size:13px;font-weight:bold;font-family:'Helvetica Neue', Helvetica, Aria=\nl, sans-serif;\"><img width=3D\"18\" height=3D\"14\" src=3D\"https://ea.twimg.com=\n/email/t1/blue_bird.png\" style=3D\"border:0;\" /></a></td>\n<td class=3D\"follow_button\" style=3D\"color:#333333;font-size:13px;font-weig=\nht:bold;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;padding-=\nright:10px;white-space:nowrap;\"><a class=3D\"button_link\" href=3D\"https://tw=\nitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2Fintent%2Ffollow%3Fea=\n_u%3D796789%26ea_e%3D1380507566%26screen_name%3Dfcogama%26ea_s%3D7d6ca8e93a=\n6e2ff7199932bc381cca702683a8dd%26refsrc%3Demail&amp;sig=3D2e9fbe824ff314977=\nbc34ec965f934222a922263&amp;uid=3D796789&amp;iid=3D5508071c-6e78-4544-8537-=\n381ca8ef1e19&amp;nid=3D10+490+20130916&amp;t=3D1\" style=3D\"border:none;colo=\nr:#0084b4;text-decoration:none;color:#333333;font-size:13px;font-weight:bol=\nd;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\">Follow</a></=\ntd>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n<td valign=3D\"bottom\" align=3D\"right\" width=3D\"100\"> <a class=3D\"report_spa=\nm\" href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2=\nFuser_spam_reports%2FHerraBRE%2Freport%2Ffcogama%3Ft%3D1%26sig%3Dfbf7271d13=\n3c7a46dbc7c756123372db297ab168%26iid%3D5508071c-6e78-4544-8537-381ca8ef1e19=\n%26uid%3D796789%26accused%3Dfcogama%26nid%3D10%2B466%2B20130916&amp;sig=3Dc=\n290664f8bceae9809ac319b3a59137d7455d950&amp;uid=3D796789&amp;iid=3D5508071c=\n-6e78-4544-8537-381ca8ef1e19&amp;nid=3D10+466+20130916&amp;t=3D1\" style=3D\"=\nborder:none;color:#0084b4;text-decoration:none;font-size:11px;color:#999999=\n;\">Report for spam</a> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n<tr>\n<td height=3D\"2\" class=3D\"vcut\"></td>\n</tr>\n<tr>\n<td class=3D\"more media_more\" width=3D\"500\" style=3D\"border-radius:5px;back=\nground-color:#ededed;padding:10px;font-family:'Helvetica Neue', Helvetica, =\nArial, sans-serif;color:#333333;\">\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" width=3D\"100%\">\n<tbody>\n<tr>\n<td class=3D\"cut\"><span class=3D\"suggestions_tip\" style=3D\"color:#666666;fo=\nnt-size:14px;text-shadow:0px 1px 0px #ffffff;font-family:'Helvetica Neue', =\nHelvetica, Arial, sans-serif;\">Check out your followers page for more.</spa=\nn></td>\n<td align=3D\"right\">\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\">\n<tbody>\n<tr>\n<td>  <style type=3D\"text/css\">\n@media only screen and (max-device-width: 480px) {\ntable[class=3Dbig-button-template] {\nwidth: 300px !important;\n}\ntr[class=3Dbig-button-template-vertical-padding] {\nheight: 3px !important;\n}\n}\n@media only screen and (min-device-width: 768px) and (max-device-width: 102=\n4px) {\ntd[class=3Dbig-button-template] {\npadding-left: 10px !important;\npadding-right: 10px !important;\n}\n}\n</style>\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" class=3D\"big-button=\n-template\" style=3D\"background-image:url('https://ea.twimg.com/email/t1/but=\nton_bg_long.png');background-color:#33a9e5;border-radius:5px;border:1px;hei=\nght:28px;border:1px solid #28C;word-wrap:break-word;text-align:center;\">\n<tbody>\n<tr>\n<td align=3D\"center\" style=3D\"padding-left:10px;padding-right:10px;\">\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" class=3D\"big-button=\n-template-inner\">\n<tbody>\n<tr>\n<td height=3D\"28\" class=3D\"big-button-template-font\" style=3D\"color:white;f=\nont-size:13px;font-weight:bold;font-family:'Helvetica Neue', Helvetica, Ari=\nal, sans-serif;text-align:center;padding-left:10px;padding-right:10px;paddi=\nng-left:0px;padding-right:0px;\"> <a href=3D\"https://twitter.com/i/redirect?=\nurl=3Dhttps%3A%2F%2Ftwitter.com%2FHerraBRE%2Ffollowers%3Frefsrc%3Demail&amp=\n;sig=3D8ec2b97945faa1e20725a419334616465aabffd4&amp;uid=3D796789&amp;iid=3D=\n5508071c-6e78-4544-8537-381ca8ef1e19&amp;nid=3D10+96+20130916&amp;t=3D1\" st=\nyle=3D\"border:none;color:#0084b4;text-decoration:none;color:white;\"> See al=\nl your followers </a> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n<tr>\n<td height=3D\"22\" class=3D\"vcut\"></td>\n</tr>\n</tbody>\n</table> </td>\n<td class=3D\"col cut\" style=3D\"width:85px;\"></td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n<tr>\n<td>\n<table bgcolor=3D\"#eeeeee\" border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\"=\n width=3D\"670\" class=3D\"frame footer\" style=3D\"background-color:#eee;backgr=\nound-image:url('https://ea.twimg.com/email/t1/shadow-bottom.jpg');backgroun=\nd-position:top;background-repeat:repeat-x;border-top-color:#ddd;border-top-=\nstyle:solid;border-top-width:1px;\">\n<tbody>\n<tr>\n<td colspan=3D\"4\" height=3D\"16\" class=3D\"footer-padding-top\"></td>\n</tr>\n<tr>\n<td class=3D\"col cut\" style=3D\"width:85px;\"></td>\n<td class=3D\"footer_body media_footer\" style=3D\"font-family:'Helvetica Neue=\n', Helvetica, Arial, sans-serif;font-size:12px;line-height:17px;color:#777;=\ntext-shadow:0 1px 0 #fff;\">\n<div>\nForgot your Twitter password?\n<a class=3D\"reset\" href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F=\n%2Ftwitter.com%2Faccount%2Fresend_password&amp;sig=3Df5d73c6d6c8577b4b95889=\n880e513526930e35d0&amp;uid=3D796789&amp;iid=3D5508071c-6e78-4544-8537-381ca=\n8ef1e19&amp;nid=3D10+24+20130916&amp;t=3D1\" style=3D\"border:none;color:#008=\n4b4;text-decoration:none;\">Get instructions on how to reset it.</a>\n</div>\n<div>\nYou can also\n<a href=3D\"https://twitter.com/i/u?t=3D1&amp;sig=3Df9556f720643c6906a7850bf=\n363f6a158f4e2cd9&amp;iid=3D5508071c-6e78-4544-8537-381ca8ef1e19&amp;uid=3D7=\n96789&amp;nid=3D10+26+20130916\" style=3D\"border:none;color:#0084b4;text-dec=\noration:none;\">unsubscribe from these emails</a>\n<span class=3D\"reset\">or change your <a href=3D\"https://twitter.com/i/redir=\nect?url=3Dhttps%3A%2F%2Ftwitter.com%2Fsettings%2Fnotifications&amp;sig=3Ddf=\ne6fc4e06fdfbeffa510642835ff34af916458c&amp;uid=3D796789&amp;iid=3D5508071c-=\n6e78-4544-8537-381ca8ef1e19&amp;nid=3D10+27+20130916&amp;t=3D1\" style=3D\"bo=\nrder:none;color:#0084b4;text-decoration:none;\">notification settings</a>. N=\need <a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Fsupport.t=\nwitter.com&amp;sig=3Dd5a93ec8dc3f5086b47a0022aac6b1bd71661580&amp;uid=3D796=\n789&amp;iid=3D5508071c-6e78-4544-8537-381ca8ef1e19&amp;nid=3D10+97+20130916=\n&amp;t=3D1\" style=3D\"border:none;color:#0084b4;text-decoration:none;\">help<=\n/a>?</span>\n</div>\n<div class=3D\"reset\">\nIf you received this message in error and did not sign up for Twitter, clic=\nk\n<a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2=\nFaccount%2Fnot_my_account%2FHerraBRE%2FBEGF4-BA35A-137929&amp;sig=3Db276c88=\n9801938eeb9c2884b5ccf58f734dda658&amp;uid=3D796789&amp;iid=3D5508071c-6e78-=\n4544-8537-381ca8ef1e19&amp;nid=3D10+25+20130916&amp;t=3D1\" style=3D\"border:=\nnone;color:#0084b4;text-decoration:none;\">not my account</a>.\n</div>\n<div>\n<a href=3D\"#\" class=3D\"address\" style=3D\"border:none;color:#0084b4;text-dec=\noration:none;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;tex=\nt-decoration:none;font-size:11px;line-height:17px;color:#999999;text-shadow=\n:0 1px 0 #fff;\">Twitter, Inc. 1355 Market St., Suite 900 <span class=3D\"nob=\nreak\" style=3D\"white-space:nowrap;\">San Francisco, CA 94103 </span></a>\n</div> </td>\n<td class=3D\"col cut\" style=3D\"width:85px;\"></td>\n</tr>\n<tr>\n<td colspan=3D\"3\" class=3D\"footer-padding-bottom\" height=3D\"25\"></td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table>\n<img width=3D\"1\" height=3D\"1\" src=3D\"loadimage\" style=3D\"border:0;\" />\n</body>\n</html>\n\n------=_Part_20901379_212901501.1379297966645--\n"
  },
  {
    "path": "mailpile/tests/data/Maildir/cur/1379857166.25979_3.hottie,2,S",
    "content": "Return-path: <feministinn-bounces@listar.hi.is>\nEnvelope-to: bre@localhost\nDelivery-date: Mon, 16 Sep 2013 14:57:36 +0000\nReceived: from localhost ([::1] helo=hottie)\n\tby hottie with esmtp (Exim 4.80)\n\t(envelope-from <feministinn-bounces@listar.hi.is>)\n\tid 1VLaDD-0000WS-Fy\n\tfor bre@localhost; Mon, 16 Sep 2013 14:55:39 +0000\nDelivered-To: fake@example.com\nReceived: from gmail-pop.l.google.com [173.194.71.109]\n\tby hottie with POP3 (fetchmail-6.3.21)\n\tfor <bre@localhost> (single-drop); Mon, 16 Sep 2013 14:55:39 +0000 (GMT)\nReceived: by 10.58.13.104 with SMTP id g8csp85577vec;\n        Mon, 16 Sep 2013 04:37:55 -0700 (PDT)\nX-Received: by 10.52.37.69 with SMTP id w5mr18203vdj.32.1379331471846;\n        Mon, 16 Sep 2013 04:37:51 -0700 (PDT)\nDomainKey-Status: bad format\nReceived-SPF: pass (google.com: domain of feministinn-bounces@listar.hi.is designates 130.208.165.103 as permitted sender) client-ip=130.208.165.103;\nReceived: by 10.230.42.12 with POP3 id q12mf4525162vbe.11;\n        Mon, 16 Sep 2013 04:37:51 -0700 (PDT)\nX-Gmail-Fetch-Info: fake@example.com 1 example.com 110 bre\nReceived: from fenja.rhi.hi.is (fenja.rhi.hi.is [130.208.165.103])\n\tby example.com (8.12.11.20060308/8.12.11) with ESMTP id r8GB1A4k025882\n\tfor <fake@example.com>; Mon, 16 Sep 2013 11:01:10 GMT\nReceived: from listar.hi.is (horn.rhi.hi.is [130.208.165.14])\n\tby fenja.rhi.hi.is (8.13.8/8.13.8) with ESMTP id r8GB0cZ1018409;\n\tMon, 16 Sep 2013 11:00:38 GMT\nReceived: from horn.rhi.hi.is (localhost.localdomain [127.0.0.1])\n\tby listar.hi.is (8.13.8/8.13.8) with ESMTP id r8GB0aTM009377;\n\tMon, 16 Sep 2013 11:00:37 GMT\nX-Mailman-Handler: $Id: mm-handler 5100 2007-07-08 03:14:09Z $\nReceived: from menja.rhi.hi.is (menja.rhi.hi.is [130.208.165.104])\n\tby listar.hi.is (8.13.8/8.13.8) with ESMTP id r8GB0Z17009370\n\tfor <feministinn@listar.hi.is>; Mon, 16 Sep 2013 11:00:35 GMT\nReceived: from smtp.hi.is (smtp.hi.is [130.208.165.149])\n\tby menja.rhi.hi.is (8.13.8/8.13.8) with ESMTP id r8GB0ZSw028769\n\tfor <feministinn@hi.is>; Mon, 16 Sep 2013 11:00:35 GMT\nReceived: from gipcxxxPC (gi-pc159.rhi.hi.is [130.208.126.191])\n\tby smtp.hi.is (8.14.4/8.14.4) with ESMTP id r8GB0Y6Z009277\n\tfor <feministinn@hi.is>; Mon, 16 Sep 2013 11:00:34 GMT\nFrom: =?iso-8859-1?Q?Ranns=F3knastofa_=ED_kvenna-_og_kynjafr=E6=F0um?=\n\t<rikk@hi.is>\nTo: <feministinn@hi.is>\nDate: Mon, 16 Sep 2013 11:00:37 -0000\nMessage-ID: <017101ceb2cb$f956bec0$ec043c40$@is>\nMIME-Version: 1.0\nX-Mailer: Microsoft Office Outlook 12.0\nThread-Index: Ac6yw1llHRERUNZkS3K3nThIuMkC0AAAA4ZQAAANLXA=\nContent-Language: is\nSubject: [Feministinn] =?iso-8859-1?q?=C1_fimmtudaginn=3A_Konurnar_flykkja?=\n\t=?iso-8859-1?q?st_=ED_fjarn=E1mi=F0_-_sta=F0a_og_r=FDmi_h=E1sk=F3l?=\n\t=?iso-8859-1?q?amennta=F0ra_kvenna_=ED_dreifb=FDli?=\nX-BeenThere: feministinn@listar.hi.is\nX-Mailman-Version: 2.1.9\nPrecedence: list\nReply-To: feministinn@listar.hi.is\nList-Id: <feministinn.listar.hi.is>\nList-Unsubscribe: <http://listar.hi.is/mailman/listinfo/feministinn>,\n\t<mailto:feministinn-request@listar.hi.is?subject=unsubscribe>\nList-Archive: <http://listar.hi.is/mailman/private/feministinn>\nList-Post: <mailto:feministinn@listar.hi.is>\nList-Help: <mailto:feministinn-request@listar.hi.is?subject=help>\nList-Subscribe: <http://listar.hi.is/mailman/listinfo/feministinn>,\n\t<mailto:feministinn-request@listar.hi.is?subject=subscribe>\nContent-Type: multipart/mixed; boundary=\"===============8944841122039029855==\"\nSender: feministinn-bounces@listar.hi.is\nErrors-To: feministinn-bounces@listar.hi.is\nX-BRE-Whitelisted: procmail (feministinn-bounces@listar.hi.is)\nContent-Length: 54619\nLines: 883\n\nThis is a multi-part message in MIME format.\n\n--===============8944841122039029855==\nContent-Type: multipart/related;\n\tboundary=\"----=_NextPart_000_0172_01CEB2CB.F956BEC0\"\nContent-Language: is\n\nThis is a multi-part message in MIME format.\n\n------=_NextPart_000_0172_01CEB2CB.F956BEC0\nContent-Type: multipart/alternative;\n\tboundary=\"----=_NextPart_001_0173_01CEB2CB.F956BEC0\"\n\n\n------=_NextPart_001_0173_01CEB2CB.F956BEC0\nContent-Type: text/plain;\n\tcharset=\"iso-8859-1\"\nContent-Transfer-Encoding: quoted-printable\n\nKonurnar flykkjast =ED fjarn=E1mi=F0 =96 sta=F0a og r=FDmi =\nh=E1sk=F3lamennta=F0ra kvenna =ED\ndreifb=FDli\n\n-       H=E1degisrabb fimmtudaginn 19. september =ED fyrirlestrasal\n=DEj=F3=F0minjasafnsins, kl. 12:00-13:00.\n\nAnnaE_9991.jpg=20\n\nFimmtudaginn 19. september flytur Anna Gu=F0r=FAn Edvardsd=F3ttir, =\ndoktorsnemi =E1\nmenntav=EDsindasvi=F0i, fyrirlestur sem ber heiti=F0 =84Konurnar =\nflykkjast =ED\nfjarn=E1mi=F0 =96 sta=F0a og r=FDmi h=E1sk=F3lamennta=F0ra kvenna =ED =\ndreifb=FDli=93.\nFyrirlesturinn fer fram =ED fyrirlestrasal =DEj=F3=F0minjasafnsins, kl. =\n12:00-13:00.\n\n=20\n\nFyrirlesturinn byggir =E1 vi=F0t=F6lum vi=F0 =E1tta konur =E1 =\nVestfj=F6r=F0um og er hluti af\ndoktorsverkefni =D6nnu Gu=F0r=FAnar sem fjallar um =E1hrif =\n=FEekkingarsamf=E9lagsins =E1\nbygg=F0a=FEr=F3un og sj=E1lfb=E6rni samf=E9laga. =CD =FEessum hluta er =\nsko=F0a=F0 hva=F0 gerist\n=FEegar konur fara =ED n=E1m til =FEess a=F0 styrkja st=F6=F0u s=EDna og =\nv=EDkka =FAt\nathafnar=FDmi sitt innan =FEess samf=E9lags sem =FE=E6r b=FAa. Fleiri =\nkonur stunda\nh=E1sk=F3lan=E1m en karlar og skiptir ekki m=E1li hvort um er a=F0 =\nr=E6=F0a sta=F0- e=F0a\nfjarn=E1m. =DE=E6r vir=F0ast =FEv=ED n=FDta s=E9r m=F6guleikana sem =\nfelast =ED  uppbyggingu\n=FEekkingarsamf=E9lagsins =E1 landsbygg=F0inni er var=F0ar a=F0gengi =\na=F0 h=E1sk=F3lan=E1mi. =CD\nfyrirlestri s=EDnum mun Anna Gu=F0r=FAn velta upp nokkrum =E1litam=E1lum =\ner var=F0a\natvinnum=F6guleikum, b=FAsetu, bygg=F0a=FEr=F3un og sj=E1lfb=E6rni =\nsamf=E9laga.\n\n=20\n\n Fyrirlesturinn er haldinn =ED samstarfi vi=F0 =DEj=F3=F0minjasafn =\n=CDslands.\n\n =D6ll velkomin!=20\n\n Ranns=F3knastofa =ED kvenna- og kynjafr=E6=F0um\n\n <http://www.rikk.hi.is/> www.rikk.hi.is\n\nRIKK merki.jpg\n\n=20\n\n=20\n\n=20\n\n=20\n\n\n------=_NextPart_001_0173_01CEB2CB.F956BEC0\nContent-Type: text/html;\n\tcharset=\"iso-8859-1\"\nContent-Transfer-Encoding: quoted-printable\n\n<META HTTP-EQUIV=3D\"Content-Type\" CONTENT=3D\"text/html; =\ncharset=3Diso-8859-1\">\n<html xmlns:v=3D\"urn:schemas-microsoft-com:vml\" =\nxmlns:o=3D\"urn:schemas-microsoft-com:office:office\" =\nxmlns:w=3D\"urn:schemas-microsoft-com:office:word\" =\nxmlns:m=3D\"http://schemas.microsoft.com/office/2004/12/omml\" =\nxmlns=3D\"http://www.w3.org/TR/REC-html40\"><head><meta name=3DGenerator =\ncontent=3D\"Microsoft Word 12 (filtered medium)\"><!--[if =\n!mso]><style>v\\:* {behavior:url(#default#VML);}\no\\:* {behavior:url(#default#VML);}\nw\\:* {behavior:url(#default#VML);}\n.shape {behavior:url(#default#VML);}\n</style><![endif]--><style><!--\n/* Font Definitions */\n@font-face\n\t{font-family:\"Cambria Math\";\n\tpanose-1:2 4 5 3 5 4 6 3 2 4;}\n@font-face\n\t{font-family:Calibri;\n\tpanose-1:2 15 5 2 2 2 4 3 2 4;}\n@font-face\n\t{font-family:Tahoma;\n\tpanose-1:2 11 6 4 3 5 4 4 2 4;}\n@font-face\n\t{font-family:Consolas;\n\tpanose-1:2 11 6 9 2 2 4 3 2 4;}\n/* Style Definitions */\np.MsoNormal, li.MsoNormal, div.MsoNormal\n\t{margin:0cm;\n\tmargin-bottom:.0001pt;\n\tfont-size:12.0pt;\n\tfont-family:\"Times New Roman\",\"serif\";}\na:link, span.MsoHyperlink\n\t{mso-style-priority:99;\n\tcolor:blue;\n\ttext-decoration:underline;}\na:visited, span.MsoHyperlinkFollowed\n\t{mso-style-priority:99;\n\tcolor:purple;\n\ttext-decoration:underline;}\np.MsoPlainText, li.MsoPlainText, div.MsoPlainText\n\t{mso-style-priority:99;\n\tmso-style-link:\"Plain Text Char\";\n\tmargin:0cm;\n\tmargin-bottom:.0001pt;\n\tfont-size:10.5pt;\n\tfont-family:Consolas;}\np\n\t{mso-style-priority:99;\n\tmso-margin-top-alt:auto;\n\tmargin-right:0cm;\n\tmso-margin-bottom-alt:auto;\n\tmargin-left:0cm;\n\tfont-size:12.0pt;\n\tfont-family:\"Times New Roman\",\"serif\";}\np.MsoAcetate, li.MsoAcetate, div.MsoAcetate\n\t{mso-style-priority:99;\n\tmso-style-link:\"Balloon Text Char\";\n\tmargin:0cm;\n\tmargin-bottom:.0001pt;\n\tfont-size:8.0pt;\n\tfont-family:\"Tahoma\",\"sans-serif\";}\nspan.PlainTextChar\n\t{mso-style-name:\"Plain Text Char\";\n\tmso-style-priority:99;\n\tmso-style-link:\"Plain Text\";\n\tfont-family:Consolas;}\nspan.BalloonTextChar\n\t{mso-style-name:\"Balloon Text Char\";\n\tmso-style-priority:99;\n\tmso-style-link:\"Balloon Text\";\n\tfont-family:\"Tahoma\",\"sans-serif\";}\nspan.EmailStyle22\n\t{mso-style-type:personal;\n\tfont-family:\"Calibri\",\"sans-serif\";\n\tcolor:windowtext;}\nspan.EmailStyle23\n\t{mso-style-type:personal;\n\tfont-family:\"Calibri\",\"sans-serif\";\n\tcolor:#1F497D;}\nspan.EmailStyle24\n\t{mso-style-type:personal-reply;\n\tfont-family:\"Calibri\",\"sans-serif\";\n\tcolor:#1F497D;}\n.MsoChpDefault\n\t{mso-style-type:export-only;\n\tfont-size:10.0pt;}\n@page WordSection1\n\t{size:612.0pt 792.0pt;\n\tmargin:70.85pt 70.85pt 70.85pt 70.85pt;}\ndiv.WordSection1\n\t{page:WordSection1;}\n--></style><!--[if gte mso 9]><xml>\n<o:shapedefaults v:ext=3D\"edit\" spidmax=3D\"1027\" />\n</xml><![endif]--><!--[if gte mso 9]><xml>\n<o:shapelayout v:ext=3D\"edit\">\n<o:idmap v:ext=3D\"edit\" data=3D\"1\" />\n</o:shapelayout></xml><![endif]--></head><body lang=3DIS link=3Dblue =\nvlink=3Dpurple><div class=3DWordSection1><p class=3DMsoNormal><b><span =\nstyle=3D'font-size:18.0pt;font-family:\"Calibri\",\"sans-serif\"'>Konurnar =\nflykkjast =ED fjarn=E1mi=F0 &#8211; sta=F0a og r=FDmi =\nh=E1sk=F3lamennta=F0ra kvenna =ED =\ndreifb=FDli<o:p></o:p></span></b></p><p =\nstyle=3D'mso-margin-top-alt:0cm;margin-right:0cm;margin-bottom:0cm;margin=\n-left:36.0pt;margin-bottom:.0001pt;text-indent:-18.0pt;line-height:115%'>=\n<span style=3D'font-family:\"Calibri\",\"sans-serif\"'>-</span><span =\nstyle=3D'font-size:7.0pt;line-height:115%;font-family:\"Calibri\",\"sans-ser=\nif\"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><i><span =\nstyle=3D'font-family:\"Calibri\",\"sans-serif\"'>H=E1degisrabb fimmtudaginn =\n19. september =ED fyrirlestrasal =DEj=F3=F0minjasafnsins, kl. =\n12:00-13:00.</span></i><span =\nstyle=3D'font-family:\"Calibri\",\"sans-serif\"'><o:p></o:p></span></p><p =\nclass=3DMsoPlainText style=3D'line-height:115%'><!--[if gte vml =\n1]><v:shapetype id=3D\"_x0000_t75\" coordsize=3D\"21600,21600\" o:spt=3D\"75\" =\no:preferrelative=3D\"t\" path=3D\"m@4@5l@4@11@9@11@9@5xe\" filled=3D\"f\" =\nstroked=3D\"f\">\n<v:stroke joinstyle=3D\"miter\" />\n<v:formulas>\n<v:f eqn=3D\"if lineDrawn pixelLineWidth 0\" />\n<v:f eqn=3D\"sum @0 1 0\" />\n<v:f eqn=3D\"sum 0 0 @1\" />\n<v:f eqn=3D\"prod @2 1 2\" />\n<v:f eqn=3D\"prod @3 21600 pixelWidth\" />\n<v:f eqn=3D\"prod @3 21600 pixelHeight\" />\n<v:f eqn=3D\"sum @0 0 1\" />\n<v:f eqn=3D\"prod @6 1 2\" />\n<v:f eqn=3D\"prod @7 21600 pixelWidth\" />\n<v:f eqn=3D\"sum @8 21600 0\" />\n<v:f eqn=3D\"prod @7 21600 pixelHeight\" />\n<v:f eqn=3D\"sum @10 21600 0\" />\n</v:formulas>\n<v:path o:extrusionok=3D\"f\" gradientshapeok=3D\"t\" o:connecttype=3D\"rect\" =\n/>\n<o:lock v:ext=3D\"edit\" aspectratio=3D\"t\" />\n</v:shapetype><v:shape id=3D\"Picture_x0020_0\" o:spid=3D\"_x0000_s1026\" =\ntype=3D\"#_x0000_t75\" alt=3D\"AnnaE_9991.jpg\" =\nstyle=3D'position:absolute;margin-left:1.35pt;margin-top:14.9pt;width:79.=\n6pt;height:113.35pt;z-index:251657728;visibility:visible;mso-wrap-style:s=\nquare;mso-wrap-distance-left:9pt;mso-wrap-distance-top:0;mso-wrap-distanc=\ne-right:9pt;mso-wrap-distance-bottom:0;mso-position-horizontal:absolute;m=\nso-position-horizontal-relative:text;mso-position-vertical:absolute;mso-p=\nosition-vertical-relative:text'>\n<v:imagedata src=3D\"cid:image001.jpg@01CEB2C3.59623BE0\" =\no:title=3D\"AnnaE_9991\" />\n<w:wrap type=3D\"square\"/>\n</v:shape><![endif]--><![if !vml]><img width=3D106 height=3D151 =\nsrc=3D\"cid:image002.jpg@01CEB2C3.AE206AD0\" align=3Dleft hspace=3D12 =\nalt=3D\"AnnaE_9991.jpg\" v:shapes=3D\"Picture_x0020_0\"><![endif]><span =\nlang=3DEN-US =\nstyle=3D'font-family:\"Calibri\",\"sans-serif\"'>&nbsp;<o:p></o:p></span></p>=\n<p class=3DMsoNormal style=3D'line-height:115%'><span =\nstyle=3D'font-family:\"Calibri\",\"sans-serif\"'>Fimmtudaginn 19. september =\nflytur Anna Gu=F0r=FAn Edvardsd=F3ttir, doktorsnemi =E1 =\nmenntav=EDsindasvi=F0i, fyrirlestur sem ber heiti=F0 &#8222;Konurnar =\nflykkjast =ED fjarn=E1mi=F0 &#8211; sta=F0a og r=FDmi =\nh=E1sk=F3lamennta=F0ra kvenna =ED dreifb=FDli&#8220;. Fyrirlesturinn fer =\nfram =ED fyrirlestrasal =DEj=F3=F0minjasafnsins, kl. 12:00-13:00.<span =\nstyle=3D'color:#1F497D'><o:p></o:p></span></span></p><p =\nclass=3DMsoNormal style=3D'line-height:115%'><span =\nstyle=3D'font-family:\"Calibri\",\"sans-serif\"'>&nbsp;<span =\nstyle=3D'color:#1F497D'><o:p></o:p></span></span></p><p =\nclass=3DMsoNormal style=3D'line-height:115%'><span =\nstyle=3D'font-family:\"Calibri\",\"sans-serif\"'>Fyrirlesturinn byggir =E1 =\nvi=F0t=F6lum vi=F0 =E1tta konur =E1 Vestfj=F6r=F0um og er hluti af =\ndoktorsverkefni =D6nnu Gu=F0r=FAnar sem fjallar um =E1hrif =\n=FEekkingarsamf=E9lagsins =E1 bygg=F0a=FEr=F3un og sj=E1lfb=E6rni =\nsamf=E9laga. =CD =FEessum hluta er sko=F0a=F0 hva=F0 gerist =FEegar =\nkonur fara =ED n=E1m til =FEess a=F0 styrkja st=F6=F0u s=EDna og v=EDkka =\n=FAt athafnar=FDmi sitt innan =FEess samf=E9lags sem =FE=E6r b=FAa. =\nFleiri konur stunda h=E1sk=F3lan=E1m en karlar og skiptir ekki m=E1li =\nhvort um er a=F0 r=E6=F0a sta=F0- e=F0a fjarn=E1m. =DE=E6r vir=F0ast =\n=FEv=ED n=FDta s=E9r m=F6guleikana sem felast =ED &nbsp;uppbyggingu =\n=FEekkingarsamf=E9lagsins =E1 landsbygg=F0inni er var=F0ar a=F0gengi =\na=F0 h=E1sk=F3lan=E1mi. =CD fyrirlestri s=EDnum mun Anna Gu=F0r=FAn =\nvelta upp nokkrum =E1litam=E1lum er var=F0a atvinnum=F6guleikum, =\nb=FAsetu, bygg=F0a=FEr=F3un og sj=E1lfb=E6rni =\nsamf=E9laga.<o:p></o:p></span></p><p class=3DMsoNormal =\nstyle=3D'line-height:115%'><span =\nstyle=3D'font-family:\"Calibri\",\"sans-serif\"'><o:p>&nbsp;</o:p></span></p>=\n<p class=3DMsoNormal =\nstyle=3D'mso-margin-top-alt:auto;mso-margin-bottom-alt:auto;line-height:1=\n15%'><span =\nstyle=3D'font-family:\"Calibri\",\"sans-serif\"'>&nbsp;Fyrirlesturinn er =\nhaldinn =ED samstarfi vi=F0 =DEj=F3=F0minjasafn =\n=CDslands.<o:p></o:p></span></p><p class=3DMsoNormal =\nstyle=3D'mso-margin-top-alt:auto;mso-margin-bottom-alt:auto;line-height:1=\n15%'><span style=3D'font-family:\"Calibri\",\"sans-serif\"'>&nbsp;=D6ll =\nvelkomin!&nbsp;<o:p></o:p></span></p><p class=3DMsoNormal =\nstyle=3D'mso-margin-top-alt:auto;mso-margin-bottom-alt:auto;line-height:1=\n15%'><span =\nstyle=3D'font-family:\"Calibri\",\"sans-serif\"'>&nbsp;Ranns=F3knastofa =ED =\nkvenna- og kynjafr=E6=F0um<o:p></o:p></span></p><p class=3DMsoNormal =\nstyle=3D'mso-margin-top-alt:auto;mso-margin-bottom-alt:auto;line-height:1=\n15%'><a href=3D\"http://www.rikk.hi.is/\"><span =\nstyle=3D'font-family:\"Calibri\",\"sans-serif\"'>www.rikk.hi.is</span></a><sp=\nan style=3D'font-family:\"Calibri\",\"sans-serif\"'><o:p></o:p></span></p><p =\nclass=3DMsoNormal =\nstyle=3D'mso-margin-top-alt:auto;mso-margin-bottom-alt:auto'><span =\nstyle=3D'font-family:\"Calibri\",\"sans-serif\"'><img border=3D0 width=3D305 =\nheight=3D56 id=3D\"Picture_x0020_1\" =\nsrc=3D\"cid:image003.jpg@01CEB2C3.59623BE0\" alt=3D\"RIKK =\nmerki.jpg\"><o:p></o:p></span></p><p class=3DMsoNormal =\nstyle=3D'line-height:115%'><span =\nstyle=3D'font-family:\"Calibri\",\"sans-serif\"'><o:p>&nbsp;</o:p></span></p>=\n<p class=3DMsoPlainText style=3D'line-height:115%'><span =\nstyle=3D'font-size:12.0pt;line-height:115%;font-family:\"Calibri\",\"sans-se=\nrif\"'><o:p>&nbsp;</o:p></span></p><p class=3DMsoNormal =\nstyle=3D'line-height:115%'><span lang=3DEN-US =\nstyle=3D'font-family:\"Calibri\",\"sans-serif\";color:black'><o:p>&nbsp;</o:p=\n></span></p><p class=3DMsoNormal><span =\nstyle=3D'font-size:11.0pt;font-family:\"Calibri\",\"sans-serif\"'><o:p>&nbsp;=\n</o:p></span></p></div></body></html>\n------=_NextPart_001_0173_01CEB2CB.F956BEC0--\n\n------=_NextPart_000_0172_01CEB2CB.F956BEC0\nContent-Type: image/jpeg;\n\tname=\"image001.jpg\"\nContent-Transfer-Encoding: base64\nContent-ID: <image001.jpg@01CEB2C3.59623BE0>\n\n/9j/4AAQSkZJRgABAQEA3ADcAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAMEBgUGBgYF\nBgYGBwkIBgcJBwYGCAsICQoKCgoKBggLDAsKDAkKCgr/2wBDAQICAgICAgUDAwUKBwYHCgoKCgoK\nCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgr/wAARCAFKAOoDASIA\nAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA\nAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3\nODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWm\np6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA\nAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx\nBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElK\nU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3\nuLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD9/KKK\nKACiiigAooooAKKKKACiiigAoooJA5JoAKKia/sUzvvYhjrmQcVCNf0Izi2GtWnmHpH9pXcfwzTs\nwLdFIrKw3KwI9QaWkAUUUUAFFFFABRRRQAUUUUAFFFFACMintUckGRgDipaKabQFKSEr05pnlf7P\n61eaJT2/Om+R71oqhPKSUUUVkUFFFFABRRRQAUUUUAFFYvj74h+Cvhf4bm8XePvElrpenwYDT3Uo\nUMx4CKOrMTwFGSTXyr8Yf+C0f7Kfwx0G71lPF2lDyIzJbw6trUFnLdoOvlRuS7HkcFRntnBxpCjU\nqK6WhEpxi7M+r/FfiePwxpFzqcllcSi3t5JWMNs0mAqlui8t06Dk5rxP4jftO+B/DnwBm+Nuv+Pb\n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\nKlAGBxUpCeiP/9k=\n\n------=_NextPart_000_0172_01CEB2CB.F956BEC0\nContent-Type: image/jpeg;\n\tname=\"image003.jpg\"\nContent-Transfer-Encoding: base64\nContent-ID: <image003.jpg@01CEB2C3.59623BE0>\n\n/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAoHBwgHBgoICAgLCgoLDhgQDg0NDh0VFhEYIx8lJCIf\nIiEmKzcvJik0KSEiMEExNDk7Pj4+JS5ESUM8SDc9Pjv/2wBDAQoLCw4NDhwQEBw7KCIoOzs7Ozs7\nOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozv/wAARCAA4ATEDASIA\nAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA\nAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3\nODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWm\np6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA\nAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx\nBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElK\nU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3\nuLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD2aoJb\nlUYRoQzsSo54DYyAaZe3YtoxgZdzhRg4P5Vk3N1DaQLPMjNJu/cq3yucf3vUD1oAnaa5u4UKOVdS\nQ+DtA9M/likmmjNxJIs8Cs8ZU7pVJBx0HpXO3N7dahITK5Yddg4Vfwq7baRbCAXF7dAIRkLEM/gT\n60Cube54WaaIvIAqqgRtyscck1PBfgpAs335VyCo46/pWJH9kgQxW8NxbX2fkWNi24YyCc8Yq7b3\nMhLwTBILraNzpyGUDnb7+1AzboqpZXAkLRoh8pOFk3Zzj1q3QAUUjMqKWYgADJJ6Co4rmCckQzxy\nEddjg4/KgCWimNLGjqjyKrP91ScE/Sh5ootvmSIm44XcwGT6CgB9FM8yPzDHvXeBkrnkD1xTRcwG\n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\nKKACiiigAooooAKKKKAP/9k=\n\n------=_NextPart_000_0172_01CEB2CB.F956BEC0\nContent-Type: image/jpeg;\n\tname=\"image002.jpg\"\nContent-Transfer-Encoding: base64\nContent-ID: <image002.jpg@01CEB2C3.AE206AD0>\n\n/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAoHBwgHBgoICAgLCgoLDhgQDg0NDh0VFhEYIx8lJCIf\nIiEmKzcvJik0KSEiMEExNDk7Pj4+JS5ESUM8SDc9Pjv/2wBDAQoLCw4NDhwQEBw7KCIoOzs7Ozs7\nOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozv/wAARCACXAGoDASIA\nAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA\nAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3\nODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWm\np6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA\nAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx\nBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElK\nU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3\nuLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD2aiii\ngAoopskixRs7sFVRkk9AKAHVHLPDAN00qRj1dgK5DV/ij4c0qXyWkmmfuIVyR6V5dr3xLv8AU7+K\nSGCNYIMiOOUbtx/vMOhNP1EfQCTwyf6uVG/3WBqSvl2fxLq93N5z3jqe2w7APoB0ro/DfxN1zRXW\nO7ma8ts8rK2WH0NK6Ge/0VieG/Fem+JrcvZTKZEALx/xL+FbdABRRRQAhGaTbTqKACiiigBGIVSx\nIAHJJ7V4V8QfiDc63dTWGnzNFpyHadpwZcHr9K7P4oeM10uxfRLJ/wDS7hP3zA/6tD2+p/lXh8xL\nn2o2ARQX+lIQFGcbjUkascIO/epPs7SPtUZ7cVm2UlcrYZxnHHoKaDhvbvVmaGSI+Vjae470+Owc\n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\n\n------=_NextPart_000_0172_01CEB2CB.F956BEC0--\n\n\n--===============8944841122039029855==\nContent-Type: text/plain; charset=\"utf-8\"\nMIME-Version: 1.0\nContent-Transfer-Encoding: base64\nContent-Disposition: inline\n\nX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fCsOeZXNz\naSBww7NzdGxpc3RpIGVyIHZldHR2YW5ndXIgZmVtw61uaXN0YSB0aWwgYcOwIHLDpsOwYSBqYWZu\ncsOpdHRpIGt5bmphbm5hIG9nIGZlbcOtbsOtc2sgbcOhbGVmbmkuIEVra2kgbcOhIG5vdGEgw75y\nw6HDsGlubiBmeXJpciBhdWdsw71zaW5nYXIgZcOwYSB0aWxreW5uaW5nYXIgc2VtIGVra2kgdGVu\nZ2phc3QgZmVtw61uaXNtYS4gw57DoXR0dGFrZW5kdXIgc2t1bHUgc8O9bmEgw7bDsHJ1bSDDvsOh\ndHR0YWtlbmR1bSB2aXLDsGluZ3Ugb2cgb3LDsGEgaW5ubGVnZyBzw61uIMOhIG3DoWxlZm5hbGVn\nYW4gb2cga3VydGVpc2FuIGjDoXR0LiBTa3JpZmEgc2thbCBmdWxsdCBuYWZuIHVuZGlyIGlubmxl\nZ2cgw6EgbGlzdGFubi4gw5NoZWltaWx0IGVyIGHDsCDDoWZyYW1zZW5kYSBlw7BhIGJpcnRhIGlu\nbmxlZ2cgc2VtIGJlcmFzdCDDoSBsaXN0YW5uIGFubmFyc3N0YcOwYXIgw6FuIGxleWZpcyBow7Zm\ndW5kYXIuCi0gLSAtIC0gLSAtIC0gLSAtIC0gLSAtIC0KRmVtaW5pc3Rpbm4gbWFpbGluZyBsaXN0\nCkZlbWluaXN0aW5uQGhpLmlzCmh0dHA6Ly9saXN0YXIuaGkuaXMvbWFpbG1hbi9saXN0aW5mby9m\nZW1pbmlzdGlubgoK\n\n--===============8944841122039029855==--\n"
  },
  {
    "path": "mailpile/tests/data/Maildir/cur/1379857166.25979_5.hottie,2,S",
    "content": "Return-path: <b0964ab49cbbre=example.com@bounce.twitter.com>\nEnvelope-to: bre@localhost\nDelivery-date: Mon, 16 Sep 2013 14:57:35 +0000\nReceived: from localhost ([::1] helo=hottie)\n\tby hottie with esmtp (Exim 4.80)\n\t(envelope-from <b0964ab49cbbre=example.com@bounce.twitter.com>)\n\tid 1VLaDE-0000WS-JI\n\tfor bre@localhost; Mon, 16 Sep 2013 14:55:40 +0000\nDelivered-To: fake@example.com\nReceived: from gmail-pop.l.google.com [173.194.71.109]\n\tby hottie with POP3 (fetchmail-6.3.21)\n\tfor <bre@localhost> (single-drop); Mon, 16 Sep 2013 14:55:40 +0000 (GMT)\nReceived: by 10.58.13.104 with SMTP id g8csp85583vec;\n        Mon, 16 Sep 2013 04:37:57 -0700 (PDT)\nX-Received: by 10.52.227.6 with SMTP id rw6mr22366368vdc.19.1379331472375;\n        Mon, 16 Sep 2013 04:37:52 -0700 (PDT)\nDomainKey-Status: good\nReceived-SPF: pass (google.com: domain of b0964ab49cbbre=example.com@bounce.twitter.com designates 199.59.150.74 as permitted sender) client-ip=199.59.150.74;\nReceived: by 10.230.42.12 with POP3 id q12mf4525162vbe.11;\n        Mon, 16 Sep 2013 04:37:51 -0700 (PDT)\nX-Gmail-Fetch-Info: fake@example.com 1 example.com 110 bre\nReceived: from spruce-goose-ae.twitter.com (spruce-goose-ae.twitter.com [199.59.150.74])\n\tby example.com (8.12.11.20060308/8.12.11) with ESMTP id r8GBHKVl027065\n\tfor <fake@example.com>; Mon, 16 Sep 2013 11:17:21 GMT\nDKIM-Signature: v=1; a=rsa-sha1; d=twitter.com; s=dkim-201303; c=relaxed/relaxed;\n\tq=dns/txt; i=@twitter.com; t=1379330239;\n\th=From:Subject:Date:To;\n\tbh=6a6KzvEwK2xADBqqEV+K3NA0txY=;\n\tb=Lt67yTX+vCRJCsIY3hwWfWngE6R3aUlwBG1urC0OjtmrExkPxxMEhZKRvZ5j8uac\n\t0isBpfDBiyipot5Sc6uYkrcog3Nh1jyI9aNJhfEEfJZrsrqOesumeIyI1JMt9xay\n\t8vQtJvZtSCBYrQmHPTUA6oZTZiEEXDsDzvV5g6nMBPo=;\nX-MSFBL: YnJlQGtsYWtpLm5ldEBzbWYxLWJmbi0yNC1zcjEtRXZlcnl0aGluZy4xNzRARXZl\n\tcnl0aGluZ0A=\nDate: Mon, 16 Sep 2013 11:17:19 +0000\nFrom: \"Brennan Novak (Twitter)\" <notify@twitter.com>\nTo: \"Bjarni R. Einarsson\" <fake@example.com>\nSubject: Brennan Novak (@brennannovak) mentioned you on Twitter!\nMIME-Version: 1.0\nContent-Type: multipart/alternative; \n\tboundary=\"----=_Part_24299859_24672245.1379330239558\"\nMessage-ID: <D2.79.59218.FB8E6325@spruce-goose.twitter.com>\nX-BRE-Whitelisted: procmail (notify@twitter.com)\nContent-Length: 36404\nLines: 955\n\n------=_Part_24299859_24672245.1379330239558\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 7bit\n\nBrennan Novak @brennannovak\n@ladyniasan hi :) @HerraBRE and I heard about this cave thing- sounds pretty cool, we'd like to learn more and check it out in the spring!\n04:17 AM - 16 Sep 13\n\n------------------------\n\nKeep the conversation going:\nReply to @Brennan Novak\nhttps://twitter.com/brennannovak/status/379564650884370432\n\n\n-- \n\nForgot your Twitter password? Get instructions on how to reset it:\nhttps://twitter.com/account/resend_password\n\nYou can also unsubscribe from these emails or change your notification settings:\nhttps://twitter.com/i/u?t=1&sig=1049383876de2b1d27e6fb341a6593d4b6b42a0a&iid=6a6de5f5-2df4-4749-b78b-1d143f237eb9&uid=796789&nid=4+26\nhttps://twitter.com/settings/notifications\n\nNeed help?\nhttps://support.twitter.com\n\nIf you received this message in error and did not sign up for a Twitter account, click on the url below:\nhttps://twitter.com/account/not_my_account/HerraBRE/546G5-B68A8-137933\n------=_Part_24299859_24672245.1379330239558\nContent-Type: text/html; charset=UTF-8\nContent-Transfer-Encoding: quoted-printable\n\n<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/htm=\nl4/strict.dtd\">\n<html>\n<head>\n<meta http-equiv=3D\"Content-Type\" content=3D\"text/html; charset=3Dutf-8\" />\n<meta name=3D\"viewport\" content=3D\"width=3Ddevice-width, minimum-scale=3D1.=\n0, maximum-scale=3D1.0, user-scalable=3D0\" />\n<meta name=3D\"apple-mobile-web-app-capable\" content=3D\"yes\" />\n<style type=3D\"text/css\">\n@media only screen and (max-device-width: 480px) {\ntable[class=3Douter] .global-width-670-to-320 {\nwidth: 320px !important;\n}\ntable[class=3Douter] .global-width-520-to-320 {\nwidth: 320px !important;\n}\ntable[class=3Douter] .global-width-500-to-300 {\nwidth: 300px !important;\n}\ntable[class=3Douter] .global-separator-padding {\nheight: 8px !important;\n}\ntable[class=3Douter] .global-shrinking-to-0 {\nheight: 0 !important;\n}\ntable[class=3Douter] .global-shrinking-to-10 {\nheight: 10px !important;\n}\ntable[class=3Douter] .global-h1 {\nfont-size: 14px !important;\n}\n\ntable[class=3Douter] .cut {\nwidth: 0 !important;\npadding: 0 !important;\n}\ntable[class=3Douter] .vcut {\nheight: 0 !important;\npadding: 0 !important;\n}\ntable[class=3Douter] img.cut {\ndisplay: none !important;\nwidth: 0 !important;\nheight: 0 !important;\n}\ntable[class=3Douter] .cut span, table[class=3Douter] .cut a {\ndisplay: none !important;\n}\ntable[class=3Douter] .frame {\nwidth: 320px !important;\nborder-left: 0 !important;\nborder-right: 0 !important;\n}\ntable[class=3Douter] .main_header.media_header {\nwidth: 300px !important;\nheight: 55px !important;\n}\ntable[class=3Douter] .header_left {\nheight: 55px !important;\n}\ntable[class=3Douter] .logo_header {\nheight: 62px !important;\n}\ntable[class=3Douter] .main_name {\nfont-size: 12px !important;\n}\ntable[class=3Douter] .subtitle {\nfont-size: 12px !important;\n}\ntable[class=3Douter] .media_main {\nwidth: 300px !important;\n}\ntable[class=3Douter] .media_main .intro {\nwidth: 260px !important;\nfont-size: 14px !important;\n}\ntable[class=3Douter] .media_main .intro2 {\nfont-size: 14px !important;\n}\ntable[class=3Douter] .media_main .suggestions {\nfont-size: 14px !important;\n}\ntable[class=3Douter] .media_more {\nwidth: 300px !important;\nborder-radius: 0 !important;\nbackground: #fff !important;\nborder-top: 1px solid #e8e8e8 !important;\n}\ntable[class=3Douter] .media_button {\nwidth: 300px !important;\nbackground-color: #209ae4 !important;\npadding-top: 3px !important;\npadding-bottom: 3px !important;\n}\ntable[class=3Douter] .media_footer {\nfont-size: 11px !important;\nline-height: 14px !important;\npadding: 0 10px !important;\n}\ntable[class=3Douter] .address {\nfont-size: 11px !important;\nline-height: 14px !important;\ndisplay: block !important;\n}\ntable[class=3Douter] td[class=3Dfooter-padding-top] {\nheight: 12px !important;\n}\ntable[class=3Douter] td[class=3Dfooter-padding-bottom] {\nheight: 17px !important;\n}\ntable[class=3Douter] .media_footer br {\ndisplay: none !important;\n}\ntable[class=3Douter] .spacer.ios {\ndisplay: none !important;\n}\ntable[class=3Douter] .reset {\ndisplay: block !important;\npadding-bottom: 4px !important;\n}\ntable[class=3Douter] .media_logo_div {\ndisplay: block !important;\nposition: absolute !important;\nleft: 274px !important;\ntop: 0 !important;\nbackground-image: url('https://ea.twimg.com/email/t1/ribbon.png') !importan=\nt;\nbackground-size: 100% 100% !important;\nwidth: 36px !important;\nheight: 68px !important;\nz-index: 1 !important;\n}\ntable[class=3Demployee-only-padding-top-bottom] {\nwidth: 300px !important;\n}\ntable[class=3Demployee-only] {\nwidth: 300px !important;\npadding: 10px 0;\n}\ntd[class=3Dheader_padding] {\nheight: 55px !important;\n}\n}\n\n@media only screen and (min-device-width: 768px) and (max-device-width: 102=\n4px) {\ntable[class=3Douter] .media_button {\npadding-top: 3px !important;\npadding-bottom: 3px !important;\nbackground-color: #209ae4 !important;\n}\ntable[class=3Douter] .media_button td.cut {\nwidth: 0 !important;\npadding: 0 !important;\n}\ntable[class=3Douter] .media_button {\npadding-left: 10px !important;\npadding-right: 10px !important;\n}\n}\n</style>\n</head>\n<body style=3D\"margin: 0; padding: 0; background: #fff;-webkit-text-size-ad=\njust:100%;\">\n<span style=3D\"color: white; font-size: 1px; display: none;\">@ladyniasan hi=\n :) @HerraBRE and I heard about this cave thing- sounds pretty cool, we'd l=\nike to learn more and check it out in the spring! - @brennannovak</span>\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" width=3D\"100%\" clas=\ns=3D\"outer\" style=3D\"position:relative;background:#ddd;\">\n<tbody>\n<tr>\n<td>\n<table class=3D\"inner frame\" align=3D\"center\" cellpadding=3D\"0\" cellspacing=\n=3D\"0\" border=3D\"0\" width=3D\"670\" style=3D\"background:#fff;position:relativ=\ne;border:0;border-left:1px solid #ccc;border-right:1px solid #ccc;position:=\nrelative;\">\n<tbody>\n<tr>\n<td> <a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.=\ncom%3Frefsrc%3Demail&amp;sig=3D76add118f9dfd33f63864ce34d9372e483cfba4a&amp=\n;uid=3D796789&amp;iid=3D6a6de5f5-2df4-4749-b78b-1d143f237eb9&amp;nid=3D4+21=\n&amp;t=3D1\" style=3D\"border:none;color:#0084b4;text-decoration:none;\"><span=\n class=3D\"media_logo_div\"></span></a>\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" width=3D\"670\" class=\n=3D\"header frame\" style=3D\"background:#f2f2f2;table-layout:fixed;position:r=\nelative;\">\n<tbody>\n<tr>\n<td class=3D\"header_left cut\" style=3D\"width:19px;height:77px;\"> &nbsp; </t=\nd>\n<td height=3D\"94\" width=3D\"46\" valign=3D\"top\" rowspan=3D\"2\" class=3D\"logo_h=\neader cut\" style=3D\"background:#fff;line-height:100%;\"><a href=3D\"https://t=\nwitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%3Frefsrc%3Demail&amp;=\nsig=3D76add118f9dfd33f63864ce34d9372e483cfba4a&amp;uid=3D796789&amp;iid=3D6=\na6de5f5-2df4-4749-b78b-1d143f237eb9&amp;nid=3D4+21&amp;t=3D1\" style=3D\"bord=\ner:none;color:#0084b4;text-decoration:none;\"><img class=3D\"logo cut\" src=3D=\n\"https://ea.twimg.com/email/t1/ribbon.png\" width=3D\"46\" height=3D\"94\" style=\n=3D\"border:0;line-height:100%;border:0;\" /></a></td>\n<td class=3D\"cut\" width=3D\"9\"> &nbsp; </td>\n<td width=3D\"10\" height=3D\"77\" class=3D\"header_padding\"> &nbsp; </td>\n<td width=3D\"458\" height=3D\"77\" class=3D\"main_header media_header\" style=3D=\n\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;color:#333;\">\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\">\n<tbody>\n<tr>\n<td class=3D\"main_name\" style=3D\"font-size:14px;font-weight:bold;color:#000=\n;\"> <span dir=3D\"ltr\">Bjarni R. Einarsson,</span> </td>\n</tr>\n<tr>\n<td class=3D\"subtitle\" style=3D\"font-size:14px;color:#666;\"> You were menti=\noned in a conversation! </td>\n</tr>\n</tbody>\n</table> </td>\n<td width=3D\"10\" height=3D\"77\" class=3D\"header_padding\"> &nbsp; </td>\n<td class=3D\"main_avatar cut\" width=3D\"32\" style=3D\"text-align:right;\"> <a =\nhref=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2FHe=\nrraBRE%3Frefsrc%3Demail&amp;sig=3De63e99778c0859b62c6cc0df2d3f9b7187011e39&=\namp;uid=3D796789&amp;iid=3D6a6de5f5-2df4-4749-b78b-1d143f237eb9&amp;nid=3D4=\n+22&amp;t=3D1\" style=3D\"border:none;color:#0084b4;text-decoration:none;\"><i=\nmg class=3D\"cut\" src=3D\"https://si0.twimg.com/profile_images/1260879678/Bja=\nrni-tie-done_reasonably_small.jpg\" width=3D\"32\" height=3D\"32\" alt=3D\"Bjarni=\n R. Einarsson\" style=3D\"background:#fff;border-radius:5px;border:0;\" /></a>=\n </td>\n<td class=3D\"col cut\" style=3D\"width:85px;\"></td>\n</tr>\n<tr>\n<td class=3D\"main header_drop cut\" style=3D\"background:#fff;border-top:1px =\nsolid #ddd;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\">&nb=\nsp;</td>\n<td class=3D\"main header_drop cut\" style=3D\"background:#fff;border-top:1px =\nsolid #ddd;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\">&nb=\nsp;</td>\n<td class=3D\"main header_drop media_header\" height=3D\"17\" style=3D\"backgrou=\nnd:#fff;border-top:1px solid #ddd;font-family:'Helvetica Neue', Helvetica, =\nArial, sans-serif;\"><img width=3D\"1\" height=3D\"1\" style=3D\"display: block;b=\norder:0;\" src=3D\"https://twitter.com/scribe/ibis?uid=3D796789&amp;iid=3D6a6=\nde5f5-2df4-4749-b78b-1d143f237eb9&amp;nid=3D4+20&amp;t=3D1\" /></td>\n<td class=3D\"main header_drop cut\" height=3D\"17\" colspan=3D\"4\" style=3D\"bac=\nkground:#fff;border-top:1px solid #ddd;font-family:'Helvetica Neue', Helvet=\nica, Arial, sans-serif;\">&nbsp;</td>\n</tr>\n</tbody>\n</table> </td>\n<td rowspan=3D\"3\"></td>\n</tr>\n<tr>\n<td class=3D\"content\" style=3D\"background:#fff;\">  <style type=3D\"text/css\"=\n>\n@media only screen and (min-device-width: 768px) and (max-device-width: 102=\n4px) {\ntd[class=3D\"tweet-action\"] {\nline-height: 15px !important;\n}\n}\n\n@media only screen and (max-device-width: 480px) {\ntable[class=3D\"main\"] {\nwidth: 320px !important;\n}\ntable[class=3D\"general-text-rules\"] {\nwidth: 300px !important;\n}\ntable[class=3D\"separator-padding\"] {\nwidth: 320px !important;\n}\ntable[class=3D\"separator-padding\"] td {\nheight: 8px !important;\n}\nhr[class=3D\"separator\"] {\nwidth: 320px !important;\n}\ntable[class=3D\"action\"] {\nwidth: 300px !important;\nborder-radius: 0 !important;\nbackground-color: #ffffff !important;\n}\ntd[class=3D\"top-height\"] {\nheight: 8px !important;\n}\ntd[class=3D\"tweet-action\"] {\nline-height: 15px !important;\n}\ntable[class=3Douter] .main.header_drop.cut {\nwidth: 100% !important;\n}\ntable[class=3Douter] .main-content .cut {\ndisplay: none !important;\n}\ntable[class=3Douter] .cut span, table[class=3Douter] .cut a {\ndisplay: none !important;\n}\ntable[class=3Douter] .header_drop_cut{\ndisplay:none !important;\n}\ntable[class=3Douter] .date{\npadding-left:9px !important;\npadding-top: 4px !important;\n}\ntable[class=3Douter] .top-spacer{\nheight: 8px !important;\n}\ntable[class=3Douter] .frame {\nborder-bottom: medium none !important;\nwidth: 320px !important;\nmargin:0 auto !important;\n}\ntable[class=3Douter] img.cut {\ndisplay: none !important;\nheight: 0 !important;\nwidth: 0 !important;\n}\ntable[class=3Douter] .section-container-top, table[class=3Douter] .section-=\ncontainer-bottom, table[class=3Douter] .date-top, table[class=3D\"outer\"] .d=\nate-bottom, table[class=3Douter] .main-avatar-left, table[class=3Douter] .s=\ntyled-x-top {\ndisplay: none !important;\n}\ntable[class=3Douter] .frame.main-content {\nbackground-color: #EEEEEE !important;\n}\ntable[class=3Douter] .media_button {\nmargin: 0 auto !important;\nwidth: 300px !important;\nleft: 10px !important;\npadding-top: 0px !important;\npadding-bottom: 0px !important;\nmargin-bottom: 8px !important;\nmargin-right: 10px !important;\n}\n}\n</style>\n<table width=3D\"670\" border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" class=\n=3D\"main frame\" style=3D\"background:#fff;font-family:'Helvetica Neue', Helv=\netica, Arial, sans-serif;position:relative;\">\n<tbody>\n<tr>\n<td>\n<table width=3D\"520\" border=3D\"0\" align=3D\"center\" cellpadding=3D\"0\" cellsp=\nacing=3D\"0\" class=3D\"general-text-rules\" style=3D\"font-family:'Helvetica Ne=\nue', Helvetica, Arial, sans-serif;\">\n<tbody>\n<tr>\n<td height=3D\"20\" class=3D\"top-height\" style=3D\"height:20px;\"></td>\n</tr>\n<tr>\n<td>  <style type=3D\"text/css\">\n@media only screen and (max-device-width: 480px) {\ntable[class=3Douter] .avatar-small {\npadding:0px !important;\nheight: 24px !important;\n}\ntable[class=3Douter] .avatar-small img {\nwidth: 24px !important;\nheight: 24px !important;\ndisplay:block !important;\npadding-left:0px !important;\nmargin-left:6px !important;\n}\ntable[class=3Douter] .avatar-mid {\npadding:0px !important;\nheight: 32px !important;\n}\ntable[class=3Douter] .avatar-mid img {\nwidth: 32px !important;\nheight: 32px !important;\ndisplay:block !important;\nmargin-left:10px !important;\npadding-left: 0px !important;\n}\ntable[class=3Douter] .real_name{\nfont-size:13px !important;\npadding-top:2px !important;\n}\ntable[class=3Douter] .nick_name{\nfont-size:12px !important;\ndisplay:block !important;\n}\ntable[class=3Douter] .nick_name_big{\nfont-size:12px !important;\ndisplay:block !important;\n}\ntable[class=3Douter] .date{\npadding-left:9px !important;\npadding-top: 4px !important;\n}\ntable[class=3Douter] .photo{\npadding-left:0px !important;\npadding-bottom:12px !important;\npadding-top:6px !important;\nborder: 0px !important;\ndisplay:block !important;\n}\ntable[class=3Douter] .leading-blurb-text-spacer {\nwidth:23px !important;\n}\ntable[class=3Douter] .leading-blurb-username-spacer {\npadding-left: 0px !important;\n}\ntable[class=3Douter] .leading-blurb-text-small{\nfont-size:12px !important;\nline-height:16px !important;\nmargin-bottom:0px !important;\npadding-top:2px !important;\n}\ntable[class=3Douter] .leading-blurb-text{\nfont-size:18px !important;\npadding-left:9px !important;\nline-height:22px !important;\npadding-top: 6px !important;\n}\ntable[class=3Douter] .section-container {\n-moz-border-bottom-colors: none !important;\n-moz-border-left-colors: none !important;\n-moz-border-right-colors: none !important;\n-moz-border-top-colors: none !important;\nbackground: none repeat scroll 0 0 white !important;\nborder-color: -moz-use-text-color !important;\nborder-image: none !important;\nborder-radius: 0 0 0 0 !important;\nborder-style: none !important;\nborder-width: medium 0 !important;\nmargin: 0 auto !important;\npadding: 0 0 0px !important;\nwidth: 320px !important;\n}\ntable[class=3Douter] .section-container-inner {\nmargin: 0 auto !important;\npadding: 0 0 0px !important;\nwidth: 320px !important;\n}\ntable[class=3Douter] .avatar-spacer {\npadding-left:10px !important;\n}\ntable[class=3Douter] .btn-follow {\nbackground: none repeat scroll 0 0 transparent !important;\nborder: medium none !important;\nborder-radius: 0 0 0 0 !important;\nheight: auto !important;\npadding-right: 8px !important;\nwhite-space: nowrap !important;\n}\ntable[class=3Douter] .btn-follow .spacer {\ndisplay: none !important;\n}\ntable[class=3Douter] .btn-follow a {\nbackground-image: url(\"https://ea.twimg.com/email/t1/btn_follow_ios.png\");\ndisplay: block !important;\nheight: 30px !important;\nwidth: 50px !important;\n}\ntable[class=3Douter] .btn-follow img {\ndisplay: none !important;\n}\ntable[class=3Douter] .btn-follow .follow_text {\ndisplay: none !important;\n}\ntable[class=3Douter] .section-container-top, table[class=3Douter] .section-=\ncontainer-bottom, table[class=3Douter] .date-top, table[class=3D\"outer\"] .d=\nate-bottom, table[class=3Douter] .main-avatar-left, table[class=3Douter] .s=\ntyled-x-top {\ndisplay: none !important;\n}\ntable[class=3Douter] .mobile-relative-move {\nposition: relative !important;\ndisplay: block !important;\nmargin-left: -57px !important;\n}\n}\n</style>\n<table width=3D\"100%\" cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\">\n<tbody>\n<tr>\n<td class=3D\"spacer cut\" width=3D\"14\" style=3D\"font-size:1px;font-size:1px;=\n\">&nbsp;</td>\n<td>\n<table width=3D\"100%\" border=3D\"0\" cellspacing=3D\"0\" cellpadding=3D\"0\">\n<tbody>\n<tr>\n<td valign=3D\"top\">\n<table class=3D\"section-container\" width=3D\"100%\" border=3D\"0\" cellspacing=\n=3D\"0\" cellpadding=3D\"0\" style=3D\"background:white;\">\n<tbody>\n<tr>\n<td>\n<table class=3D\"section-container-inner\" width=3D\"100%\" border=3D\"0\" cellsp=\nacing=3D\"0\">\n<tbody>\n<tr>\n<td>\n<table width=3D\"100%\" border=3D\"0\" cellspacing=3D\"0\" cellpadding=3D\"0\" clas=\ns=3D\"nick\" style=3D\"height:32px;\">\n<tbody>\n<tr>\n<td class=3D\"spacer\" width=3D\"8\" style=3D\"font-size:1px;font-size:1px;\">&nb=\nsp;</td>\n<td class=3D\"avatar-small\" width=3D\"32\"> <a href=3D\"https://twitter.com/i/r=\nedirect?url=3Dhttps%3A%2F%2Ftwitter.com%2Fladyniasan%2Fstatus%2F37889888932=\n5023234&amp;sig=3D5ee1dda7c05e55a563bc2e9d90df8ca770fcb8b1&amp;uid=3D796789=\n&amp;iid=3D6a6de5f5-2df4-4749-b78b-1d143f237eb9&amp;nid=3D4+1282&amp;t=3D1\"=\n style=3D\"border:none;color:#0084b4;text-decoration:none;\"><img src=3D\"http=\ns://si0.twimg.com/profile_images/2575747875/jufjporheed6vdomifdb_reasonably=\n_small.jpeg\" alt=3D\"\" width=3D\"32\" height=3D\"32\" border=3D\"0\" align=3D\"left=\n\" style=3D\"border:0;background-color:#ffffff;border-radius:4px;border:none;=\nmargin-right:0px;display:inline-block;float:left;\" /></a></td>\n<td class=3D\"avatar-spacer\" valign=3D\"top\" style=3D\"padding-left:21px;\"> <s=\npan class=3D\"real_name\" style=3D\"font-family:'Helvetica Neue', Helvetica, A=\nrial, sans-serif;margin:0;padding:0;font-size:14px;line-height:14px;font-we=\night:bold;text-decoration:none;color:#333333;display:block;\"><a href=3D\"htt=\nps://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2Fladyniasan%2F=\nstatus%2F378898889325023234&amp;sig=3D5ee1dda7c05e55a563bc2e9d90df8ca770fcb=\n8b1&amp;uid=3D796789&amp;iid=3D6a6de5f5-2df4-4749-b78b-1d143f237eb9&amp;nid=\n=3D4+1284&amp;t=3D1\" class=3D\"leading-blurb-username\" style=3D\"border:none;=\ncolor:#0084b4;text-decoration:none;font-weight:bold;font-size:14px;color:#3=\n33333;\">Nadia EL-Imam</a></span> <span class=3D\"nick_name\" style=3D\"font-fa=\nmily:'Helvetica Neue', Helvetica, Arial, sans-serif;font-weight:normal;font=\n-size:12px;line-height:14px;color:#999999;text-decoration:none;\"><a href=3D=\n\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2Fladyniasa=\nn%2Fstatus%2F378898889325023234&amp;sig=3D5ee1dda7c05e55a563bc2e9d90df8ca77=\n0fcb8b1&amp;uid=3D796789&amp;iid=3D6a6de5f5-2df4-4749-b78b-1d143f237eb9&amp=\n;nid=3D4+1284&amp;t=3D1\" class=3D\"screenName-link\" style=3D\"border:none;col=\nor:#0084b4;text-decoration:none;color:#999999 !important;\">@ladyniasan</a><=\n/span> </td>\n<td align=3D\"right\" class=3D\"follow\" valign=3D\"top\" style=3D\"padding-right:=\n1px;padding-right:1px;\">\n<table bgcolor=3D\"#f1f1f1\" class=3D\"button btn-follow\" border=3D\"0\" cellspa=\ncing=3D\"0\" cellpadding=3D\"0\" style=3D\"white-space:nowrap;background:#f1f1f1=\n url(&quot;https://ea.twimg.com/email/t1/bg-follow-button.jpg&quot;) top re=\npeat-x;border-radius:5px;border-color:#cccccc;border-style:solid;border-wid=\nth:1px;text-align:center;height:28px;color:#333333;\">\n<tbody>\n<tr>\n<td class=3D\"spacer\" width=3D\"10\" style=3D\"font-size:1px;font-size:1px;\">&n=\nbsp;</td>\n<td height=3D\"28\" align=3D\"center\"> <span class=3D\"button_text\" style=3D\"co=\nlor:#333333;font-size:13px;font-weight:bold;text-shadow:1px 1px 0px #ffffff=\n;white-space:nowrap;overflow:hidden;padding:0px;margin:0px;font-family:'Hel=\nvetica Neue', Helvetica, Arial, sans-serif;color:#ffffff;font-size:13px;fon=\nt-weight:bold;white-space:nowrap;overflow:hidden;padding:0px;margin:0px;fon=\nt-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\"> <a class=3D\"butt=\non_link\" href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter=\n.com%2Fintent%2Ffollow%3Fea_u%3D796789%26ea_e%3D1380539839%26screen_name%3D=\nladyniasan%26ea_s%3Dba4cd6f947b450b96238841ea150ef2973185e42&amp;sig=3D292b=\n8222f1d7618df55fdf0340a8c86f3370a268&amp;uid=3D796789&amp;iid=3D6a6de5f5-2d=\nf4-4749-b78b-1d143f237eb9&amp;nid=3D4+1297&amp;t=3D1\" style=3D\"border:none;=\ncolor:#0084b4;text-decoration:none;color:#ffffff;text-decoration:none;color=\n:#333333;text-decoration:none;color:#ffffff;text-decoration:none;\"> <img sr=\nc=3D\"https://ea.twimg.com/email/t1/blue_bird.png\" alt=3D\"Follow\" width=3D\"1=\n6\" height=3D\"14\" border=3D\"0\" align=3D\"absmiddle\" style=3D\"border: 0;border=\n:0;\" /> <span class=3D\"follow_text\" style=3D\"color:#333333;\">Follow</span> =\n</a> </span> </td>\n<td class=3D\"spacer\" width=3D\"10\" style=3D\"font-size:1px;font-size:1px;\">&n=\nbsp;</td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n<tr>\n<td class=3D\"section-holder\">\n<table width=3D\"100%\" border=3D\"0\" cellspacing=3D\"0\" cellpadding=3D\"0\">\n<tbody>\n<tr>\n<td class=3D\"ios leading-blurb\" valign=3D\"top\" colspan=3D\"3\" style=3D\"paddi=\nng: 0 0 2px 0;\">\n<table border=3D\"0\" cellspacing=3D\"0\" cellpadding=3D\"0\">\n<tbody>\n<tr>\n<td width=3D\"24\" class=3D\"spacer\" style=3D\"font-size:1px;font-size:1px;\">&n=\nbsp;</td>\n<td width=3D\"2\" bgcolor=3D\"#cccccc\" class=3D\"spacer\" style=3D\"font-size:1px=\n;font-size:1px;\">&nbsp;</td>\n<td width=3D\"33\" class=3D\"spacer leading-blurb-text-spacer\" style=3D\"font-s=\nize:1px;font-size:1px;\">&nbsp;</td>\n<td valign=3D\"top\" style=3D\"padding-bottom:12px;\">\n<div class=3D\"ios-blurb\">\n<p class=3D\"leading-blurb-text-small\" style=3D\"margin:0;color:#333333;font-=\nfamily:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:14px;line-h=\neight:17px;margin-bottom:7px;margin-top:0;margin:0;\">Hi <a class=3D\"screen-=\nname\" href=3D\"https://twitter.com/i/redirect?url=3Dhttp%3A%2F%2Ftwitter.com=\n%2Fbrennannovak%3Frefsrc%3Demail&amp;sig=3D892d2ca4c47b6be5d019b23c62727ac7=\na829a4c2&amp;uid=3D796789&amp;iid=3D6a6de5f5-2df4-4749-b78b-1d143f237eb9&am=\np;nid=3D4+1291&amp;t=3D1\" style=3D\"direction:ltr;unicode-bidi:embed;text-de=\ncoration:none;color:#999;border:none;color:#0084b4;text-decoration:none;\">@=\nbrennannovak</a> <a class=3D\"screen-name\" href=3D\"https://twitter.com/i/red=\nirect?url=3Dhttp%3A%2F%2Ftwitter.com%2FHerraBRE%3Frefsrc%3Demail&amp;sig=3D=\n2dfde72c09e3346a21859447861993614fa49e5b&amp;uid=3D796789&amp;iid=3D6a6de5f=\n5-2df4-4749-b78b-1d143f237eb9&amp;nid=3D4+1291&amp;t=3D1\" style=3D\"directio=\nn:ltr;unicode-bidi:embed;text-decoration:none;color:#999;border:none;color:=\n#0084b4;text-decoration:none;\">@HerraBRE</a><a href=3D\"https://twitter.com/=\ni/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2Fladyniasan%2Fstatus%2F37889888=\n9325023234&amp;sig=3D5ee1dda7c05e55a563bc2e9d90df8ca770fcb8b1&amp;uid=3D796=\n789&amp;iid=3D6a6de5f5-2df4-4749-b78b-1d143f237eb9&amp;nid=3D4+1287&amp;t=\n=3D1\" class=3D\"date\" style=3D\"text-decoration:none;color:#999;border:none;c=\nolor:#0084b4;text-decoration:none;font-family:'Helvetica Neue', Helvetica, =\nArial, sans-serif;font-size:12px;color:#bbbbbb;font-family:'Helvetica Neue'=\n, Helvetica, Arial, sans-serif;font-size:12px;color:#bbbbbb;white-space:now=\nrap;\"> - 14 Sep</a> </p>\n</div> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table>\n<table width=3D\"100%\" border=3D\"0\" cellspacing=3D\"0\" cellpadding=3D\"0\" clas=\ns=3D\"section-container\" style=3D\"background:white;\">\n<tbody>\n<tr>\n<td width=3D\"48\" valign=3D\"top\">\n<table width=3D\"48\" cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\">\n<tbody>\n<tr>\n<td class=3D\"avatar-mid\" width=3D\"48\"><a href=3D\"https://twitter.com/i/redi=\nrect?url=3Dhttps%3A%2F%2Ftwitter.com%2Fbrennannovak%2Fstatus%2F379564650884=\n370432&amp;sig=3Da57939d5ff73819c8fbfe85bd14eb733527110eb&amp;uid=3D796789&=\namp;iid=3D6a6de5f5-2df4-4749-b78b-1d143f237eb9&amp;nid=3D4+1262&amp;t=3D1\" =\nstyle=3D\"border:none;color:#0084b4;text-decoration:none;\"><img src=3D\"https=\n://si0.twimg.com/profile_images/3465346200/3887a9d367bf61ad50fa794a32c7ce21=\n_reasonably_small.jpeg\" alt=3D\"\" width=3D\"48\" height=3D\"48\" border=3D\"0\" al=\nign=3D\"left\" style=3D\"border:0;display:block;border-radius:4px;width:100%;\"=\n /></a></td>\n</tr>\n</tbody>\n</table> </td>\n<td width=3D\"100%\" valign=3D\"top\">\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" width=3D\"100%\">\n<tbody>\n<tr>\n<td class=3D\"leading-blurb-username-spacer\" style=3D\"padding-left:10px;\" va=\nlign=3D\"top\"> <span class=3D\"real_name\" style=3D\"font-family:'Helvetica Neu=\ne', Helvetica, Arial, sans-serif;margin:0;padding:0;font-size:14px;line-hei=\nght:14px;font-weight:bold;text-decoration:none;color:#333333;display:block;=\n\"> <a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.co=\nm%2Fbrennannovak%2Fstatus%2F379564650884370432&amp;sig=3Da57939d5ff73819c8f=\nbfe85bd14eb733527110eb&amp;uid=3D796789&amp;iid=3D6a6de5f5-2df4-4749-b78b-1=\nd143f237eb9&amp;nid=3D4+1264&amp;t=3D1\" class=3D\"leading-blurb-username\" st=\nyle=3D\"border:none;color:#0084b4;text-decoration:none;font-weight:bold;font=\n-size:14px;color:#333333;\">Brennan Novak</a> </span> <span class=3D\"nick_na=\nme_big\" dir=3D\"ltr\" style=3D\"font-family:'Helvetica Neue', Helvetica, Arial=\n, sans-serif;font-weight:normal;font-size:14px;line-height:17px;color:#9999=\n99;text-decoration:none;\"> <a href=3D\"https://twitter.com/i/redirect?url=3D=\nhttps%3A%2F%2Ftwitter.com%2Fbrennannovak%2Fstatus%2F379564650884370432&amp;=\nsig=3Da57939d5ff73819c8fbfe85bd14eb733527110eb&amp;uid=3D796789&amp;iid=3D6=\na6de5f5-2df4-4749-b78b-1d143f237eb9&amp;nid=3D4+1264&amp;t=3D1\" class=3D\"sc=\nreenName-link\" style=3D\"border:none;color:#0084b4;text-decoration:none;colo=\nr:#999999 !important;\">@brennannovak</a> </span> </td>\n</tr>\n</tbody>\n</table>\n<table border=3D\"0\" cellspacing=3D\"0\" cellpadding=3D\"0\" class=3D\"mobile-rel=\native-move\">\n<tbody>\n<tr>\n<td width=3D\"10\" class=3D\"spacer\" style=3D\"font-size:1px;font-size:1px;\">&n=\nbsp;</td>\n<td>\n<div class=3D\"ios-blurb\">\n<p class=3D\"leading-blurb-text\" dir=3D\"ltr\" style=3D\"margin:0;color:#333333=\n;font-family:Georgia,'Times New Roman',serif;font-size:22px;line-height:27p=\nx;margin-bottom:0;margin-top:0;\"><a class=3D\"screen-name\" href=3D\"https://t=\nwitter.com/i/redirect?url=3Dhttp%3A%2F%2Ftwitter.com%2Fladyniasan%3Frefsrc%=\n3Demail&amp;sig=3D8a6f1eb11267c53fdfef59b588adcab8c657d88a&amp;uid=3D796789=\n&amp;iid=3D6a6de5f5-2df4-4749-b78b-1d143f237eb9&amp;nid=3D4+1271&amp;t=3D1\"=\n style=3D\"direction:ltr;unicode-bidi:embed;border:none;color:#0084b4;text-d=\necoration:none;\">@ladyniasan</a> hi :) <a class=3D\"screen-name\" href=3D\"htt=\nps://twitter.com/i/redirect?url=3Dhttp%3A%2F%2Ftwitter.com%2FHerraBRE%3Fref=\nsrc%3Demail&amp;sig=3D2dfde72c09e3346a21859447861993614fa49e5b&amp;uid=3D79=\n6789&amp;iid=3D6a6de5f5-2df4-4749-b78b-1d143f237eb9&amp;nid=3D4+1271&amp;t=\n=3D1\" style=3D\"direction:ltr;unicode-bidi:embed;border:none;color:#0084b4;t=\next-decoration:none;\">@HerraBRE</a> and I heard about this cave thing- soun=\nds pretty cool, we'd like to learn more and check it out in the spring!</p>\n</div> </td>\n</tr>\n</tbody>\n</table>\n<table class=3D\"date mobile-relative-move\" border=3D\"0\" cellspacing=3D\"0\" c=\nellpadding=3D\"0\" style=3D\"font-family:'Helvetica Neue', Helvetica, Arial, s=\nans-serif;font-size:12px;color:#bbbbbb;font-family:'Helvetica Neue', Helvet=\nica, Arial, sans-serif;font-size:12px;color:#bbbbbb;white-space:nowrap;\">\n<tbody>\n<tr>\n<td width=3D\"10\" class=3D\"spacer\" style=3D\"font-size:1px;font-size:1px;\">&n=\nbsp;</td>\n<td class=3D\"date-top spacer\" height=3D\"7\" style=3D\"font-size:1px;font-size=\n:1px;\">&nbsp;</td>\n</tr>\n<tr>\n<td width=3D\"10\" class=3D\"spacer\" style=3D\"font-size:1px;font-size:1px;\">&n=\nbsp;</td>\n<td><a class=3D\"date\" href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A=\n%2F%2Ftwitter.com%2Fbrennannovak%2Fstatus%2F379564650884370432&amp;sig=3Da5=\n7939d5ff73819c8fbfe85bd14eb733527110eb&amp;uid=3D796789&amp;iid=3D6a6de5f5-=\n2df4-4749-b78b-1d143f237eb9&amp;nid=3D4+1267&amp;t=3D1\" style=3D\"border:non=\ne;color:#0084b4;text-decoration:none;font-family:'Helvetica Neue', Helvetic=\na, Arial, sans-serif;font-size:12px;color:#bbbbbb;font-family:'Helvetica Ne=\nue', Helvetica, Arial, sans-serif;font-size:12px;color:#bbbbbb;white-space:=\nnowrap;\">11:17 AM - 16 Sep 13</a></td>\n</tr>\n<tr>\n<td width=3D\"10\" class=3D\"spacer\" style=3D\"font-size:1px;font-size:1px;\">&n=\nbsp;</td>\n<td class=3D\"date-bottom spacer\" height=3D\"12\" style=3D\"font-size:1px;font-=\nsize:1px;\">&nbsp;</td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n<td class=3D\"spacer cut\" width=3D\"14\" style=3D\"font-size:1px;font-size:1px;=\n\">&nbsp;</td>\n</tr>\n</tbody>\n</table>\n<table width=3D\"500\" border=3D\"0\" align=3D\"center\" cellpadding=3D\"0\" cellsp=\nacing=3D\"0\" class=3D\"separator-padding\">\n<tbody>\n<tr>\n<td width=3D\"500\" height=3D\"12\"></td>\n</tr>\n</tbody>\n</table>\n<hr class=3D\"separator\" style=3D\"height:1px;border:0px;margin:0px;backgroun=\nd-color:#e8e8e8;width:520px;\" />\n<!-- -------------------- -->\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" width=3D\"500\" align=3D\"center\" c=\nlass=3D\"action\">\n<tbody>\n<tr>\n<td height=3D\"12\" colspan=3D\"2\"></td>\n</tr>\n<tr>\n<td>\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\">\n<tbody>\n<tr>\n<td align=3D\"left\">  <style type=3D\"text/css\">\n@media only screen and (max-device-width: 480px) {\ntable[class=3Dbutton-template] {\nwidth: 130px !important;\n}\ntr[class=3Dbutton-template-vertical-padding] {\nheight: 3px !important;\n}\ntable[class=3Dbutton-template] .hide-mobile {\ndisplay: none;\n}\n}\n@media only screen and (min-device-width: 768px) and (max-device-width: 102=\n4px) {\ntd[class=3Dbutton-template] {\npadding-left: 10px !important;\npadding-right: 10px !important;\n}\n}\n</style>\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" class=3D\"button-tem=\nplate\" style=3D\"background-image:url('https://ea.twimg.com/email/t1/button_=\nbg_long.png');background-color:#33a9e5;border-radius:5px;height:28px;border=\n:1px solid #28C;word-wrap:break-word;text-align:center;\">\n<tbody>\n<tr>\n<td align=3D\"center\" style=3D\"padding-left:15px;padding-right:15px;\">\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" class=3D\"button-tem=\nplate-inner\">\n<tbody>\n<tr>\n<td style=3D\"padding-left:15px;padding-right:15px;padding-left:0px;padding-=\nright:0px;\"> <a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2F=\ntwitter.com%2Fintent%2Ftweet%3Fin_reply_to%3D379564650884370432%26refsrc%3D=\nemail&amp;sig=3D5996e54e9974dc804cfe5ce1e02134240573d3cf&amp;uid=3D796789&a=\nmp;iid=3D6a6de5f5-2df4-4749-b78b-1d143f237eb9&amp;nid=3D4+1281&amp;t=3D1\" s=\ntyle=3D\"border:none;color:#0084b4;text-decoration:none;color:white;\"> <img =\nsrc=3D\"https://ea.twimg.com/email/t1/reply_arrow.png\" alt=3D\"\" style=3D\"bor=\nder:0;\" /> </a> </td>\n<td width=3D\"6\" class=3D\"button-template-inner-vertical-padding\" style=3D\"p=\nadding-left:15px;padding-right:15px;padding-left:0px;padding-right:0px;heig=\nht:6px;\"> <a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwi=\ntter.com%2Fintent%2Ftweet%3Fin_reply_to%3D379564650884370432%26refsrc%3Dema=\nil&amp;sig=3D5996e54e9974dc804cfe5ce1e02134240573d3cf&amp;uid=3D796789&amp;=\niid=3D6a6de5f5-2df4-4749-b78b-1d143f237eb9&amp;nid=3D4+1281&amp;t=3D1\" styl=\ne=3D\"border:none;color:#0084b4;text-decoration:none;color:white;\"> </a> </t=\nd>\n<td height=3D\"28\" class=3D\"button-template-font\" style=3D\"color:white;font-=\nsize:13px;font-weight:bold;font-family:'Helvetica Neue', Helvetica, Arial, =\nsans-serif;padding-left:15px;padding-right:15px;padding-left:0px;padding-ri=\nght:0px;\"> <a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftw=\nitter.com%2Fintent%2Ftweet%3Fin_reply_to%3D379564650884370432%26refsrc%3Dem=\nail&amp;sig=3D5996e54e9974dc804cfe5ce1e02134240573d3cf&amp;uid=3D796789&amp=\n;iid=3D6a6de5f5-2df4-4749-b78b-1d143f237eb9&amp;nid=3D4+1281&amp;t=3D1\" sty=\nle=3D\"border:none;color:#0084b4;text-decoration:none;color:white;\"> Reply<s=\npan class=3D\"hide-mobile\"> to <span class=3D\"screen-name\" style=3D\"directio=\nn:ltr;unicode-bidi:embed;\">@brennannovak</span></span> </a> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n<td class=3D\"tweet-action\" style=3D\"line-height:12px;padding-left:10px;font=\n-size:13px;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\"> <a=\n href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2Fi=\nntent%2Fretweet%3Fea_u%3D796789%26ea_e%3D1380539839%26tweet_id%3D3795646508=\n84370432%26ea_s%3D5140bf7ead11d4073fafcfa0d167a766aff2b1cb%26refsrc%3Demail=\n&amp;sig=3D717106487eaac9f6258a85852ca4a6d0bfc7720f&amp;uid=3D796789&amp;ii=\nd=3D6a6de5f5-2df4-4749-b78b-1d143f237eb9&amp;nid=3D4+1279&amp;t=3D1\" style=\n=3D\"border:none;color:#0084b4;text-decoration:none;\"><img src=3D\"https://ea=\n.twimg.com/email/t1/actions/icon-blue-retweet.png\" width=3D\"30\" height=3D\"1=\n5\" style=3D\"border:0;vertical-align:bottom;\" />Retweet</a> </td>\n<td class=3D\"tweet-action\" style=3D\"line-height:12px;padding-left:10px;font=\n-size:13px;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\"> <a=\n href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2Fi=\nntent%2Ffavorite%3Fea_u%3D796789%26ea_e%3D1380539839%26tweet_id%3D379564650=\n884370432%26ea_s%3D8a12466dc4cdd4849071f02be9cdb8b2e969ae51%26refsrc%3Demai=\nl&amp;sig=3D27dae7716f76e0e33f3cf43a13477d7e9c7944f4&amp;uid=3D796789&amp;i=\nid=3D6a6de5f5-2df4-4749-b78b-1d143f237eb9&amp;nid=3D4+1280&amp;t=3D1\" style=\n=3D\"border:none;color:#0084b4;text-decoration:none;\"><img src=3D\"https://ea=\n.twimg.com/email/t1/actions/icon-blue-favourite.png\" width=3D\"21\" height=3D=\n\"15\" style=3D\"border:0;vertical-align:bottom;\" /> Favorite</a> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n<tr>\n<td height=3D\"12\" colspan=3D\"2\"></td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n<tr>\n<td>\n<table bgcolor=3D\"#eeeeee\" border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\"=\n width=3D\"670\" class=3D\"frame footer\" style=3D\"background-color:#eee;backgr=\nound-image:url('https://ea.twimg.com/email/t1/shadow-bottom.jpg');backgroun=\nd-position:top;background-repeat:repeat-x;border-top-color:#ddd;border-top-=\nstyle:solid;border-top-width:1px;margin-left:auto;margin-right:auto;positio=\nn:relative;\">\n<tbody>\n<tr>\n<td colspan=3D\"4\" height=3D\"16\" class=3D\"footer-padding-top\"></td>\n</tr>\n<tr>\n<td class=3D\"col cut\" style=3D\"width:85px;\"></td>\n<td class=3D\"footer_body media_footer\" style=3D\"font-family:'Helvetica Neue=\n', Helvetica, Arial, sans-serif;font-size:12px;line-height:17px;color:#777;=\ntext-shadow:0 1px 0 #fff;\">\n<div>\nForgot your Twitter password?\n<a class=3D\"reset\" href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F=\n%2Ftwitter.com%2Faccount%2Fresend_password&amp;sig=3Df5d73c6d6c8577b4b95889=\n880e513526930e35d0&amp;uid=3D796789&amp;iid=3D6a6de5f5-2df4-4749-b78b-1d143=\nf237eb9&amp;nid=3D4+24&amp;t=3D1\" style=3D\"border:none;color:#0084b4;text-d=\necoration:none;\">Get instructions on how to reset it.</a>\n</div>\n<div>\nYou can also\n<a href=3D\"https://twitter.com/i/u?t=3D1&amp;sig=3D1049383876de2b1d27e6fb34=\n1a6593d4b6b42a0a&amp;iid=3D6a6de5f5-2df4-4749-b78b-1d143f237eb9&amp;uid=3D7=\n96789&amp;nid=3D4+26\" style=3D\"border:none;color:#0084b4;text-decoration:no=\nne;\">unsubscribe from these emails</a>\n<span class=3D\"reset\">or change your <a href=3D\"https://twitter.com/i/redir=\nect?url=3Dhttps%3A%2F%2Ftwitter.com%2Fsettings%2Fnotifications&amp;sig=3Ddf=\ne6fc4e06fdfbeffa510642835ff34af916458c&amp;uid=3D796789&amp;iid=3D6a6de5f5-=\n2df4-4749-b78b-1d143f237eb9&amp;nid=3D4+27&amp;t=3D1\" style=3D\"border:none;=\ncolor:#0084b4;text-decoration:none;\">notification settings</a>. Need <a hre=\nf=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Fsupport.twitter.com=\n&amp;sig=3Dd5a93ec8dc3f5086b47a0022aac6b1bd71661580&amp;uid=3D796789&amp;ii=\nd=3D6a6de5f5-2df4-4749-b78b-1d143f237eb9&amp;nid=3D4+97&amp;t=3D1\" style=3D=\n\"border:none;color:#0084b4;text-decoration:none;\">help</a>?</span>\n</div>\n<div class=3D\"reset\">\nIf you received this message in error and did not sign up for Twitter, clic=\nk\n<a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2=\nFaccount%2Fnot_my_account%2FHerraBRE%2F546G5-B68A8-137933&amp;sig=3Da5717e7=\n5936f575368d18624780550b8135feb9b&amp;uid=3D796789&amp;iid=3D6a6de5f5-2df4-=\n4749-b78b-1d143f237eb9&amp;nid=3D4+25&amp;t=3D1\" style=3D\"border:none;color=\n:#0084b4;text-decoration:none;\">not my account</a>.\n</div>\n<div>\n<a href=3D\"#\" class=3D\"address\" style=3D\"border:none;color:#0084b4;text-dec=\noration:none;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;tex=\nt-decoration:none;font-size:11px;line-height:17px;color:#999999;text-shadow=\n:0 1px 0 #fff;\">Twitter, Inc. 1355 Market St., Suite 900 <span class=3D\"nob=\nreak\" style=3D\"white-space:nowrap;\">San Francisco, CA 94103 </span></a>\n</div> </td>\n<td class=3D\"col cut\" style=3D\"width:85px;\"></td>\n</tr>\n<tr>\n<td colspan=3D\"3\" class=3D\"footer-padding-bottom\" height=3D\"25\"></td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table>\n<img width=3D\"1\" height=3D\"1\" src=3D\"loadimage\" style=3D\"border:0;\" />\n</body>\n</html>\n\n------=_Part_24299859_24672245.1379330239558--\n"
  },
  {
    "path": "mailpile/tests/data/Maildir/cur/1379857166.25979_7.hottie,2,S",
    "content": "Return-path: <b0965969b92bre=example.com@bounce.twitter.com>\nEnvelope-to: bre@localhost\nDelivery-date: Thu, 19 Sep 2013 09:42:54 +0000\nReceived: from localhost ([::1] helo=hottie)\n\tby hottie with esmtp (Exim 4.80)\n\t(envelope-from <b0965969b92bre=example.com@bounce.twitter.com>)\n\tid 1VMalB-0007Z5-LV\n\tfor bre@localhost; Thu, 19 Sep 2013 09:42:53 +0000\nDelivered-To: fake@example.com\nReceived: from gmail-pop.l.google.com [173.194.73.109]\n\tby hottie with POP3 (fetchmail-6.3.21)\n\tfor <bre@localhost> (single-drop); Thu, 19 Sep 2013 09:42:53 +0000 (GMT)\nReceived: by 10.58.13.104 with SMTP id g8csp142274vec;\n        Mon, 16 Sep 2013 19:59:52 -0700 (PDT)\nX-Received: by 10.220.164.70 with SMTP id d6mr8657279vcy.19.1379386788205;\n        Mon, 16 Sep 2013 19:59:48 -0700 (PDT)\nDomainKey-Status: good\nReceived-SPF: pass (google.com: domain of b0965969b92bre=example.com@bounce.twitter.com designates 199.16.156.167 as permitted sender) client-ip=199.16.156.167;\nReceived: by 10.220.239.138 with POP3 id kw10mf5139208vcb.20;\n        Mon, 16 Sep 2013 19:59:47 -0700 (PDT)\nX-Gmail-Fetch-Info: fake@example.com 1 example.com 110 bre\nReceived: from spring-chicken-bb.twitter.com (spring-chicken-bb.twitter.com [199.16.156.167])\n\tby example.com (8.12.11.20060308/8.12.11) with ESMTP id r8H2Qk2w020167\n\tfor <fake@example.com>; Tue, 17 Sep 2013 02:26:47 GMT\nDKIM-Signature: v=1; a=rsa-sha1; d=twitter.com; s=dkim-201303; c=relaxed/relaxed;\n\tq=dns/txt; i=@twitter.com; t=1379384805;\n\th=From:Subject:Date:To;\n\tbh=XzIf4/AyaAHm6IhBlrHxhe04LYc=;\n\tb=OltMNg1hfrUtSUj86HB/5EsVfRI8RGj899WV6qk/TGl1dtxLOeQ5bnIQPZpuIy/0\n\tWRyRyirGPixZPOfO3uWBoZtpZpfEnmai8UsaTugrJsv1fMBZUB8B4Pwev+OP219U\n\toZxEE7Keo8UHa4bKl7nQdsGsDABv3w9lF9AcnB1AWqI=;\nX-MSFBL: YnJlQGtsYWtpLm5ldEBhdGxhLWJlYi0wOC1zcjEtRXZlcnl0aGluZy4xODdARXZl\n\tcnl0aGluZ0A=\nDate: Tue, 17 Sep 2013 02:26:45 +0000\nFrom: Twitter <info@twitter.com>\nTo: \"Bjarni R. Einarsson\" <fake@example.com>\nSubject: Bjarni R. Einarsson, you have new followers on Twitter!\nMIME-Version: 1.0\nContent-Type: multipart/alternative; \n\tboundary=\"----=_Part_2325007_782750442.1379384805206\"\nPrecedence: Bulk\nMessage-ID: <1B.55.54750.5EDB7325@spring-chicken.twitter.com>\nX-BRE-Whitelisted: procmail (info@twitter.com)\nContent-Length: 37422\nLines: 840\n\n------=_Part_2325007_782750442.1379384805206\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: quoted-printable\n\nBjarni R. Einarsson,\nYou have new followers on Twitter!\n\n------------------------\n\nTom Wills @TomWills\nFreelance data journalist. Visualisation, analysis, multimedia. Investigati=\nve Journalism MA, @cityjournalism. [RT=E2=89=A0endorse|PGP 0x2D05954C|OTR t=\nomw@jabberd.eu]\nhttps://twitter.com/TomWills\n\nReport for spam: https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter=\n.com%2Fuser_spam_reports%2FHerraBRE%2Freport%2FTomWills%3Ft%3D1%26sig%3D1b5=\n26e1caac85b92337d61e2b0c29521bfda0bff%26iid%3D86b54be0-b3b0-474c-b314-d0e52=\n52ad118%26uid%3D796789%26accused%3DTomWills%26nid%3D10%2B465%2B20130917&sig=\n=3Dbb78c87850765ceb5736ff7bf999413b763f5478&uid=3D796789&iid=3D86b54be0-b3b=\n0-474c-b314-d0e5252ad118&nid=3D10+465+20130917&t=3D1\n\n\nSamuel Faunt @fauntty\n\nhttps://twitter.com/fauntty\n\nReport for spam: https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter=\n.com%2Fuser_spam_reports%2FHerraBRE%2Freport%2Ffauntty%3Ft%3D1%26sig%3Dc3fe=\n4e0df1c0fd671fd3af2f5bb37f8ed8c50289%26iid%3D86b54be0-b3b0-474c-b314-d0e525=\n2ad118%26uid%3D796789%26accused%3Dfauntty%26nid%3D10%2B466%2B20130917&sig=\n=3D7abe4dacc1b39b3f5aece56352a602ba93df8161&uid=3D796789&iid=3D86b54be0-b3b=\n0-474c-b314-d0e5252ad118&nid=3D10+466+20130917&t=3D1\n\n\n=C3=86gir =C3=96rn S=C3=ADmonarson @agirorn\nWeb JuJu @ http://t.co/YG6nOzpc8t, rubyist and some other stuff...\nhttps://twitter.com/agirorn\n\nReport for spam: https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter=\n.com%2Fuser_spam_reports%2FHerraBRE%2Freport%2Fagirorn%3Ft%3D1%26sig%3Dea90=\n3a5c3ffb2ab56828a9d7de723c972eecb868%26iid%3D86b54be0-b3b0-474c-b314-d0e525=\n2ad118%26uid%3D796789%26accused%3Dagirorn%26nid%3D10%2B467%2B20130917&sig=\n=3D10a2d48fe96469aa34dc8df04be13a0d3980ec53&uid=3D796789&iid=3D86b54be0-b3b=\n0-474c-b314-d0e5252ad118&nid=3D10+467+20130917&t=3D1\n\n\nSee all your followers:\nhttps://twitter.com/HerraBRE/followers\n\n--=20\n\nForgot your Twitter password? Get instructions on how to reset it:\nhttps://twitter.com/account/resend_password\n\nYou can also unsubscribe from these emails or change your notification sett=\nings:\nhttps://twitter.com/i/u?t=3D1&sig=3Daf396a74ff663ce86bf8f2fed5de7ce49ad759f=\ne&iid=3D86b54be0-b3b0-474c-b314-d0e5252ad118&uid=3D796789&nid=3D10+26+20130=\n917\nhttps://twitter.com/settings/notifications\n\nNeed help?\nhttps://support.twitter.com\n\nIf you received this message in error and did not sign up for a Twitter acc=\nount, click on the url below:\nhttps://twitter.com/account/not_my_account/HerraBRE/87D2F-9EF9G-137938\n------=_Part_2325007_782750442.1379384805206\nContent-Type: text/html; charset=UTF-8\nContent-Transfer-Encoding: quoted-printable\n\n<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/htm=\nl4/strict.dtd\">\n<html>\n<head>\n<meta http-equiv=3D\"Content-Type\" content=3D\"text/html; charset=3Dutf-8\" />\n<meta name=3D\"viewport\" content=3D\"width=3Ddevice-width, minimum-scale=3D1.=\n0, maximum-scale=3D1.0, user-scalable=3D0\" />\n<meta name=3D\"apple-mobile-web-app-capable\" content=3D\"yes\" />\n<style type=3D\"text/css\">\n@media only screen and (max-device-width: 480px) {\ntable[class=3Douter] .global-width-670-to-320 {\nwidth: 320px !important;\n}\ntable[class=3Douter] .global-width-520-to-320 {\nwidth: 320px !important;\n}\ntable[class=3Douter] .global-width-500-to-300 {\nwidth: 300px !important;\n}\ntable[class=3Douter] .global-separator-padding {\nheight: 8px !important;\n}\ntable[class=3Douter] .global-shrinking-to-0 {\nheight: 0 !important;\n}\ntable[class=3Douter] .global-shrinking-to-10 {\nheight: 10px !important;\n}\ntable[class=3Douter] .global-h1 {\nfont-size: 14px !important;\n}\n\ntable[class=3Douter] .cut {\nwidth: 0 !important;\npadding: 0 !important;\n}\ntable[class=3Douter] .vcut {\nheight: 0 !important;\npadding: 0 !important;\n}\ntable[class=3Douter] img.cut {\ndisplay: none !important;\nwidth: 0 !important;\nheight: 0 !important;\n}\ntable[class=3Douter] .cut span, table[class=3Douter] .cut a {\ndisplay: none !important;\n}\ntable[class=3Douter] .frame {\nwidth: 320px !important;\nborder-left: 0 !important;\nborder-right: 0 !important;\n}\ntable[class=3Douter] .main_header.media_header {\nwidth: 300px !important;\nheight: 55px !important;\n}\ntable[class=3Douter] .header_left {\nheight: 55px !important;\n}\ntable[class=3Douter] .logo_header {\nheight: 62px !important;\n}\ntable[class=3Douter] .main_name {\nfont-size: 12px !important;\n}\ntable[class=3Douter] .subtitle {\nfont-size: 12px !important;\n}\ntable[class=3Douter] .media_main {\nwidth: 300px !important;\n}\ntable[class=3Douter] .media_main .intro {\nwidth: 260px !important;\nfont-size: 14px !important;\n}\ntable[class=3Douter] .media_main .intro2 {\nfont-size: 14px !important;\n}\ntable[class=3Douter] .media_main .suggestions {\nfont-size: 14px !important;\n}\ntable[class=3Douter] .media_more {\nwidth: 300px !important;\nborder-radius: 0 !important;\nbackground: #fff !important;\nborder-top: 1px solid #e8e8e8 !important;\n}\ntable[class=3Douter] .media_button {\nwidth: 300px !important;\nbackground-color: #209ae4 !important;\npadding-top: 3px !important;\npadding-bottom: 3px !important;\n}\ntable[class=3Douter] .media_footer {\nfont-size: 11px !important;\nline-height: 14px !important;\npadding: 0 10px !important;\n}\ntable[class=3Douter] .address {\nfont-size: 11px !important;\nline-height: 14px !important;\ndisplay: block !important;\n}\ntable[class=3Douter] td[class=3Dfooter-padding-top] {\nheight: 12px !important;\n}\ntable[class=3Douter] td[class=3Dfooter-padding-bottom] {\nheight: 17px !important;\n}\ntable[class=3Douter] .media_footer br {\ndisplay: none !important;\n}\ntable[class=3Douter] .spacer.ios {\ndisplay: none !important;\n}\ntable[class=3Douter] .reset {\ndisplay: block !important;\npadding-bottom: 4px !important;\n}\ntable[class=3Douter] .media_logo_div {\ndisplay: block !important;\nposition: absolute !important;\nleft: 274px !important;\ntop: 0 !important;\nbackground-image: url('https://ea.twimg.com/email/t1/ribbon.png') !importan=\nt;\nbackground-size: 100% 100% !important;\nwidth: 36px !important;\nheight: 68px !important;\nz-index: 1 !important;\n}\ntable[class=3Demployee-only-padding-top-bottom] {\nwidth: 300px !important;\n}\ntable[class=3Demployee-only] {\nwidth: 300px !important;\npadding: 10px 0;\n}\ntd[class=3Dheader_padding] {\nheight: 55px !important;\n}\n}\n\n@media only screen and (min-device-width: 768px) and (max-device-width: 102=\n4px) {\ntable[class=3Douter] .media_button {\npadding-top: 3px !important;\npadding-bottom: 3px !important;\nbackground-color: #209ae4 !important;\n}\ntable[class=3Douter] .media_button td.cut {\nwidth: 0 !important;\npadding: 0 !important;\n}\ntable[class=3Douter] .media_button {\npadding-left: 10px !important;\npadding-right: 10px !important;\n}\n}\n</style>\n</head>\n<body style=3D\"margin: 0; padding: 0; background: #fff;-webkit-text-size-ad=\njust:100%;\">\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" width=3D\"100%\" clas=\ns=3D\"outer\" style=3D\"position:relative;background:#ddd;\">\n<tbody>\n<tr>\n<td>\n<table class=3D\"inner frame\" align=3D\"center\" cellpadding=3D\"0\" cellspacing=\n=3D\"0\" border=3D\"0\" width=3D\"670\" style=3D\"background:#fff;position:relativ=\ne;border:0;border-left:1px solid #ccc;border-right:1px solid #ccc;\">\n<tbody>\n<tr>\n<td> <a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.=\ncom%3Frefsrc%3Demail&amp;sig=3D76add118f9dfd33f63864ce34d9372e483cfba4a&amp=\n;uid=3D796789&amp;iid=3D86b54be0-b3b0-474c-b314-d0e5252ad118&amp;nid=3D10+2=\n1+20130917&amp;t=3D1\" style=3D\"border:none;color:#0084b4;text-decoration:no=\nne;\"><span class=3D\"media_logo_div\"></span></a>\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" width=3D\"670\" class=\n=3D\"header frame\" style=3D\"background:#f2f2f2;table-layout:fixed;\">\n<tbody>\n<tr>\n<td class=3D\"header_left cut\" style=3D\"width:19px;height:77px;\"> &nbsp; </t=\nd>\n<td height=3D\"94\" width=3D\"46\" valign=3D\"top\" rowspan=3D\"2\" class=3D\"logo_h=\neader cut\" style=3D\"background:#fff;line-height:100%;\"><a href=3D\"https://t=\nwitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%3Frefsrc%3Demail&amp;=\nsig=3D76add118f9dfd33f63864ce34d9372e483cfba4a&amp;uid=3D796789&amp;iid=3D8=\n6b54be0-b3b0-474c-b314-d0e5252ad118&amp;nid=3D10+21+20130917&amp;t=3D1\" sty=\nle=3D\"border:none;color:#0084b4;text-decoration:none;\"><img class=3D\"logo c=\nut\" src=3D\"https://ea.twimg.com/email/t1/ribbon.png\" width=3D\"46\" height=3D=\n\"94\" style=3D\"border:0;line-height:100%;border:0;\" /></a></td>\n<td class=3D\"cut\" width=3D\"9\"> &nbsp; </td>\n<td width=3D\"10\" height=3D\"77\" class=3D\"header_padding\"> &nbsp; </td>\n<td width=3D\"458\" height=3D\"77\" class=3D\"main_header media_header\" style=3D=\n\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;color:#333;\">\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\">\n<tbody>\n<tr>\n<td class=3D\"main_name\" style=3D\"font-size:14px;font-weight:bold;color:#000=\n;\"> <span dir=3D\"ltr\">Bjarni R. Einarsson,</span> </td>\n</tr>\n<tr>\n<td class=3D\"subtitle\" style=3D\"font-size:14px;color:#666;\"> You have new f=\nollowers on Twitter! </td>\n</tr>\n</tbody>\n</table> </td>\n<td width=3D\"10\" height=3D\"77\" class=3D\"header_padding\"> &nbsp; </td>\n<td class=3D\"main_avatar cut\" width=3D\"32\" style=3D\"text-align:right;\"> <a =\nhref=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2FHe=\nrraBRE%3Frefsrc%3Demail&amp;sig=3De63e99778c0859b62c6cc0df2d3f9b7187011e39&=\namp;uid=3D796789&amp;iid=3D86b54be0-b3b0-474c-b314-d0e5252ad118&amp;nid=3D1=\n0+22+20130917&amp;t=3D1\" style=3D\"border:none;color:#0084b4;text-decoration=\n:none;\"><img class=3D\"cut\" src=3D\"https://si0.twimg.com/profile_images/1260=\n879678/Bjarni-tie-done_reasonably_small.jpg\" width=3D\"32\" height=3D\"32\" alt=\n=3D\"Bjarni R. Einarsson\" style=3D\"background:#fff;border-radius:5px;border:=\n0;\" /></a> </td>\n<td class=3D\"col cut\" style=3D\"width:85px;\"></td>\n</tr>\n<tr>\n<td class=3D\"main header_drop cut\" style=3D\"background:#fff;border-top:1px =\nsolid #ddd;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\">&nb=\nsp;</td>\n<td class=3D\"main header_drop cut\" style=3D\"background:#fff;border-top:1px =\nsolid #ddd;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\">&nb=\nsp;</td>\n<td class=3D\"main header_drop media_header\" height=3D\"17\" style=3D\"backgrou=\nnd:#fff;border-top:1px solid #ddd;font-family:'Helvetica Neue', Helvetica, =\nArial, sans-serif;\"><img width=3D\"1\" height=3D\"1\" style=3D\"display: block;b=\norder:0;\" src=3D\"https://twitter.com/scribe/ibis?uid=3D796789&amp;iid=3D86b=\n54be0-b3b0-474c-b314-d0e5252ad118&amp;nid=3D10+20+20130917&amp;t=3D1\" /></t=\nd>\n<td class=3D\"main header_drop cut\" height=3D\"17\" colspan=3D\"4\" style=3D\"bac=\nkground:#fff;border-top:1px solid #ddd;font-family:'Helvetica Neue', Helvet=\nica, Arial, sans-serif;\">&nbsp;</td>\n</tr>\n</tbody>\n</table> </td>\n<td rowspan=3D\"3\"></td>\n</tr>\n<tr>\n<td class=3D\"content\" style=3D\"background:#fff;\">  <style type=3D\"text/css\"=\n>\n@media only screen and (max-device-width: 480px) {\ntable[class=3Douter] .fd_avatar img {\nwidth: 48px !important;\nheight: 48px !important;\n}\ntable[class=3Douter] .fd_avatar {\nwidth: 58px !important;\n}\ntable[class=3Douter] .fd_button, table[class=3Douter] .following {\npadding-top: 3px !important;\npadding-bottom: 3px !important\n}\n}\n\n@media only screen and (min-device-width: 768px) and (max-device-width: 102=\n4px) {\ntable[class=3Douter] .fd_button, table[class=3Douter] .following {\npadding-top: 3px !important;\npadding-bottom: 3px !important;\n}\n}\n</style>\n<table width=3D\"670\" border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" class=\n=3D\"main frame\" style=3D\"background:#fff;font-family:'Helvetica Neue', Helv=\netica, Arial, sans-serif;\">\n<tbody>\n<tr>\n<td class=3D\"col cut\" style=3D\"width:85px;\"></td>\n<td class=3D\"media_main\" colspan=3D\"2\">\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\">\n<tbody>\n<tr class=3D\"padder\">\n<td class=3D\"vcut\" height=3D\"5px;\"></td>\n</tr>\n<tr>\n<td class=3D\"mid top_border mid_more\" style=3D\"padding:10px;font-family:'He=\nlvetica Neue', Helvetica, Arial, sans-serif;color:#333;border-top:1px solid=\n #e8e8e8;padding-top:15px;padding-bottom:15px;border-top:none;\">\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" class=3D\"media_main=\n entry\" width=3D\"500\" style=3D\"table-layout:fixed;\">\n<tbody>\n<tr>\n<td rowspan=3D\"4\" width=3D\"138\" class=3D\"fd_avatar\" valign=3D\"top\" style=3D=\n\"padding-right:10px;line-height:0;\"><a href=3D\"https://twitter.com/i/redire=\nct?url=3Dhttps%3A%2F%2Ftwitter.com%2FTomWills%3Frefsrc%3Demail&amp;sig=3Dee=\n6aba98019061d6821959a56d8ad0892c06fa4d&amp;uid=3D796789&amp;iid=3D86b54be0-=\nb3b0-474c-b314-d0e5252ad118&amp;nid=3D10+1100+20130917&amp;t=3D1\" style=3D\"=\nborder:none;color:#0084b4;text-decoration:none;\"><img src=3D\"https://si0.tw=\nimg.com/profile_images/1546897245/mugshot-squarecrop_reasonably_small.jpg\" =\nwidth=3D\"128\" height=3D\"128\" style=3D\"border:0;background-color:#f2f2f2;bor=\nder-radius:5px;\" /></a></td>\n<td colspan=3D\"2\" valign=3D\"top\" class=3D\"name_handler\" dir=3D\"ltr\" style=\n=3D\"line-height:12px;\"><a href=3D\"https://twitter.com/i/redirect?url=3Dhttp=\ns%3A%2F%2Ftwitter.com%2FTomWills%3Frefsrc%3Demail&amp;sig=3Dee6aba98019061d=\n6821959a56d8ad0892c06fa4d&amp;uid=3D796789&amp;iid=3D86b54be0-b3b0-474c-b31=\n4-d0e5252ad118&amp;nid=3D10+1120+20130917&amp;t=3D1\" style=3D\"border:none;c=\nolor:#0084b4;text-decoration:none;\"><span class=3D\"name\" style=3D\"color:#33=\n3333;font-size:14px;font-weight:bold;line-height:100%;\">Tom Wills</span></a=\n> <a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com=\n%2FTomWills%3Frefsrc%3Demail&amp;sig=3Dee6aba98019061d6821959a56d8ad0892c06=\nfa4d&amp;uid=3D796789&amp;iid=3D86b54be0-b3b0-474c-b314-d0e5252ad118&amp;ni=\nd=3D10+1110+20130917&amp;t=3D1\" style=3D\"border:none;color:#0084b4;text-dec=\noration:none;\"><span class=3D\"handle screen-name\" style=3D\"direction:ltr;un=\nicode-bidi:embed;font-size:12px;color:#777777;\">@TomWills</span></a></td>\n</tr>\n<tr>\n<td colspan=3D\"2\" class=3D\"bio\" dir=3D\"ltr\" style=3D\"padding-top:2px;paddin=\ng-bottom:2px;font-size:14px;line-height:18px;font-style:italic;font-family:=\n'Georgia', 'Helvetica Neue', Helvetica, Arial, sans-serif;color:#777;\">Free=\nlance data journalist. Visualisation, analysis, multimedia. Investigative J=\nournalism MA, <a class=3D\"screen-name\" href=3D\"https://twitter.com/i/redire=\nct?url=3Dhttp%3A%2F%2Ftwitter.com%2Fcityjournalism%3Frefsrc%3Demail&amp;sig=\n=3D4aa6598fddca82a0043958df399b8a64067e9332&amp;uid=3D796789&amp;iid=3D86b5=\n4be0-b3b0-474c-b314-d0e5252ad118&amp;nid=3D10+925+20130917&amp;t=3D1\" style=\n=3D\"direction:ltr;unicode-bidi:embed;border:none;color:#0084b4;text-decorat=\nion:none;color:#777;\">@cityjournalism</a>. [RT=E2=89=A0endorse|PGP 0x2D0595=\n4C|OTR tomw@jabberd.eu]</td>\n</tr>\n<tr>\n<td colspan=3D\"2\" class=3D\"followed_by\" valign=3D\"top\" style=3D\"font-size:1=\n2px;line-height:18px;color:#333333;font-family:'Helvetica Neue', Helvetica,=\n Arial, sans-serif;\"> Following: <a href=3D\"https://twitter.com/i/redirect?=\nurl=3Dhttps%3A%2F%2Ftwitter.com%2FTomWills%2Ffollowing%3Frefsrc%3Demail&amp=\n;sig=3D3649e87fbb8cc5f20aa1475177829ece5d02e8b0&amp;uid=3D796789&amp;iid=3D=\n86b54be0-b3b0-474c-b314-d0e5252ad118&amp;nid=3D10+1150+20130917&amp;t=3D1\" =\nstyle=3D\"border:none;color:#0084b4;text-decoration:none;\">1505</a> &middot;=\n Followers: <a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ft=\nwitter.com%2FTomWills%2Ffollowers%3Frefsrc%3Demail&amp;sig=3D95ee7548f34afb=\n74ce8db606caa24c0cfe4733a6&amp;uid=3D796789&amp;iid=3D86b54be0-b3b0-474c-b3=\n14-d0e5252ad118&amp;nid=3D10+1140+20130917&amp;t=3D1\" style=3D\"border:none;=\ncolor:#0084b4;text-decoration:none;\">872</a> </td>\n</tr>\n<tr>\n<td class=3D\"fd_follow\" valign=3D\"bottom\" style=3D\"padding-top:10px;\">\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\">\n<tbody>\n<tr>\n<td>\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" background=3D\"https=\n://ea.twimg.com/email/t1/grey_background.png\" class=3D\"fd_button\" style=3D\"=\nbackground-color:#eeeeee;border-radius:5px;border:1px solid #cccccc;padding=\n:5px 0 5px 0;text-align:center;height:28px;width:1px;\">\n<tbody>\n<tr>\n<td valign=3D\"middle\" class=3D\"button_icon\" style=3D\"padding-right:4px;padd=\ning-left:10px;line-height:0;color:#333333;font-size:13px;font-weight:bold;f=\nont-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\"><a class=3D\"but=\nton_link\" href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitte=\nr.com%2Fintent%2Ffollow%3Fea_u%3D796789%26ea_e%3D1380594405%26screen_name%3=\nDTomWills%26ea_s%3D87182111052ae0f9f3b7471d0d4144a0e11d2f3c%26refsrc%3Demai=\nl&amp;sig=3D7d5908984c50e60de7a3230db5e9b5815cdf6795&amp;uid=3D796789&amp;i=\nid=3D86b54be0-b3b0-474c-b314-d0e5252ad118&amp;nid=3D10+489+20130917&amp;t=\n=3D1\" style=3D\"border:none;color:#0084b4;text-decoration:none;color:#333333=\n;font-size:13px;font-weight:bold;font-family:'Helvetica Neue', Helvetica, A=\nrial, sans-serif;\"><img width=3D\"18\" height=3D\"14\" src=3D\"https://ea.twimg.=\ncom/email/t1/blue_bird.png\" style=3D\"border:0;\" /></a></td>\n<td class=3D\"follow_button\" style=3D\"color:#333333;font-size:13px;font-weig=\nht:bold;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;padding-=\nright:10px;white-space:nowrap;\"><a class=3D\"button_link\" href=3D\"https://tw=\nitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2Fintent%2Ffollow%3Fea=\n_u%3D796789%26ea_e%3D1380594405%26screen_name%3DTomWills%26ea_s%3D871821110=\n52ae0f9f3b7471d0d4144a0e11d2f3c%26refsrc%3Demail&amp;sig=3D7d5908984c50e60d=\ne7a3230db5e9b5815cdf6795&amp;uid=3D796789&amp;iid=3D86b54be0-b3b0-474c-b314=\n-d0e5252ad118&amp;nid=3D10+489+20130917&amp;t=3D1\" style=3D\"border:none;col=\nor:#0084b4;text-decoration:none;color:#333333;font-size:13px;font-weight:bo=\nld;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\">Follow</a><=\n/td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n<td valign=3D\"bottom\" align=3D\"right\" width=3D\"100\"> <a class=3D\"report_spa=\nm\" href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2=\nFuser_spam_reports%2FHerraBRE%2Freport%2FTomWills%3Ft%3D1%26sig%3D1b526e1ca=\nac85b92337d61e2b0c29521bfda0bff%26iid%3D86b54be0-b3b0-474c-b314-d0e5252ad11=\n8%26uid%3D796789%26accused%3DTomWills%26nid%3D10%2B465%2B20130917&amp;sig=\n=3Dbb78c87850765ceb5736ff7bf999413b763f5478&amp;uid=3D796789&amp;iid=3D86b5=\n4be0-b3b0-474c-b314-d0e5252ad118&amp;nid=3D10+465+20130917&amp;t=3D1\" style=\n=3D\"border:none;color:#0084b4;text-decoration:none;font-size:11px;color:#99=\n9999;\">Report for spam</a> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n<tr>\n<td class=3D\"mid top_border mid_more\" style=3D\"padding:10px;font-family:'He=\nlvetica Neue', Helvetica, Arial, sans-serif;color:#333;border-top:1px solid=\n #e8e8e8;padding-top:15px;padding-bottom:15px;\">\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" class=3D\"media_main=\n entry\" width=3D\"500\" style=3D\"table-layout:fixed;\">\n<tbody>\n<tr>\n<td rowspan=3D\"4\" width=3D\"138\" class=3D\"fd_avatar\" valign=3D\"top\" style=3D=\n\"padding-right:10px;line-height:0;\"><a href=3D\"https://twitter.com/i/redire=\nct?url=3Dhttps%3A%2F%2Ftwitter.com%2Ffauntty%3Frefsrc%3Demail&amp;sig=3D801=\naa839d1d68b1bdc498680029d6e26007f1abd&amp;uid=3D796789&amp;iid=3D86b54be0-b=\n3b0-474c-b314-d0e5252ad118&amp;nid=3D10+1101+20130917&amp;t=3D1\" style=3D\"b=\norder:none;color:#0084b4;text-decoration:none;\"><img src=3D\"https://si0.twi=\nmg.com/profile_images/378800000379543316/71956f0f7a40739d851ec5a6704712bb_r=\neasonably_small.png\" width=3D\"128\" height=3D\"128\" style=3D\"border:0;backgro=\nund-color:#f2f2f2;border-radius:5px;\" /></a></td>\n<td colspan=3D\"2\" valign=3D\"top\" class=3D\"name_handler\" dir=3D\"ltr\" style=\n=3D\"line-height:12px;\"><a href=3D\"https://twitter.com/i/redirect?url=3Dhttp=\ns%3A%2F%2Ftwitter.com%2Ffauntty%3Frefsrc%3Demail&amp;sig=3D801aa839d1d68b1b=\ndc498680029d6e26007f1abd&amp;uid=3D796789&amp;iid=3D86b54be0-b3b0-474c-b314=\n-d0e5252ad118&amp;nid=3D10+1121+20130917&amp;t=3D1\" style=3D\"border:none;co=\nlor:#0084b4;text-decoration:none;\"><span class=3D\"name\" style=3D\"color:#333=\n333;font-size:14px;font-weight:bold;line-height:100%;\">Samuel Faunt</span><=\n/a> <a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.c=\nom%2Ffauntty%3Frefsrc%3Demail&amp;sig=3D801aa839d1d68b1bdc498680029d6e26007=\nf1abd&amp;uid=3D796789&amp;iid=3D86b54be0-b3b0-474c-b314-d0e5252ad118&amp;n=\nid=3D10+1111+20130917&amp;t=3D1\" style=3D\"border:none;color:#0084b4;text-de=\ncoration:none;\"><span class=3D\"handle screen-name\" style=3D\"direction:ltr;u=\nnicode-bidi:embed;font-size:12px;color:#777777;\">@fauntty</span></a></td>\n</tr>\n<tr>\n<td colspan=3D\"2\" class=3D\"bio\" dir=3D\"ltr\" style=3D\"padding-top:2px;paddin=\ng-bottom:2px;font-size:14px;line-height:18px;font-style:italic;font-family:=\n'Georgia', 'Helvetica Neue', Helvetica, Arial, sans-serif;color:#777;\"></td=\n>\n</tr>\n<tr>\n<td colspan=3D\"2\" class=3D\"followed_by\" valign=3D\"top\" style=3D\"font-size:1=\n2px;line-height:18px;color:#333333;font-family:'Helvetica Neue', Helvetica,=\n Arial, sans-serif;\"> Following: <a href=3D\"https://twitter.com/i/redirect?=\nurl=3Dhttps%3A%2F%2Ftwitter.com%2Ffauntty%2Ffollowing%3Frefsrc%3Demail&amp;=\nsig=3Dea2163b9d250927725af2a5f83d4c586cc148c0c&amp;uid=3D796789&amp;iid=3D8=\n6b54be0-b3b0-474c-b314-d0e5252ad118&amp;nid=3D10+1151+20130917&amp;t=3D1\" s=\ntyle=3D\"border:none;color:#0084b4;text-decoration:none;\">298</a> &middot; F=\nollowers: <a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwi=\ntter.com%2Ffauntty%2Ffollowers%3Frefsrc%3Demail&amp;sig=3De490853b76bb966ea=\n5b309944ae4751248db3155&amp;uid=3D796789&amp;iid=3D86b54be0-b3b0-474c-b314-=\nd0e5252ad118&amp;nid=3D10+1141+20130917&amp;t=3D1\" style=3D\"border:none;col=\nor:#0084b4;text-decoration:none;\">89</a> </td>\n</tr>\n<tr>\n<td class=3D\"fd_follow\" valign=3D\"bottom\" style=3D\"padding-top:10px;\">\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\">\n<tbody>\n<tr>\n<td>\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" background=3D\"https=\n://ea.twimg.com/email/t1/grey_background.png\" class=3D\"fd_button\" style=3D\"=\nbackground-color:#eeeeee;border-radius:5px;border:1px solid #cccccc;padding=\n:5px 0 5px 0;text-align:center;height:28px;width:1px;\">\n<tbody>\n<tr>\n<td valign=3D\"middle\" class=3D\"button_icon\" style=3D\"padding-right:4px;padd=\ning-left:10px;line-height:0;color:#333333;font-size:13px;font-weight:bold;f=\nont-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\"><a class=3D\"but=\nton_link\" href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitte=\nr.com%2Fintent%2Ffollow%3Fea_u%3D796789%26ea_e%3D1380594405%26screen_name%3=\nDfauntty%26ea_s%3Dda922ac68f5bcc0b0c75ca37af35746b8f187d3b%26refsrc%3Demail=\n&amp;sig=3D0b012befe98b97105132c0e06eddfab3cc59f1ea&amp;uid=3D796789&amp;ii=\nd=3D86b54be0-b3b0-474c-b314-d0e5252ad118&amp;nid=3D10+490+20130917&amp;t=3D=\n1\" style=3D\"border:none;color:#0084b4;text-decoration:none;color:#333333;fo=\nnt-size:13px;font-weight:bold;font-family:'Helvetica Neue', Helvetica, Aria=\nl, sans-serif;\"><img width=3D\"18\" height=3D\"14\" src=3D\"https://ea.twimg.com=\n/email/t1/blue_bird.png\" style=3D\"border:0;\" /></a></td>\n<td class=3D\"follow_button\" style=3D\"color:#333333;font-size:13px;font-weig=\nht:bold;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;padding-=\nright:10px;white-space:nowrap;\"><a class=3D\"button_link\" href=3D\"https://tw=\nitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2Fintent%2Ffollow%3Fea=\n_u%3D796789%26ea_e%3D1380594405%26screen_name%3Dfauntty%26ea_s%3Dda922ac68f=\n5bcc0b0c75ca37af35746b8f187d3b%26refsrc%3Demail&amp;sig=3D0b012befe98b97105=\n132c0e06eddfab3cc59f1ea&amp;uid=3D796789&amp;iid=3D86b54be0-b3b0-474c-b314-=\nd0e5252ad118&amp;nid=3D10+490+20130917&amp;t=3D1\" style=3D\"border:none;colo=\nr:#0084b4;text-decoration:none;color:#333333;font-size:13px;font-weight:bol=\nd;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\">Follow</a></=\ntd>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n<td valign=3D\"bottom\" align=3D\"right\" width=3D\"100\"> <a class=3D\"report_spa=\nm\" href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2=\nFuser_spam_reports%2FHerraBRE%2Freport%2Ffauntty%3Ft%3D1%26sig%3Dc3fe4e0df1=\nc0fd671fd3af2f5bb37f8ed8c50289%26iid%3D86b54be0-b3b0-474c-b314-d0e5252ad118=\n%26uid%3D796789%26accused%3Dfauntty%26nid%3D10%2B466%2B20130917&amp;sig=3D7=\nabe4dacc1b39b3f5aece56352a602ba93df8161&amp;uid=3D796789&amp;iid=3D86b54be0=\n-b3b0-474c-b314-d0e5252ad118&amp;nid=3D10+466+20130917&amp;t=3D1\" style=3D\"=\nborder:none;color:#0084b4;text-decoration:none;font-size:11px;color:#999999=\n;\">Report for spam</a> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n<tr>\n<td class=3D\"mid top_border mid_more\" style=3D\"padding:10px;font-family:'He=\nlvetica Neue', Helvetica, Arial, sans-serif;color:#333;border-top:1px solid=\n #e8e8e8;padding-top:15px;padding-bottom:15px;\">\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" class=3D\"media_main=\n entry\" width=3D\"500\" style=3D\"table-layout:fixed;\">\n<tbody>\n<tr>\n<td rowspan=3D\"4\" width=3D\"138\" class=3D\"fd_avatar\" valign=3D\"top\" style=3D=\n\"padding-right:10px;line-height:0;\"><a href=3D\"https://twitter.com/i/redire=\nct?url=3Dhttps%3A%2F%2Ftwitter.com%2Fagirorn%3Frefsrc%3Demail&amp;sig=3Db68=\nd4ce08740b9d3db092312127496078fda66d3&amp;uid=3D796789&amp;iid=3D86b54be0-b=\n3b0-474c-b314-d0e5252ad118&amp;nid=3D10+1102+20130917&amp;t=3D1\" style=3D\"b=\norder:none;color:#0084b4;text-decoration:none;\"><img src=3D\"https://si0.twi=\nmg.com/profile_images/1897653368/gravatar-1_reasonably_small.jpg\" width=3D\"=\n128\" height=3D\"128\" style=3D\"border:0;background-color:#f2f2f2;border-radiu=\ns:5px;\" /></a></td>\n<td colspan=3D\"2\" valign=3D\"top\" class=3D\"name_handler\" dir=3D\"ltr\" style=\n=3D\"line-height:12px;\"><a href=3D\"https://twitter.com/i/redirect?url=3Dhttp=\ns%3A%2F%2Ftwitter.com%2Fagirorn%3Frefsrc%3Demail&amp;sig=3Db68d4ce08740b9d3=\ndb092312127496078fda66d3&amp;uid=3D796789&amp;iid=3D86b54be0-b3b0-474c-b314=\n-d0e5252ad118&amp;nid=3D10+1122+20130917&amp;t=3D1\" style=3D\"border:none;co=\nlor:#0084b4;text-decoration:none;\"><span class=3D\"name\" style=3D\"color:#333=\n333;font-size:14px;font-weight:bold;line-height:100%;\">&AElig;gir &Ouml;rn =\nS&iacute;monarson</span></a> <a href=3D\"https://twitter.com/i/redirect?url=\n=3Dhttps%3A%2F%2Ftwitter.com%2Fagirorn%3Frefsrc%3Demail&amp;sig=3Db68d4ce08=\n740b9d3db092312127496078fda66d3&amp;uid=3D796789&amp;iid=3D86b54be0-b3b0-47=\n4c-b314-d0e5252ad118&amp;nid=3D10+1112+20130917&amp;t=3D1\" style=3D\"border:=\nnone;color:#0084b4;text-decoration:none;\"><span class=3D\"handle screen-name=\n\" style=3D\"direction:ltr;unicode-bidi:embed;font-size:12px;color:#777777;\">=\n@agirorn</span></a></td>\n</tr>\n<tr>\n<td colspan=3D\"2\" class=3D\"bio\" dir=3D\"ltr\" style=3D\"padding-top:2px;paddin=\ng-bottom:2px;font-size:14px;line-height:18px;font-style:italic;font-family:=\n'Georgia', 'Helvetica Neue', Helvetica, Arial, sans-serif;color:#777;\">Web =\nJuJu @ <a href=3D\"https://t.co/redirect?url=3Dhttp%3A%2F%2Ft.co%2FYG6nOzpc8=\nt&amp;sig=3D66582b044390d21d1373b3c99cc570df23d5dcf3&amp;uid=3D796789&amp;i=\nid=3D86b54be0-b3b0-474c-b314-d0e5252ad118&amp;nid=3D10+923+20130917&amp;t=\n=3D1\" style=3D\"border:none;color:#0084b4;text-decoration:none;color:#777;\">=\nhttp://t.co/YG6nOzpc8t</a>, rubyist and some other stuff...</td>\n</tr>\n<tr>\n<td colspan=3D\"2\" class=3D\"followed_by\" valign=3D\"top\" style=3D\"font-size:1=\n2px;line-height:18px;color:#333333;font-family:'Helvetica Neue', Helvetica,=\n Arial, sans-serif;\"> Followed by <a href=3D\"https://twitter.com/i/redirect=\n?url=3Dhttps%3A%2F%2Ftwitter.com%2Feinarj%3Frefsrc%3Demail&amp;sig=3D433961=\nbca69cc60bc423f20835606833f5754cba&amp;uid=3D796789&amp;iid=3D86b54be0-b3b0=\n-474c-b314-d0e5252ad118&amp;nid=3D10+1009+20130917&amp;t=3D1\" style=3D\"bord=\ner:none;color:#0084b4;text-decoration:none;\">Einar J&oacute;nsson</a> and <=\na href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2F=\ngisli%3Frefsrc%3Demail&amp;sig=3D6a3662498b174acc0b35aef6bef5e1d06ab32038&a=\nmp;uid=3D796789&amp;iid=3D86b54be0-b3b0-474c-b314-d0e5252ad118&amp;nid=3D10=\n+1009+20130917&amp;t=3D1\" style=3D\"border:none;color:#0084b4;text-decoratio=\nn:none;\">gisli</a>.<br /> Following: <a href=3D\"https://twitter.com/i/redir=\nect?url=3Dhttps%3A%2F%2Ftwitter.com%2Fagirorn%2Ffollowing%3Frefsrc%3Demail&=\namp;sig=3D705d99f85406f5ac418d5add32b769cb42f6ec88&amp;uid=3D796789&amp;iid=\n=3D86b54be0-b3b0-474c-b314-d0e5252ad118&amp;nid=3D10+1152+20130917&amp;t=3D=\n1\" style=3D\"border:none;color:#0084b4;text-decoration:none;\">153</a> &middo=\nt; Followers: <a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2=\nFtwitter.com%2Fagirorn%2Ffollowers%3Frefsrc%3Demail&amp;sig=3D54147409c17b2=\nf3f0ffcafa7b198b20d70561eec&amp;uid=3D796789&amp;iid=3D86b54be0-b3b0-474c-b=\n314-d0e5252ad118&amp;nid=3D10+1142+20130917&amp;t=3D1\" style=3D\"border:none=\n;color:#0084b4;text-decoration:none;\">22</a> </td>\n</tr>\n<tr>\n<td class=3D\"fd_follow\" valign=3D\"bottom\" style=3D\"padding-top:10px;\">\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\">\n<tbody>\n<tr>\n<td>\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" background=3D\"https=\n://ea.twimg.com/email/t1/grey_background.png\" class=3D\"fd_button\" style=3D\"=\nbackground-color:#eeeeee;border-radius:5px;border:1px solid #cccccc;padding=\n:5px 0 5px 0;text-align:center;height:28px;width:1px;\">\n<tbody>\n<tr>\n<td valign=3D\"middle\" class=3D\"button_icon\" style=3D\"padding-right:4px;padd=\ning-left:10px;line-height:0;color:#333333;font-size:13px;font-weight:bold;f=\nont-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\"><a class=3D\"but=\nton_link\" href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitte=\nr.com%2Fintent%2Ffollow%3Fea_u%3D796789%26ea_e%3D1380594405%26screen_name%3=\nDagirorn%26ea_s%3D0fd72f7c66a3a7825a1355ab3de27bbe823d0e90%26refsrc%3Demail=\n&amp;sig=3D70cfb47395ed6439216e9347b575641088a5cfc0&amp;uid=3D796789&amp;ii=\nd=3D86b54be0-b3b0-474c-b314-d0e5252ad118&amp;nid=3D10+491+20130917&amp;t=3D=\n1\" style=3D\"border:none;color:#0084b4;text-decoration:none;color:#333333;fo=\nnt-size:13px;font-weight:bold;font-family:'Helvetica Neue', Helvetica, Aria=\nl, sans-serif;\"><img width=3D\"18\" height=3D\"14\" src=3D\"https://ea.twimg.com=\n/email/t1/blue_bird.png\" style=3D\"border:0;\" /></a></td>\n<td class=3D\"follow_button\" style=3D\"color:#333333;font-size:13px;font-weig=\nht:bold;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;padding-=\nright:10px;white-space:nowrap;\"><a class=3D\"button_link\" href=3D\"https://tw=\nitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2Fintent%2Ffollow%3Fea=\n_u%3D796789%26ea_e%3D1380594405%26screen_name%3Dagirorn%26ea_s%3D0fd72f7c66=\na3a7825a1355ab3de27bbe823d0e90%26refsrc%3Demail&amp;sig=3D70cfb47395ed64392=\n16e9347b575641088a5cfc0&amp;uid=3D796789&amp;iid=3D86b54be0-b3b0-474c-b314-=\nd0e5252ad118&amp;nid=3D10+491+20130917&amp;t=3D1\" style=3D\"border:none;colo=\nr:#0084b4;text-decoration:none;color:#333333;font-size:13px;font-weight:bol=\nd;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;\">Follow</a></=\ntd>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n<td valign=3D\"bottom\" align=3D\"right\" width=3D\"100\"> <a class=3D\"report_spa=\nm\" href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2=\nFuser_spam_reports%2FHerraBRE%2Freport%2Fagirorn%3Ft%3D1%26sig%3Dea903a5c3f=\nfb2ab56828a9d7de723c972eecb868%26iid%3D86b54be0-b3b0-474c-b314-d0e5252ad118=\n%26uid%3D796789%26accused%3Dagirorn%26nid%3D10%2B467%2B20130917&amp;sig=3D1=\n0a2d48fe96469aa34dc8df04be13a0d3980ec53&amp;uid=3D796789&amp;iid=3D86b54be0=\n-b3b0-474c-b314-d0e5252ad118&amp;nid=3D10+467+20130917&amp;t=3D1\" style=3D\"=\nborder:none;color:#0084b4;text-decoration:none;font-size:11px;color:#999999=\n;\">Report for spam</a> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n<tr>\n<td height=3D\"2\" class=3D\"vcut\"></td>\n</tr>\n<tr>\n<td class=3D\"more media_more\" width=3D\"500\" style=3D\"border-radius:5px;back=\nground-color:#ededed;padding:10px;font-family:'Helvetica Neue', Helvetica, =\nArial, sans-serif;color:#333333;\">\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" width=3D\"100%\">\n<tbody>\n<tr>\n<td class=3D\"cut\"><span class=3D\"suggestions_tip\" style=3D\"color:#666666;fo=\nnt-size:14px;text-shadow:0px 1px 0px #ffffff;font-family:'Helvetica Neue', =\nHelvetica, Arial, sans-serif;\">Check out your followers page for more.</spa=\nn></td>\n<td align=3D\"right\">\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\">\n<tbody>\n<tr>\n<td>  <style type=3D\"text/css\">\n@media only screen and (max-device-width: 480px) {\ntable[class=3Dbig-button-template] {\nwidth: 300px !important;\n}\ntr[class=3Dbig-button-template-vertical-padding] {\nheight: 3px !important;\n}\n}\n@media only screen and (min-device-width: 768px) and (max-device-width: 102=\n4px) {\ntd[class=3Dbig-button-template] {\npadding-left: 10px !important;\npadding-right: 10px !important;\n}\n}\n</style>\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" class=3D\"big-button=\n-template\" style=3D\"background-image:url('https://ea.twimg.com/email/t1/but=\nton_bg_long.png');background-color:#33a9e5;border-radius:5px;border:1px;hei=\nght:28px;border:1px solid #28C;word-wrap:break-word;text-align:center;\">\n<tbody>\n<tr>\n<td align=3D\"center\" style=3D\"padding-left:10px;padding-right:10px;\">\n<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" class=3D\"big-button=\n-template-inner\">\n<tbody>\n<tr>\n<td height=3D\"28\" class=3D\"big-button-template-font\" style=3D\"color:white;f=\nont-size:13px;font-weight:bold;font-family:'Helvetica Neue', Helvetica, Ari=\nal, sans-serif;text-align:center;padding-left:10px;padding-right:10px;paddi=\nng-left:0px;padding-right:0px;\"> <a href=3D\"https://twitter.com/i/redirect?=\nurl=3Dhttps%3A%2F%2Ftwitter.com%2FHerraBRE%2Ffollowers%3Frefsrc%3Demail&amp=\n;sig=3D8ec2b97945faa1e20725a419334616465aabffd4&amp;uid=3D796789&amp;iid=3D=\n86b54be0-b3b0-474c-b314-d0e5252ad118&amp;nid=3D10+96+20130917&amp;t=3D1\" st=\nyle=3D\"border:none;color:#0084b4;text-decoration:none;color:white;\"> See al=\nl your followers </a> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n<tr>\n<td height=3D\"22\" class=3D\"vcut\"></td>\n</tr>\n</tbody>\n</table> </td>\n<td class=3D\"col cut\" style=3D\"width:85px;\"></td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n<tr>\n<td>\n<table bgcolor=3D\"#eeeeee\" border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\"=\n width=3D\"670\" class=3D\"frame footer\" style=3D\"background-color:#eee;backgr=\nound-image:url('https://ea.twimg.com/email/t1/shadow-bottom.jpg');backgroun=\nd-position:top;background-repeat:repeat-x;border-top-color:#ddd;border-top-=\nstyle:solid;border-top-width:1px;\">\n<tbody>\n<tr>\n<td colspan=3D\"4\" height=3D\"16\" class=3D\"footer-padding-top\"></td>\n</tr>\n<tr>\n<td class=3D\"col cut\" style=3D\"width:85px;\"></td>\n<td class=3D\"footer_body media_footer\" style=3D\"font-family:'Helvetica Neue=\n', Helvetica, Arial, sans-serif;font-size:12px;line-height:17px;color:#777;=\ntext-shadow:0 1px 0 #fff;\">\n<div>\nForgot your Twitter password?\n<a class=3D\"reset\" href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F=\n%2Ftwitter.com%2Faccount%2Fresend_password&amp;sig=3Df5d73c6d6c8577b4b95889=\n880e513526930e35d0&amp;uid=3D796789&amp;iid=3D86b54be0-b3b0-474c-b314-d0e52=\n52ad118&amp;nid=3D10+24+20130917&amp;t=3D1\" style=3D\"border:none;color:#008=\n4b4;text-decoration:none;\">Get instructions on how to reset it.</a>\n</div>\n<div>\nYou can also\n<a href=3D\"https://twitter.com/i/u?t=3D1&amp;sig=3Daf396a74ff663ce86bf8f2fe=\nd5de7ce49ad759fe&amp;iid=3D86b54be0-b3b0-474c-b314-d0e5252ad118&amp;uid=3D7=\n96789&amp;nid=3D10+26+20130917\" style=3D\"border:none;color:#0084b4;text-dec=\noration:none;\">unsubscribe from these emails</a>\n<span class=3D\"reset\">or change your <a href=3D\"https://twitter.com/i/redir=\nect?url=3Dhttps%3A%2F%2Ftwitter.com%2Fsettings%2Fnotifications&amp;sig=3Ddf=\ne6fc4e06fdfbeffa510642835ff34af916458c&amp;uid=3D796789&amp;iid=3D86b54be0-=\nb3b0-474c-b314-d0e5252ad118&amp;nid=3D10+27+20130917&amp;t=3D1\" style=3D\"bo=\nrder:none;color:#0084b4;text-decoration:none;\">notification settings</a>. N=\need <a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Fsupport.t=\nwitter.com&amp;sig=3Dd5a93ec8dc3f5086b47a0022aac6b1bd71661580&amp;uid=3D796=\n789&amp;iid=3D86b54be0-b3b0-474c-b314-d0e5252ad118&amp;nid=3D10+97+20130917=\n&amp;t=3D1\" style=3D\"border:none;color:#0084b4;text-decoration:none;\">help<=\n/a>?</span>\n</div>\n<div class=3D\"reset\">\nIf you received this message in error and did not sign up for Twitter, clic=\nk\n<a href=3D\"https://twitter.com/i/redirect?url=3Dhttps%3A%2F%2Ftwitter.com%2=\nFaccount%2Fnot_my_account%2FHerraBRE%2F87D2F-9EF9G-137938&amp;sig=3Dc6072fc=\ndbcac6c1e6f74e7b58eb9cfec77cca22b&amp;uid=3D796789&amp;iid=3D86b54be0-b3b0-=\n474c-b314-d0e5252ad118&amp;nid=3D10+25+20130917&amp;t=3D1\" style=3D\"border:=\nnone;color:#0084b4;text-decoration:none;\">not my account</a>.\n</div>\n<div>\n<a href=3D\"#\" class=3D\"address\" style=3D\"border:none;color:#0084b4;text-dec=\noration:none;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;tex=\nt-decoration:none;font-size:11px;line-height:17px;color:#999999;text-shadow=\n:0 1px 0 #fff;\">Twitter, Inc. 1355 Market St., Suite 900 <span class=3D\"nob=\nreak\" style=3D\"white-space:nowrap;\">San Francisco, CA 94103 </span></a>\n</div> </td>\n<td class=3D\"col cut\" style=3D\"width:85px;\"></td>\n</tr>\n<tr>\n<td colspan=3D\"3\" class=3D\"footer-padding-bottom\" height=3D\"25\"></td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table> </td>\n</tr>\n</tbody>\n</table>\n<img width=3D\"1\" height=3D\"1\" src=3D\"loadimage\" style=3D\"border:0;\" />\n</body>\n</html>\n\n------=_Part_2325007_782750442.1379384805206--\n"
  },
  {
    "path": "mailpile/tests/data/Maildir/cur/1379857166.25979_9.hottie,2,S",
    "content": "Return-path: <feministinn-bounces@listar.hi.is>\nEnvelope-to: bre@localhost\nDelivery-date: Thu, 19 Sep 2013 09:42:57 +0000\nReceived: from localhost ([::1] helo=hottie)\n\tby hottie with esmtp (Exim 4.80)\n\t(envelope-from <feministinn-bounces@listar.hi.is>)\n\tid 1VMalD-0007Z5-NQ\n\tfor bre@localhost; Thu, 19 Sep 2013 09:42:57 +0000\nDelivered-To: fake@example.com\nReceived: from gmail-pop.l.google.com [173.194.73.109]\n\tby hottie with POP3 (fetchmail-6.3.21)\n\tfor <bre@localhost> (single-drop); Thu, 19 Sep 2013 09:42:55 +0000 (GMT)\nReceived: by 10.58.13.104 with SMTP id g8csp159544vec;\n        Tue, 17 Sep 2013 03:59:48 -0700 (PDT)\nX-Received: by 10.221.56.194 with SMTP id wd2mr32491841vcb.7.1379415588092;\n        Tue, 17 Sep 2013 03:59:48 -0700 (PDT)\nDomainKey-Status: bad format\nReceived-SPF: pass (google.com: domain of feministinn-bounces@listar.hi.is designates 130.208.165.103 as permitted sender) client-ip=130.208.165.103;\nReceived: by 10.220.219.75 with POP3 id ht11mf5414539vcb.14;\n        Tue, 17 Sep 2013 03:59:46 -0700 (PDT)\nX-Gmail-Fetch-Info: fake@example.com 1 example.com 110 bre\nReceived: from fenja.rhi.hi.is (fenja.rhi.hi.is [130.208.165.103])\n\tby example.com (8.12.11.20060308/8.12.11) with ESMTP id r8HAUT3t019307\n\tfor <fake@example.com>; Tue, 17 Sep 2013 10:30:29 GMT\nReceived: from listar.hi.is (horn.rhi.hi.is [130.208.165.14])\n\tby fenja.rhi.hi.is (8.13.8/8.13.8) with ESMTP id r8HAUES6006954;\n\tTue, 17 Sep 2013 10:30:14 GMT\nReceived: from horn.rhi.hi.is (localhost.localdomain [127.0.0.1])\n\tby listar.hi.is (8.13.8/8.13.8) with ESMTP id r8HAUC7J024378;\n\tTue, 17 Sep 2013 10:30:13 GMT\nX-Mailman-Handler: $Id: mm-handler 5100 2007-07-08 03:14:09Z $\nReceived: from menja.rhi.hi.is (menja.rhi.hi.is [130.208.165.104])\n\tby listar.hi.is (8.13.8/8.13.8) with ESMTP id r8HAUBJd024373\n\tfor <feministinn@listar.hi.is>; Tue, 17 Sep 2013 10:30:11 GMT\nReceived: from smtp.hi.is (smtp.hi.is [130.208.165.149])\n\tby menja.rhi.hi.is (8.13.8/8.13.8) with ESMTP id r8HAUBnr005979\n\tfor <feministinn@hi.is>; Tue, 17 Sep 2013 10:30:11 GMT\nReceived: from gipcxxxPC (gi-pc159.rhi.hi.is [130.208.126.191])\n\tby smtp.hi.is (8.14.4/8.14.4) with ESMTP id r8HAUBn1029828\n\tfor <feministinn@hi.is>; Tue, 17 Sep 2013 10:30:11 GMT\nFrom: =?iso-8859-1?Q?Ranns=F3knastofa_=ED_kvenna-_og_kynjafr=E6=F0um?=\n\t<rikk@hi.is>\nTo: <feministinn@hi.is>\nDate: Tue, 17 Sep 2013 10:30:14 -0000\nMessage-ID: <00e001ceb390$e573ff10$b05bfd30$@is>\nMIME-Version: 1.0\nContent-Type: multipart/mixed;\n\tboundary=\"----=_NextPart_000_00E1_01CEB390.E573FF10\"\nX-Mailer: Microsoft Office Outlook 12.0\nThread-Index: Ac6y6d6v7+iL4+DfSkefBF0/ndZn4QAALu2wACet3bAAABIOQA==\nContent-Language: is\nSubject: [Feministinn] Emerging ideas in masculinity research - Second call\nX-BeenThere: feministinn@listar.hi.is\nX-Mailman-Version: 2.1.9\nPrecedence: list\nReply-To: feministinn@listar.hi.is\nList-Id: <feministinn.listar.hi.is>\nList-Unsubscribe: <http://listar.hi.is/mailman/listinfo/feministinn>,\n\t<mailto:feministinn-request@listar.hi.is?subject=unsubscribe>\nList-Archive: <http://listar.hi.is/mailman/private/feministinn>\nList-Post: <mailto:feministinn@listar.hi.is>\nList-Help: <mailto:feministinn-request@listar.hi.is?subject=help>\nList-Subscribe: <http://listar.hi.is/mailman/listinfo/feministinn>,\n\t<mailto:feministinn-request@listar.hi.is?subject=subscribe>\nSender: feministinn-bounces@listar.hi.is\nErrors-To: feministinn-bounces@listar.hi.is\nX-BRE-Whitelisted: procmail (feministinn-bounces@listar.hi.is)\nContent-Length: 518908\nLines: 6896\n\nThis is a multi-part message in MIME format.\n\n------=_NextPart_000_00E1_01CEB390.E573FF10\nContent-Type: multipart/alternative;\n\tboundary=\"----=_NextPart_001_00E2_01CEB390.E573FF10\"\n\n\n------=_NextPart_001_00E2_01CEB390.E573FF10\nContent-Type: text/plain;\n\tcharset=\"iso-8859-1\"\nContent-Transfer-Encoding: quoted-printable\n\nRIKK vekur athygli =E1 eftirfarandi:\n\n=20\n\n=20\n\nEmerging ideas in masculinity research=20\n\n- Masculinity studies in the North=20\n\n=20\n\nSecond call for papers and suggestions for workshop themes.=20\n\n- Deadline November, 15th. 2013=20\n\n=20\n\nThe next Nordic conference on research on men and masculinities will be =\nheld\n4th.-6th. June 2014, at the University of Iceland, Reykjav=EDk.=20\n\n=20\n\nKeynote speakers include Raewyn Connell and Michael Kimmel.\n\n=20\n\nSuggested workshops are: Economy/financial crisis, the affective turn =\nand\nmasculinity studies, relations between queer theory and masculinity =\nstudies,\nglobal masculinities, family work balance, antifeminism and xenophobia, =\nmen\nand feminism =84up North=93, confronting sexual violence, theory and =\npractice in\nworking with men, masculinity and media, fatherhood and parental leave,\ntrafficking in the North, men and health, indigenous masculinities in =\nNorth\nAmerica, Masculinity and UNSCR 1325, doing food and doing masculinity =\nand\nWomen=91s views on masculinity.\n\n=20\n\nThe conference is organized by the Nordic Association for Research on =\nMen\nand Masculinities (NFMM), as part of the Icelandic Presidency of the =\nNordic\nCouncil of Ministers in 2014, in cooperation with the Ministry of =\nWelfare,\nCentre for Gender Equality in Iceland and committee on gender equality =\nat\nthe University of Iceland.\n\n=20\n\nPlease upload you abstracts here:\n<http://nfmm2014.yourhost.is/call-for-papers/8-nfmm2014/6-abstract-form>\nhttp://nfmm2014.yourhost.is/call-for-papers/8-nfmm2014/6-abstract-form=20\n\nSend suggested workshops and general enquiries to\n<mailto:yourhost@yourhost.is> yourhost@yourhost.is=20\n\n=20\n\nSwedish:=20\n\n=20\n\nFramv=E4xande id=E9er i maskulinitetsforskning=20\n\n- Maskulinitetsstudier i Norden\n\n=20\n\nAndra kallelsen f=F6r papper och teman f=F6r arbetsgrupper (Workshops)=20\n\n- Deadline 15 november 2013=20\n\n=20\n\nN=E4sta nordiska konferens om m=E4n och maskuliniteter =E4ger rum vid =\nIslands\nUniversitet i Reykjav=EDk mellan 4 och 6 juni 2014.\n\n=20\n\nBland keynote f=F6rel=E4sare =E4r Raewyn Connell och Michael Kimmel.\n\n=20\n\nRedan f=F6reslagna arbetsgrupper inkluderar: Den ekonomiska/finansiella\nkrisen; den affektiva v=E4ndningen och maskulinitetsstudier; relationen =\nmellan\nqueerteori och maskulinitetsforskning; globala maskuliniteter; balansen\nfamilj arbete; anti-feminism och xenofobi; m=E4n och feminism =84uppe i =\nNorden=93;\natt konfrontera sexuellt v=E5ld; teori och praxis i arbete med m=E4n;\nmaskulinitet och massmedia; faderskap och f=F6r=E4ldraledighet; =\nm=E4nniskohandel i\nNorden; m=E4n och h=E4lsa; maskulinitet bland ursprungsbefolkningen i\nNord-Amerika; maskulinitet och UNSCR 1325; att g=F6ra mat och g=F6ra\nmaskulinitet; kvinnors =E5sikter om maskulinitet.=20\n\n=20\n\nKonferensen organiseras av Nordiska f=F6reningen for forskning om m=E4n =\noch\nmaskuliniteter (NFMM) som en del av Islands ordf=F6randeskap i Nordiska\nministerr=E5det 2014, i samarbete med V=E4lf=E4rdsministeriet,\nJ=E4mst=E4lldhetscentret p=E5 Island och j=E4mst=E4lldhetskommitteen vid =\nIslands\nUniversitet.=20\n\n=20\n\nIntresserade b=F6r s=E4nda abstrakt h=E4r:\n<http://nfmm2014.yourhost.is/call-for-papers/8-nfmm2014/6-abstract-form>\nhttp://nfmm2014.yourhost.is/call-for-papers/8-nfmm2014/6-abstract-form=20\n\nF=F6rslag till arbetsgrupper och allm=E4nna fr=E5gor till b=F6r s=E4nda =\ntill\n<mailto:yourhost@yourhost.is> yourhost@yourhost.is.=20\n\n\n------=_NextPart_001_00E2_01CEB390.E573FF10\nContent-Type: text/html;\n\tcharset=\"iso-8859-1\"\nContent-Transfer-Encoding: quoted-printable\n\n<META HTTP-EQUIV=3D\"Content-Type\" CONTENT=3D\"text/html; =\ncharset=3Diso-8859-1\">\n<html xmlns:v=3D\"urn:schemas-microsoft-com:vml\" =\nxmlns:o=3D\"urn:schemas-microsoft-com:office:office\" =\nxmlns:w=3D\"urn:schemas-microsoft-com:office:word\" =\nxmlns:m=3D\"http://schemas.microsoft.com/office/2004/12/omml\" =\nxmlns=3D\"http://www.w3.org/TR/REC-html40\"><head><meta name=3DGenerator =\ncontent=3D\"Microsoft Word 12 (filtered medium)\"><style><!--\n/* Font Definitions */\n@font-face\n\t{font-family:\"Cambria Math\";\n\tpanose-1:2 4 5 3 5 4 6 3 2 4;}\n@font-face\n\t{font-family:Calibri;\n\tpanose-1:2 15 5 2 2 2 4 3 2 4;}\n/* Style Definitions */\np.MsoNormal, li.MsoNormal, div.MsoNormal\n\t{margin:0cm;\n\tmargin-bottom:.0001pt;\n\tfont-size:11.0pt;\n\tfont-family:\"Calibri\",\"sans-serif\";}\na:link, span.MsoHyperlink\n\t{mso-style-priority:99;\n\tcolor:blue;\n\ttext-decoration:underline;}\na:visited, span.MsoHyperlinkFollowed\n\t{mso-style-priority:99;\n\tcolor:purple;\n\ttext-decoration:underline;}\nspan.EmailStyle17\n\t{mso-style-type:personal;\n\tfont-family:\"Calibri\",\"sans-serif\";\n\tcolor:windowtext;}\nspan.EmailStyle18\n\t{mso-style-type:personal;\n\tfont-family:\"Calibri\",\"sans-serif\";\n\tcolor:#1F497D;}\nspan.EmailStyle19\n\t{mso-style-type:personal;\n\tfont-family:\"Calibri\",\"sans-serif\";\n\tcolor:#1F497D;}\nspan.EmailStyle20\n\t{mso-style-type:personal-reply;\n\tfont-family:\"Calibri\",\"sans-serif\";\n\tcolor:#1F497D;}\n.MsoChpDefault\n\t{mso-style-type:export-only;\n\tfont-size:10.0pt;}\n@page WordSection1\n\t{size:612.0pt 792.0pt;\n\tmargin:70.85pt 70.85pt 70.85pt 70.85pt;}\ndiv.WordSection1\n\t{page:WordSection1;}\n--></style><!--[if gte mso 9]><xml>\n<o:shapedefaults v:ext=3D\"edit\" spidmax=3D\"1026\" />\n</xml><![endif]--><!--[if gte mso 9]><xml>\n<o:shapelayout v:ext=3D\"edit\">\n<o:idmap v:ext=3D\"edit\" data=3D\"1\" />\n</o:shapelayout></xml><![endif]--></head><body lang=3DIS link=3Dblue =\nvlink=3Dpurple><div class=3DWordSection1><p class=3DMsoNormal>RIKK vekur =\nathygli =E1 eftirfarandi:<o:p></o:p></p><p =\nclass=3DMsoNormal><o:p>&nbsp;</o:p></p><p class=3DMsoNormal><span =\nstyle=3D'color:#1F497D'><o:p>&nbsp;</o:p></span></p><p =\nclass=3DMsoNormal><b><span lang=3DEN-US>Emerging ideas in masculinity =\nresearch <o:p></o:p></span></b></p><p class=3DMsoNormal><span =\nlang=3DEN-US>- Masculinity studies in the North <o:p></o:p></span></p><p =\nclass=3DMsoNormal><span lang=3DEN-US><o:p>&nbsp;</o:p></span></p><p =\nclass=3DMsoNormal><span lang=3DEN-US>Second call for papers and =\nsuggestions for workshop themes. <o:p></o:p></span></p><p =\nclass=3DMsoNormal><span lang=3DEN-US>- Deadline November, 15th. 2013 =\n<o:p></o:p></span></p><p class=3DMsoNormal><span =\nlang=3DEN-US><o:p>&nbsp;</o:p></span></p><p class=3DMsoNormal><span =\nlang=3DEN-US>The next Nordic conference on research on men and =\nmasculinities will be held 4th.-6th. June 2014, at the University of =\nIceland, Reykjav=EDk. <o:p></o:p></span></p><p class=3DMsoNormal><span =\nlang=3DEN-US><o:p>&nbsp;</o:p></span></p><p class=3DMsoNormal><span =\nlang=3DEN-US>Keynote speakers include Raewyn Connell and Michael =\nKimmel.<o:p></o:p></span></p><p class=3DMsoNormal><span =\nlang=3DEN-US><o:p>&nbsp;</o:p></span></p><p class=3DMsoNormal><span =\nlang=3DEN-US>Suggested workshops are: Economy/financial crisis, the =\naffective turn and masculinity studies, relations between queer theory =\nand masculinity studies, global masculinities, family work balance, =\nantifeminism and xenophobia, men and feminism &#8222;up North&#8220;, =\nconfronting sexual violence, theory and practice in working with men, =\nmasculinity and media, fatherhood and parental leave, trafficking in the =\nNorth, men and health, indigenous masculinities in North America, =\nMasculinity and UNSCR 1325, doing food and doing masculinity and =\nWomen&#8216;s views on masculinity.<o:p></o:p></span></p><p =\nclass=3DMsoNormal><span lang=3DEN-US><o:p>&nbsp;</o:p></span></p><p =\nclass=3DMsoNormal><span lang=3DEN-US>The conference is organized by the =\nNordic Association for Research on Men and Masculinities (NFMM), as part =\nof the Icelandic Presidency of the Nordic Council of Ministers in 2014, =\nin cooperation with the Ministry of Welfare, Centre for Gender Equality =\nin Iceland and committee on gender equality at the University of =\nIceland.<o:p></o:p></span></p><p class=3DMsoNormal><span =\nlang=3DEN-US><o:p>&nbsp;</o:p></span></p><p class=3DMsoNormal><span =\nlang=3DEN-US>Please upload you abstracts here: </span><a =\nhref=3D\"http://nfmm2014.yourhost.is/call-for-papers/8-nfmm2014/6-abstract=\n-form\"><span =\nlang=3DEN-US>http://nfmm2014.yourhost.is/call-for-papers/8-nfmm2014/6-abs=\ntract-form</span></a> <span lang=3DEN-US><o:p></o:p></span></p><p =\nclass=3DMsoNormal><span lang=3DEN-US>Send suggested workshops and =\ngeneral enquiries to </span><a =\nhref=3D\"mailto:yourhost@yourhost.is\"><span =\nlang=3DEN-US>yourhost@yourhost.is</span></a> <span =\nlang=3DEN-US><o:p></o:p></span></p><p class=3DMsoNormal><span =\nlang=3DEN-US><o:p>&nbsp;</o:p></span></p><p =\nclass=3DMsoNormal><b><u><span lang=3DSV>Swedish: =\n<o:p></o:p></span></u></b></p><p class=3DMsoNormal><b><span =\nlang=3DSV><o:p>&nbsp;</o:p></span></b></p><p class=3DMsoNormal><b><span =\nlang=3DSV>Framv=E4xande id=E9er i maskulinitetsforskning =\n<o:p></o:p></span></b></p><p class=3DMsoNormal><span lang=3DSV>- =\nMaskulinitetsstudier i Norden<o:p></o:p></span></p><p =\nclass=3DMsoNormal><span lang=3DSV><o:p>&nbsp;</o:p></span></p><p =\nclass=3DMsoNormal><span lang=3DSV>Andra kallelsen f=F6r papper och teman =\nf=F6r arbetsgrupper (Workshops) <o:p></o:p></span></p><p =\nclass=3DMsoNormal><span lang=3DSV>- Deadline 15 november 2013 =\n<o:p></o:p></span></p><p class=3DMsoNormal><span =\nlang=3DSV><o:p>&nbsp;</o:p></span></p><p class=3DMsoNormal><span =\nlang=3DSV>N=E4sta nordiska konferens om m=E4n och maskuliniteter =E4ger =\nrum vid Islands Universitet i Reykjav=EDk mellan 4 och 6 juni =\n2014.<o:p></o:p></span></p><p class=3DMsoNormal><span =\nlang=3DSV><o:p>&nbsp;</o:p></span></p><p class=3DMsoNormal><span =\nlang=3DSV>Bland keynote f=F6rel=E4sare =E4r Raewyn Connell och Michael =\nKimmel.<o:p></o:p></span></p><p class=3DMsoNormal><span =\nlang=3DSV><o:p>&nbsp;</o:p></span></p><p class=3DMsoNormal><span =\nlang=3DSV>Redan f=F6reslagna arbetsgrupper inkluderar: Den =\nekonomiska/finansiella krisen; den affektiva v=E4ndningen och =\nmaskulinitetsstudier; relationen mellan queerteori och =\nmaskulinitetsforskning; globala maskuliniteter; balansen familj arbete; =\nanti-feminism och xenofobi; m=E4n och feminism &#8222;uppe i =\nNorden&#8220;; att konfrontera sexuellt v=E5ld; teori och praxis i =\narbete med m=E4n; maskulinitet och massmedia; faderskap och =\nf=F6r=E4ldraledighet; m=E4nniskohandel i Norden; m=E4n och h=E4lsa; =\nmaskulinitet bland ursprungsbefolkningen i Nord-Amerika; maskulinitet =\noch UNSCR 1325; att g=F6ra mat och g=F6ra maskulinitet; kvinnors =\n=E5sikter om maskulinitet. <o:p></o:p></span></p><p =\nclass=3DMsoNormal><span lang=3DSV><o:p>&nbsp;</o:p></span></p><p =\nclass=3DMsoNormal><span lang=3DSV>Konferensen organiseras av Nordiska =\nf=F6reningen for forskning om m=E4n och maskuliniteter (NFMM) som en del =\nav Islands ordf=F6randeskap i Nordiska ministerr=E5det 2014, i samarbete =\nmed V=E4lf=E4rdsministeriet, J=E4mst=E4lldhetscentret p=E5 Island och =\nj=E4mst=E4lldhetskommitteen vid Islands Universitet. =\n<o:p></o:p></span></p><p class=3DMsoNormal><span =\nlang=3DSV><o:p>&nbsp;</o:p></span></p><p class=3DMsoNormal><span =\nlang=3DSV>Intresserade b=F6r s=E4nda abstrakt h=E4r: </span><a =\nhref=3D\"http://nfmm2014.yourhost.is/call-for-papers/8-nfmm2014/6-abstract=\n-form\"><span =\nlang=3DSV>http://nfmm2014.yourhost.is/call-for-papers/8-nfmm2014/6-abstra=\nct-form</span></a> <span lang=3DSV><o:p></o:p></span></p><p =\nclass=3DMsoNormal><span lang=3DSV>F=F6rslag till arbetsgrupper och =\nallm=E4nna fr=E5gor till b=F6r s=E4nda till </span><a =\nhref=3D\"mailto:yourhost@yourhost.is\"><span =\nlang=3DSV>yourhost@yourhost.is</span></a><u><span lang=3DSV>. =\n</span></u><span lang=3DSV><o:p></o:p></span></p></div></body></html>\n------=_NextPart_001_00E2_01CEB390.E573FF10--\n\n------=_NextPart_000_00E1_01CEB390.E573FF10\nContent-Type: application/pdf;\n\tname=\"Second call - masculinity.pdf\"\nContent-Transfer-Encoding: base64\nContent-Disposition: attachment;\n\tfilename=\"Second call - masculinity.pdf\"\n\nJVBERi0xLjUNCiW1tbW1DQoxIDAgb2JqDQo8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFIvTGFu\nZyhpcy1JUykgL1N0cnVjdFRyZWVSb290IDMyIDAgUi9NYXJrSW5mbzw8L01hcmtlZCB0cnVlPj4+\nPg0KZW5kb2JqDQoyIDAgb2JqDQo8PC9UeXBlL1BhZ2VzL0NvdW50IDIvS2lkc1sgMyAwIFIgMjQg\nMCBSXSA+Pg0KZW5kb2JqDQozIDAgb2JqDQo8PC9UeXBlL1BhZ2UvUGFyZW50IDIgMCBSL1Jlc291\ncmNlczw8L0ZvbnQ8PC9GMSA1IDAgUi9GMiA4IDAgUi9GMyAxMCAwIFIvRjQgMTIgMCBSL0Y1IDE3\nIDAgUi9GNiAyMiAwIFI+Pi9FeHRHU3RhdGU8PC9HUzcgNyAwIFI+Pi9Qcm9jU2V0Wy9QREYvVGV4\ndC9JbWFnZUIvSW1hZ2VDL0ltYWdlSV0gPj4vQW5ub3RzWyAxOSAwIFIgMjAgMCBSIDIxIDAgUl0g\nL01lZGlhQm94WyAwIDAgNTk1LjMyIDg0MS45Ml0gL0NvbnRlbnRzIDQgMCBSL0dyb3VwPDwvVHlw\nZS9Hcm91cC9TL1RyYW5zcGFyZW5jeS9DUy9EZXZpY2VSR0I+Pi9UYWJzL1MvU3RydWN0UGFyZW50\ncyAwPj4NCmVuZG9iag0KNCAwIG9iag0KPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0xlbmd0aCAyODU4\nPj4NCnN0cmVhbQ0KeJydGl1z2zbyPTP5D3iUbiyY+CLITMZzjet02l46nSSde0jvQbFsSxebdOWo\nudx/vv9wuwsQBCiCYjMeSwKxWOz3LhZk57+yly/P31z++D0rzv+xbu7Y4qZZ/fZueXHBXn1/yV69\nf/7s/LVgNa9L9v72+TPBCvgTzBa8kprZsuAGZh6ePyvYHcD+8M6yuycc/fD82YfF1XIlF81SL+7u\nlyu72C1X9eJp+S/2/qfnz64AN+LvUNYV1ylKtip4oUrL3l9/WGxfDNadv5ZMyIQqUZTcJhgKWssy\nW4qiTuFTUHb15pKxSEYiJ6OhWLThxSyMMi91ySSwr0fFDjO6w3/1APK92aOk73aAawly3uCj9RNb\nmsWuYcuVWjysn2Di+nCPj2C22X3Gx19hyPYI/XSz3l9vcZgRllaam3TrSdbUlEEJwY0cZa2QXEqP\n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\nxnoandRsT+Z+uYJHR2LBX/8MzkJM95V50udKy6yCS5GSf5aFVXhTlcBmHQ0b9DaFvYxq/l7xwWd+\nuGlQ92AU7Ar4/eMQbl6HVYzXSN7UcCIXLvBWpZDzuNDYXkhBuyrF6Tg0zx4SMj93UdCf1KZNR1lM\n0T4ANiwqbze+KUVffxyCOkNYGB7U4nuL/pR48t5CBDfrbWnKkTM6rwwXNuUn1+OWleW1SmGnM9bc\nC2YtFRbm8xJmDvZ4+7nXpxpOF2XXtscQdx/icAiik1YhCqpVEjyHx77Ht57KhQLfYhkQkd0IZK8H\nG5EVtIdsxtb4dsw87IYuHhNYIv2jC2SZVZbu9ZJV7gx+TdbuwiDbRiXHCxfej3T37nHd9Oo7eVct\nZcl1He8MYUJA0VfACamCT2ss29+NPn5Lb4NtQwynD1IZEXcePly99BBujWSBY6H5V4qBh/22Rbek\n9Twkp3OXM5NXUXI3RxYLgER8uetbVQlqncSwPhTnVtSa+l+zsNfVESVHr8ycAytV9n0sYKWct5nW\ngg84OZa00CTH4RV1h8MU4sjt4v0y1tCZTQHs1nA2rrg2MF3CEUiEM/RImFC1xXUTdpa6i+tFhW5g\nLnTgC4IR6rzIBPBpdArrtf8wGQdPXiTD0R4VFzOIrQD/ymQ+GMFpXKb0sNPiD9I0qDV8F0bDrKnr\nusp3MPTcq2tVaS4jHqJXxNKroOROEnSgk7UfFtnXwU5cT8IuUTXgHCe64sm+G6Cw9koocBhcCUGB\nxTWUovNBF7zanEcXFr0/wXoq7OqTN9PYjLMmkfRfDLsjofPvy5UZe96F1KkXE7OvrIZgI+gtuqFt\nnLBvLQyXNi+9DNdBPBLdQkCMKvVReDlmYvrl1Gqsz6DAeXTSAJ9+gbPrZjz9uXp3lW5R5rZQFVe5\nkpved06XSUiMlckvI6L+DxYooX0NCmVuZHN0cmVhbQ0KZW5kb2JqDQo1IDAgb2JqDQo8PC9UeXBl\nL0ZvbnQvU3VidHlwZS9UcnVlVHlwZS9OYW1lL0YxL0Jhc2VGb250L0FCQ0RFRStQYWxhdGlubyMy\nMExpbm90eXBlL0VuY29kaW5nL1dpbkFuc2lFbmNvZGluZy9Gb250RGVzY3JpcHRvciA2IDAgUi9G\naXJzdENoYXIgMzIvTGFzdENoYXIgMTIxL1dpZHRocyAxMDEgMCBSPj4NCmVuZG9iag0KNiAwIG9i\nag0KPDwvVHlwZS9Gb250RGVzY3JpcHRvci9Gb250TmFtZS9BQkNERUUrUGFsYXRpbm8jMjBMaW5v\ndHlwZS9GbGFncyAzMi9JdGFsaWNBbmdsZSAwL0FzY2VudCA3MzIvRGVzY2VudCAtMjg0L0NhcEhl\naWdodCA3MzIvQXZnV2lkdGggNDQ1L01heFdpZHRoIDE1ODkvRm9udFdlaWdodCA0MDAvWEhlaWdo\ndCAyNTAvU3RlbVYgNDQvRm9udEJCb3hbIC0xNzAgLTI4NCAxNDE5IDczMl0gL0ZvbnRGaWxlMiAx\nMDIgMCBSPj4NCmVuZG9iag0KNyAwIG9iag0KPDwvVHlwZS9FeHRHU3RhdGUvQk0vTm9ybWFsL0NB\nIDE+Pg0KZW5kb2JqDQo4IDAgb2JqDQo8PC9UeXBlL0ZvbnQvU3VidHlwZS9UcnVlVHlwZS9OYW1l\nL0YyL0Jhc2VGb250L0FCQ0RFRStQYWxhdGlubyMyMExpbm90eXBlLEJvbGQvRW5jb2RpbmcvV2lu\nQW5zaUVuY29kaW5nL0ZvbnREZXNjcmlwdG9yIDkgMCBSL0ZpcnN0Q2hhciAzMi9MYXN0Q2hhciAy\nMzMvV2lkdGhzIDEwMyAwIFI+Pg0KZW5kb2JqDQo5IDAgb2JqDQo8PC9UeXBlL0ZvbnREZXNjcmlw\ndG9yL0ZvbnROYW1lL0FCQ0RFRStQYWxhdGlubyMyMExpbm90eXBlLEJvbGQvRmxhZ3MgMzIvSXRh\nbGljQW5nbGUgMC9Bc2NlbnQgNzMyL0Rlc2NlbnQgLTI4NC9DYXBIZWlnaHQgNzMyL0F2Z1dpZHRo\nIDQ1OS9NYXhXaWR0aCAxNjE5L0ZvbnRXZWlnaHQgNzAwL1hIZWlnaHQgMjUwL1N0ZW1WIDQ1L0Zv\nbnRCQm94WyAtMTc0IC0yODQgMTQ0NSA3MzJdIC9Gb250RmlsZTIgMTA0IDAgUj4+DQplbmRvYmoN\nCjEwIDAgb2JqDQo8PC9UeXBlL0ZvbnQvU3VidHlwZS9UcnVlVHlwZS9OYW1lL0YzL0Jhc2VGb250\nL1RpbWVzIzIwTmV3IzIwUm9tYW4vRW5jb2RpbmcvV2luQW5zaUVuY29kaW5nL0ZvbnREZXNjcmlw\ndG9yIDExIDAgUi9GaXJzdENoYXIgMzIvTGFzdENoYXIgMjQ2L1dpZHRocyAxMDggMCBSPj4NCmVu\nZG9iag0KMTEgMCBvYmoNCjw8L1R5cGUvRm9udERlc2NyaXB0b3IvRm9udE5hbWUvVGltZXMjMjBO\nZXcjMjBSb21hbi9GbGFncyAzMi9JdGFsaWNBbmdsZSAwL0FzY2VudCA4OTEvRGVzY2VudCAtMjE2\nL0NhcEhlaWdodCA2OTMvQXZnV2lkdGggNDAxL01heFdpZHRoIDI1NjgvRm9udFdlaWdodCA0MDAv\nWEhlaWdodCAyNTAvTGVhZGluZyA0Mi9TdGVtViA0MC9Gb250QkJveFsgLTU2OCAtMjE2IDIwMDAg\nNjkzXSA+Pg0KZW5kb2JqDQoxMiAwIG9iag0KPDwvVHlwZS9Gb250L1N1YnR5cGUvVHlwZTAvQmFz\nZUZvbnQvVGltZXMjMjBOZXcjMjBSb21hbi9FbmNvZGluZy9JZGVudGl0eS1IL0Rlc2NlbmRhbnRG\nb250cyAxMyAwIFIvVG9Vbmljb2RlIDEwNSAwIFI+Pg0KZW5kb2JqDQoxMyAwIG9iag0KWyAxNCAw\nIFJdIA0KZW5kb2JqDQoxNCAwIG9iag0KPDwvQmFzZUZvbnQvVGltZXMjMjBOZXcjMjBSb21hbi9T\ndWJ0eXBlL0NJREZvbnRUeXBlMi9UeXBlL0ZvbnQvQ0lEVG9HSURNYXAvSWRlbnRpdHkvRFcgMTAw\nMC9DSURTeXN0ZW1JbmZvIDE1IDAgUi9Gb250RGVzY3JpcHRvciAxNiAwIFIvVyAxMDcgMCBSPj4N\nCmVuZG9iag0KMTUgMCBvYmoNCjw8L09yZGVyaW5nKElkZW50aXR5KSAvUmVnaXN0cnkoQWRvYmUp\nIC9TdXBwbGVtZW50IDA+Pg0KZW5kb2JqDQoxNiAwIG9iag0KPDwvVHlwZS9Gb250RGVzY3JpcHRv\nci9Gb250TmFtZS9UaW1lcyMyME5ldyMyMFJvbWFuL0ZsYWdzIDMyL0l0YWxpY0FuZ2xlIDAvQXNj\nZW50IDg5MS9EZXNjZW50IC0yMTYvQ2FwSGVpZ2h0IDY5My9BdmdXaWR0aCA0MDEvTWF4V2lkdGgg\nMjU2OC9Gb250V2VpZ2h0IDQwMC9YSGVpZ2h0IDI1MC9MZWFkaW5nIDQyL1N0ZW1WIDQwL0ZvbnRC\nQm94WyAtNTY4IC0yMTYgMjAwMCA2OTNdIC9Gb250RmlsZTIgMTA2IDAgUj4+DQplbmRvYmoNCjE3\nIDAgb2JqDQo8PC9UeXBlL0ZvbnQvU3VidHlwZS9UcnVlVHlwZS9OYW1lL0Y1L0Jhc2VGb250L0FC\nQ0RFRStDb3VyaWVyIzIwTmV3L0VuY29kaW5nL1dpbkFuc2lFbmNvZGluZy9Gb250RGVzY3JpcHRv\nciAxOCAwIFIvRmlyc3RDaGFyIDMyL0xhc3RDaGFyIDMyL1dpZHRocyAxMDkgMCBSPj4NCmVuZG9i\nag0KMTggMCBvYmoNCjw8L1R5cGUvRm9udERlc2NyaXB0b3IvRm9udE5hbWUvQUJDREVFK0NvdXJp\nZXIjMjBOZXcvRmxhZ3MgMzIvSXRhbGljQW5nbGUgMC9Bc2NlbnQgODMzL0Rlc2NlbnQgLTE4OC9D\nYXBIZWlnaHQgNjEzL0F2Z1dpZHRoIDYwMC9NYXhXaWR0aCA3NDQvRm9udFdlaWdodCA0MDAvWEhl\naWdodCAyNTAvU3RlbVYgNjAvRm9udEJCb3hbIC0xMjIgLTE4OCA2MjMgNjEzXSAvRm9udEZpbGUy\nIDExMCAwIFI+Pg0KZW5kb2JqDQoxOSAwIG9iag0KPDwvU3VidHlwZS9MaW5rL1JlY3RbIDIyNC4x\nMiA0MDcuOTcgNTI2LjcgNDIxLjE5XSAvQlM8PC9XIDA+Pi9GIDQvQTw8L1R5cGUvQWN0aW9uL1Mv\nVVJJL1VSSShodHRwOi8vbmZtbTIwMTQueW91cmhvc3QuaXMvY2FsbC1mb3ItcGFwZXJzLzgtbmZt\nbTIwMTQvNi1hYnN0cmFjdC1mb3JtKSA+Pi9TdHJ1Y3RQYXJlbnQgMT4+DQplbmRvYmoNCjIwIDAg\nb2JqDQo8PC9TdWJ0eXBlL0xpbmsvUmVjdFsgNjguNiAzOTQuNzQgMTM1LjA0IDQwNy45N10gL0JT\nPDwvVyAwPj4vRiA0L0E8PC9UeXBlL0FjdGlvbi9TL1VSSS9VUkkoaHR0cDovL25mbW0yMDE0Lnlv\ndXJob3N0LmlzL2NhbGwtZm9yLXBhcGVycy84LW5mbW0yMDE0LzYtYWJzdHJhY3QtZm9ybSkgPj4v\nU3RydWN0UGFyZW50IDI+Pg0KZW5kb2JqDQoyMSAwIG9iag0KPDwvU3VidHlwZS9MaW5rL1JlY3Rb\nIDMwOC40NSAzODEuNTIgNDE0LjU5IDM5NC43NF0gL0JTPDwvVyAwPj4vRiA0L0E8PC9UeXBlL0Fj\ndGlvbi9TL1VSSS9VUkkobWFpbHRvOnlvdXJob3N0QHlvdXJob3N0LmlzKSA+Pi9TdHJ1Y3RQYXJl\nbnQgMz4+DQplbmRvYmoNCjIyIDAgb2JqDQo8PC9UeXBlL0ZvbnQvU3VidHlwZS9UcnVlVHlwZS9O\nYW1lL0Y2L0Jhc2VGb250L0FCQ0RFRStDYWxpYnJpLEJvbGQvRW5jb2RpbmcvV2luQW5zaUVuY29k\naW5nL0ZvbnREZXNjcmlwdG9yIDIzIDAgUi9GaXJzdENoYXIgMzIvTGFzdENoYXIgMzIvV2lkdGhz\nIDExMSAwIFI+Pg0KZW5kb2JqDQoyMyAwIG9iag0KPDwvVHlwZS9Gb250RGVzY3JpcHRvci9Gb250\nTmFtZS9BQkNERUUrQ2FsaWJyaSxCb2xkL0ZsYWdzIDMyL0l0YWxpY0FuZ2xlIDAvQXNjZW50IDc1\nMC9EZXNjZW50IC0yNTAvQ2FwSGVpZ2h0IDc1MC9BdmdXaWR0aCA1MzYvTWF4V2lkdGggMTc1OS9G\nb250V2VpZ2h0IDcwMC9YSGVpZ2h0IDI1MC9TdGVtViA1My9Gb250QkJveFsgLTUxOSAtMjUwIDEy\nNDAgNzUwXSAvRm9udEZpbGUyIDExMiAwIFI+Pg0KZW5kb2JqDQoyNCAwIG9iag0KPDwvVHlwZS9Q\nYWdlL1BhcmVudCAyIDAgUi9SZXNvdXJjZXM8PC9Gb250PDwvRjEgNSAwIFIvRjIgOCAwIFIvRjMg\nMTAgMCBSL0Y0IDEyIDAgUi9GNyAyOSAwIFI+Pi9Qcm9jU2V0Wy9QREYvVGV4dC9JbWFnZUIvSW1h\nZ2VDL0ltYWdlSV0gPj4vQW5ub3RzWyAyNiAwIFIgMjcgMCBSIDI4IDAgUl0gL01lZGlhQm94WyAw\nIDAgNTk1LjMyIDg0MS45Ml0gL0NvbnRlbnRzIDI1IDAgUi9Hcm91cDw8L1R5cGUvR3JvdXAvUy9U\ncmFuc3BhcmVuY3kvQ1MvRGV2aWNlUkdCPj4vVGFicy9TL1N0cnVjdFBhcmVudHMgND4+DQplbmRv\nYmoNCjI1IDAgb2JqDQo8PC9GaWx0ZXIvRmxhdGVEZWNvZGUvTGVuZ3RoIDI3MTk+Pg0Kc3RyZWFt\nDQp4nJVazXPbuBW/e8b/A45SZwUTBECQScbTtWN3sq0znSTbHpwe5Ei2tbJIL2V5t/+v99BDrzkX\n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\nIDQwOS41MV0gL0JTPDwvVyAwPj4vRiA0L0E8PC9UeXBlL0FjdGlvbi9TL1VSSS9VUkkoaHR0cDov\nL25mbW0yMDE0LnlvdXJob3N0LmlzL2NhbGwtZm9yLXBhcGVycy84LW5mbW0yMDE0LzYtYWJzdHJh\nY3QtZm9ybSkgPj4vU3RydWN0UGFyZW50IDU+Pg0KZW5kb2JqDQoyNyAwIG9iag0KPDwvU3VidHlw\nZS9MaW5rL1JlY3RbIDY4LjYgMzgzLjA3IDEzNS4wNCAzOTYuMjldIC9CUzw8L1cgMD4+L0YgNC9B\nPDwvVHlwZS9BY3Rpb24vUy9VUkkvVVJJKGh0dHA6Ly9uZm1tMjAxNC55b3VyaG9zdC5pcy9jYWxs\nLWZvci1wYXBlcnMvOC1uZm1tMjAxNC82LWFic3RyYWN0LWZvcm0pID4+L1N0cnVjdFBhcmVudCA2\nPj4NCmVuZG9iag0KMjggMCBvYmoNCjw8L1N1YnR5cGUvTGluay9SZWN0WyAzNjEuNzcgMzY5Ljg0\nIDQ2Ny45MSAzODMuMDddIC9CUzw8L1cgMD4+L0YgNC9BPDwvVHlwZS9BY3Rpb24vUy9VUkkvVVJJ\nKG1haWx0bzp5b3VyaG9zdEB5b3VyaG9zdC5pcykgPj4vU3RydWN0UGFyZW50IDc+Pg0KZW5kb2Jq\nDQoyOSAwIG9iag0KPDwvVHlwZS9Gb250L1N1YnR5cGUvVHJ1ZVR5cGUvTmFtZS9GNy9CYXNlRm9u\ndC9BQkNERUUrQ2FsaWJyaS9FbmNvZGluZy9XaW5BbnNpRW5jb2RpbmcvRm9udERlc2NyaXB0b3Ig\nMzAgMCBSL0ZpcnN0Q2hhciAzMi9MYXN0Q2hhciAzMi9XaWR0aHMgMTEzIDAgUj4+DQplbmRvYmoN\nCjMwIDAgb2JqDQo8PC9UeXBlL0ZvbnREZXNjcmlwdG9yL0ZvbnROYW1lL0FCQ0RFRStDYWxpYnJp\nL0ZsYWdzIDMyL0l0YWxpY0FuZ2xlIDAvQXNjZW50IDc1MC9EZXNjZW50IC0yNTAvQ2FwSGVpZ2h0\nIDc1MC9BdmdXaWR0aCA1MjEvTWF4V2lkdGggMTc0My9Gb250V2VpZ2h0IDQwMC9YSGVpZ2h0IDI1\nMC9TdGVtViA1Mi9Gb250QkJveFsgLTUwMyAtMjUwIDEyNDAgNzUwXSAvRm9udEZpbGUyIDExNCAw\nIFI+Pg0KZW5kb2JqDQozMSAwIG9iag0KPDwvQXV0aG9yKP7/AMEAcwB0AGEpIC9DcmVhdG9yKP7/\nAE0AaQBjAHIAbwBzAG8AZgB0AK4AIABXAG8AcgBkACAAMgAwADEAMCkgL0NyZWF0aW9uRGF0ZShE\nOjIwMTMwOTE2MTQzMjE0KzAwJzAwJykgL01vZERhdGUoRDoyMDEzMDkxNjE0MzIxNCswMCcwMCcp\nIC9Qcm9kdWNlcij+/wBNAGkAYwByAG8AcwBvAGYAdACuACAAVwBvAHIAZAAgADIAMAAxADApID4+\nDQplbmRvYmoNCjM4IDAgb2JqDQo8PC9UeXBlL09ialN0bS9OIDY4L0ZpcnN0IDUyMy9GaWx0ZXIv\nRmxhdGVEZWNvZGUvTGVuZ3RoIDExMDc+Pg0Kc3RyZWFtDQp4nM1X227USBB9R+If6g/c94uEkHa5\nCMgSRplI+4B4cBJvMmQyjowjwd/vaZedDIy9sRuQ9sXldvc5demq6rZ2JEhHspKMICkUGUnSajKK\nlDBkNClryRjSwpGxpK0ngzcRyODNRjIBaEEGJE6RFeSkSXxe4YsiHySBL2hNIArBEhii9mQDRY+P\nEWpFAkM6SQ5mSA0GAxkcgVMqTDqMNbQ6Cwl7nIaM+A6cwSKX7IZm5yEDZCDpNObB70Igj3kPHz34\nfIRdwAfwePBG+O3BG6HUOzgOX72HhI0+kJIS7kRIGBEEKSU9wS2lvKCAQGmVHIQEGKqVgetwVBlM\nBvBYDc+Bt7AjAu+UpQi8g934pLzEGDwei+CyCoh+BE+UmuCiinAyemyBwBh7IBCPGEhL6I0aEnpj\nJK2wJQgmaZ0eWKgRea1IO3iDDYB9QGFXBbYWW2AErIEPxmADnz0rVgkk6KRYF6vi9NttVazb5u68\nfbWtboqjjyQ+UbG6JJ3WPH/+9MkMiFwOUcshejnELIfY5RC3HOKXQ8JySMzYypztz9h/iQpFcaIu\nUZKobuQ0ahnlg8pJRYc6Q2mhmlBAqBmUCyoERYE66JI9Ixl0RjbojHTQo/mArsOY9W25O4ANy4sj\nlGyP6dZ8+PPdSfHh7HOKTZr/gff7NQjeyJpVarszdYdDe//a7K5HC8WnBanDd6Iz7+ccj3NL1LLK\nzqt0lkxoxmkxT7MR4+GU4+F0ei6vPLRlKpzOsDf2571Rc/uWYpWahZvS/EiPzenLGaXYNyZllh9M\nizAqA4OrFNnhHFiE9BmYkIGJOdHLCnlOzGVO0KXOAZkckM0B5aSDzMkHOZ4Q4bGuz+vRMWQc739u\n6H/fMf+wyI8uWqU770z1ShzaPNUmQ3fOpAt2J+RDz8r2Xsm5dccnHjuW7vJTuuNjB8SDbjUe1DAR\n1GhnM+tDc6aCGvkkjf5XeGTmdiQ+8Nij9KczpfuRdp1Tm2qiNtUAKpt2/GrXGas5CQ0noZEs+DQ1\nfJoa9s6wd4ZxhhPIcAIZZrHMYpnFMotlFttfCnjOMdwx3DHc85xnnGecZ5xn7Z61e4Z7hnuGB9Ye\nmCUwS2CWwCyBWUKfJDwXGZ5+RLt9209jjtlpU1Undd0WJ/W2el/epn/TLqnLptp1s+kvtUuaj/12\npU25nz2uvrZH1TcarqSvwbWr26o4To9Xu4uHwSmWntVfi3V13hZvqvKiavg9YYb3t7vtZletr8pk\nYfrwxw4MZbupd/24aTf/lHjpRn/XzfVZXV8XL+vzuxvY1H35clVVLafH+/K8qffGL67w3Bu/3JTb\n+nLvw3q7uaj21rIeLLtsypvi9ebyrql6X4/vbr7gxpN+37soD3ddNbzo4dZm0q99lypDX3LDi+9r\nizfn9ybv/1CM1lP/58DG85/D/TWY55w7rLVPlOL3nwU3Kn5HFTKcD77+DLpvpf1cGK3Qp0/+BeO3\n56oNCmVuZHN0cmVhbQ0KZW5kb2JqDQoxMDEgMCBvYmoNClsgMjUwIDAgMCAwIDAgMCAwIDAgMCAw\nIDAgMCAwIDMzMyAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAyNTAgMCAwIDAgMCAwIDAgMCAwIDAg\nMCA2MTEgMCAwIDAgMCAwIDAgMCA5NDYgODMxIDAgMCAwIDAgNTI1IDAgMCAwIDAgMCAwIDAgMCAw\nIDAgMCAwIDAgNTAwIDAgNDQ0IDYxMSA0NzkgMCA1NTYgNTgyIDI5MSAwIDU1NiAyOTEgMCA1ODIg\nNTQ2IDAgMCAzOTUgNDI0IDMyNiA2MDMgMCA4MzQgMCA1NTZdIA0KZW5kb2JqDQoxMDIgMCBvYmoN\nCjw8L0ZpbHRlci9GbGF0ZURlY29kZS9MZW5ndGggNTU0MjEvTGVuZ3RoMSAxMTY4MDg+Pg0Kc3Ry\nZWFtDQp4nOxdC0BUVfr/nTsPhmFghuE1OMDMiK8EBcFUUGOQhyIYpFiglaBiGD7IR4+tFs1Iwx72\n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\n/Zbvd+sfzYvZ1so/f3vWmvKz0WvrGUa2vt/Eu3CI+62+u2onX0CjIh3HjUoRlmQpl/PD/3JJ2lYu\nSRGHuGB+Mfpz9Tm/At+C2mN9fmsxGvT6ALmMLT93tvyoiN9Zgeb7/9QKDPl8BdJZHpepvdQtGkv6\nX8s6lssd+rCj2uw909H3e06cOPLc/oLxTdf9wfGc0+EXBo+zM64OWihx2Ti87d6oE6Nvj6oyemXd\nmUku7d6Vbp8bIT6+oEd/s4nfrNI884jyqB34VDlZ5fNqV6l7/gNbw/7kjIt/mxc/7oB+2uvxhuxa\na4vmDpuz8dXUekO7BqZ5dIi49LjEThJ7PqNgTq5c+cHq1ITHabusFlx849TTd74saG+2aMOwsXsL\nv5/o45/5U2j67hn6AW923OriZl3r+I2fz4YEdmzlFu4wOLv2keWJj2af0v6txe3ndiN/+Wl4UfpQ\n5YGF3dtzoTU3Fq6vHh/e4OKUNfUthl2ounnAsF8XLdeUh4//jsslzuAC3vIuwAEdQBPDw/Ocfmrx\nUn6/rJUpYgQ8gLZibdu4+Hz8ipefvB79Ukbjz76OFCj15jz5ym5f/aKStCZXg5+mqh+fR2s0BklE\nmiFZo1Masqh7aNKYk0o5rrHgHoI4aVCwVLj9N2j0h1u5aM8B7a1mT7t5+C2ZkxnH3StcPbnOoNfl\n+V2KtpUvKpS0GN6jcEHh1MFBKT+1Tsh6WJx+LPbS078tHOs5dcmYxM2HU7Lja533Cr/qgGfcmX1o\nX0Di/PnJvvNON/XfZ1vSx/dAu9vWLcJm+6/2a7LqfsfRra+Pcdg1X9VTVpw7fOnggIwud+dtSWg2\nP8pTalnbdcnq29MbVL3VfK7cdXAfM8USr8bR416tfDRLdMTjzL6ebTePH7Wv6f3YWd3WfViZnWro\ntr7q8dlWfjVR72mDlY13dXa2CO9l7P9uWaK15Yqfc3r1frS1WZx7Tga59HLvulH55RtOfHN+ZXXd\ngPDS3Y8ti3y4zebfHtssyXD5tkzwG6u4nOVcTiFdl5jkzOdy5oxy7H9a+0ipW1yrx0jXTV2nGH9c\nqvvXz1/uH9g48wr5d2z2T342p2rog+249oUMp2cDBgctWWzzYwuz6XlTjzW9VfPp494z/UsK2h+N\nf/T+L8ebNeu3ulGssrx2astjx9dcNRt+RTq5+RJH7ZBd5c7dqyr3vz8ded2pn6T7vfhh69dUO9qg\n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\nGEVnTnkYeF9+9Pghh4Q8fR1p9MTjh98MLL2qEQ/23XLwVIruezdd8M8eW6r93P2wV+HJCyE3JpS+\nHr7x/kmrq9L39TMdjXf7rV5Qy7VKg+GxaXOkz9qW1xvc5Mr5WwXDVCGFP3KH+54aI2q2acH9Lo+P\nmVlfenAku+MDcraZZ2jL1VWSnIclhk5/nRcgapS4v4pDrHnp9kEDIxN77+laeG3E8guJl7LvVb1Q\n5OI7/dqyFqNvLZsz7NkDj3vbL81Y0O7A7JZzjm0zeNgcOji50/Wzg4tmjzzWdnDWygnHHN6cbzaK\n253mfb30Yp7/cnft9XrEfPO7ma36TkuYG/Z+7oB7T5q2Lc52frHquOps0ocBNzsvzYWwKBe/+zhj\n5tJcfB+K7lDzTvqnvNT8yqtUW3NLXgEReJmCvlxVU9uz+fjRDgbTq3xiJnWg+z1s8EFBwUFBIaH9\nwP+amJ4zcfRpu2X0w6dvxg35VvnMrK31+q+YgO/1S08bpHS5d8Kn3UvXFXjv0ylXT4u45p1TVt1Z\nt96+ddkTt4fOhm5rCm8GlIzLER/xavjjLIfJ3YcEND7y/YgdA34ty+/eaU+NvVe6nPZpPVO5qziv\nrNpt4wfzPvV6jQndMavZrA9t/cd5ZTcbbn9p3pULa3YPeXtuiGPU9OuXpZlrIxduWLFzc4uzmhWq\n877lcdXzCh/1sNy3d7+xvUsb5bTMhjbR62rlLF4vjWykPdXvTcjyC8XyltffHqtrW3K6U86UQ0F3\nk05deRF0rGSMu8fRnPVTN9ZtIOu2xOtcI0XBYTfjNa/R6Zt6oDrLdmZJJ6X2W3K0R9TyjRGqRsh9\nxU6rlpMOnvBsfSl708XikP8BOmaESQ0KZW5kc3RyZWFtDQplbmRvYmoNCjEwMyAwIG9iag0KWyAy\nNTAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAg\nMCAwIDAgMCAwIDAgMCAwIDAgNjA2IDU1OCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAw\nIDAgMCAwIDAgMCAwIDAgMCAwIDAgNTAwIDAgNDQ0IDYxMSA1MDAgMzg5IDU1NiA2MTEgMzMzIDAg\nNjExIDMzMyA4ODkgNjExIDU1NiAwIDAgMzg5IDQ0NCAzMzMgNjExIDU1NiAwIDUwMCA1NTYgMCAw\nIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAg\nMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAw\nIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAg\nMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCA1MDAgMCAwIDAgMCA1MDBdIA0K\nZW5kb2JqDQoxMDQgMCBvYmoNCjw8L0ZpbHRlci9GbGF0ZURlY29kZS9MZW5ndGggNTIyMDIvTGVu\nZ3RoMSAxMTc1Njg+Pg0Kc3RyZWFtDQp4nOx9CWBU1dX/uW/e7NubNbMkeTNMFiBAQhKWsGUSkgBG\nIEDQBIgkQDCgQJRF6lKolooBC5YibVWI+hVbW+uQWBtQC1ZK/Vq3utC9oAjaVgtatf208P6/+2ZC\nYsD8aWFU2pzJ/d3l3OW8c+897973bmaIEZEHIFJV2YxJE1x/efePxB5+kyhYM6GsvIKytMOIfeMb\nyJU+oWrqjHe/3+8RxDuI9G9OmDGz9JH62p3EWluJ7BMvrZ4xsf7Y4W8TecuItIVTZ+TmfzH9+yeJ\n2K9Rvv6yssk1H2qO3IK6VxOZ98xf0tA89u7cUUQtPuR5b/6qFaHnj//xRaLtBiLNxIXNVy6Ztmr7\nz4k2WlDf4SsbljeTHh/2jY2oz3Ll1V9YeFPaYpS/t4ro4LVNC5asvvX9315KlF1INO/1psaGBYde\n+eVW1D0H+Yc3IcH5iOVZxL+OeEbTkhWrn/iVr5ZIGEnktly9bH7DRwP24/qu209kmrekYXWzxWNd\njPy4XgotbVjSeN+jjfOI3dwf11jSvGz5CqWAVkOeqzm/+drG5k2trdlEd7wF8VKJ61aLq33we1vm\n2se8bwjiskAP5d+zj/tPlr558NTdJ+8ybzYsRF4jCRQnlNP+7uQe4UFz66m7T91t3qzW1I3Eg2qe\nPHoS2phNOpSUKJdmEuma0a4ArqD5vvA4WjdoteJzvEzcF/LoesHJREHUCzqN1iSIh0m4NUrk6qz7\n0klTplKIQh/p4jIYFgqNIWI71HaztHv4lZJGJ1I+T9HRmST+hqLiKoqehaUStDDlk3h99J9FIlEV\n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\n56ZRDQplbmRzdHJlYW0NCmVuZG9iag0KMTA2IDAgb2JqDQo8PC9GaWx0ZXIvRmxhdGVEZWNvZGUv\nTGVuZ3RoIDcwMTU5L0xlbmd0aDEgMjE5MTI0Pj4NCnN0cmVhbQ0KeJzsnQt8FNX595+Z2fv9ks1u\nskl2N5tNyI2ERO5BNlcCQUASJaFUEkIEFSQVsGpRsK23eMFW/lq1CrWV2qJls1Eb1H+lttYLKqBW\n0VpAARVrK9Vq66XO+5uzmwgb0mza8uZ9PznfzfmdmXN9ZnbyZJ6dmQ0JROSBqGhDTePMGZsKnvyE\npPAlRO51M2pq65bals0h8ZJ5ROLdM+bNbfz6l723kXjZMaKffTij8ayqR03zUknKOUzkvWhWY1Pd\nyoLzNOjfjFGzZjc11nfsvfJDookbiMxfm9tYUuZsuuQajKVDfeu86tlNLS9deQjje7A+4eyaM5rn\nBJd/m6jmDSL7pvaVbZ2L3qn/lITbUC9+1n7xGv+XpfuPkbD9ZiLt6+d2Llv5m62ZAgl3pBBpxi1r\nW91JHtJjPGV827IVl567oSX7ThJ+uYLo9kuWL115yYfHfhMlOruThPB9yzvalu6f+cAfiYQblPmX\no8DxQepCrP8W6znLV665pHGG/VHMvZxovOeCjosu/OLDf2L8CzahzYcrVrW3lf+i5TUS52H70yMr\n2y7pdL5vy0Pdu+jvv7BtZceYuqJ0EldeS2Ro7Vy1eo2cQ0tJ/Car77yoo3Py20+bSdhST2StJeW9\nUNvf39Iye+Jia8XHujRlM4juOZT5hJI//UT5bZ+v+ecNNtJZsKpn7RWQawNf1tICG32+5rMDNuqv\niWMer5SYF9BCkuhsvNci2agES6QJYV4RtZI0XriZ1KRT36EuxwChWC5toXNFh6AWRY2kVqlFSXWQ\nCuSddEklswA0nVHtpzD5/VnqG76sE8q1AeHRMAmyLKP3JvVsZUtJpZksZCitxb70czos1dBVdBJQ\n14Q0tn+d6EKsNyL/njiZJBXRLKRjSEVIjUh+pCVIynGHzaN1rN9kiiDdlDi+9hxqUz9FNvXZlI00\nC8tB1SEqUK2mAJbr4zaUS5lUgPVs1icTbZ+Sj7DxV8fK0DeI5Q3oezraGZEc2hvJe7JtSkS6kWaq\nSP4ceR3srUE+G3POxfI0JDPsrhAny+1YtmN5mmYy2bFsQqpFv0+VPmhvho1LUZ+CdVFpCxvMyL1K\nW4yZn4wtg9l3fJ6IYt+/6q/Y+e/O3T/Hz+l9pMn/6Tj/bbBPqpUctu34T8f4TxjsPYBdt/6nY3M4\nHA6Hw+EI98mPjLQNyaL2/v9jK4fD4YwkAsmP6JBsxP0mh8PhcDgcDofD4XA4HA6Hw+FwOBwO59Sj\n3Ac70jZwOBwOh8MZ3Qz17MvwMC/QCoKwbDlb0WiI3F/XTcuKQVk0g1WU19Zbwp4lM6ijo508YWqh\nwsnzw9Uv949Sds70+obDS5umRpfM+KjpnHPOKckub2pb1P5Ux4mzafFS0MXXdZTA3+VkjP6Atp1Y\n8Flii6lMhcRiDmcYBwU/fv57wMuMtAkcDofD4QwK/yvFGYqpSbaTSBIU1JIkiDj/8ajfN+6kf+hk\nBD46+UvSkwFqYGoko/xPMpEJamZqITPUCv2CbGSF2pk6yAZ1Qj+nFLJDXeSAplIK1A39jDzkgqaR\nG5rO1Ese+VPKoHRoJtMs8kJ9lAH1Q/9BAcqEZpMPGiQ/NAf6dwpRAJpL2dA8pmMoR/6E8ikELaBc\naCHlQYtojPwxFVM+dCwVQEuYllKh/DcaR0XQMiqGljM9jUrkj2g8lUInMJ1I46CTqEz+kCZTOXQK\njYdOZVpBE6DToH+l02kidDpNgoZpCrQSeoyqaCq0miqgNTQNWgv9gOpoOnQGhaH1TGdSpfwXmkVV\n0Aaqhs6mGugZVCv/meZQHXQuzYDOY3om1cvv03yaBW1k2kQN0LNotvwnOpvOgC5g2kxzoS00D7qQ\nzpTfo68xXUTzoV+nRug51CQfpcV0FrSVzoa20QLoEui71E7N0KW0ENpBX4OeC32HltEi6HL6OvQ8\npufTYvltuoBaoSuoDbqS6YW0RD5Cq6gd2klLod+gDuhFdK58mFbTMugapmtpOfRiOg/6TbpAPkSX\nML2UVkAvo5XQb9GF8lu0junl1Am9gr4BXQ99kzbQRdAraTX027QG+h1aKx+k79LF0Kvom9Cr6RLo\nNdADdC1dCr2OvgXtYno9rZP30w10OfRGugJ6E9ONtEH+I91MV0K/R9+Gfp/pLfQd6Cb6rvwG/Q9d\nBb2VrobeRteg1w/oWtTezvQOug56J10P/SHdgDZ3Mb2bboRuppugW6B/oB/RzdB76HvQH9P3oT+B\nvk730i3QrbQJ+lO6FXof9DX6Gd0G/Tn9ALqNbkf5/UwfoDtR8gv6IXQ70wjdBe2mu+V9FKXN0B7a\nAn2QfgR9iO6RX6WH6cfQXzLtpZ9Ad9BW+RV6hOmj9FPoY3Qf9H/pZ/Lv6VdMH6dt0J10P/TX9ID8\nMj3B9Df0C+hvKQJ9EvoS/Y66oU9RD/RpehD6DNNn6SH5RdpFD0Ofo19Cn6de6Au0Q95Lu+kR6B6m\ne+lR6Iv0mLyHXqJfQV9mCiugr9BOeTe9Sr+G7mP6Gv0G+jr9Vn6B/sD0DXoS+kf6HXQ/PSU/Twfo\naehBegb6Jj0LfYt2yc/RIaaH6TnoEXoB+jbTd2i3vIvepT3Qo7QX+h7TP9FL8rP0Pr0M/TP9HvoX\nph/QK9Bj9Cr0r7QP+iG9Bv2IXpefob/RH6AfM/2E3oD+nfbLT9M/6AD0U6af0UHo5/Sm/BR9wfSf\ndAj6JR2GynRE/h336aPcp/+J+fQ/MZ/+HvPp7zGf/h7z6e8xn36U+fSjzKcfZT79KPPpR5lPP8p8\n+lHm048yn/4u8+nvMp/+LvPp7zKf/g7z6e8wn/4O8+nvMJ/+NvPpbzOf/jbz6W8zn/428+lHmE8/\nwnz6EebTjzCffpj59MPMpx9mPv0w8+mHmE8/xHz6IebTDzGf/hbz6W8xn/4W8+lvMZ/+JvPpbzKf\n/ibz6W8yn36Q+fSDzKcfZD79IPPpB5hPP8B8+gHm0w8wn36A+fT9zKfvZz59/wj69B/Effpr/5ZP\n38d8+j7m0/cxn76P+fR9zKfvYz59H/PprzKf/irz6a8yn/4q8+mvMp/+CvPprzCf/grz6a8wn/57\n5tNfZj79ZebTX2Y+/WXm019iPv0l5tNfYj79JebTX2Q+/UXm019kPv1F5tP3Mp++l/n0vcynv8h8\n+l7m0/cyn76X+fS9zKfvYT59D/Ppe5hP38N8+m7m03czn76b+fTdzKe/wHz6C8ynv8B8+gvMp7/A\nfPrzzKc/z3z688ynP8d8+i7m03cxn76L+fRdzKfvYj59F/Ppu5hPf4759F3Mp+9iPn0X8+m7mE9/\nlvn0Z5lPf5b59GeZT3+G+fRnmE9/hvn0Z5hPf3oU+fRC7tO5Tx81Pv32/8inv3qKfPp27tP/L/h0\ngscl8yJjqp4kSaVmn9KoVESSSpI0MUiDKqXcoNNqdVqNFg10WFEuXqt1Oq2x/+MdtYJKrUFXSY+e\napUKa1osnPgxkJrU8Y+DYmgSPydSLBgaDSU0Uye2iE2Q1GCc0YWYfFPtqbNi1CEYU0faBA6Hw+Fw\n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\n9Z4rLfa1rxzhJo3GVpoArpAOpB72H3az9fwna1okXw0KZW5kc3RyZWFtDQplbmRvYmoNCjEwNyAw\nIG9iag0KWyAwWyA3NzhdICAzWyAyNTBdICAxMVsgMzMzIDMzM10gIDE1WyAyNTAgMzMzIDI1MCAy\nNzggNTAwIDUwMCA1MDAgNTAwIDUwMCA1MDAgNTAwXSAgMjdbIDUwMF0gIDI5WyAyNzggMjc4XSAg\nMzVbIDkyMSA3MjIgNjY3IDY2NyA3MjIgNjExIDU1NiA3MjJdICA0NFsgMzMzIDM4OSA3MjJdICA0\nOFsgODg5IDcyMl0gIDUxWyA1NTZdICA1M1sgNjY3IDU1NiA2MTEgNzIyIDcyMiA5NDRdICA2OFsg\nNDQ0IDUwMCA0NDQgNTAwIDQ0NCAzMzMgNTAwIDUwMCAyNzggMjc4IDUwMCAyNzggNzc4IDUwMCA1\nMDAgNTAwIDUwMCAzMzMgMzg5IDI3OCA1MDAgNTAwIDcyMiA1MDAgNTAwIDQ0NF0gIDEwOFsgNDQ0\nXSAgMTEwWyA0NDRdICAxMTZbIDI3OF0gIDEyNFsgNTAwXSAgMTc5WyA0NDRdICAxODFbIDMzM10g\nIDE5NlsgNDQ0XSBdIA0KZW5kb2JqDQoxMDggMCBvYmoNClsgMjUwIDAgMCAwIDAgMCAwIDAgMzMz\nIDMzMyAwIDAgMjUwIDMzMyAyNTAgMjc4IDUwMCA1MDAgNTAwIDUwMCA1MDAgNTAwIDUwMCAwIDUw\nMCAwIDI3OCAyNzggMCAwIDAgMCA5MjEgNzIyIDY2NyA2NjcgNzIyIDYxMSA1NTYgNzIyIDAgMzMz\nIDM4OSA3MjIgMCA4ODkgNzIyIDAgNTU2IDAgNjY3IDU1NiA2MTEgNzIyIDcyMiA5NDQgMCAwIDAg\nMCAwIDAgMCAwIDAgNDQ0IDUwMCA0NDQgNTAwIDQ0NCAzMzMgNTAwIDUwMCAyNzggMjc4IDUwMCAy\nNzggNzc4IDUwMCA1MDAgNTAwIDUwMCAzMzMgMzg5IDI3OCA1MDAgNTAwIDcyMiA1MDAgNTAwIDQ0\nNCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAw\nIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAg\nMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAw\nIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCA0NDQgNDQ0IDAgMCAwIDAg\nMCAwIDAgMjc4IDAgMCAwIDAgMCAwIDAgMCA1MDBdIA0KZW5kb2JqDQoxMDkgMCBvYmoNClsgNjAw\nXSANCmVuZG9iag0KMTEwIDAgb2JqDQo8PC9GaWx0ZXIvRmxhdGVEZWNvZGUvTGVuZ3RoIDI0OTM5\nL0xlbmd0aDEgNTYzOTI+Pg0Kc3RyZWFtDQp4nOx8eXyN19b/Wvt5TnJkkEEQiZOcODJwEjGFiENO\nJDGlCIJEtTIIUXXRoKpFOugQWnTUcnE7tylOIq0EJS6dqQ6GDu5FS/VWVSfu7SV5ft+9zzkR2t72\n/b3vH+/n9+uzsr57P3vea6+99trHOYiJqA1Ap+5ZY4cNCajqU0fU8DRR5NNDsrIHh/kFjiO6KZFI\nXDMkd9TYiEe3XiSau4AodcyQseMG+Zh2/Uj00j40cnjU2OSeQfZrU4gYbVDh+KwR+T1rRsSjrQqi\nkIdLZhbNLo4e05soKQxlniyZP9f6YutSxNOziHyXT509bWZ1aJ+eRN1Q3/TRtKLy2WSnVui/I9oL\nnnbjLVPv4ud9iAa/h/KLy0qLpnz4hc8NaMuB/D5lSPDbJBDnuXjvXDZz7oIuZ9s8ibGHE3WYeuOs\nkqKyCVPnEY3MxXvxzKIFs4N/8o9E+dUob/1T0czS+z66JpBoWi2RX/bsWeVzm+II859zSebPvql0\n9o4NK6KJeq9EfjVJ2Zk6dlmUsil4cpDjvLmVmeTzVOzoIhm+tm50vOH775X6RbMdr61Uefkg9Hmh\n8XEIfb/ha3ygX2zO8T6aTAlwUT8S6l1QMCXLkWgn0K9K0ffzSjKR2fSEqReatLhDrYimilCTSfhq\nrYQwCV0/Tl2NBlqQqUaAJ29EppWcZKUN+ummxXIkYpqT2DAM1C7WH5QzJV3fT9NkaYRywGfYj7bT\nSXATnxDXcRJ9RivZTtt4P31Bp5BTRXvoMO3lUPqQTnMb3s+pVEyl9DC3oSMUQhNoCa2jfFpPFTQD\nNaqoALFw6kZltAWcT/W0gsZinrGUSyV0SAygz7GuJ9H7DlpJSaixGDWO0CLI4VWqpV0YTVu6kVYh\n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\nh5zZvZDsVFldcu7toYqt3a5iJxp10lD1Fyq21ZNrVd7R4Wfyt3qwwxT+puv8muiO6NKQjebhMlyO\nK3AlrsLVuAbX4uWwOVqMV7AES7EMy/FnrMCrWIlWrMbasCVahzfQhvVox4awPHoTb6EvrIr6w/oo\nxibk8S7+Glqj99T0ADbjv/E3bDGXELIlEUqKXTGTnB56k/8e8skTvdeHfOmKkC19FSvRijRWYTXW\n4DX8Ba9jLTrD5tIuvI13kMVGdKMHvcihD/2IYS6lWxDU7MiwvPzQsLn8qzgCR+KbYX35d7wfh+mu\nn4ATw6Lyk0K2vB4n43uu/cD7uTjP8Y9wARp9vsj7xd4vweWOrwAfyq/3foP3n+AmxzfjFtyK24x/\nt/O/cNzkeKHjRx0/Ax6V86icR+U8Kv9L2FL+OnhUzqNyHpW3meN6tINH5V1hVfnbeEcsWWwMreXd\n6DF2r7Fz6EPsXt6V551/12ceVZyCU3EavxLR/CjFqYEoGc0Pa7atXsN8esqna32aLctXJ5dFu0Ul\nzuajr8jMtMxMy8y0zEzLzLTMTMvMtMxMy8y0zEy7e51M2yzTNsu0zTJts0zbLNM2y6KsjMnLmLyM\nycuYvO9b4vvakv9HJfwHTg5vJU8Jb8matKxJy5q0rEnLmrSsScuatKxJy5q0rEnLmrSsSXMyz8k8\nJ/NcTHMxzbk819JcS3Mrz6k8p9JcSXMjTfXNVN9M9c1U30z1zVTNUjVL0TxF8xTNUzFNxTwV01RM\nUzFdrNglUTktD1TJZdben1l7b08uj3ZN/jkambTaFPXtGNJ3fVHfq3z6vE9fpu8Fhb1FNNU6mbJO\npqyTKetkyjqZsk6mrJMp62TKOpmyTqZ800Rr5Xhr5Xg1u07NrlOz69TsWjW7Sc1uUrOb1OwmNbvJ\nelqnZteo2TVqdo2aXaNm+a3bHh9NUKcb1WlWnW5Up9nkydE+yVNwZnTq0Dq6s3U0Ze1MWTtT1s6U\ntTNl7UxZO1PWzpS1M2XtTFk7U9bOlLUzpRbXqMU1anGNWlyn9japuXVqbp2aW2ONS1njUta3lPUt\nZV1LqZU11raUtW28WlljfUvJ/3Xyf538Xyf/18n/tfJ/rfzfJP83Wf/qrH918n+NnF8n5zfJ+TXW\nwJT1L2X9S1n/UpyaGjYWsl6Matsubb7ufZy16/iwTle/0/Ur+fGkq/fL+UnJFY5VZbLVOlbwcJW7\n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\nnmKeY46S2eZo8xo5zLzRfFKOMGeaT8lbzdnmM3KcOc+cJ+80F5gL5V3mInORvNtcbC6V95jLzGXy\nfvN183X5gLnSXCkfNAvMP8hc833zj3KaWWR+LGeaxWaxfNrcYpbI2Wbc/EIuMHeZnnzB/Iv5F5ln\n7jHL5WIzaX4lXzR985h82RKWLldblnWyfNsaYA2TxdZwa6T8wrrAukCWWT+zRsm/WL+0Lpfl1hhr\njKy0rraulvuta61fySprrHWzPGDdZo2T9dZ4a7w8aN1jPSx9a4o1XSrrcesJw7Cesp4xTGuetcTo\nZL1kvWR0s5ZaS43u1ivWq0YP63VrhdHLKrAKjROtTdYWY4i13Wo0hlmHcHLX2gPsAcZv7EH2qcZt\n9o/t04077GH2MOMu+1x7uDHePs8eadxjX2z/0rjXvtS+1Pitfbk92rjfvsq+xnjQ/rX9a2OyfZt9\np/GQfZ99vzHVnmJPMR6zp9nTjMftx+0njSfsWfZsY6b9jP2sMcueZ88zZtsL7YXGHDvPzjeesd+w\n3zQW2AV2gfG8vcZeY7xgN9pNxiK72W42FtuH7cPGf1H3JfA2VW3cz1777LX3OWftcy/3cu/lknme\nCbmG0PBWSnO9NNA8vqk0KYlSlEoqFImo3hC9lTHNmicRSaGoDCEzIev7P885riOu4Srf9+3zW2uv\nu/azhrPPWv/1f/bwv0OjALPIU9FINBIZFvWjfmR4FFvkmWhGNDMyIpoVLRUZGc2L5kVGR8tG8yNj\nouWj5SMvxM6KdYq8GOsS6xKZGLssdlnkldhVsasj/4tdG7s28lrs+th/Iq/HusW6RSbHbo3dGpkS\n6xHrEZkauyvWKzIt9kBsfOTN2DuxjyNLY3NjP0RWxRbFfolsjP0RLxPZEa8cH+iVjw+Kj/IGxCfH\n3/KGx7+Kr/eeN77J9T4xtc3x3vfmfHOVt9lca7pp39xkuuvQ3Gpu15mmh+mhs8xdpq/ONv3MI7q8\nGWgG6qpmkHlCVzODzUhd0zxnntNNzBgzXh9tJprXdWszxbyhjzNvmjf1SeZt87Y+2bxrPtanmM/N\nbH2W+cZ8ozuZeWa+7mwWmMX6IvOTWasvMxvMVt3dbDM7dA+zMyTdM1Sh0veEkVDr3mE0DPV9YWZY\nWvcPc8NcPTAsE+brx8LyYRX9RFgtrKaHhb3CXnp42Dvsq58J+4UP6+fCx8LH9X/DJ8PBelz4VPiU\n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\nN9wv3D88wH3L7em+7fZye7t93Hfcvu67bj93gDvQHeQOdoe477lD3ffdYe5wd4Q71h3njncnuBPd\nSe5k91N3ivuZO9Wd5k53Z7gz3c/dWe4X7pfuHPcr92t3rvuN+607z/3O/dld4i51l7nL3V/cFe5K\nd627zt3gbnQ3uZvdX90t7lY3xd3mbnd3uofcI+4x94R7yj3jXnAvuVfcq+4197r7m3vDvenecm+7\nd9yom+qRxzzuCc/ybM/xjnhHvWPece+Ed9I75Z32znhnvXPeee+Cd9G75F32rnhXvWvede8374Z3\n07vl3fbueHe9qJcaoQiL8IiIWBE74kRkJBSJiYQjbsSLRCIqki4SG0kfyRB5IBIXyRjJFMkcyRJ5\nMJI1ki2SPZIjkjPyUCRXJHckTyRvJF8kf6RAZFxkfGRCZGJkUmRy5NPIlMhnkamRaZHpkRmRmXj6\njL197LH35pO4jqDYOf9U1NT5fZt4Tuf3HaKhaES7RFPxKu1BNt0nOolOtF9nvHfogPhQfEhHxBgx\nho4isx9D3jqOvHUCeesk8tYpMV98T6eRIc5aFayKjLADz+2wHWYl7Fg7lpXEHnsp55BznJ2UJWQ8\nO4/99ivhgeFxnIenhX/mmcNrwjd4Key6N8d++3Sd7S9TDGWhPDrn19Z3QGN1Bliso7Puwu1PXK2B\nNBuSeUYTS5kou7tKn+9wV2vc5a7RuMdd/w/bHVpaSiF9P5GFcuo7gCL+0yN3l9G7ezSuc/dp3OAe\n0LjJPWdqqoymRZXJtKgymxbR1l20+vszmhh9tkKFNa5S7n0l6VASi5L095VkQcmDKMmKEk4x+qqV\n0NeuPDffM6/EKxHn1Xg1ErwGr0EWr8PrkB3+KPwROeHvw9+TDF8MX9TtcXsm//V/KMfen2H//86v\n/zsZ1uTQv5o3/ydzZgbZQraSbeRbOgOZzPm0zpm1kM2e15lpGPJkA50jTXb0c2PLv5gVe/6TfPjH\nbPiJzoP/mQHvzS7/r2XDf2Q7nRfH6Px9b1Z8Ut99mHsP/87D3HfU1XceN4P7jtv6ruMVfccxEfcc\nk/Qdxy3ttS9pT33V+OXvuZO3vz9verFeei+D94AX52X0MnmZvSzeg15WL5uX3cvh5fQe8nJ5ub08\nXl4vn5ffK+AV9Ap5hb0if5pt+/95vlUxKqzcv5R1Z/8x76p0Klal/0P2XeWudtcgB6//0yy8Q+fh\n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\ncrbz0/Rp9ftWHXk0yuw7bBYCJNjKkF9fP3J6z6TRO9dNMlj2tlr8YNXd/m0fdp+wLfT68EeXJx5a\nnp9n+Ju5fvnUycZn/JVhPu03vtF52Xpq+uZjGc+qm2+YKRW/n+766u0V65jqwePOlb5WH5N6tkVN\n4dsTt7Y4OJ46bWX4aDhz2CtSWjU2fXob75hHD49qODKLK6+UJpzv7uKmJ/VvXxE0PX3tdX6cRzcj\nj7Njq1zf5C7ts6CgZE3gwqU2VwK8Ti9fEMKs4Ys3h72cGnM4f65rxyT92ms1d46vnrb5if3WNpnb\n+x6ZWzM/U+XAiBdrTqZnDFymjLqoiC6z+sk0bkCPkRHdxhuMXd/kQOCBsi6eC08+sEsdPqqnfIPh\n0tKxzWMJ+T9M6HE4DQplbmRzdHJlYW0NCmVuZG9iag0KMTEzIDAgb2JqDQpbIDIyNl0gDQplbmRv\nYmoNCjExNCAwIG9iag0KPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0xlbmd0aCA3NzA5MC9MZW5ndGgx\nIDE2ODg2OD4+DQpzdHJlYW0NCnic7H0JfJTVuf453+xbZhIyWRhgvjAkLIGEnbAIA1nY1zCYAEIm\nySQZyeZkAgRBI6DSiIq7qFXUqlVchsEl7rhUrRvW2mrdiq1ttYrVVm1dIP/nfO+cEKj68/+7vdf2\n3jkfzzzPec973u/sOWlHYJwx5saHni0pKZ87++CmUSczpWM0Y/2jpbNKlheOcN7G2K5djPGnSmct\nKL6qoczB2AVVjCmjZ5eUlv3piU8h2/2M6T6avWRxebh26lbGLq5g/Br77PLALJ1u+BdMKahlrOz1\nxeWFY794s3sjYr2Gt1bVNAVb02/r9wFjQ7sR5O6a9VE1dvWTLzN28pWMGQbUtdY3ff75QjtjIxoZ\ns/SvD7a1sgHMh/cvQX1XfWNH3cjffX4JY6vvhv/LDaFg7Ydvjz2E+KtRPrEBBscdxteRvxT5IQ1N\n0Y3ZW3VT8K4ixnLPWBeKNPPB/BzGOkV7shpbaoLLh5cfZKx6B2ODFjUFN7bmjBki+o72MbU52BTK\nvu200+H/MWOO6a0tbdEeD0P9C0aL8tZIqHXdXcpRxsah/hAXE2Nr6F42gf8+tNY57TOWbWYiPfjB\n5ucFP/f6/m1ffXnkPMuHpnuRtTCFUUI9IzvK+JPWPV99+eUey4dapD5Jt1tYnHlsMTNoBoW5WCEL\n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\nztcX2BYMWzBhwZ0L5i9YseCRBRsWvLPggwV5Cw4unL/w2YVbFzYsEouSF92waPaiTxYVLx6xePbi\nV5bMWPLIksCSxqX2pYOXXrf0naXHlk1Y1pzZI/O6zIzMhZmvZG7PLF3ea/lvln+wvHR58wr3ii4r\nrlhx9Yo5K7asKF05cOWElbeu3Lhy28pDK/X7x9+/4v5PsuxZ47MWZr2XtSvr1Kpuq+5etWWVunrU\n6szV27PT/81c9cG589HZs0320jPFmEeyXz9TWmaQfzP2Jp074s4eJy2Z/pOzzumZp005e+7I3nWm\nGLNDdtGZ0jIvGHNo+20pu7quZx4uG1vHrGnOweaR+bZDOvPrxqRN7Td4ClvnTJ7toPeeY9T1fJC0\n8czc2aISs/MEc/5teapX0qbT6hlXjbnYfLbMuG8+H1eQdj/wHGUm30SNMrO1Qnq3gWOZWc6sDtFz\nVoUJbdaBMyvBJqPfP5r9t/1o9k+Mz/mPmfO9Ocub7VA7aQLfN56eCfFja9wv5qaW+adlfov7yJzI\nDGi4Nqd1djztKHNcyqTsSqPGGY97T8uuzK6kNeOpRu6lp1b2nvbjnGAeLGozo/7EPNt2Xv3xnBqf\nuXeZ2dQyi045PX8a8zpX+NVsNXUrV6alpF8+Im1/F1vLOmYeWbO6Nnc+QlYln159Tq8qyT262M6s\nQC1Zaaxt5tM24wnq7uySbNwxrhhPGdeTe3gKT2dqSrfkHqyAyUZ943vL1TPraNuV1OiLuWrG1802\nK2cyLZy7Tq4/a3UsjK+MnU73nvvNLb9u/P7k9M5HUibQn7PUN1QzNMapNiP2tMYtI9FQsyVTes9B\n70mGm4YSKemdXjL93mp402ZUj0rdTqynV9iillaz1ZTsbLWlGL9gHHtPM1wxvrVkmnHMVvsO6TOs\nhZYVrs8wc1VqU4wVrmV1M9fH/8dirqltyo+fMFfaNiW+4raWH9cwVtr/rphr8c8urSv2vynnKmWU\n1nX83xRzZf/Zxdxt/MxyrjrmHqVN+bF+5t6lTTHyvsXp/678uOX/3LufV1p0NvYuSZvG2Cf1GnPK\nU2bseszytHnFbux0zLOnJ/Uy9kDxexR2UFcYu6aWq8bcb3wzirk7mmnurIw9VN3YOnN/xO6IbzvH\nPG3uTrJbdzFG2TI1O+3Q1GxjB2OebYnvc1q+b2EXVGlcMXY0Rr20eDF3PIvNvRHPmne3GJ+p23l6\n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\nCFEZISoeB/A4gMcBPA7gcQCPA3gcwOMAHgfxOIjHQTwO4nEQj4N4HMTjIB4H8TiIx0E8DuJxEI+D\neBzE4yAeB/E4iMdBPA7icRCPg3gcFD4cDONgGAdP4vdOXDyBc2U49w3O1eJcLc7V4lwt/rvxfwfu\nqbinWh/n2pM4vU7+GQercbAaB6txsBoHa3Cwnjz5By6W42I5Lqq4qOKiiosqLqq4qOJiGBfDuBjG\nxTAuhnExjIthXAzjYhgXw7gYxsUwLoZxMYyLYVwM42IYF8O4GMbFMC6GcTGMi2FcDONiGJdqcakW\nl2pxqRaXanGpFpdqcakWl2pxqRaXanGpFpdqcakWl2pxqRaXVFxScUnFJRWXVFxScUnFJRWXynGp\nHJfKcakcl8pxqRyXynGpHJfKcakcl8pxqRyXynGpHJfKcakcl8pxqRyXynGpHJfKcakcl8rFJbik\n45JujsYWFxpxoR4X6nFAxwHjvakedetRtx5161G3HnXrUVdHXR11ddTVUVdHXR11ddTVUVdHXR11\nddTVUVdHXR11ddTVUVdHXR11ddTVUVdHXR11ddTVUVdHnXrUqUedetSpR5161KlHnXrUqRcDmRlO\nMTOcYvSrrOeJ1seJ4gmiMHvP9w2wkfX+Rdbt7uzqesD5cAH0hAvBC71gDs/cAXfCXXA3sINE6ya0\nbkLrJrRuQusmtG5C6ya0bkLrJrRuQusmtG5C6ya0bkLrJrRuQusmcTdaV6N1NT1W6bHKKIgyCqKM\ngiijIGrqf3oEoPuPMp8dvNX4y8a/z/Zq/KjGj2r8qMaPavyoxo9q/KjGj2r8qMaPavyoxo9q/KjG\nj2r8qMaPavyoxo9q/KjGj2r8qMaPavyoxo9qFFRRUEVBFQVVFFRRUEVBFQVVRkOU0RBlNEQZDVFG\nQ5TREGU0RBkNUUZDlNEQZTREGQ1RRkOU0RBlNEQZDdGfMRqiOBTFoSgORXEoikNRHIriUBSHojgU\nxaEoDkVxKIpDURyK4lAUh6I4FMWhKA5FcSiKQ1EcipprfJ35XyEvxysVr1RmG5XZJoz2KtobGqto\nrKKxisYqGqtorKKxisYqGqtorKKxisYqGqtorKKxisYqGqtorKKxisYqGqtorKKxisYqGhsxqsSo\nEqNKjCoxqsSoEqNKjCoxqsSoEqNKjCoxqsSoEqNKjKrLyIUlsBSWAflGjCoxqqI9c7F29pgh0x43\n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\nNH2eps/T9HmaPk/T52n6PE2fp+nzNH2eps/T9HmaPk/T52n6PE2fp+nzNH2eps/T9HmaPl/29ahP\n2dn4Br6Jb+HekJOJcjJRTibKyUQ5mSgnE+VkopxMlJOJcjJRTibKyUQ5mSgnE+VkopxMlJOJcjJR\nTibKyUQ5mSgnE+VkopxMlJOJcmqJ+WqJhWqJhWqJhWqJhWqJhWqJ+WqJ+WqJ+WqJ+WqJ+WV/iTJl\ntViMJVFGFsvKYllZLFs+tPh/VB2/6Hh6mCKbnSWbnVXKZueHtvKLMVJ2+1BWKx8T2mS2U2W2UTLb\nqTLbKLX4zYnLwxOJBeHVxItRj8Qrst8S9Xy9Or0h6ivLtcpyicQK9f0Hma5CphtU+o3JVtc3yzzj\noqwsl5XlsrJcVpbLynJZWS4ry2Vluawsl5XlsrJclpJupaRbKelWSrqVkm6lpFsp6VZKupWSbqWk\nWynpVkq6lZJuTd4d4uR03IN7MQP3YSZ+i/vDcJlzuMw5XN01X901X901XxbNyKIZWTQji2Zk0Yws\nmpFFM7JoRhbNyKIZWTQji2bozJjOjOnMmM6M6cyYzozpzJjOjOnMmM6M6cyYzozpzDi5PbQld2An\ndmE39mAv9sGekJknyMwTZOZLZOaczDxW/ZdX/+XVf3n1X179l1f/5VUJBVVCQZXQqkooyODDK9aF\nWKVQUCkUZPJLZPJLKoypwphk9OEyelbVUKjoch5CnIpQhnIkoqxMn1VRFFQUBRVFQUVRkPmzMn9W\nZVFQWRRSh3j2Ixjo2mDnQyDWqjIKlMFwyiCbOs59PkgdHKDqKFAIwymErMqjoPIoqDwKKo+CyqOg\n8ihQDpdQDpdQDpdQDpekxNGUOJoSR1OXYxzGh5HUxEhq4lJq4lIqYrh6Nk9J5CiJXOq3pV9k6pOa\nhz+WfpWpT+oNx7own8rIpaylujef2hn1oThyFEeO4shRHDm18Hy18Hy18EK18EIKJKceXqgenp8+\nJcqoieerC2J1QawuiNUFsbqgiUqZrS6I1QUxtTKWWhmb/m5oS1+AEWGC+iBOj/bZnkr/BD/FpRir\nzctgXmqHJrVDrHaI1Q4xhZOhcDJqiFgNEaev8/z1pV8VjKmejHoiVk/E6olYPRFTQROooAwV1E9d\nEVNCEyihjNoiVlvEaotYbRGrLWK1RUwhjaWQxlJIYymksel12l6PDRDr02I91XQ31XQ31TSbappN\n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\nT1kfb+wZ6+ONDbc+3tgY6+ONjbU+3tgE6+ONTbY+3thM6+ONzbI+3thc2UY+yRZa726cWe9u3LPe\n3bhvvbtxZb278UCOleN5ovXrxlOsXzeeav268bLWrxs/yfp141XlHLmIV7ce3fhZ1qMbbyjXyU28\nsfXoxptZj278MuvRjV9uPbrxu6xHN/6w/T6O9wh4wHnPQAaKPxokBAm8d5AUJPPHgrQgjT8RpAcZ\n/MkgK8jifYKKQSXe13pc4/2txzU+wHpc4wOD2kFtPtj6XeNDrN81PtT6XePPBU2DZny49bvGn7d+\n1/go63eNv2j9rvEx1u8afym4PWjPx1u/a3xi0CnoxF+13tf4JOt9jb9mva/xyUHfoC+fEgwIBvA3\ngoHBIP6m9b7G37be1/g71vsa/8B6X+MzgneCmfzD4ONgHv8qWBAs5EuCX4Jf+bJgabCOrww2BDv4\nZuuVje+xXtn43sBEGN9nvbLxg9YrGz9kvbIJFsmIlBOh9ccmUiOVItVEWqRG5DRRJlInUkeUj5wR\nOUNUiDSINBIVI00i54oqkeaR5qJm5ILIReLUyCWRFqJW5LJIS1Encm3kOnFG5N5IR9EgWiFaWTS2\n3t1EM+vdTVxsvbWJS6y3NnG/9dYmHrbe2kQv661N9E24KuEWMdl+tSdmWG9t4jOtdJL41vppE/N1\na32H2Gb9tInD1k+b51k/bZ6yftq8qPXT5iVYP21eSeunzStr/bR5WdZPm1fB+mnzaugJerJX0/pp\n8+pZP21eQ+unzTvH+mnzmlo/bV4z66fNu9j6afMut37avCusnzbvKr1Sr/JaWS9r3g3Wy5rXxnpZ\n8262Xta8O6yXNe9u62XN65DIEwPv3kSdmOg9mJiSmOZ1sZ7VvEcS9yTu8XokURLzehJnq4B6ibD4\nkiiZGJXALigF47BH6Ri7fYzqJyO9CnZFVTEKBlQTKBkBHjYiDTy0/+fhbPcfMCxiJjrETAJiXoNc\n12IvAdxsgxJvpFuoKd0KDG0GDO0I5vAA9nOpE3WhkvQw9lLUlXqg5p5A2HQgrKYMFrJEynRfCJdh\nycDcU4G5VZFSjVWjWqw6OwXpNVgNxGsCizMcFtcGFrdEeDkQ+XznLzSDtQEu13G4XMfhcl3gcjek\nd2dPUT3Wh/VBmX2B1GWA1AOpPhvEnqMGbBhQu7ZD7doOtWs71K4F1J6E+GvA7lrA7tkYD75gX1Aj\n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\nxrclu4fo3OE+0BvXlkYZ6od7E9sy3cO9KcAnVjnHfbo3vS3ffbY3q60ItuKjnq6t1H2+N7etwn2p\nt6Ct2j3eW9xW557qLWtLpuMn8t22be6rwQU6WvZWItfoQ+5Zf0Jbo/tGb32b3n27d7uu0H3HP9tm\nct/tbWqzupd7DchmOk722la8FXCvu83Zqez1RX1Wm7czpjfUFuiM693ZFulM7B1o6+9M6d3Ttrsz\nHXioM6t3Hx0zew8iH2k70Jnbewy4wM+2He4s7j3ZNtxZ1nsyOqe0neis7D3Tdrqzpvdc29nO+t4L\nbec7t/debrvU2RQswVGUbxvvNMjGtqlOc+9E29VOW++VttlOd+81ndjp81e03egM+UvbbnfulE9F\nZyjKvdd1MsyGUO8cCHmjyq01rnNP7822O537eud1pPNg72Lb3c4jvffaljuPhZbbcjpP9qablJ1n\nenNNMZ3nwsQU13khzJkSOy+HNaaUzgl50JTuHgrHr92bKavzSjjJlNt5LZxqKui8Hs4wFXfeDGeb\nyjrnw3mmys7FcKGppvNeuMRU7yHhctN2DxeuMjV5NOFak8ETD2z2JIXjV9jmSZXnTG5PRniryefJ\n7g2ZQp68cINpp6cwrDMNeErCRtMeT3lYNO3zVIUl00FPbdhDf9+wbDqi84TDpmOereFdphQPjPmm\nkx5deDD625nOeIzhvaZzHjEwYLrgkcL7TZc9HuAJjxw+ZLoCmx41XfPsCiXoqjzgsEzXPXuBb3r2\nh4+b5j2HwqdMi56jwPc6i8Mj7cRzPDjTznlOyVy7xjMSHm2P94yGL7YneS7KYnuqZyw81p7hmQxP\ntmd7psPT7XnWiWBJe6Fnpre4vcQzF56BNW/BmuWehfBc9FvaqzxL4VvttZ77gYn2rV1seEHHmbLk\npfaGLj68pCvpivWnteu6EsL3241dyX1su9iV1se3SyZfH6/b2gWzc7unK6cPtFxXvn9bu9xV1JfQ\nHu4q7Utu39VV0ZfWPthV3ZfZlt9VF1yg3JcTdf3te7u29eW37+9q7Cui6qWvlKqUvgr6V5S+6ugd\nh3/B6F/5S8Un745zK38rwL8M9NW1H+rS92bR+b1vG/XgfY20N/bpo38dwvHhbvtR9xDsH5VY+/Eu\nk3+qLbPL6p9a+esN/l2l/ZTV1mdqu9Pl7LNGXX/7SJe3z0l/60A9YckjzALzL4Qwv2eWCMvcYz4k\n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\nCmVuZG9iag0KMTE1IDAgb2JqDQo8PC9UeXBlL1hSZWYvU2l6ZSAxMTUvV1sgMSA0IDJdIC9Sb290\nIDEgMCBSL0luZm8gMzEgMCBSL0lEWzwzNzM3NEYxNUREOTEyRTQxOURBOUI3MTA4RTYzOEQyND48\nMzczNzRGMTVERDkxMkU0MTlEQTlCNzEwOEU2MzhEMjQ+XSAvRmlsdGVyL0ZsYXRlRGVjb2RlL0xl\nbmd0aCAyOTY+Pg0Kc3RyZWFtDQp4nDXSyy5DcRDH8dPTulXv91bd9fRGtUpp0brsWEjEVjckJOIF\nhKUXsPIUnsBWYmUpLFhbkHgBqXN+X/6L+WSSmUkm8zcM+/V6LjuGDcPhCu6FGRL+axFoiWADzkQo\nJ8IWPIvIg4juwbmIReFQxLsiERBJNzyJ1K1IN+FDZN5FNiKsH5F/FIUbUazDmyjZw0x7FcvowAZs\nQhv+SrbshvLdf+YCE9zggT7ohwEYhCHwwjD4wA8BCEICQhCDMEQgCnHIQhIykII0jMAojME4bMME\nTMIUTMMM5MCCPBSgCCUowyzMQQWWYB7qUIUaLMAirEIDmrAMK9CCNVi3D1c91cFrxw4ub1v4suLl\nSLzqE5mfB+KrJ7534cTBvV+BHQfPZRcuDOMX6ng0Zw0KZW5kc3RyZWFtDQplbmRvYmoNCnhyZWYN\nCjAgMTE2DQowMDAwMDAwMDMyIDY1NTM1IGYNCjAwMDAwMDAwMTcgMDAwMDAgbg0KMDAwMDAwMDEy\nNSAwMDAwMCBuDQowMDAwMDAwMTg4IDAwMDAwIG4NCjAwMDAwMDA1MjggMDAwMDAgbg0KMDAwMDAw\nMzQ2MSAwMDAwMCBuDQowMDAwMDAzNjQyIDAwMDAwIG4NCjAwMDAwMDM4OTQgMDAwMDAgbg0KMDAw\nMDAwMzk0NyAwMDAwMCBuDQowMDAwMDA0MTMzIDAwMDAwIG4NCjAwMDAwMDQzOTAgMDAwMDAgbg0K\nMDAwMDAwNDU2NiAwMDAwMCBuDQowMDAwMDA0ODA1IDAwMDAwIG4NCjAwMDAwMDQ5NDMgMDAwMDAg\nbg0KMDAwMDAwNDk3MyAwMDAwMCBuDQowMDAwMDA1MTM5IDAwMDAwIG4NCjAwMDAwMDUyMTMgMDAw\nMDAgbg0KMDAwMDAwNTQ3MCAwMDAwMCBuDQowMDAwMDA1NjQ2IDAwMDAwIG4NCjAwMDAwMDU4OTEg\nMDAwMDAgbg0KMDAwMDAwNjA5NCAwMDAwMCBuDQowMDAwMDA2Mjk2IDAwMDAwIG4NCjAwMDAwMDY0\nNTcgMDAwMDAgbg0KMDAwMDAwNjYzMiAwMDAwMCBuDQowMDAwMDA2ODc4IDAwMDAwIG4NCjAwMDAw\nMDcxODYgMDAwMDAgbg0KMDAwMDAwOTk4MSAwMDAwMCBuDQowMDAwMDEwMTg0IDAwMDAwIG4NCjAw\nMDAwMTAzODYgMDAwMDAgbg0KMDAwMDAxMDU0NyAwMDAwMCBuDQowMDAwMDEwNzE3IDAwMDAwIG4N\nCjAwMDAwMTA5NTggMDAwMDAgbg0KMDAwMDAwMDAzMyA2NTUzNSBmDQowMDAwMDAwMDM0IDY1NTM1\nIGYNCjAwMDAwMDAwMzUgNjU1MzUgZg0KMDAwMDAwMDAzNiA2NTUzNSBmDQowMDAwMDAwMDM3IDY1\nNTM1IGYNCjAwMDAwMDAwMzggNjU1MzUgZg0KMDAwMDAwMDAzOSA2NTUzNSBmDQowMDAwMDAwMDQw\nIDY1NTM1IGYNCjAwMDAwMDAwNDEgNjU1MzUgZg0KMDAwMDAwMDA0MiA2NTUzNSBmDQowMDAwMDAw\nMDQzIDY1NTM1IGYNCjAwMDAwMDAwNDQgNjU1MzUgZg0KMDAwMDAwMDA0NSA2NTUzNSBmDQowMDAw\nMDAwMDQ2IDY1NTM1IGYNCjAwMDAwMDAwNDcgNjU1MzUgZg0KMDAwMDAwMDA0OCA2NTUzNSBmDQow\nMDAwMDAwMDQ5IDY1NTM1IGYNCjAwMDAwMDAwNTAgNjU1MzUgZg0KMDAwMDAwMDA1MSA2NTUzNSBm\nDQowMDAwMDAwMDUyIDY1NTM1IGYNCjAwMDAwMDAwNTMgNjU1MzUgZg0KMDAwMDAwMDA1NCA2NTUz\nNSBmDQowMDAwMDAwMDU1IDY1NTM1IGYNCjAwMDAwMDAwNTYgNjU1MzUgZg0KMDAwMDAwMDA1NyA2\nNTUzNSBmDQowMDAwMDAwMDU4IDY1NTM1IGYNCjAwMDAwMDAwNTkgNjU1MzUgZg0KMDAwMDAwMDA2\nMCA2NTUzNSBmDQowMDAwMDAwMDYxIDY1NTM1IGYNCjAwMDAwMDAwNjIgNjU1MzUgZg0KMDAwMDAw\nMDA2MyA2NTUzNSBmDQowMDAwMDAwMDY0IDY1NTM1IGYNCjAwMDAwMDAwNjUgNjU1MzUgZg0KMDAw\nMDAwMDA2NiA2NTUzNSBmDQowMDAwMDAwMDY3IDY1NTM1IGYNCjAwMDAwMDAwNjggNjU1MzUgZg0K\nMDAwMDAwMDA2OSA2NTUzNSBmDQowMDAwMDAwMDcwIDY1NTM1IGYNCjAwMDAwMDAwNzEgNjU1MzUg\nZg0KMDAwMDAwMDA3MiA2NTUzNSBmDQowMDAwMDAwMDczIDY1NTM1IGYNCjAwMDAwMDAwNzQgNjU1\nMzUgZg0KMDAwMDAwMDA3NSA2NTUzNSBmDQowMDAwMDAwMDc2IDY1NTM1IGYNCjAwMDAwMDAwNzcg\nNjU1MzUgZg0KMDAwMDAwMDA3OCA2NTUzNSBmDQowMDAwMDAwMDc5IDY1NTM1IGYNCjAwMDAwMDAw\nODAgNjU1MzUgZg0KMDAwMDAwMDA4MSA2NTUzNSBmDQowMDAwMDAwMDgyIDY1NTM1IGYNCjAwMDAw\nMDAwODMgNjU1MzUgZg0KMDAwMDAwMDA4NCA2NTUzNSBmDQowMDAwMDAwMDg1IDY1NTM1IGYNCjAw\nMDAwMDAwODYgNjU1MzUgZg0KMDAwMDAwMDA4NyA2NTUzNSBmDQowMDAwMDAwMDg4IDY1NTM1IGYN\nCjAwMDAwMDAwODkgNjU1MzUgZg0KMDAwMDAwMDA5MCA2NTUzNSBmDQowMDAwMDAwMDkxIDY1NTM1\nIGYNCjAwMDAwMDAwOTIgNjU1MzUgZg0KMDAwMDAwMDA5MyA2NTUzNSBmDQowMDAwMDAwMDk0IDY1\nNTM1IGYNCjAwMDAwMDAwOTUgNjU1MzUgZg0KMDAwMDAwMDA5NiA2NTUzNSBmDQowMDAwMDAwMDk3\nIDY1NTM1IGYNCjAwMDAwMDAwOTggNjU1MzUgZg0KMDAwMDAwMDA5OSA2NTUzNSBmDQowMDAwMDAw\nMTAwIDY1NTM1IGYNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAxMjM5MiAwMDAwMCBuDQowMDAw\nMDEyNjQ0IDAwMDAwIG4NCjAwMDAwNjgxNTggMDAwMDAgbg0KMDAwMDA2ODYzNiAwMDAwMCBuDQow\nMDAwMTIwOTMxIDAwMDAwIG4NCjAwMDAxMjEzMjAgMDAwMDAgbg0KMDAwMDE5MTU3MiAwMDAwMCBu\nDQowMDAwMTkxOTk5IDAwMDAwIG4NCjAwMDAxOTI1ODcgMDAwMDAgbg0KMDAwMDE5MjYxNSAwMDAw\nMCBuDQowMDAwMjE3NjQ2IDAwMDAwIG4NCjAwMDAyMTc2NzQgMDAwMDAgbg0KMDAwMDI5Mzk4MiAw\nMDAwMCBuDQowMDAwMjk0MDEwIDAwMDAwIG4NCjAwMDAzNzExOTMgMDAwMDAgbg0KdHJhaWxlcg0K\nPDwvU2l6ZSAxMTYvUm9vdCAxIDAgUi9JbmZvIDMxIDAgUi9JRFs8MzczNzRGMTVERDkxMkU0MTlE\nQTlCNzEwOEU2MzhEMjQ+PDM3Mzc0RjE1REQ5MTJFNDE5REE5QjcxMDhFNjM4RDI0Pl0gPj4NCnN0\nYXJ0eHJlZg0KMzcxNjkyDQolJUVPRg0KeHJlZg0KMCAwDQp0cmFpbGVyDQo8PC9TaXplIDExNi9S\nb290IDEgMCBSL0luZm8gMzEgMCBSL0lEWzwzNzM3NEYxNUREOTEyRTQxOURBOUI3MTA4RTYzOEQy\nND48MzczNzRGMTVERDkxMkU0MTlEQTlCNzEwOEU2MzhEMjQ+XSAvUHJldiAzNzE2OTIvWFJlZlN0\nbSAzNzExOTM+Pg0Kc3RhcnR4cmVmDQozNzQxNzINCiUlRU9G\n\n------=_NextPart_000_00E1_01CEB390.E573FF10\nContent-Type: text/plain; charset=\"utf-8\"\nMIME-Version: 1.0\nContent-Transfer-Encoding: base64\nContent-Disposition: inline\n\nX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fCsOeZXNz\naSBww7NzdGxpc3RpIGVyIHZldHR2YW5ndXIgZmVtw61uaXN0YSB0aWwgYcOwIHLDpsOwYSBqYWZu\ncsOpdHRpIGt5bmphbm5hIG9nIGZlbcOtbsOtc2sgbcOhbGVmbmkuIEVra2kgbcOhIG5vdGEgw75y\nw6HDsGlubiBmeXJpciBhdWdsw71zaW5nYXIgZcOwYSB0aWxreW5uaW5nYXIgc2VtIGVra2kgdGVu\nZ2phc3QgZmVtw61uaXNtYS4gw57DoXR0dGFrZW5kdXIgc2t1bHUgc8O9bmEgw7bDsHJ1bSDDvsOh\ndHR0YWtlbmR1bSB2aXLDsGluZ3Ugb2cgb3LDsGEgaW5ubGVnZyBzw61uIMOhIG3DoWxlZm5hbGVn\nYW4gb2cga3VydGVpc2FuIGjDoXR0LiBTa3JpZmEgc2thbCBmdWxsdCBuYWZuIHVuZGlyIGlubmxl\nZ2cgw6EgbGlzdGFubi4gw5NoZWltaWx0IGVyIGHDsCDDoWZyYW1zZW5kYSBlw7BhIGJpcnRhIGlu\nbmxlZ2cgc2VtIGJlcmFzdCDDoSBsaXN0YW5uIGFubmFyc3N0YcOwYXIgw6FuIGxleWZpcyBow7Zm\ndW5kYXIuCi0gLSAtIC0gLSAtIC0gLSAtIC0gLSAtIC0KRmVtaW5pc3Rpbm4gbWFpbGluZyBsaXN0\nCkZlbWluaXN0aW5uQGhpLmlzCmh0dHA6Ly9saXN0YXIuaGkuaXMvbWFpbG1hbi9saXN0aW5mby9m\nZW1pbmlzdGlubgoK\n\n------=_NextPart_000_00E1_01CEB390.E573FF10--\n"
  },
  {
    "path": "mailpile/tests/data/Maildir/cur/1379857166.25980_9.hottie,2,S",
    "content": "From nobody Fri Mar  7 15:17:43 2014\nContent-Type: multipart/signed; protocol=\"application/pgp-signature\";\n micalg=\"pgp-sha1\"; boundary=\"===============7038572445886209563==\"\nMIME-Version: 1.0\nSubject: Test message\nFrom: test@test.local\nTo: signer@test.local\n\n--===============7038572445886209563==\nContent-Type: text/plain; charset=\"us-ascii\"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\n\n\nThis is the original message text.\n\n:)\n\n--===============7038572445886209563==\nContent-Type: application/pgp-signature; name=signature.asc\nContent-Description: OpenPGP digital signature\nContent-Disposition: attachment; filename=signature.asc\n\n-----BEGIN PGP SIGNATURE-----\n\niQEcBAABCgAGBQJTGdUHAAoJEKdd9P2OMVZyMmkH/0vPH7x+iX7vh1LOK9a6CAbm\nHR+ncLX20ilasMzXwcHCPADcdl5oxJz9O3h3WFLh1oMoazXhND2DeQx9XaHWVlTv\n6HFMnl7UDTSEbMirgzNBOxcT3u/COY7bbndxEmQwZIKk1sat8CEZQ6h9+qKRAX7v\n/oMemggzthlMzZw10QOgH6Jy2mLBCslMrT6oZ/fzjhdWOWcvE31GL3zCGK6vpb8a\nNcgB+CLMQQAFiKIxgnaqOg+YRByJPbiYBLz6bIYtLUckV34CKKEDWnORTIu+RssC\nUD0dVWg/9anmuX9asKCu0xliOfGpYrmFZjSECivYpraez2Lwsrg5Hj9jRypQ1Bw=\n=tHXr\n-----END PGP SIGNATURE-----\n\n--===============7038572445886209563==--\n"
  },
  {
    "path": "mailpile/tests/data/Maildir/cur/1379857166.25981_10.hottie,2,S",
    "content": "From nobody Fri Mar  7 17:36:01 2014\nEnvelope-to: encrypter@test.local\nContent-Type: multipart/encrypted; boundary=\"===============8775766059736482912==\"; protocol=\"application/pgp-encrypted\";\nMime-Version: 1.0\nSubject: Encrypted test message\nFrom: signer@test.local\nTo: encrypter@test.local\n\nThis is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)\n--===============8775766059736482912==\nContent-Transfer-Encoding: 7bit\nContent-Type: application/pgp-encrypted\nContent-Description: PGP/MIME Versions Identification\n\nVersion: 1\n\n--===============8775766059736482912==\nContent-Transfer-Encoding: 7bit\nContent-Type: application/octet-stream; name=encrypted.asc\nContent-Description: OpenPGP encrypted message\nContent-Disposition: inline; filename=encrypted.asc\n\n-----BEGIN PGP MESSAGE-----\n\nhQEMAyx3pxxZI/uNAQf/QXuuaSpKQo++LV6+EBvfNe7BRf2FpuzgGcMLFRLWqKHg\nHNKsuHcDQX0tFwNKZe25v6SYWEWJ1oq+jqWfHLCt5M56dR4/Ia5wJItb+PBkWukb\n8miLpP7t1uI5hunqu66fJxlQKCn0UszwtAIQf7DSqS2xuArsiut8g/4bjoVckZhv\nRp1cDnrt3+YyWUxpMEVWDPQrIXb9KOOiQETu3/bTnVtTP5osJlM206FWhUnAAJTH\nAX0MyYve4em9nkcfLmNmDpgIbPk3qvqBAoSrrPb+RNvy7IwNVzatKL+87n96+AcG\nyuWYrVhX5QpNlRTBWjEFFwJLQTXI4M1Tm2OdVGGTENLBMAHAavaYPD6fSIf4IlxP\nr9f1R7sacsfeesC6V4gxtSOqqAFIQkKj3IZfWuTKr+JDOfQX4SAonHppJCps+PjS\nhgVHeycF58Se5EdPyPeoTFqWz4azd4E9To+5N1Z/957elFfshZVZjClKXwXrtSX+\niwG+chKbPwuo9Dhj41jzmuw0VX9A61yOzWVBs7cP7u8ECrf3OGMbBSR6B15/aqiy\nKbufd8p2TUgb4oyr+ylQR7Kt7FWtR1d9uZPPj9bmqa9U5yMAK1Uzz6PqVZYwSyo8\nxvteOKbp/7Z2JZrHErj6nz7OFgcD/DRYyF5oVrqrNOk3+B+lU8RSCxkRHYtAZn2L\nDSupuHNt0a3+CHrmU1rsFrgg4LbQbbcvr6udlhy9Ximl6mfqFgtZVqGUq4k6vYcd\n3Fg80MIE5gSnJrkoz5MavLV7YaYlBidLOYFJ/rbKYkvQVt39Zz/maK39i/9/ADJT\n1iBcmxizWdBiqA6nxEw3bXP04HB2m4LR9XrJNyY0HOuEOhCHkSJuscQWidWiL+S0\nLNH9LYjdtJ8gIMibVRIxQNC8RqIqai2kGsA55Vd1DnW3jB/9smWjlP5xWPmMHnSG\nP8X6mvhjOlvqpFfmvQC/4zp8xsoEAydUsy813mWykR3tihV/yFzDr0+Y6AOWncdH\nTCM=\n=0ep3\n-----END PGP MESSAGE-----\n\n--===============8775766059736482912==--\n"
  },
  {
    "path": "mailpile/tests/data/Maildir/cur/broken_email_1396694612",
    "content": "Return-Path: <bounce-mc.us6_13990351.453669-hi=brennannovak.com@mail351.us3.mcdlv.net>\nX-Original-To: reverse@heart.reverseproductions.com\nDelivered-To: reverse@heart.reverseproductions.com\nReceived: by heart.reverseproductions.com (Postfix, from userid 5001)\n\tid 7B2086C925; Fri,  4 Apr 2014 19:39:28 -0700 (PDT)\nX-Spam-Checker-Version: SpamAssassin 3.3.2 (2011-06-06) on\n\theart.reverseproductions.com\nX-Spam-Level: \nX-Spam-Status: No, score=-4.5 required=3.0 tests=BAYES_00,HTML_MESSAGE,\n\tMIME_QP_LONG_LINE,RCVD_IN_DNSWL_NONE,RCVD_IN_IADB_DK,RCVD_IN_IADB_LISTED,\n\tRCVD_IN_IADB_RDNS,RCVD_IN_IADB_SENDERID,RCVD_IN_IADB_SPF,RCVD_IN_IADB_VOUCHED,\n\tSPF_HELO_PASS,T_DKIM_INVALID autolearn=ham version=3.3.2\nReceived: from mail351.us3.mcdlv.net (mail351.us3.mcdlv.net [173.231.177.95])\n\tby heart.reverseproductions.com (Postfix) with ESMTP id 236D86C7F9\n\tfor <hi@brennannovak.com>; Fri,  4 Apr 2014 19:39:26 -0700 (PDT)\nDKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; s=k1; d=mail351.us3.mcdlv.net;\n h=Subject:From:Reply-To:To:Date:Message-ID:List-Unsubscribe:Sender:Content-Type:MIME-Version; i=hello=3Dtheheretic.me@mail351.us3.mcdlv.net;\n bh=UmTI1tp07MXJ25va+t4UbFOaUXA=;\n b=rzkoqSDfjClEHI2cNjGYV5Qr5sY8e/GZxNC8Mmpo7YFr1GS6fzRb5NyS8jw4D5QC5nziVzAZxjyY\n   Fi8AhnY+3u0bBjIuEjoniux8OqiDlyp8l8sQr+ZM8jpaW4RST5uXhOodEDul9ViazZNWvsgAO5L2\n   52l1OcucGl6npAp8HSE=\nDomainKey-Signature: a=rsa-sha1; c=nofws; q=dns; s=k1; d=mail351.us3.mcdlv.net;\n b=rhvgbxaFJVX9S+/sEzS/EHtaNPcTjNvHkNvJbmJcqcmwQnyh+HWUfLkjESUrxiRlDmeKrhsl8UX6\n   ryoPLB+0bAiSEa/nvHByZ1siM6MImaFbe3qkLI8S1nZWB3p6mpZT8xuvqssvSD5mFiWSQ6GiGJSe\n   6bdeyTaEXg+mRjFi+k8=;\nReceived: from (127.0.0.1) by mail351.us3.mcdlv.net id h7tlbi174i4j for <hi@brennannovak.com>; Sat, 5 Apr 2014 02:30:16 +0000 (envelope-from <bounce-mc.us6_13990351.453669-hi=brennannovak.com@mail351.us3.mcdlv.net>)\nSubject: =?utf-8?Q?Verb.=20Target.=20Outcome.?=\nFrom: =?utf-8?Q?The=20Heretic=20=28Pascal=20Finette=29?= <hello@theheretic.me>\nReply-To: =?utf-8?Q?The=20Heretic=20=28Pascal=20Finette=29?= <hello@theheretic.me>\nTo: <hi@brennannovak.com>\nDate: Sat, 5 Apr 2014 02:30:16 +0000\nMessage-ID: <73ed86d918cf26bfbf6cc6c1d3e3df2695d.20140405023914@mail351.us3.mcdlv.net>\nX-Mailer: MailChimp Mailer - **CID21d0da1bd63e3df2695d**\nX-Campaign: mailchimp73ed86d918cf26bfbf6cc6c1d.21d0da1bd6\nX-campaignid: mailchimp73ed86d918cf26bfbf6cc6c1d.21d0da1bd6\nX-Report-Abuse: Please report abuse for this campaign here: http://www.mailchimp.com/abuse/abuse.phtml?u=73ed86d918cf26bfbf6cc6c1d&id=21d0da1bd6&e=3e3df2695d\nX-MC-User: 73ed86d918cf26bfbf6cc6c1d\nX-Feedback-ID: 13990351:13990351.453669:us6:mc\nX-Accounttype: pd\nList-Unsubscribe: <mailto:unsubscribe-73ed86d918cf26bfbf6cc6c1d-21d0da1bd6-3e3df2695d@mailin1.us2.mcsv.net?subject=unsubscribe>, <http://theheretic.us6.list-manage.com/unsubscribe?u=73ed86d918cf26bfbf6cc6c1d&id=7eed73b8d8&e=3e3df2695d&c=21d0da1bd6>\nSender: \"The Heretic (Pascal Finette)\" <hello=theheretic.me@mail351.us3.mcdlv.net>\nx-mcda: FALSE\nContent-Type: multipart/alternative; boundary=\"_----------=_MCPart_2029475464\"\nMIME-Version: 1.0\n\nThis is a multi-part message in MIME format\n\n--_----------=_MCPart_2029475464\nContent-Type: text/plain; charset=\"us-ascii\";\n\nhttp://theheretic.me/ Apr 4, 2014\n\n\n** Verb. Target. Outcome.\n------------------------------------------------------------\n\nHere's another gem from the amazing Kevin Starr - this one is so good that I consider it mandatory for any entrepreneur:\n\nWhen you describe your company's mission (which truly is the core and essence of your company - what and why you do what you do) use eight words (or less) in the format: Verb. Target. Outcome.\n\nSimple, concise, to the point.\n\nLet me make this clearer with some of Kevin's examples:\n* Living Goods (http://livinggoods.org/) : Save (verb) African kids' (target) lifes (measurable outcome).\n\nBrilliant! Want another example?\n* One Acre (http://www.oneacrefund.org/) : Get (verb) African families (target) out of extreme poverty (measurable outcome).\n\nYou get it, right? That's your 5 second pitch - right there.\n\nWith that being said - here's mine: Inspire and support young leaders to become entrepreneurs.\n\nWhat's yours?\n\nP.S. I made some changes to the email template I'm using (adding more compatibility with legacy email clients). Let me know if something doesn't work.\n\nAlways Run. Never Walk.\nPascal\n\nP.S.: Want to comment, ask a question or just say hi? Simply hit reply!\n\nJoin the discussion in our Facebook Group (https://www.facebook.com/groups/theheretic/) . Find help within the Heretic Network (http://theheretic.me/network/) . And grab your Golden Ticket (http://theheretic.me/goldenticket/) .\n\nThe Heretic (http://theheretic.me/) | Archive (http://theheretic.me/archive/) | Subscribe (http://theheretic.me/#newsletter) | Email Me (mailto:hello@theheretic.me) | Tweet This (http://twitter.com/share?url=http://theheretic.me/2014/04/04/verb-target-outcome/&text=The%20Heretic:%20%22Verb. Target. Outcome.%22%20%2D)\n\nThis email was sent to hi@brennannovak.com\nwhy did I get this? (http://theheretic.us6.list-manage.com/about?u=73ed86d918cf26bfbf6cc6c1d&id=7eed73b8d8&e=3e3df2695d&c=21d0da1bd6)     unsubscribe from this list (http://theheretic.us6.list-manage.com/unsubscribe?u=73ed86d918cf26bfbf6cc6c1d&id=7eed73b8d8&e=3e3df2695d&c=21d0da1bd6)     update subscription preferences (http://theheretic.us6.list-manage1.com/profile?u=73ed86d918cf26bfbf6cc6c1d&id=7eed73b8d8&e=3e3df2695d)\nThe Heretic (aka Pascal Finette) · 20445 Williams Ave · Saratoga, CA 95070 · USA\n--_----------=_MCPart_2029475464\nContent-Type: text/html; charset=\"utf-8\"\nContent-Transfer-Encoding: quoted-printable\n\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.or=\ng/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n<html xmlns=3D\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta http-equiv=3D\"Content-Type\" content=3D\"text/html; charset=3Dutf-8\">\n<meta name=3D\"viewport\" content=3D\"width=3Ddevice-width\">\n<!-- Facebook sharing information tags -->\n<meta property=3D\"og:title\" content=3D\"Verb. Target. Outcome.\">\n<title>Verb. Target. Outcome.</title>\n<style type=3D\"text/css\">\n=09=09a:hover{\n=09=09=09color:#2795b6 !important;\n=09=09}\n=09=09a:active{\n=09=09=09color:#2795b6 !important;\n=09=09}\n=09=09a:visited{\n=09=09=09color:#2ba6cb !important;\n=09=09}\n=09=09h1 a:active{\n=09=09=09color:#2ba6cb !important;\n=09=09}\n=09=09h2 a:active{\n=09=09=09color:#2ba6cb !important;\n=09=09}\n=09=09h3 a:active{\n=09=09=09color:#2ba6cb !important;\n=09=09}\n=09=09h4 a:active{\n=09=09=09color:#2ba6cb !important;\n=09=09}\n=09=09h5 a:active{\n=09=09=09color:#2ba6cb !important;\n=09=09}\n=09=09h6 a:active{\n=09=09=09color:#2ba6cb !important;\n=09=09}\n=09=09h1 a:visited{\n=09=09=09color:#2ba6cb !important;\n=09=09}\n=09=09h2 a:visited{\n=09=09=09color:#2ba6cb !important;\n=09=09}\n=09=09h3 a:visited{\n=09=09=09color:#2ba6cb !important;\n=09=09}\n=09=09h4 a:visited{\n=09=09=09color:#2ba6cb !important;\n=09=09}\n=09=09h5 a:visited{\n=09=09=09color:#2ba6cb !important;\n=09=09}\n=09=09h6 a:visited{\n=09=09=09color:#2ba6cb !important;\n=09=09}\n=09=09table.button:hover td{\n=09=09=09background:#2795b6 !important;\n=09=09}\n=09=09table.button:visited td{\n=09=09=09background:#2795b6 !important;\n=09=09}\n=09=09table.button:active td{\n=09=09=09background:#2795b6 !important;\n=09=09}\n=09=09table.button:hover td a{\n=09=09=09color:#fff !important;\n=09=09}\n=09=09table.button:visited td a{\n=09=09=09color:#fff !important;\n=09=09}\n=09=09table.button:active td a{\n=09=09=09color:#fff !important;\n=09=09}\n=09=09table.button:hover td{\n=09=09=09background:#2795b6 !important;\n=09=09}\n=09=09table.tiny-button:hover td{\n=09=09=09background:#2795b6 !important;\n=09=09}\n=09=09table.small-button:hover td{\n=09=09=09background:#2795b6 !important;\n=09=09}\n=09=09table.medium-button:hover td{\n=09=09=09background:#2795b6 !important;\n=09=09}\n=09=09table.large-button:hover td{\n=09=09=09background:#2795b6 !important;\n=09=09}\n=09=09table.button:hover td a{\n=09=09=09color:#ffffff !important;\n=09=09}\n=09=09table.button:active td a{\n=09=09=09color:#ffffff !important;\n=09=09}\n=09=09table.button td a:visited{\n=09=09=09color:#ffffff !important;\n=09=09}\n=09=09table.tiny-button:hover td a{\n=09=09=09color:#ffffff !important;\n=09=09}\n=09=09table.tiny-button:active td a{\n=09=09=09color:#ffffff !important;\n=09=09}\n=09=09table.tiny-button td a:visited{\n=09=09=09color:#ffffff !important;\n=09=09}\n=09=09table.small-button:hover td a{\n=09=09=09color:#ffffff !important;\n=09=09}\n=09=09table.small-button:active td a{\n=09=09=09color:#ffffff !important;\n=09=09}\n=09=09table.small-button td a:visited{\n=09=09=09color:#ffffff !important;\n=09=09}\n=09=09table.medium-button:hover td a{\n=09=09=09color:#ffffff !important;\n=09=09}\n=09=09table.medium-button:active td a{\n=09=09=09color:#ffffff !important;\n=09=09}\n=09=09table.medium-button td a:visited{\n=09=09=09color:#ffffff !important;\n=09=09}\n=09=09table.large-button:hover td a{\n=09=09=09color:#ffffff !important;\n=09=09}\n=09=09table.large-button:active td a{\n=09=09=09color:#ffffff !important;\n=09=09}\n=09=09table.large-button td a:visited{\n=09=09=09color:#ffffff !important;\n=09=09}\n=09=09table.secondary:hover td{\n=09=09=09background:#d0d0d0 !important;\n=09=09=09color:#555;\n=09=09}\n=09=09table.secondary:hover td a{\n=09=09=09color:#555 !important;\n=09=09}\n=09=09table.secondary td a:visited{\n=09=09=09color:#555 !important;\n=09=09}\n=09=09table.secondary:active td a{\n=09=09=09color:#555 !important;\n=09=09}\n=09=09table.success:hover td{\n=09=09=09background:#457a1a !important;\n=09=09}\n=09=09table.alert:hover td{\n=09=09=09background:#970b0e !important;\n=09=09}\n=09=09a:hover{\n=09=09=09color:#111111 !important;\n=09=09}\n=09=09a:active{\n=09=09=09color:#111111 !important;\n=09=09}\n=09=09a:visited{\n=09=09=09color:#333332 !important;\n=09=09}\n=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] img{\n=09=09=09width:auto !important;\n=09=09=09height:auto !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] center{\n=09=09=09min-width:0 !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .container{\n=09=09=09width:95% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .row{\n=09=09=09width:100% !important;\n=09=09=09display:block !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .wrapper{\n=09=09=09display:block !important;\n=09=09=09padding-right:0 !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .columns{\n=09=09=09table-layout:fixed !important;\n=09=09=09float:none !important;\n=09=09=09width:100% !important;\n=09=09=09padding-right:0px !important;\n=09=09=09padding-left:0px !important;\n=09=09=09display:block !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .column{\n=09=09=09table-layout:fixed !important;\n=09=09=09float:none !important;\n=09=09=09width:100% !important;\n=09=09=09padding-right:0px !important;\n=09=09=09padding-left:0px !important;\n=09=09=09display:block !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .wrapper.first .columns{\n=09=09=09display:table !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .wrapper.first .column{\n=09=09=09display:table !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] table.columns td{\n=09=09=09width:100% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] table.column td{\n=09=09=09width:100% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .columns td.one{\n=09=09=09width:8.333333% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .column td.one{\n=09=09=09width:8.333333% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .columns td.two{\n=09=09=09width:16.666666% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .column td.two{\n=09=09=09width:16.666666% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .columns td.three{\n=09=09=09width:25% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .column td.three{\n=09=09=09width:25% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .columns td.four{\n=09=09=09width:33.333333% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .column td.four{\n=09=09=09width:33.333333% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .columns td.five{\n=09=09=09width:41.666666% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .column td.five{\n=09=09=09width:41.666666% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .columns td.six{\n=09=09=09width:50% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .column td.six{\n=09=09=09width:50% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .columns td.seven{\n=09=09=09width:58.333333% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .column td.seven{\n=09=09=09width:58.333333% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .columns td.eight{\n=09=09=09width:66.666666% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .column td.eight{\n=09=09=09width:66.666666% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .columns td.nine{\n=09=09=09width:75% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .column td.nine{\n=09=09=09width:75% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .columns td.ten{\n=09=09=09width:83.333333% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .column td.ten{\n=09=09=09width:83.333333% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .columns td.eleven{\n=09=09=09width:91.666666% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .column td.eleven{\n=09=09=09width:91.666666% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .columns td.twelve{\n=09=09=09width:100% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .column td.twelve{\n=09=09=09width:100% !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] td.offset-by-one{\n=09=09=09padding-left:0 !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] td.offset-by-two{\n=09=09=09padding-left:0 !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] td.offset-by-three{\n=09=09=09padding-left:0 !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] td.offset-by-four{\n=09=09=09padding-left:0 !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] td.offset-by-five{\n=09=09=09padding-left:0 !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] td.offset-by-six{\n=09=09=09padding-left:0 !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] td.offset-by-seven{\n=09=09=09padding-left:0 !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] td.offset-by-eight{\n=09=09=09padding-left:0 !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] td.offset-by-nine{\n=09=09=09padding-left:0 !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] td.offset-by-ten{\n=09=09=09padding-left:0 !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] td.offset-by-eleven{\n=09=09=09padding-left:0 !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] table.columns td.expander{\n=09=09=09width:1px !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .right-text-pad{\n=09=09=09padding-left:10px !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .text-pad-right{\n=09=09=09padding-left:10px !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .left-text-pad{\n=09=09=09padding-right:10px !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .text-pad-left{\n=09=09=09padding-right:10px !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .hide-for-small{\n=09=09=09display:none !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .show-for-desktop{\n=09=09=09display:none !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .show-for-small{\n=09=09=09display:inherit !important;\n=09=09}\n\n}=09@media only screen and (max-width: 600px){\n=09=09table[class=3Dbody] .hide-for-desktop{\n=09=09=09display:inherit !important;\n=09=09}\n\n}</style></head>\n<body style=3D\"width: 100% !important; min-width: 100%; -webkit-text-size-=\nadjust: 100%; -ms-text-size-adjust: 100%; color: #333332; font-family: 'He=\nlvetica Neue'=2C'Helvetica'=2C 'Arial'=2C sans-serif; font-weight: normal;=\n text-align: left; line-height: 19px; font-size: 14px; margin: 0; padding:=\n 0;\">\n<table class=3D\"body\" style=3D\"border-spacing: 0; border-collapse: collaps=\ne; vertical-align: top; text-align: left; height: 100%; width: 100%; color=\n: #333332; font-family: 'Helvetica Neue'=2C'Helvetica'=2C 'Arial'=2C sans-=\nserif; font-weight: normal; line-height: 19px; font-size: 14px; margin: 0;=\n padding: 0;\"><tr style=3D\"vertical-align: top; text-align: left; padding:=\n 0;\" align=3D\"left\"><td class=3D\"center\" align=3D\"center\" valign=3D\"top\" s=\ntyle=3D\"word-break: break-word; -webkit-hyphens: auto; -moz-hyphens: auto;=\n hyphens: auto; border-collapse: collapse !important; vertical-align: top;=\n text-align: center; color: #333332; font-family: 'Helvetica Neue'=2C'Helv=\netica'=2C 'Arial'=2C sans-serif; font-weight: normal; line-height: 19px; f=\nont-size: 14px; margin: 0; padding: 0;\">\n<center style=3D\"width: 100%; min-width: 580px;\">\n\n<!-- HEADER -->\n<table class=3D\"row header\" style=3D\"border-spacing: 0; border-collapse: c=\nollapse; vertical-align: top; text-align: left; width: 100%; position: rel=\native; background: #EEEEEE; padding: 0px;\" bgcolor=3D\"#EEEEEE\"><tr style=\n=3D\"vertical-align: top; text-align: left; padding: 0;\" align=3D\"left\"><td=\n class=3D\"center\" align=3D\"center\" style=3D\"word-break: break-word; -webki=\nt-hyphens: auto; -moz-hyphens: auto; hyphens: auto; border-collapse: colla=\npse !important; vertical-align: top; text-align: center; color: #333332; f=\nont-family: 'Helvetica Neue'=2C'Helvetica'=2C 'Arial'=2C sans-serif; font-=\nweight: normal; line-height: 19px; font-size: 14px; margin: 0; padding: 0;=\n\" valign=3D\"top\">\n<center style=3D\"width: 100%; min-width: 580px;\">\n\n<table class=3D\"container\" style=3D\"border-spacing: 0; border-collapse: co=\nllapse; vertical-align: top; text-align: inherit; width: 580px; margin: 0=\n auto; padding: 0;\"><tr style=3D\"vertical-align: top; text-align: left; pa=\ndding: 0;\" align=3D\"left\"><td class=3D\"wrapper last\" style=3D\"word-break:=\n break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; bor=\nder-collapse: collapse !important; vertical-align: top; text-align: left;=\n position: relative; color: #333332; font-family: 'Helvetica Neue'=2C'Helv=\netica'=2C 'Arial'=2C sans-serif; font-weight: normal; line-height: 19px; f=\nont-size: 14px; margin: 0; padding: 10px 0px 0px;\" align=3D\"left\" valign=\n=3D\"top\">\n\n<table class=3D\"twelve columns\" style=3D\"border-spacing: 0; border-collaps=\ne: collapse; vertical-align: top; text-align: left; width: 580px; margin:=\n 0 auto; padding: 0;\"><tr style=3D\"vertical-align: top; text-align: left;=\n padding: 0;\" align=3D\"left\"><td class=3D\"six sub-columns\" style=3D\"word-b=\nreak: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto=\n; border-collapse: collapse !important; vertical-align: top; text-align: l=\neft; min-width: 0px; width: 50%; color: #333332; font-family: 'Helvetica N=\neue'=2C'Helvetica'=2C 'Arial'=2C sans-serif; font-weight: normal; line-hei=\nght: 19px; font-size: 14px; margin: 0; padding: 0px 10px 10px 0px;\" align=\n=3D\"left\" valign=3D\"top\">\n<a href=3D\"http://theheretic.us6.list-manage2.com/track/click?u=3D73ed86d9=\n18cf26bfbf6cc6c1d&id=3Dced461ca4b&e=3D3e3df2695d\" style=3D\"color: #333332=\n; text-decoration: underline;\"><img heigth=3D\"64\" width=3D\"210\" src=3D\"htt=\np://theheretic.me/img/masthead-logo.png\" style=3D\"outline: none; text-deco=\nration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100=\n%; float: left; clear: both; display: block; border: none;\" align=3D\"left\"=\n></a>\n</td>\n<td class=3D\"six sub-columns last\" style=3D\"text-align: right; vertical-al=\nign: middle; word-break: break-word; -webkit-hyphens: auto; -moz-hyphens:=\n auto; hyphens: auto; border-collapse: collapse !important; min-width: 0px=\n; width: 50%; color: #333332; font-family: 'Helvetica Neue'=2C'Helvetica'=\n=2C 'Arial'=2C sans-serif; font-weight: normal; line-height: 19px; font-si=\nze: 14px; margin: 0; padding: 0px 0px 10px;\" align=3D\"right\" valign=3D\"mid=\ndle\">\n<span class=3D\"template-label\" style=3D\"color: #333332; font-weight: bold;=\n font-size: 11px;\">Apr 4=2C 2014</span>\n</td>\n<td class=3D\"expander\" style=3D\"word-break: break-word; -webkit-hyphens: a=\nuto; -moz-hyphens: auto; hyphens: auto; border-collapse: collapse !importa=\nnt; vertical-align: top; text-align: left; visibility: hidden; width: 0px;=\n color: #333332; font-family: 'Helvetica Neue'=2C'Helvetica'=2C 'Arial'=2C=\n sans-serif; font-weight: normal; line-height: 19px; font-size: 14px; marg=\nin: 0; padding: 0;\" align=3D\"left\" valign=3D\"top\"></td>\n</tr></table></td>\n</tr></table></center>\n</td>\n</tr></table><!-- HEADER --><!-- MAIN --><table class=3D\"container\" style=\n=3D\"border-spacing: 0; border-collapse: collapse; vertical-align: top; tex=\nt-align: inherit; width: 580px; margin: 0 auto; padding: 0;\"><tr style=3D\"=\nvertical-align: top; text-align: left; padding: 0;\" align=3D\"left\"><td sty=\nle=3D\"word-break: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; h=\nyphens: auto; border-collapse: collapse !important; vertical-align: top; t=\next-align: left; color: #333332; font-family: 'Helvetica Neue'=2C'Helvetic=\na'=2C 'Arial'=2C sans-serif; font-weight: normal; line-height: 19px; font-=\nsize: 14px; margin: 0; padding: 0;\" align=3D\"left\" valign=3D\"top\">\n\n\n<!-- CONTENT -->\n<table class=3D\"row\" style=3D\"border-spacing: 0; border-collapse: collapse=\n; vertical-align: top; text-align: left; width: 100%; position: relative;=\n display: block; padding: 0px;\"><tr style=3D\"vertical-align: top; text-ali=\ngn: left; padding: 0;\" align=3D\"left\"><td class=3D\"wrapper last\" style=3D\"=\nword-break: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens=\n: auto; border-collapse: collapse !important; vertical-align: top; text-al=\nign: left; position: relative; color: #333332; font-family: 'Helvetica Neu=\ne'=2C'Helvetica'=2C 'Arial'=2C sans-serif; font-weight: normal; line-heigh=\nt: 19px; font-size: 14px; margin: 0; padding: 10px 0px 0px;\" align=3D\"left=\n\" valign=3D\"top\">\n\n<table class=3D\"twelve columns\" style=3D\"border-spacing: 0; border-collaps=\ne: collapse; vertical-align: top; text-align: left; width: 580px; margin:=\n 0 auto; padding: 0;\"><tr style=3D\"vertical-align: top; text-align: left;=\n padding: 0;\" align=3D\"left\"><td style=3D\"word-break: break-word; -webkit-=\nhyphens: auto; -moz-hyphens: auto; hyphens: auto; border-collapse: collaps=\ne !important; vertical-align: top; text-align: left; color: #333332; font-=\nfamily: 'Helvetica Neue'=2C'Helvetica'=2C 'Arial'=2C sans-serif; font-weig=\nht: normal; line-height: 19px; font-size: 14px; margin: 0; padding: 0px 0p=\nx 10px;\" align=3D\"left\" valign=3D\"top\">\n<h3 style=3D\"color: #333332; font-family: 'Helvetica Neue'=2C'Helvetica'=\n=2C 'Arial'=2C sans-serif; font-weight: lighter; text-align: left; line-he=\night: 1.3; word-break: normal; font-size: 32px; margin: 0; padding: 24px 0=\n 12px;\" align=3D\"left\">Verb. Target. Outcome.</h3>\n<div class=3D\"mainContent\" style=3D\"font-size: 16px; line-height: 1.6;\">\n<div><p>Here's another gem from the amazing Kevin Starr - this one is so g=\nood that I consider it <strong>mandatory</strong> for any entrepreneur:</p=\n>\n<p>When you describe your company's mission (which truly is the core and e=\nssence of your company - what and why you do what you do) use <strong>eigh=\nt words (or less)</strong> in the format: <strong>Verb. Target. Outcome.</=\nstrong></p>\n<p>Simple=2C concise=2C to the point.</p>\n<p>Let me make this clearer with some of Kevin's examples: </p>\n<ul><li><em><a href=3D\"http://theheretic.us6.list-manage.com/track/click?u=\n=3D73ed86d918cf26bfbf6cc6c1d&id=3D543ee7e569&e=3D3e3df2695d\">Living Goods=\n</a>:</em> Save (verb) African kids' (target) lifes (measurable outcome).<=\n/li></ul>\n<p>Brilliant! Want another example?</p>\n<ul><li><em><a href=3D\"http://theheretic.us6.list-manage1.com/track/click?=\nu=3D73ed86d918cf26bfbf6cc6c1d&id=3D163df8d873&e=3D3e3df2695d\">One Acre</a=\n>:</em> Get (verb) African families (target) out of extreme poverty (measu=\nrable outcome).</li></ul>\n<p>You get it=2C right? That's your 5 second pitch - right there.</p>\n<p>With that being said - here's mine: <strong>Inspire and support young l=\neaders to become entrepreneurs.</strong></p>\n<p>What's yours?</p>\n<p><em>P.S. I made some changes to the email template I'm using (adding mo=\nre compatibility with legacy email clients). Let me know if something does=\nn't work.</em></p>\n</div>\n<div><p style=3D\"color: #333332; font-family: 'Helvetica Neue'=2C'Helvetic=\na'=2C 'Arial'=2C sans-serif; font-weight: normal; text-align: left; line-h=\neight: 1.6; font-size: 16px; margin: 0 0 10px; padding: 0;\" align=3D\"left\"=\n>Always Run. Never Walk.<br>Pascal</p></div>\n<p style=3D\"color: #333332; font-family: 'Helvetica Neue'=2C'Helvetica'=2C=\n 'Arial'=2C sans-serif; font-weight: normal; text-align: left; line-height=\n: 1.6; font-size: 16px; margin: 0 0 10px; padding: 0;\" align=3D\"left\"><em>=\nP.S.: Want to comment=2C ask a question or just say hi? Simply hit reply!<=\n/em></p>\n</div>\n</td>\n<td class=3D\"expander\" style=3D\"word-break: break-word; -webkit-hyphens: a=\nuto; -moz-hyphens: auto; hyphens: auto; border-collapse: collapse !importa=\nnt; vertical-align: top; text-align: left; visibility: hidden; width: 0px;=\n color: #333332; font-family: 'Helvetica Neue'=2C'Helvetica'=2C 'Arial'=2C=\n sans-serif; font-weight: normal; line-height: 19px; font-size: 14px; marg=\nin: 0; padding: 0;\" align=3D\"left\" valign=3D\"top\"></td>\n</tr></table></td>\n</tr></table><!-- CONTENT --><!-- CALLOUT --><table class=3D\"row callout\"=\n style=3D\"border-spacing: 0; border-collapse: collapse; vertical-align: to=\np; text-align: left; width: 100%; position: relative; display: block; padd=\ning: 0px;\"><tr style=3D\"vertical-align: top; text-align: left; padding: 0;=\n\" align=3D\"left\"><td class=3D\"wrapper last\" style=3D\"word-break: break-wor=\nd; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; border-collap=\nse: collapse !important; vertical-align: top; text-align: left; position:=\n relative; color: #333332; font-family: 'Helvetica Neue'=2C'Helvetica'=2C=\n 'Arial'=2C sans-serif; font-weight: normal; line-height: 19px; font-size:=\n 14px; margin: 0; padding: 10px 0px 20px;\" align=3D\"left\" valign=3D\"top\">\n\n<table class=3D\"twelve columns\" style=3D\"border-spacing: 0; border-collaps=\ne: collapse; vertical-align: top; text-align: left; width: 580px; margin:=\n 0 auto; padding: 0;\"><tr style=3D\"vertical-align: top; text-align: left;=\n padding: 0;\" align=3D\"left\"><td class=3D\"panel\" style=3D\"word-break: brea=\nk-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; border-c=\nollapse: collapse !important; vertical-align: top; text-align: left; color=\n: #333332; font-family: 'Helvetica Neue'=2C'Helvetica'=2C 'Arial'=2C sans-=\nserif; font-weight: normal; line-height: 19px; font-size: 14px; background=\n: #E7E7E7; margin: 0; padding: 10px; border: 1px solid #d2d2d2;\" align=3D\"=\nleft\" bgcolor=3D\"#E7E7E7\" valign=3D\"top\">\n<div class=3D\"mainContent\" style=3D\"font-size: 16px; line-height: 1.6;\">\n<p style=3D\"color: #333332; font-family: 'Helvetica Neue'=2C'Helvetica'=2C=\n 'Arial'=2C sans-serif; font-weight: normal; text-align: left; line-height=\n: 1.6; font-size: 16px; margin: 0; padding: 0;\" align=3D\"left\">Join the di=\nscussion in our <a href=3D\"http://theheretic.us6.list-manage.com/track/cli=\nck?u=3D73ed86d918cf26bfbf6cc6c1d&id=3Def72e160cd&e=3D3e3df2695d\" target=\n=3D\"_blank\" style=3D\"color: #333332; text-decoration: underline;\">Facebook=\n Group</a>. Find help within the <a href=3D\"http://theheretic.us6.list-man=\nage.com/track/click?u=3D73ed86d918cf26bfbf6cc6c1d&id=3Dadf2637f73&e=3D=\n3e3df2695d\" target=3D\"_blank\" style=3D\"color: #333332; text-decoration: under=\nline;\">Heretic Network</a>. And grab your <a href=3D\"http://theheretic.us6=\n=2Elist-manage.com/track/click?u=3D73ed86d918cf26bfbf6cc6c1d&id=3D177ce84b18=\n&e=3D3e3df2695d\" target=3D\"_blank\" style=3D\"color: #333332; text-decorati=\non: underline;\">Golden Ticket</a>.</p>\n</div>\n</td>\n<td class=3D\"expander\" style=3D\"word-break: break-word; -webkit-hyphens: a=\nuto; -moz-hyphens: auto; hyphens: auto; border-collapse: collapse !importa=\nnt; vertical-align: top; text-align: left; visibility: hidden; width: 0px;=\n color: #333332; font-family: 'Helvetica Neue'=2C'Helvetica'=2C 'Arial'=2C=\n sans-serif; font-weight: normal; line-height: 19px; font-size: 14px; marg=\nin: 0; padding: 0;\" align=3D\"left\" valign=3D\"top\"></td>\n</tr></table></td>\n</tr></table><!-- CALLOUT --><!-- FOOTER --><table class=3D\"row\" style=3D\"=\nborder-spacing: 0; border-collapse: collapse; vertical-align: top; text-al=\nign: left; width: 100%; position: relative; display: block; padding: 0px;\"=\n><tr style=3D\"vertical-align: top; text-align: left; padding: 0;\" align=3D=\n\"left\"><td class=3D\"wrapper last\" style=3D\"word-break: break-word; -webkit=\n-hyphens: auto; -moz-hyphens: auto; hyphens: auto; border-collapse: collap=\nse !important; vertical-align: top; text-align: left; position: relative;=\n color: #333332; font-family: 'Helvetica Neue'=2C'Helvetica'=2C 'Arial'=2C=\n sans-serif; font-weight: normal; line-height: 19px; font-size: 14px; marg=\nin: 0; padding: 10px 0px 0px;\" align=3D\"left\" valign=3D\"top\">\n\n<table class=3D\"twelve columns\" style=3D\"border-spacing: 0; border-collaps=\ne: collapse; vertical-align: top; text-align: left; width: 580px; margin:=\n 0 auto; padding: 0;\"><tr style=3D\"vertical-align: top; text-align: left;=\n padding: 0;\" align=3D\"left\"><td align=3D\"center\" style=3D\"word-break: bre=\nak-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; border-=\ncollapse: collapse !important; vertical-align: top; text-align: left; colo=\nr: #333332; font-family: 'Helvetica Neue'=2C'Helvetica'=2C 'Arial'=2C sans=\n-serif; font-weight: normal; line-height: 19px; font-size: 14px; margin: 0=\n; padding: 0px 0px 10px;\" valign=3D\"top\">\n<center style=3D\"width: 100%; min-width: 580px;\">\n<div class=3D\"mainContent\" style=3D\"font-size: 16px; line-height: 1.6;\">\n<p style=3D\"text-align: center; color: #333332; font-family: 'Helvetica Ne=\nue'=2C'Helvetica'=2C 'Arial'=2C sans-serif; font-weight: normal; line-heig=\nht: 1.6; font-size: 16px; margin: 0 0 10px; padding: 0;\" align=3D\"center\">=\n<a href=3D\"http://theheretic.us6.list-manage.com/track/click?u=3D73ed86d91=\n8cf26bfbf6cc6c1d&id=3D5ca2720ed0&e=3D3e3df2695d\" target=3D\"_blank\" style=\n=3D\"color: #333332; text-decoration: underline;\">The Heretic</a> | <a href=\n=3D\"http://theheretic.us6.list-manage.com/track/click?u=3D73ed86d918cf26bf=\nbf6cc6c1d&id=3De8b6793105&e=3D3e3df2695d\" target=3D\"_blank\" style=3D\"colo=\nr: #333332; text-decoration: underline;\">Archive</a> | <a href=3D\"http://t=\nheheretic.us6.list-manage1.com/track/click?u=3D73ed86d918cf26bfbf6cc6c1d&i=\nd=3D4b4a7a073f&e=3D3e3df2695d\" target=3D\"_blank\" style=3D\"color: #333332;=\n text-decoration: underline;\">Subscribe</a> | <a href=3D\"mailto:hello@theh=\neretic.me\" style=3D\"color: #333332; text-decoration: underline;\">Email Me<=\n/a> | <span><a href=3D\"http://theheretic.us6.list-manage1.com/track/click?=\nu=3D73ed86d918cf26bfbf6cc6c1d&id=3Daa589c8771&e=3D3e3df2695d\" style=3D\"co=\nlor: #333332; text-decoration: underline;\" target=3D\"_blank\">Tweet This</a=\n></span></p>\n</div>\n</center>\n</td>\n<td class=3D\"expander\" style=3D\"word-break: break-word; -webkit-hyphens: a=\nuto; -moz-hyphens: auto; hyphens: auto; border-collapse: collapse !importa=\nnt; vertical-align: top; text-align: left; visibility: hidden; width: 0px;=\n color: #333332; font-family: 'Helvetica Neue'=2C'Helvetica'=2C 'Arial'=2C=\n sans-serif; font-weight: normal; line-height: 19px; font-size: 14px; marg=\nin: 0; padding: 0;\" align=3D\"left\" valign=3D\"top\"></td>\n</tr></table></td>\n</tr></table><!-- FOOTER --></td>\n</tr></table><!-- MAIN --></center>\n</td>\n</tr></table>            <center>\n                <br />\n                <br />\n                <br />\n                <br />\n                <br />\n                <br />\n                <table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" wi=\ndth=3D\"100%\" id=3D\"canspamBarWrapper\" style=3D\"background-color:#FFFFFF; b=\norder-top:1px solid #E5E5E5;\">\n                    <tr>\n                        <td align=3D\"center\" valign=3D\"top\" style=3D\"paddi=\nng-top:20px; padding-bottom:20px;\">\n                            <table border=3D\"0\" cellpadding=3D\"0\" cellspac=\ning=3D\"0\" id=3D\"canspamBar\">\n                                <tr>\n                                    <td align=3D\"center\" valign=3D\"top\" st=\nyle=3D\"color:#606060; font-family:Helvetica=2C Arial=2C sans-serif; font-s=\nize:11px; line-height:150%; padding-right:20px; padding-bottom:5px; paddin=\ng-left:20px; text-align:center;\">\n                                        This email was sent to <a href=3D\"=\nhi@brennannovak.com\" target=3D\"_blank\" style=3D\"color:#404040 !important;\">=\nhi@brennannovak.com</a>\n                                        <br />\n                                        <a href=3D\"http://theheretic.us6.l=\nist-manage.com/about?u=3D73ed86d918cf26bfbf6cc6c1d&id=3D7eed73b8d8&e=3D=\n3e3df2695d&c=3D21d0da1bd6\" target=3D\"_blank\" style=3D\"color:#404040 !important;=\n\"><em>why did I get this?</em></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href=3D\"http:=\n//theheretic.us6.list-manage.com/unsubscribe?u=3D73ed86d918cf26bfbf6cc6c1d=\n&id=3D7eed73b8d8&e=3D3e3df2695d&c=3D21d0da1bd6\" style=3D\"color:#404040 !impo=\nrtant;\">unsubscribe from this list</a>&nbsp;&nbsp;&nbsp;&nbsp;<a href=3D\"h=\nttp://theheretic.us6.list-manage1.com/profile?u=3D73ed86d918cf26bfbf6cc6c1=\nd&id=3D7eed73b8d8&e=3D3e3df2695d\" style=3D\"color:#404040 !important;\">update=\n subscription preferences</a>\n                                        <br />\n                                        The Heretic (aka Pascal Finette) &=\nmiddot; 20445 Williams Ave &middot; Saratoga=2C CA 95070 &middot; USA\n                                        <br />\n                                        <br />\n\n                                    </td>\n                                </tr>\n                            </table>\n                        </td>\n                    </tr>\n                </table>\n                <style type=3D\"text/css\">\n                    @media only screen and (max-width: 480px){\n                        table[id=3D\"canspamBar\"] td{font-size:14px !impor=\ntant;}\n                        table[id=3D\"canspamBar\"] td a{display:block !impo=\nrtant; margin-top:10px !important;}\n                    }\n                </style>\n            </center><img src=3D\"http://theheretic.us6.list-manage.com/tra=\nck/open.php?u=3D73ed86d918cf26bfbf6cc6c1d&id=3D21d0da1bd6&e=3D3e3df2695d\"=\n height=3D\"1\" width=3D\"1\"></body>\n</html>\n--_----------=_MCPart_2029475464--\n"
  },
  {
    "path": "mailpile/tests/data/Maildir/cur/encrypted-empty-content-w-key.eml",
    "content": "Message-ID: <569ACDC4.2020300@example.com>\nDate: Sun, 17 Jan 2016 00:09:56 +0100\nFrom: John Doe <test@example.com>\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:31.0) Gecko/20100101 Icedove/31.7.0\nMIME-Version: 1.0\nTo: team+testing@mailpile.is\nSubject: my key\nContent-Type: multipart/encrypted;\n protocol=\"application/pgp-encrypted\";\n boundary=\"NRWmMk5TRLqGQG7eWRXvlom7SxxIDxfRD\"\nX-Peer: 127.0.0.1\n\nThis is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)\n--NRWmMk5TRLqGQG7eWRXvlom7SxxIDxfRD\nContent-Type: application/pgp-encrypted\nContent-Description: PGP/MIME version identification\n\nVersion: 1\n\n--NRWmMk5TRLqGQG7eWRXvlom7SxxIDxfRD\nContent-Type: application/octet-stream; name=\"encrypted.asc\"\nContent-Description: OpenPGP encrypted message\nContent-Disposition: inline; filename=\"encrypted.asc\"\n\n-----BEGIN PGP MESSAGE-----\nVersion: GnuPG v1\n\nhIwDPobfLEGaNZgBBADZWDp1I76T0XbbZ7b+fiYqD7wMpiA3ZVPGxBWbfj6UBvin\nCiGrYs2ed3ChebhJc2vjheWjrH/q32wi90r5ZXf3aAny937r6iDqvrzTEl2tHNHt\n2Hf0DkFNOu5o/szPXdLx4JhLstPm409z3z4D59vZhAASwNw9F3uVoyeamB7bW4SM\nA9m6dMu4ctKuAQP+Pbp/46c6YJvC60gVcTUcUqDOXmpZiHfTGatAD5GZYHYzh0KM\nQ6QtB8fA1ZCJEOkoKu1kDoKGjD5s0L0oqB1oo8sWz2cBpksItZEkhMn/E3WfwVl2\ncrox0V+YyvA4K+8NSyVGTRn+VbArLRy8u/ADys67zMq0WgFjJZ292FW5ZUrS6gEx\nflbSsDTz4t4Oo+bVBVYVW+AZ5eph2WFegeCdFGC7hrie08J03XX/4Q+9iXkzij3w\neix8E6Q3U5U4lFKzbj3YoaEX0Ol4+0QjSQwB7NinQ6t7Vn/G6t1x4tQvxtrSPN/T\nBrYI0tP7SpJedxVJnawAwBbz9ziHrrY0ZZnVLzQjXGk5VtfXlZhKGqiuTJ+tvHWe\nP/KMPn/vOlklC6mNAPOIM0Cu8vCwuAevrPDwzWy+na0XM/AVpYKepv9qoMg2ppHe\ntx9XFgOFHibeVz8Ap5tXoCxO6aSjYbk/puDJePtjYRIhCCiNQalOEKp6d84DJBID\nSAak4v1oXIedBtT3eAyHJ+qmRAt8FEv702tV89L7o0jniBFLUVymszZF2o6eIMKX\nDGo9rt1zp4NSVgc4wQJozSHeIjsbA+M7+fSdZtSfRLsWaIwI3ouu6cY3Vfyrv00T\nL+za+kzJPWejAbu0SCD0RvW4DVNii7tC762Dzt/6Qhvci0tkgGneL84FYKDtJ+PX\neL2Z1O1lHf/nuNYXytaaYweLnFToqhm3/ic+tURefunkPy4XFUrtg+e7WzLsJnwQ\n7XNawhByJe3/Uirp5ftWUylDD9/kZMvCWlzdoWHlA8j5bzBmC/68hmT8fRsbPrC+\nP2tlJ3zNEXfRr9fB1lBXB0lbTtUaW/lQdm/ce8EsBagFLDcZbwxC7IcuYRsYSiWX\nGcBU2oFQ7DpAqX482TJBGEW+RIQCE5IFFWmshlAUzh95M+eMooll6v6fKK0ddjxq\nPs7LIVsbFZmhXfWLhCRvDJFIGxdPzTmTrdVEFiBWPhI6fA+WpLmIyaFDm1OlbM7V\nm/v3Yf/1oPFYJOWfBbrKGDU1DPh67z6VsR9vRJNJoRDy3oM00kbvfFize/Ui3xyT\n6a76AbrraO5MKVjiWkn5p+LU4ujATfeg9UKyKmgT6au/2s3A1i0XTvT6kEsewzmj\n/z3s0i06NUZHIOIzP9mj4WBrkFh6F699GhTC47Hae5svt0GsihiRwaXcTw3ZnX/z\nH9G6/DTVXHaoaiJsK7UPxjNcm+OWtE+OI7xmevgkzwDAm6ylcDPpkyYVwloOeXRA\nkpd8MDXrj/Px5kfU+N4+xP08E/jin2V5Yz2VwPjWpBeh8z1G1vjvVSnBweri5GB2\njPzdN41HD2Cv6Gp6IumfUAIrS40jhy/p9OGOIaEOZBbCqYxHsgSXbJxzcMfVRXZA\njaPAQAuPccBYP4EQJNwoGxW4tUq5wlLu0GunM/7piYg7r/iNcQFJQDfeBF2jSxqV\n9+nnOaP1dP2fs1ACSycPPNSUwv7wfdm+W/5gcE7ue4/mOXqI1B00w/uL4IIcIGT8\nuh/9JDuvd2qbg7hzCFVC37aYNp9p0jN5bLwnwlI9MK/IA3pcoLGtE3N9L9F1Y1FV\ndDMb7UtnYZH4MGo9I9E78MPVaI6mpIUZipFVXjgyCFuR\n=g2m9\n-----END PGP MESSAGE-----\n\n--NRWmMk5TRLqGQG7eWRXvlom7SxxIDxfRD--\n"
  },
  {
    "path": "mailpile/tests/data/Maildir/cur/fw-question-1443197278.mbx",
    "content": "From nobody Fri Mar  7 15:17:43 2014\nContent-Type: text/plain; charset=UTF-8\nMIME-Version: 1.0\nSubject: Fw: About shrubberies\nFrom: you@test.local\nTo: another@test.local\n\nTest\n"
  },
  {
    "path": "mailpile/tests/data/Maildir/cur/mailpile-1398950855.mbx",
    "content": "From MAILER-DAEMON Thu May  1 13:27:35 2014\nReturn-Path: <smari@mailpile.is>\nDelivered-To: smari@smarimccarthy.is\nReceived: from mx1-01.1984.is (mx1-01.1984.is [93.95.224.17])\n\tby www-dennis.1984.is (Postfix) with ESMTP id B9C79105CF9E\n\tfor <smari@smarimccarthy.is>; Thu,  1 May 2014 09:26:30 +0000 (GMT)\nReceived: from mail-02.1984.is ([93.95.224.7])\n\tby mx1-01.1984.is with esmtps (TLS1.0:DHE_RSA_AES_256_CBC_SHA1:32)\n\t(Exim 4.72)\n\t(envelope-from <smari@mailpile.is>)\n\tid 1WfnG8-0005FW-Hu\n\tfor smari@smarimccarthy.is; Thu, 01 May 2014 09:26:30 +0000\nReceived: from vefpostur.1984.is ([93.95.224.15])\n\tby mail-02.1984.is with esmtpa (Exim 4.80)\n\t(envelope-from <smari@mailpile.is>)\n\tid 1WfnG3-0004aY-5R\n\tfor smari@smarimccarthy.is; Thu, 01 May 2014 09:26:23 +0000\nMIME-Version: 1.0\nContent-Type: multipart/mixed;\n boundary=\"=_719c52dffcf6edaa9d3b3c0248688168\"\nDate: Thu, 01 May 2014 10:26:17 +0100\nFrom: =?UTF-8?Q?Sm=C3=A1ri_McCarthy?= <smari@mailpile.is>\nTo: smari@smarimccarthy.is\nSubject: My key\nMessage-ID: <48d72c96395882ac2883572e25a47827@anarchism.is>\nX-Sender: smari@mailpile.is\nUser-Agent: RoundCube Webmail/0.8.1\n\n--=_719c52dffcf6edaa9d3b3c0248688168\nContent-Transfer-Encoding: 7bit\nContent-Type: text/plain; charset=UTF-8;\n format=flowed\n\nHere is a key.\n--=_719c52dffcf6edaa9d3b3c0248688168\nContent-Transfer-Encoding: base64\nContent-Type: application/pgp-keys;\n name=pubkey.pgp\nContent-Disposition: attachment;\n filename=pubkey.pgp;\n size=44282\n\nLS0tLS1CRUdJTiBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tClZlcnNpb246IEdudVBHIHYxLjQu\nMTQgKEdOVS9MaW51eCkKCm1RSU5CRTBjeHBrQkVBRGMybHBlNzF5V3B4WmpEQ3Z6Qm50NnVPRTB6\ndml3OWVhM0QzdmxBMWZMUnZYTlV4SXcKM21CRGZZT0NZd2R5Y3AxQlhXNkl1eis0YWU4WTdSdHZi\nQjNrQjZFZTlBZnRDcXJUcm9rMXBCTEptRmptV2tIMApiYWJDaDJHVHlsTnFrN3JtSmNPS0hKbnRk\naUluZXhGcTh5WWUwWUJOck5IMHVNQWpFTkd6RWtHd0Z6UVVnblVrCjVuMGt1M1FSbm5wN3k5QWlC\nY01LY1JEOTRiVDhuMHovZWxza1BDeHBIZCt5c2R4YjZHaUxoa1NGM09aMGdiSTUKZHhXbk9tS2Vw\ndlMrVlAvNHAyc2wwZGxQeDQrTE9tTXZvNkJRbWdTeVQrbnNEZEk3cW9MMXRFZlp4Q2phUUcvbQpy\nbERKMTRwdWtDMEE2MmttaXg1bjFlVnFDNWRmMUErUHFsY1FiTXN1YUVFeEZQMlF0ZUs5ZG9LanA4\nN0FhelNWCkFkeGREMVBidDJ3aXpySW83NG5OVmtoQ2VxS3B5V1QwMTZpRHp6Um1oS0g5TXBRQ2U1\nU0k2dURlRnVkcFB3NUYKSnA2Zkk0WG9vZjBmUS8yM0tBU0oxOHR1RC9PeW1ZNzRHcm1SUDRJd2Zs\nMW9kcXp3bnlTMENUUXRQSXFvWWlXaQpXdzBxUFFuVmZJNWRqVk1aN2ViK3BwMUQyOVF1dE5jTWJ1\nOWt1SGVWQzdFMEVGVit0NW01RnRRQmd3NkI5U21lClJ1MHlyQmkrOC9BTGhjUFFjODVDeUMwRFBq\nVkZwcG53VjhUSW1zZ2ZicWF4eFZHRlJ4VnJXb1Y3bjlqS2lVYWsKUS8wNVhaMTBtOS8wak9SU3R6\nZ1RvV0J1UXVGUDRGMEZzMHFJVHp4R1dSQVlOZFV5cVpVWUJUOERsUUFSQVFBQgp0QjlUYmNPaGNt\na2dUV05EWVhKMGFIa2dQSE50WVhKcFFHbHRiV2t1YVhNK2lRSTdCQk1CQWdBbEFoc0RCZ3NKCkNB\nY0RBZ1lWQ0FJSkNnc0VGZ0lEQVFJZUFRSVhnQVVDVFlEVVhRSVpBUUFLQ1JEVjNDcDV3dVN1a3RE\neUQvOWUKY01VMUpPQ0k4eDdYWTNTb1RnbWlydTR4Z0VkTHB1YjRJdCtEQU1jY3hzRVFNRkJBTHJS\nMlRjSXgzVDdtUUlpSgpmeHdxVkxmTWh2a1BaYlZYU3M2RmJoRUNod3lpTUwxM3B2cnJvN2l3NmE1\nOEZmMi91dmdiVDg5SGdyU3NOdzhFCk91bE5Qd3M1MElxMktEbGhUdjc0NmpReml6aGVHemhseXlW\nV2FOS0lvMmQ4YThqNVVYajQ2eHZQclpGVFBvTGMKV3puaEpIcmZSRmxjcUgyTzBOekFQVDZieUJF\na05lcHNTc3JPWnM1cWZxQU0vNUxHT2swZ0o4MkpaOVhDWnV6Kwpic2dCWGxWSlJJUmRhbUF4YUg0\nWFV3Ulg4TTZmUmNNSTFBRE4yTUFkT3BlR3AxcjVXK2hxMFo4RGpkSHROS2dyCkUvMUtOc2ljNHFi\nM0g1cUpDTENkcXNiWmpocm5jY0lKc2c1YktkQjJUbjFsdjZjeWFnQnZTWlVuaURnMHlDcVAKeHFW\neVBQbUVlWDN3RmVTdCtoVG9aNHNxdGxPSlZFVHNUVSs2Zmw0U3ZSZmp4dGhZZGZDcFpWVnpGb1lx\nQ3VTbApnSW9FcFo4cG1mMUZmME1RY3JwaGRDR1NHOUIyT2I1UHIyMTRVU0NuQ2RsV01jb25VWTBh\nai9vU2F2aCtBRFAwCjlNVDViM3BZbms2dFRnT1NLUHpMMkxNd3BFc04vcDBjaDVLYi9wWUtWSDla\nS29BaUxTcnpyUkY4S1hTNTdOUksKbW1qWTV5NVY0VFAwMy9GQTFYUEx5Ny9nbnpiSHJzTGFtY2t1\nQmlVTjZrOEpYYzFQSG5QeURMRGlWTjR1cU9jZwplelZQR0ZranJQQXFxQzIrUWxScUljSHFJZkJp\nNGw1TllCNk9BbDVvNlloR0JCSVJDQUFHQlFKTkhNcEpBQW9KCkVJbk5TeUZnZFZubUo1WUFvUDNv\nLzNyREZSd1ZEbHl5WVc1UkVCa0YwaTM3QUo5QjVTUXBLam84Y2dFZy9Nc3AKRW9seFlWZ212SWhH\nQkJJUkNBQUdCUUpOSE1wbUFBb0pFTlZPcmt2Sm1IQ3hubE1Bb0xTNUtiQ1NGb3V5VDU1MApTc2Ni\nTy8yTExrSDdBS0NXbnZTS1ZaNFgvYWxVM0lOTmhNdFJXSk1pS1lrQ0hBUVNBUWdBQmdVQ1RSeks3\nQUFLCkNSQ2VvelFVOVlVdlRvNWlFQUNickVocXJJUXgzQ3BrSnFDUjZIMXZXVmd0dG5WTlNqbWh3\nQ2YxNXY3ek5Da2wKaHNxVnFkTlBtVEU5YlNLeTV2ZFU2NCt3dEcrOTJYWEpVMzI1eEVFRFg3b0ll\naEdRTU9WOWZoV2d6d3djMWIzWQprdTVlYWZsWkhxUkRpTmdxN0hiUnJuMzNDQnhvLzU0RkR0MFA2\nRUtrQVIvaUR1aUJTd0ZWdVE2eXp3Y3N5cTJWCmlrdFQ2Qy9qMVVHazhZR0hYM0FHRXlXY1ltak9V\nNGZpTGVDTzFHK3hpMWlaKy9kOTVFOUhiUkZyWXczYnlqbWwKQVVIeVZEUTBKUFpxbjEzLzEyK3J5\nOXFtRjdTc04wNldSbDFCSUlNbSsvRUt3MExNNkFOU0wzbkJXbldCRzM5VApKR1pYb0tsT3daZTJS\nNHcrUXF6WC9BUkpudjhBa2tLQmpiNlkwazdYQVE0UHVSaEswRXNWNWJBejJ4Y0FTaS9lCkhqNWpJ\nZGtZMWFFbGpIQ0pBTUdsM1lrRENoNDRrTXdkYmoxSnhjMUM2RjkxVUdHK0I3cW9YekhVUHdjdmYy\nbC8KUjliclh6a3huaklwSzQrMXA5dFFnbDFTdU1lVktkS1pteFV2SXhrSXF2Y2lhdFFHWHhQSkph\nanVKMGdaTzlKOApJTllHTTBJUVlxWXVpUGo5Z2pzcUJGRFhoakJnVmpvS3BVQ1N5Qmkva2EvdWIr\nNG5RWmRRZGJUa255VXVJdi84CmxDSS9tdjFEbFZ2eTlvekhEVWVSQkFOOEdOK3R2N2J6dEx1blI2\nR2R4cXAxa0FmQm5OMDZrS0ZmQi9BL1ZESFMKaUVVdkpoVHZoay9Ja2psWkpzSWFnc3RDcFBHSm5k\nTVJpa2NmYW9RTi9uVitLSW10OE5VcUN1NEZSUjRwNG9rQwpIQVFRQVFJQUJnVUNUU0pEMHdBS0NS\nRGlQWlJGUVBVZkZkUjNFQUNYanI2TTYyQjRTRTZUWFl5b0xxVkpTMVpuCmJ3VnVuYWxxakJSZGZp\nemE0d3lBV2crRkNzbEY3T3NMcS9MbFhiTURDeHFYS2pWd2NpYVY5VTZqVjZ4N1c5bEIKa2hvVlVK\nZEJOajlpNHN0dEovd1RWc3YyREZOenN6MStzWXNjRGhXMzNnQVBEVnNBU2hzUWNYL0JVS3dicGRv\nTgpnQW1TbzhUcmY1VnU1Ti9kQ0djRm1vem9zRGE5K0xmcjh1TUtwUSt1ZW9yQnF4VGxhRkVYWm0w\nZWIvSytRTFgxCnA0cjd4Unl6TnVzVUlrWWMwWE1DQjR3dUlRUEVZRG9oKzdqWDRzOGhRUHZqSStD\nTkluYmx5TzNuS2RBZFNVY3YKUU1oc1dDQ1JLcTVvdTVhc3RScS9uUzdnR3NyWDRRWWRrZzBtblo3\neVdvb2Q4SGdUdDJ2UzNsTGh4REFLTTBYNApUSGFtN2ZkMkdUUjAwejVIc0NlTVlGbGg0YklIWkNW\nU0EyUW9CUUIvT2F0dEtnc2Z6K0NvTzA2Y2MwSEY0QXdxCldFYWg2RmRxa0llSlQ1V3lGSFJmZUtN\nUjJIb0h4RXc4UzRweDNMRVRsOXNIT2k2VnkzL2xhQkh5YjN0SlpKQXQKQ1QzMjUrVy9wU2w1OG9I\neEs2d1hqcmIrUnk1Vi9wSms3U0dKVFRhRndSUklTRWRRbXZGUk1keVdGV0RyMnJxUApBMGJVVm00\nWWNTbUNHS1luK2pVUXdNNzFEdW5hU2dqWllrempyd0F4cHdQdmJtUWk5N1k0Z1FxM2JsZVM4ZHpW\nCmFSZTh1aTRJNjNFNlZaY1ZJRzB1RDJUMGFYZVFPOXFsbldKNWU4ZGxTY1VwWVl5SEFySklqazFG\nUE53WjJBVmsKN01kTDlWOGtIWGcxYnVWYmVva0NIQVFRQVFJQUJnVUNUU0pDMlFBS0NSRExtUG5j\neU0zSWFCR3BFQUNQMFp2aQpLZmZ1cklTL1g5NkRaZDlBdHltMmxHNk1ORGE2WEFseTN3Vm5qZEZl\ndEJQYk1LNVFIOUl6ak1KbXhGdllKNWNvCkd2aWFBbTRWY0NBdW9id1hmS2hVYjJrQVgxb2J5dkFv\nckVJSU10NXllOHNscFE0Vjg5OHFiMEVUOUZlbXpYeWcKemYxZElRUHB1Tkh5UnhnYnhHNjZ3R25N\nNFVIRHNaNzdCcFFMNnpIN3RJdWxGS0RpTHprdXZtSno0Q29tNVo1eQpMRExkR0hDeUdGWFZ1WHBN\nMFkwSllCMDRlUFZ0d2JKUnlDNm5BNUVwTU5FYW93MURCZ1RoYjBkMzV1V2VyTG05CmY5NSt1ZVJL\nLzN1TFp3NFNqWmxDMVZ1S3hnd0I0Zno2ZEMrbnpjMHUvdERxRDRWQUVjRGR5LzFoMS9Bc1p0WUwK\ndFhxVGNXSm1GdXNPcGtHU0RDL3h2RW8vS00vY0lYQlVPdzdDNTdwd25pRXdKOG91ek1ZMm41UlQ5\ndDJ5MFhodgpGa2EwQm84RzdPV1R2MWdFMmdZNXBXRkZQcHp6Skh6aVZVdWdFL2xZWnZJNnNLTnB6\nYnJUVzFDV01SNzdjNS85CmdyTm9CUmMrOHRwZ041T3d1S1NGc3dJelVlUFp6WlVqYjhEd3BCTzI2\nbVMrTUZ6d1J0WVZvZTlqTm1WaUFZWVYKMWx4Z2hyZTNYREk2VlB2dUlyNi84M0RvMTZYZzZXQUxG\nbkFHSGZ1Z0tLT3h5SzNQK2ZsWDFpaDNqdVBhS3d2UApPTVJ0ckhJbm4wM0lOV2JJdDFta0w0d1dj\ndzVGMnZIMkw3clNsUzYvV0k5aGpzWEovUklQNUVTdFVIMDVkOXA1CkF5R0lQcDRid1BkdTJvaC83\nNmNtay9OejdRTHdWejNlem1zYjdva0NPQVFUQVFJQUlnVUNUUnpHbVFJYkF3WUwKQ1FnSEF3SUdG\nUWdDQ1FvTEJCWUNBd0VDSGdFQ0Y0QUFDZ2tRMWR3cWVjTGtycEx5VVJBQW9HUndmRnZBRENNRQpO\nTWt5bjZsQlNtSEZwcmsrRERaSEw4bzFVN3JGbnlNMVhod3RkUHNjb3FYdGtWeFVEdHUyZjIxS3lM\nVWQ5cFMxCjhhLytUUGc1dmJ0MDVtVFJhMi9RVkNXcWh0VzVaRDBsMEEwUVM2b1QwVndDQ3FtNzFV\nN2lVOWUwRjBCYzZHcHEKOFNYT2ZkaTBuSlp0MmxpN1RERisvSE40U2IzYmZ1RjJ2SnM0STl3T0ov\nRE4zRHFzY245dGl3a3ZjU1pvakZMdgpRTXc0bGg2a3N5cUZ5WmlUZ25oQ3JrSGw3NStvbTZGT0du\nVlMrYTJqZjlEQWxGRnE2a1NRb3JQMUhaY3ViTFNtClllOGVVMmlLRnRRRFpubmd4YWVIbWE2TTB2\nVm5HSWRWMFFUdXhSRXpybEJRMU5HVlVYdXl3Z29TSHlhKzRLb2QKdEtzbTFrRWtUZU1QdENBT3d3\nRTVDL2Ivb0hkMkVXVTQ1UU96NFNjWU56MnNKSEY5NFJLWGV3VGtQKzhTZVNxTQpxSHlwdTkzMm5m\nVHZpcGN0aytVU3REdzNnbGJMVlFjcklFZ1BhUG5jREN1dy9IUldvNUgyQ0pKTTgvV2pNTGlaCkFU\naFhCallBc1lrR1ZiYlVOaWRJVnE5aVl5RkY3U3JWYXV0dytSK2ZGNXJJZkJkbVhoQ1djWDdsNkVs\neUdWM04KWkZtSjdoRUlRWFRRVktTN2t0RkNTWGJGUXNzS1Fla1UxbTl4WFZNc3pEWmc2VHZNM2sr\nMC9lbWxxc3VHSHhzNApISlFxUmo3UXIxYVVJTU81OWFkZG5BbVo1dTFlV3dvK3RHZ2ZnRHlOcjRY\nZngvMkw4bDEvb0h6a1QxVWNkMWJtCmNhNXZkNHhDNVRFVU03dnIyVDdyV1NXYkNDUVhKV2FKQVp3\nRUVBRUtBQVlGQWsyTjNmUUFDZ2tRNTZUR3o2aEsKY1VEREVRd0F6NTJTcHFSSUo0OXhFVUROWnhF\nQnF5c3dHWVVwNlRnYjFiV0tUTlZuN2pvb3lUd2tpbXJwcU5lVwpzL2NBWTZmaFovd1R1dThjM0JW\nYzB5UHo5TkFaM3J4Z1c2Rm5Ebi92bkxvZWFQaVpYSVVuRG45Zm1XRStyVkdsCmlsYzVqTzVERGRS\nc05pTFZubTl3MXMzR3pVV01TSUtWWVIrQmJnT3lCNXlocGJlYzB0RUdGYmZaUjBoZlkxb1kKV0cy\najRKYkJ1SmswQk9lYzFHQlBHYUI2c0t4bGFJWjZiWEQ5eXF5b3FYUTlWMFhMSlZ4aXlZVDc5QzJK\nNzhDcgpYYkdtdVdjcWlxWlRLK2FZRlplTjdzZEtZMGNTRXA0eWNQN3J1ZFJkVXl3VXJrU1lQOHR5\nMWhlblJMZWFIR0l2Clk3WWpKOEM3bjdTLzluYzE5NmhIemtSSStlR2t5YTR6V2U5M293OEFxT1lB\nUUlGcGV1TEZTc1VRMjh0MVlwelUKNzY1U3FkOU51YnI2S29aRTkzdVp6akYybzdlMHlGckMvZHVs\nYmVKWTRzUzFJdG5ab09Xb0VwVWlnZGFIV21HbwpieGpPTzRjaXhUcWZwUVVsQ2J0L1NNM3RLL1Yy\nTDJvYkdiZzJXOFdQNkEwOERpWVloaFNaTnYvWnAvaENZUWFvCnM4NGtzWHBWaVFHY0JCQUJBZ0FH\nQlFKTmpnWUZBQW9KRU9UVWxGK002YjJrSUNRTC8xajYxKzZHZlBWeVFVM1QKWklMeTFQTTZMV3cv\naEN4ZzZRZmhWNzg0UHU1d3RQSGczaHl2ZUZzQjVBa2VvYklncjdTS3MxQlNVUEY1WnppQQpub1I4\nb1pxbzcyN0g0MnMwRFFoSkhHWWVqNjBBV3dseTBHcmdkMVBiNWhaKzZucmVodHRhY29KNlRudlNF\ncGVFCkEybXRUVm0rQUNDaENUNjhWTTkxZUtUQUFXbUZYcnZQNnpIc2xPaldESVJqS2ZBTWdXNDVi\nOUxvZ0ZMTmkrenUKWnZ5UjI0dWZ3V3NZMjJLbE9PSUJveEpHWVlWemp0MWtzZm41WTlSaG5XQjlG\ncUdaWWUvbzQwVVFlcGlnWmxCcQpLNzgwTzE3NDlIT0VSUlg1TmJLWDk4MElrTE9VTElWZmlLdkJq\nYjNBUklhQkY4SnNNZnpJZnUwRmpLazJBemZLCjFvbEo0MVhrM3EwelNsYjR5dTZlWW0vU1hrNEYy\nYVVxUlJsRUpBOGxXaVAwWGcxUnRPUk11bE1Kd1B4R0lQb1YKbWdXb2Z4TUNlU0JiOHJLTHpTanVp\nVEhadkFuNCtXT3BhaHdVRzVReHBlUjFseXoyMFIvTW5jdEkraENoSmVReQpmWS9IOG5ETGtXU1JZ\nb3g1OG0ycHVFVEFZVHh4clYrNUJyKzAxSUpHY2NOK1gyc3NHSWhHQkJBUkFnQUdCUUpOCnJZWWhB\nQW9KRUkwdCtqNENUV2FLc2VzQW9KWVVwUkxqc0hBZWFqNHp1K1JCOUkxWll1aS9BSjRxdnlYRlp0\nWnMKejFFSXdTc0ZDRm15b3lQSmFva0NIQVFUQVFJQUJnVUNUYzJjNVFBS0NSQUhxcVRaWHoxbWxj\naXVEL29ESEZEVAphSTVpaDY0UlFYOHhwYUpOQVhKME9tT0ZRWFFYYktvNGFwN1E1TUl2WmhHeTlz\nbmJJNWpTdTQvelRRMEFJWE5OCkhFL1pFM0xuZENKNUMzeHVDa1RIRlJjUzYvWmdubFl3Y2dVRkdX\nWloyRDM0WXgrR25Rb20vSytqQ25OaGpWWkIKOWVCVndjNDFBRG5pVHFUMVR0eVZXaFk5UjJMU1pV\nMmNSTTJLVW1xeklJakladDFCTVdHMytaeklhV3NjS29DLwozZVU2OHdWYTZtM2EyOUd3ZkFKTjM1\nSXErblJRTGhieS84R0NIN04xWmR4b2NCOGtWV3J2QUlaOE02Ri9tYkdDCkNzOFluVUN6T1ZJQ2lx\nTklBUmFBa0lPSGl0WE94NjR4YmJQWFlKNXFuQnFsNDBXTkJSTTc1aVFDVFR2VExYdDAKdHFjOEI2\nTDYwUjIzSnN0YUkxckY3NHZyM0lLL3F6b05UYURsNkM5MkRvcVlJOStCUzVCdTBqM1dvazVlbVNv\nQQpTL1NoQitHRlA3K3doU1F5U21uT0VSemhLRkNBczNzM1A1Y1B1SFV3Tnp0d1Z2NkhXZ0srMWtO\naTQ2OFpXMXZTCk16YjFkdzczdnBDZlZTMmlDYVdCNHE5T1ZzNjIydzZYNm5qSE9rNmhSc3kyclFp\nWHZpcVVBbnpFa2NYZkpiKzQKd21IMW9tSGZ2WXd6anhwK3Y5azlkVWx6MkE0SFp6T28vZCs3Q3Rr\neGZzb2poYVMwVVNEa0ttL0szdjlaRTRmcQpRQ2RxQmFTQmZ0c0VnT0UraUxlNlh1L2JVZHlzOVdH\nNmNOS1BWR2E3NDFWbW9PT2ZoRUp3WDVHSk9Eb3ZUTTUxCnMxUGtyYXozMjFNSzhWcktyTFFaTWN6\nQzNUOVZDSHlQNFltTnA0a0JIQVFUQVFJQUJnVUNUZzBDUFFBS0NSQWIKaW55MVNxQjBNQWEwQ0FD\nMk91cnV5OHlSOGhPaXdXcnB1MjJMNGliMlpneSsycmVycnRCU0JPRWhvdDRiNFAvQwp2MXRGOTB6\nUFFNbWt1MGtRdGNYV1VKRlNROGl6RDR1ZDRhVEZBK2FVVzlyN2NXb1E4UGF4aEhXenF4UXJueDlq\nClJiSTFiUjhQRlZNbGV6TExRRVhxQ3NFa3JpenJFa0VrMDBNYzJjNW5za28xTHEwVXdWLzBuemZJ\nSDh1U1J5bXUKTzl6UjdPYzYzU3NxMFQxSHFaV1RKVE10S2ZXY1NQK0k4UktwQzlPOS9ONVpWMlZL\naU04M3BwaURFQ21tNzBMMwp6ZzZXV2ZGODd5aXJ4S1ZBUHc5clNkTDc1UDJjbDR3ZDhrVVZpaGFy\nbWk0cjhGOXJwRCtVUXdoNk9ERUdab3c1CjExUGM3YzZndElqZU10UjJhYW5Yak5UNzJQUTYrT21n\nekNYeGlRSWNCQk1CQWdBR0JRSk9EUTNxQUFvSkVOQW8KK2laczRiekQwNm9RQUlBV2xTc0I3RURM\nc3JRWTRabGtrUk9PT0p5OVlyRVg2NHo2cDJsdmtyNGJIekFpYlZicgpJdm9wNytUMTlPVE1kaTlE\nYkpzVUY2Mi8wOUF0eUY4RkRUVDhheEZRYzFjckxoYlhnNzhLVUlUNGFwQUszeGlGCmxBS0hhZ1hJ\nSHR5UkQ2Q200TXVuRVZNYTRCaHAvWUR1WTNSNi80ZGJ3WGs5K2pVb1Vya2ZOam1nWWgrOU5YVWwK\nZGczRndCTnVkTkhqQ1hHQTFTRUZsc3FFUk5DUUZVZUpzZ1pIeHhWTEZHYkxUTUVNSU1PbnB2Y0Jq\nUEF5UnVqdQpFVGNOdWMrdXVLTU5oWkRZRVVGMHlWR3F1bnJSdkVodkpyUXR4WmlEeDNZblI0WnZu\nMkJRNmMwUmdGemtISmIyCkg3VEhYUUNXa200V2RFY25xMU8wanNaaG1SVVI1VjJPTEduOHFPUjYx\nU1VlSWdkMXN6d01sMkY5Slg5d2QzcmsKMVZ5U0VwU3hKVjQzNGE3bGV0WUlIYWZUR2RvK2ppOWNm\nb2d5d3VjN25QR3d4bENjdmpRbGIzSllqRnBjU01uRgpBaE41V2ZWNGZsT045VnNHMzZoZHc4NytF\nYjY2bmg1ZnNyc21GVmxlcng2OEpodjhNUGIzR3htYXcwYU9VZW1yClAzZDJtVElWVlJneGQvSGdW\nVW9TVDZPbll0R1ArOWxMZlpkL1VpQlZoSTNhOUFCV3VuanlvRkd2WDkybzB6UWQKcng4bkhwcHZO\nTE9IWWFiK0NnUzlzMU5Bb29ydW1tSkpvaENrb2hmMUJ0NERvVDRhdVFVWTk1a3hreUFWRExOUwpO\nRXFybzVQS05sTXdLL0FDSjB2OGJvcmNUZ3lYa0IzL1F6T2VBcmd5NTk3c01pUUVxOG5kUmZ0MmlF\nWUVFeEVDCkFBWUZBazZxL0xBQUNna1FDSDBXTTJ1TWhHbWNCQUNkRUw0MForWDVDOE03K2lwM2hl\nZFpOandURkpRQW4xL2YKeWxGdFROTnRvOFNJd29YYjk3Q1ZqRVFMaVFJY0JCQUJBZ0FHQlFKT00v\nYm9BQW9KRUNnWkhaczdRWm0wQk9jUAovUkFZSzd1RFlNSU1WbEpxanY0eDZzL1U3bTFERXNjSjE4\nNmg5ODVZRmdBOGdOTDF4RXhoTVN5YzRRK0QxNHZLCnhSajN5dFBuK0pLeVQ4eFVTVkY1MVQ5aVNr\nYlhmZ21qNXNVWEJNVkp6Mk8wb1d5UVJ1WTZwMEV4Q0daQzJOd1UKdFBWdG8xWXBEWW5uT0QxY1Vh\nWWQxRkdiMFBOb0UrLzRVMkpFNEgweXd1NDNHbUhoY3ZTSnZpOXFOand6UHJVVwp3Q2xmSkg0VFRO\nQ0xxNlp5djJQakNOcSswTjlJdWU3STg4YkVzZHRieStiVjJzRzZidERqd0F4em1PTlFyeHNnCkEz\nZ1hWelgvYlBmZ25JWnJ2TFdVaEFPcTV2ZGFmblViUW9yWGV6THk2dTRKSnNYNGt4c0pkdy85UjEz\nNlVWOTMKMUFNcDg0Y1FtdTVsOTl1WEdTN1Q0SzFYYnpFL0J6Qm02eFdSSElpNWw3eWt3VnFjZHVs\nK2RJbWRiTXlEbzYyagpQT2w4U2hJV2ZWbTluNWRHMDZ1MFpJQzRCTy9PNHBwSWQ5NSs3UGFlaW5Q\nK0FZUjQvZVZ6NEtIYkk2SGhtd1BICi9lREdiMnlsdXlkOFNTR3RZeThPeUFjK1Y2YzZ4RW0zUVg2\najltWVNWQnhFeC9pejZoZG45Ris2S3ptT0ZYREwKRnBDQ25LNTZtaWc0dnpIakpBU2VEcnFBQ0Er\nVXg4azkwSHExMUo0WnU5Tks1eVM1UE1vRXc2TlAxL0diVjZWcwpUOFlDVkJGcVpGSEZoWWZ0Wk94\ndTFIR1JpT0ZGL09Fc0FYb21Od29UYVpPYUlHSmRsQ0EyeTBxa3RnbVlnZWw4CnFPbmpDb1QveEpr\nNVh3Z2JVd2gyN3BLRXRkUXJiL0o0NkZ3YVRxMm94YXpXaVFJY0JCQUJDZ0FHQlFKUEx3MFcKQUFv\nSkVINlhLc3YrQ25yemlEVVAvQTRuai9FblBqdis2TjZXandnRUQ4cnQ0WEFKeGpoNVFLOS9sbjZ4\nVkVBNgpudVlwelZrbVU2QmZFZ1kyNjA3MlY5Y0NSWHhIUVpIWnV5NSt0TkE2bjRuSXBrcXZvUzJJ\naTdwQy9yckJidDBlCk9UcWt2MzZRSXFYNk43MGNLQ2hReW14YXliTnZ3Y25GM3Y2WElhakljOW9F\nU3JOcGgyN0VCN0w5MFpXaW95R1oKWXBRNlE4Y1R3dW9iUHd4cUdOWTRaYkhmVWNzd3dKRmY3OHFq\nbVR5dE1WOGFONEdmZ2pxcjVsZWNEbUhNdjBBMQo4ZDFVbWk2Ykl6NVkxZUJIQ0FEaHhmVEhHMmpS\neGxVV0srRVZvZlI0Ry9IdXdDaktVK1htVm50WlpSWTlNWTBvCnNQY0dlbGsreTU5bXRuVGQ3UW1a\nVU5LM1krak1kUEFFU3g3bUV3eGM5UWdnaFlyY3RUdHFtSmR0RmZxbkdIZlIKdVBxT20zcVRoVHF3\ndEo0VUFRREpqMVBvOFJQaVc5dWE3aWVvS1hjK2dsVkt6b3FuRlhJRU9CdlJsU1VZeEczdwpUeU8y\ncFREU1BOMGhVcC9udU1IRFBza2hJR3NzaU1XNThqblc1S0FMT3pjT1hLVE5mQTVIcnN2SFhLUWln\nZjRuCnNKRDZCOWZCTU0rcEpRZkFKL3pFOEdQVW1XdjNZbUtYTkh0RVZiNzFqc3pXWDhpMmQxOXFp\nd1BsWllkUS8zODUKcUJMRVdnM3JTVFg5ZmZtWGEzYkRXSUJIWXFGY0tiTXZxTE5kTUVDY0pFclNT\nTUFjSTNoU3ZxRklac3R4bWc1WQpQUlY1RE1aeXRmNFJ0U0MrajRvMXJYeS9laXduOEtnaHBuSVBR\nbnY1blVYdDZLZ0lDWThvaWR0a1pxQk1XWWdrCmlRSWNCQkFCQWdBR0JRSlBMNmZOQUFvSkVQbEJC\nNGg0Tkh3TUswa1AvUlk2NEhKZW03bHFjbEtkS1F2RjNleVIKTmIvR1VzTjV6WEtRMnRoRE8yWnU0\nZ0JrVHpDb0o5K3RqV2ZBZzZRN3JzVmpiYVU3ZXN0RUFrNldldlQzRS9Icgp2NCtEN1gzZFNCWUNu\nN1BOV3NHL1daN3VsZ0FCekFUSmxBaG1yd2FvcW91LzI5UlA0NDh2NUh3OCtRRFc3ZklxCk1wRHQ2\nRUNDS1RvSkdYS0prYlY4akZqaVZtQitjMmtBUXJRejN5RUl0clFMQUFDYkhHYkw3WkxObmtydVBM\nbTAKMUNaRnh4VnRibU1jbnl1Q21wZjFJMDRsUkhiV0hyc1VWdE0zLys0cnlQdE5UQUN2dTkzV1NF\nVWJsOXRrMVMxUQpXQjhITU16MTQzeGtJT3VORlYyZkkzUDRmMk94UmkwZnlzTm1lTUF4bXlabkFw\nOFp4eGtaQkhiU0JIKy9yUWJxCm9HQXNsT1dERnNMZ0kzckdCTWVKUnRzNzR6VTY1aSs4STVaYnhZ\nQWFHSElWYXVieExIdnVDaWQ4cWdiSVhNUDgKOXhIN1puc25jb0h3SkhIWnpUK2Z3amg1ajVHb0JT\nck5XV1phVTJvWDJDM25LeTVnSWZ6QStTTzgxL0RodEFtQQpjMGx6USsxVTRrZDhZcVJENkR5QXY4\nR1FySWlDYWJ5OFZncGtlZ0xOQUNGakNsbXhOVW5YSFo3M3p6NVNyQ05JCldLakFxekQwLzNybFRJ\nR090eS9GNCtsb05xZTZDbFE4SGJJeHZDN1NVT2VSN0NxNDduaEFsTzRVMzQ0SE94dk0KMVZrdERK\nQ1hzalNRa1NwbnkwekhtekRVNkZLR1lNOFFYK3ZwUHIyM3FvRmhqWGFjKytkVnQzY0s5bE9JdFN4\nMQpKTHdxQm9OTi91MTgzK1FMb2czUmlRSWNCQkFCQWdBR0JRSlBMN2cxQUFvSkVNcytIazkyc2pS\nSmdORVFBSlY0ClBobkRYQjNJNGtBSHJ0QlFQZjN5WGQzNU9HemExZnJvL3gxYXUwTDJDVG5qQ3JU\nR1lRUmJYNGxHOHhNRG92U1gKYk5GTGc3TUsrOHZESzhTelZYTTEvT1Q1UkJwM00yaVRmTk1RUU9z\nbktoc3NaaTU2SFMxZXBvMU1kYnVBUGYyOApJeXhUcVpmZHFzU3hYM09xZ0svYm9uMDY5M0o1c1VJ\nbEVtZU5VcmFHS3M3Z2JvZGtEWHFjNHVjTEVaNGgrdmxBCkp6dVJmWGVIL2dXejBQVkhZOEIwUkw0\ndXVKWG5OeENyZUhFV3ZSN1pHTElVdDg1YUtSZ05hK1hwOFo4M2VBbk4KTytQNlI2bEpkY2g2eGRi\nRTdPbkxzdnlSRUxGMTRWdmNRWEo0NlY4WmxzNnBuTWdZTE1KMHAzTjlwcytlaTdsYgpDbk5NVzJ0\nNGV2ZTNDNFJOZEtMTUtjTVV5SjhuMGh6UngrRmZiK01yVERicVJRSlRGcmtzQjBDU1A0c3U1MnJx\nCmdSV2JZWmhJY3lsTFhVdVpDR1ZSaHYydHcvVGlGeU1idUxRcnM4ck5jM2Vqb2Zwcm5YVjQwUE9P\naE5VZlpwRXIKWUp2YjJ4STE1Mm5nNzVqMEMyaXdsZmtQTTIxcTE3cy9VYkJmQmhqUWt1eFpqVXJt\nemVEQW9UcGNpREoxUWZQMwpwYmpBRlhEZVBKZUxDTW9mN0RZQXFBWkhuc0JxVFBnc2VZMmNGUU1h\nZDhocndiSEQ3TWtLMDM2Y1RpLzVxcTVpCmtkYm1DZzZQa2ttMnAyN21RQ211RjNQejZSR0I3eDl2\nTk9JMlZPb01oU3FYb3FCcitHM0ZyL3VnWUE5M1RyQkwKeE1vUWpwRXV6NjdWUGVLNDE2L3l1cVVF\nYlByYlRLcDRHQ1FvZ3hkd2lFWUVFQkVDQUFZRkFrOHZ3S0lBQ2drUQp0M3V4YTFhVWt2N3NUZ0Nn\nbHBidVBua05oUWUzZ2swMEVNZnZEeENxL0NBQW9JWXVFSE1wUlhhNmtNNjVWQ1RWCm94VGs4bk1h\naUVZRUVCRUNBQVlGQWs4djExTUFDZ2tRdkNrVXRCY2Nxa29kR1FDZmMzT3BlNnJWZzdIYjNZdG4K\nd0ovTzByTnhTcXNBbmppVzJpdG0ycW85dlZLUisrL01qV1VubUxSOGlFWUVFQkVDQUFZRkFrOHYz\nWHNBQ2drUQowNk53Qks1TkhOVGZaZ0NncFI0NmljSllVL0tSTzd5M3RuWnE2eS9STFZrQW9PZ3FY\nQkFQRXJzSFFJQU5BSUd2CmZvbTlxbitTaUVZRUVCRUNBQVlGQWs4dW9rc0FDZ2tRTjNqWExJQUpZ\nTmYrZmdDZlh4Z3FYSC9vTVF5Um04WjQKdTJodzREOXQwdUFBblJOTE1pNklqcnkrcXlzMTZGZUQ2\nL3A4VHV4T2lRSWNCQkFCQWdBR0JRSlBMdEZaQUFvSgpFRnVlb1dGbWtNK1VLV1VRQUp2UDB1Wjlr\nVmZlMGNnTTJwblVWcVVIN1pkeThCamZpbDFsQ0wzWEVNMzBON1dnCkVicjBIV0pPTVJTQ0xjQS9J\nQjVKMkRlajJzNWJJM0JDQXVwRGJVV3hGOUNMc3d6blJYSFNxRnU5bzNSRXg5NDMKdkhuTnlrVTkz\nQ1Q5bW5RcXlucXpnM1dIZWhrcThKMEUxVG5XK1JzeUFFdGJqZTdiUlZwVGU5b2NTNUNoVnhabAow\nU3BrR0x0V204bG5QaFYxWW82RytGa1NnWXluUzhqblRRMlNVd2V3THN2YXBNY1ZSVGl3cG5ubjdr\neTZvZHJ0CkF3T1l0VFlYMXQ3SXVjZnF5NTJwTEFTTmE2aVpuVFBBQUdscEZvS2p4VXFaNFB2cCtU\nZmdZVDJpN3NTR2tkS2cKZDBSOC90WmFOalFUWEFQYnZra1dTTFhqT1UwZHlHSlFZM3Y5dlN0NFJh\ndCtlejdEOG5FWXJHbWhLNVlNSWdYMwphKzNJRWdEMFV6M2YxRit5dUtvbWJWVTFjcmwyZXdnQjNw\nQkNCdkM4R3FnTzNXR09QZVJQU2wvZ3FSYkpqZXZVCkkydlZYVXVuRmtEV3dPN3ZlbXYyWFkxbTYy\nVFdqTmp6ZmdpUnF4cm11SmQwWU1raHhwaVM2MEdFOE1pNlZleWcKYjZxVlpmK0Y1YjRIMHUwOVhW\nYWUvTzZ3YkNXT1lmU20wN0w5eU8wUW9ydkhwenJlYUE1d3dRZE13aEh0SUROQQpDUHhkbmJuVkFp\nMXRlcE5yZHVsK09XWWJlYXBkVWxXL1FXY09QV2F3RVQ0Yi9LTFc5aENtMFBEYlFaUHlkTW1tCjB1\ncFVnck5mMWU0VnJNQ3VXL0pmbXEvdVJBeGhJR290VHZqT0w0YjBuVGlhOEdBU29kU2FFUityd2V6\ncWlFWUUKRUJFQ0FBWUZBazh2R1hRQUNna1FFQ3Mvb2l1WDBUOWk4d0NmWkprbndPRW54d1F5eUdZ\nU1h2Z1JrK2ZjZEk0QQpuaktTVFdsMnFYd2JTanM5Mys3YklmQmhZVTNoaVFJY0JCQUJBZ0FHQlFK\nUE1DaUxBQW9KRUd6QXNxTFE2bEJaCkdTZ1FBSjNSWGluTi9RQTNKUllDdVZmSnVFVi9TLzAwYmt6\nemZBc3I5bW1lejA0TkJhUDl0dEhKcHR5RlpBcXQKMXRUR3ZzNXlLbXNXMHE3UG5yQTgwdzdRanVL\nWXJnZ3llRjQ1N1g4bGprenF1RjdGcGpZT1l0RHZJUlZ2WUlHawphZkZOYmdoV0RUYk9RNGlsdUFY\nUC9OTXJOeDdTYkJiU0tnV3JiSjNLUDlFdXJ1d1BkK0tVb3hkU1Y2akI2NUZJCnJyWnVabVdMRElD\nRE1JWWVLWTVNV081YkUwQjZ1Mkk4UEZvSmQwREJXSUcwUzU2SnBsTjJ3OFMraEFUcnI3MTcKVVJp\nOS84cXY4R2RXb3kweWpOdE01L0RxT2FMZDJjTlIvRnlRM3Y1NnY2a2hLVk15MGFGWnhXWmRCVVhR\nNExycgpqZjZkRWdhdjF2dERvYk16OHFUM3l6M1VBQXpVR0o0bDZyN2thSmZua1ZwaWFuY2N1UlBY\ndHFvY1d1cHB6cGNxCmhXdG9FaEFYaTZnRjZZYk1iY3hLZ0RTZ3IzQ1Brd3JVd3VBbWhSMUU2OExJ\naVJIL00xQVdJb0dCNTI1WkhhSzcKRjltY3pRd0xVUmVaa3NVd2szckVsMWZOVGVnbXNUSUlTeWk2\nQzRpblJnakU5a0dvSWpnM2NWMTcvYkszUEZnYwpoUlltSVRGZjMrUzdBQmpFWnVJeExxVjJ2WWU2\nVElaV3hnRmZaSFJ2aGZnMEkwcWR0c2dmVlo5aW1IRFdaTmU4ClBMY29ubzdZdy9qTWlzSzZBRVdG\nbW83aHArY1EvaFNhdUxESi9xNUVLNHBTSlZIRkVRbzZzTTlHbGFMWjBzYjIKNzcrZktXeDdTV0NK\nKytpdURmQnV4bjUvbmE1VC83enhrVkI4Z0t3K1l0QXNCQTE0aVFJY0JCQUJBZ0FHQlFKUApNQ3Zi\nQUFvSkVBeGpkaHZ1dlFYVGFFWVAvMEYvV3Q4QTIxS0FHS0xWYTlTOG13S1Q0eW5YSmVsYU5YSmJM\nVm9SCldaeUdsN0EwWFFiWk5sZWwxcDViOGg3L1QwcjFnK1Q4cnlqYkw2QWo5b2U4Ymk1dEdqNWc4\nalUzUTE2enJxWUsKR09td1lEeHdsYTFFOUM4emtPVmhJRjdmblFZNE9aZEZyb0x2TWZES3JzNWVl\neGp1NC9MelZEQlNOQmNJcnlIUQpBOFBnQTRRc1dkVjBVaE4vM2JuUndEc3J3NzIrSklqRzlIbG9V\nSnZlQjQydFVJUnk3T1pQN210eGlnQVRrTTEwCm1mVmVCNjVyemlPUlJCUldiVGE3WWdUTUNHSlNU\nVXZQd1lpRVY5aWdvVXk0TnVTOGtrN0M4VFRoaTA0Q3RBcWsKRFNjQ1oxbHpycUVkMWhxcnkrVmNF\nbGVHS2tKcjEydDZSSThrKzZRd0EwdTNjS2dneTRkTllsOExIczROUytxOQpCTVVvS091VWpHY1JQ\nR3o1MzJpTUpkYmJ3UTlqSHdUdGIwRHNPa0VFOVk3Ujc0OEZhS0QzZSt4TzFlRUk2a0o5Cko3eDlv\nMk00cVYrVHd6dnVIMHZRdzI1c0U0amM3NXFtcTE3RjBITjdqSlVhcDNldU1IVy9FZW8rcEdSWm5q\nV0UKVmRRUTlaa2tGM2VwRlorNG9rL2E3dGFBYkZsQ1Y2VDhDTFNZZTFmZm5qMHFWYzh5NGJDakhB\nbEdmbGR5UExvago5OElMNWxIL1ZuOG14OTJkSXhRcmJoUEI5YlNCWnhMYkhuN2pyMzBoS2tDM2NW\nbGtFdTZ1NVdKQko3SkE2cCtVClZ4OUloZnA4N2tTRnphaFQzc2NDWUVOTS9Md0JkWnJKUFNIeEps\nL1p2c0NBUUFNQitCVGNLUnpyZzV3RmREVG0KSjd5V2lRSWNCQk1CQ2dBR0JRSlBNRHluQUFvSkVL\nbkliSTNUcm8wNjRWRVAvUkhvVVEzTkRDdzZvbzdxaHljUQpKamtLZy8rbzdjMEZmbFo2R21vV3RT\nelJtSWd5V0g3RDl3amVuK0NHZnpzQzB2dk1CVXowM2M2dGRISFNCRS9mCnlhMHZ3T2w4RTJUWTFp\naDZMeXNTa29KbDhhYmxVZU1HYS8rNUxXOXNRK1pQNmp4bkIxRFVpZ0VEU082OER3bCsKTkQ5V0xT\neGUzWnEwcHpNcCtaK0hEL2x4Qm1LclB6S2tyamVGOUwxVGhoM2E4T1I1VEdMbjhib1QrajhGTDYw\nLwp2SWVFZTJFYXlINzdkamZNY0Nnd05aamRsZEJkU1oxSjE0ODl0NU9iUjRrWDhsRkR5Z1ZQWDVV\ncGEwRHFYNEVPCllqdnlKZTFZSU5KZXRMQ1I1QS80c2dhUW9uMVV0Nk1IN1NrS0Q5aFR2TGxKbWIr\nTDY3SUpLOTJEVFlRc1U2Y1oKUE1Db2t4c1pxUDNLb1BnbU1XV0NabEFTMjhtV0pEUVhXaW42Y050\ncmU3TnhkcGY0MlRUUU5Ed25Od1ZnRndBNwpsb2NQSDlWQWNJc3B3NHVaUFdUN3BDQmNJS29HZmhu\nOGxBQVQxRUxTSC9SY2ZXVVBGNkxwM2I4M05HVmhjbm4wCjRidG1uQU52RVpEWW9haDVSODluazhh\nOGx4akFQeS9xbVRtR1Rta3hzZ2dqck1MSmNJWWJGYTlDVWVQMHg2RGQKbGNQOVZpY2FlM2pNdjJL\nWmMwVUozcVdWbjRPU2VoQkE3bVNndEtlYVh2SlZjYTc1K0RBT0xBS29tbDg3ZVZaLwp2ZmFrUHBl\nbkdYSGZMemplcGxHdDZCMU1vVlV6dUkraStrNTdDWFpkSUludTJ6SFB2a1FCQWpMRTVnaXlwbUFq\nCkloRk5EM2xSdUh4Tmh0MmJCbUdNTWlWSGlRSWNCQklCQ0FBR0JRSlBNRHlWQUFvSkVQYUlIcENJ\ncVIranR6Z1AKLzNYZXZOT1hld0xKV1h0RlZLRzlpK3d1dXJQK0pkOUtWYVZkTnk3ajBGWC9SM2cv\ncFhTOG5MN1Y2Nmdxd0dhNgpjcnVUYUdkcmtrcVgzS0RvemRRZ2taOHBTYUJ4RkovdytmS2x3WU5C\nZkFvblpvY0JXdXVZQmtaaUNFRksyVkI5CmdmT3VhQlFyTHdCVTJZUWptZlFjVndUZDA1blhXVEtM\nR0VlVCtJL3dRWkxBeWo3OFRVYVNNV1pnT2pISmFiNzIKMlk5MGZJVjZ1M2x1Q204Q09QcWs4RnpY\nNDM4OGs5S25LdzdXWTdOSVNuQ2ZrZUNoWXRJeWtjNGI4STQzaUJtZgpmZVNTelE1ODlTdUhFbU12\nZG1Hb0xRU01abzZKOFp2eFhVdXVKMUp5T0hySFJVWCs1TGJZclhwYmNjRXJmOVZxCktyVmZBNktM\nZ3dMRERUSk5QdngwRWh3a09IZkdNbVdHcmYrRXpEVWxZTGNab2FOM3o3U3gyYW9TbjNDQ3h5dnMK\nTWxUdGRlUWhyaHlPUE5raFFLQlZCdjVzRlRCemRYY0I2aFRGOWkrYnkvN2NjWDBYYW9YUTByVVFT\nODJ2Q21leQpQMFZqbDQvRHNhQ3d3TnRpN2RyQ0RwK3ZVUVU0ZGhZYXZ3OWdEL0IwSzVzZnk3aDJw\nNXB3K0RxZXZxemhvNmVXCnFKayswSVZzaG5Yd1NHR2g1NVNxTjMvNlZoMWVQaFp5aTBOMEJLSGxz\nR2JJdFprR3pVV0o3MElnMjdab2pldlUKbXdkSVd0RmxtRE5EbTR3SllRZXN1U2pVbFpGMXZwdk5i\nTm1RSyt4cXJkQTVrTFJkOU9aQUdMejdpTXdlUE4xYwpMUjlWY2hnMXF2djJIS1B5Q0E4cDkrRXdz\nTW1vNlUwcFNOSkZ2RHNSNFcxZmlRSWNCQklCQ0FBR0JRSlBNRHltCkFBb0pFRWRtcGpSRHA1MGda\nc29RQUpmeGZpYS9FY2lWNDVCb09ZbU1pOUZGZXFRMVZUUWFCNVoyVVRkWnE1K0kKYW5HNnIzSDdk\nOW1acmY1TmtSZlo2Tk1DUWwreldCeWZGNUhxdTZJdUxyelBjaUplRWlIYm9TWmJKQzN4REVyVwpZ\nNWgxWFZlSG9ncFJTaTVqUG9lTEVBbHhvb2c4YkpqYWUrVGlHUFZ6b2RDazZNY1g5WmdEZHlpMHlF\nb2ZMWG9RCm8wVC9yY0Nxc2NZMUxvdUtNK1J2bk9WdkRZZkdDSVBqR2M5dXdTamxvNHg5YnNmZXNI\ndDViV3AyMUc4YWdOVk8KME9NTUFieEUxUzlIb3QxcmJBMXVXR2lHOEtlRmRLc0M2MFJVemJCMXg2\nRGlIcmNvL3ZJZjM5ODVVdlN4SEVHWQp5SVpJajA3Qk5IZGRNeVN6bHdtMm01Zit4ODNTTCtIY0dV\neEg4SnZxbWpmWGEzOEsvOXYwMXk5K2FDVnRDZEZ0Ck9MNXpoQWlmbVFEQlhuTkR1M3VTZnA4eXFX\nUnFkTlpwUWxYcnBZTSsrVnNta25Xa0RpVmJpeTBqSDU4UlVyMTIKbEUwbU1QL0NqTGNLT3lWUDdO\nSVNPQmdIS0ZsSVB2L3A1YnZYRWZKU3gvZUV0dnVNZmhyT01lL0h6N1h5TXVlTQp2U0U4ZTVuZG1m\nV0FuNkYvOFh1RjJQOFJjNHBhU1Q5NG9MaXNpcSt1UnlTRHpPUk1tanB2MjBJWTVVNnJKU3hwCktW\nUXBSRTU3d1BYak9UTVV1WmpDbGRvYks3MDNLQndEb1hOK2l3Nmp6eXVFWElna2hpcFcyWVIvUFdU\nWWNaN20KNVpnckYrSEZrZEV3K3Nmdk83TUU0M0NhMkVrMWNySFAwNXlwbWJBTWU5ZnFKWUhKWnVl\nWTJrcFFobmtadExkQwppRVlFRWhFQ0FBWUZBazh3UnFnQUNna1E2RlppT0xBTXRUUHExd0NlUDBD\nQUswNXN5bm1POStSQU8raC9oSEhnCnoxQUFtZ09rcTdTMDdOUG1zelpPUkplYXhRVWZibVBGaVFJ\nY0JCQUJBZ0FHQlFKUE1GYVdBQW9KRU5JOFVDZkcKVGxtR1poa1AvaUp5bFllNksyZ0NraXJnVlFK\nVGNSdFlEWEs5ZTN0VEloQ3htbzAxSzA1Q0dNZzFWRUlmc3ZWVApBS2NyVlpUc2p6MEJEME5keFYy\nS3VnWHZSekRVWjNhOW1zN0JObUlleVMyR251eUpVdDFEZ3dHdEU0c2d1MVQ2CkxNbDVCVWpXcnBs\nK2NLcjk3aVJlbGhyOFZoNFRQYjJGQXRRTEJ4cS9BTGRYWHdxQk9ieG5tVkxWZGFtQmR0TmsKT3lF\nTXMzYS9jVytsaFRENTdPZDF2Ung2ZGhsUFlhQ01YdXZaVlIwNFpqY1c3V1ZGOUZpaE5semtYaUFT\nVGFxMApyY2NHRnVYUTN3YWt4MG1jYllBMkZ3d3lCWE1JdVpBVWUyNTA4UG5CMlgrY0dheDBQbkt2\nQk03eW1UQlhPbk1XCjNnNTBhb2hzWlRxWTRMcFJ2R2JpTEFDUTA5THJpTUhXZlA1NHYvUlUrd3M3\nRUtTOUl5bDhjelhUYi90L2ZhNXUKSGRJdGVpZDdCVFllaUVxQU44YkZSQXdJNzd5dUo5YVJxRDF5\ndVJRM1h5Z3NscVBFRnU0OCszUnJnNDNqNWo5TgpDdGhjT2tZaS95V0ZEV2hOb2hNcERVWmFtZG9O\nS0kzUm9qUXZxWkNZelRJUzkweUlNbDdYOGR1SVVsU0VnakVlCklOQUw5S2E0MEt5ejRJZ2NxYUNO\nQURWang3aUZmOUg1MzhVS1dyVEpmRVhTeUs4WFREUE8vK0pMMGhrSDlZNUoKR21jdGVjVkVlV0oz\nZTlaL01PT0Vqckd5M0J0QjVHZW5DY0hQYWxNM2FUUmczSGNNQmthL0hGbkV6ai83S0M4QgpaeXFR\nNktDdzRQZTlaUHdURmltbTBnSUgyeHE4RGZoaDZYZ1lGdVNIVVloUnNBc1N1Yjl5aVFJY0JCQUJB\nZ0FHCkJRSlBNVXpmQUFvSkVHRkt6YUNCUWQ3U2tZb1FBSXBzUjl0UDJTb3N3K0QxRUhGVzFnYjZq\nZVFIemlGa2s5NkgKMUx0eU1JaDcwQ1cxMm03ZlhRcE5UNjN2S2w3dHJ3cWFPRmtCYmJra3l0bnh2\nMERHWGNwOVlZZ05xNytDUkNPRQpoeStwQTBqSnE5RVhmNnpBTVRPK2dWUXJKdEtycVgwNTQwL0lV\nNThoMTBzaUQwOGZ6bWt4alpYZndKY2hpdXlTCjJFYy9aWityMmFCUWJUc3BRYUJvc1EyZmZteDVV\nVmg2YlhRQWFIRzNOUVFEMmYvNlBoV2hjd3ExVUlWUFFZMTcKMUV0U1pXYm9DOWR6WUJidUEzblNH\nUGhuWlQ2NWVraTdlSExIeWJDaGJLdHlXaHhYdncvTzA5WlFGdEtmUHF6Rwp1Wnk2aHFvbWxNbFNu\nVTBXUFdBRllDd0hkVW52eFFjNi9tNEVvV0JjQ0RBckN0aHRWNE9VR1NMMmpvYlRXWFJSCmZFWG4y\nZWZXU2JsRC90YlhBdTBJelpIRXlSN2lBSkZHN3FrKy9NWkd1ckkvWjRuRXkvMTU2NkE1NnBUdmNs\nbVEKYysvYk9aOTcxUEZneHF1bVhwRFp3MVpOdjJwQWh0dEMvU1hrZU5BQzR0TVgzZDBiZUNqZWND\nM0h0M01Fc2kyQQpmcTQvckhwQTl4WUJKU2ZnM2NFMFZ0NnRHQnVCRHJCbDk5WmdMTEFJWFVXUVhp\nci9SRE5oNzJQZlRRSHd6MnBCCmR2QUNRbjFJYU1aZzNrTlBmNnMvdnZhcGZhUFJmVlhzVVFNRHhI\nUmREZDU2UTlhVWZ5VUxFZXRtdjJCRG92N0MKdGhMZW02MkdDemx2YlNZa1I2MGlNVmg3aUtFRE85\nTzI5TDVRelR3a1J4WGF5a29ONTBLS3hOQ0w4MVBuRFNVeQpJeWRRM2ErNmlFWUVFQkVDQUFZRkFr\nOHhua29BQ2drUVlkaFIyYWFDSVZOdGtRQ2ZaL1JSMElCcVBMeWhwYlRMCnYxZ1lOeWZBbXdFQW5q\nOG9QeHFoWUtWVy9GMXFHeXZJd1F0c09XbVJpRjRFRUJFSUFBWUZBazh5UDlnQUNna1EKZFkraUtt\nRS9PdVNCQ1FEL2Z0NmNTL2V5cVFHbHM5S0kyS1o1MHNQbllsa1VxZ28yVWJ6UnA3TS9rNzBBLzBE\nbwpnYVl6MWRPQ0srbTArTkhjY2pYZmxkRUpJZ254SWZBdU5jM3VzdHZtaUVZRUVCRUNBQVlGQWs4\neWJKQUFDZ2tRClBjY29JRjJQbGt2aFJ3Q2d0RFB2bjVlOHNSeGI3aElVcXJmZ2V2OXAvWXdBb0w3\nd3B1YlAvSDJVVFRkcE9na1kKSnRtMGZ5Z0xpRVlFRWhFSUFBWUZBazh5OHZjQUNna1FZOUxWMlFm\nNG03aVNGZ0NnbjFRdVc0a0kvWHREblROagpMS0dUY09kQUVTRUFvS2NDamtlSEppRWFZeUZsYlJz\nc3VBSk1ySkZmaVFJY0JCSUJDQUFHQlFKUE12TWpBQW9KCkVOb0RzeVltWUFaaUtzc1AvajhVa253\nUWZVeERiZVV0ZGpLeFFaRjJJZVJOa2dpbTE2bWRyeDh4c3BBbmtxM04KQkhyODhkUWJCbXVYQnlE\nZDNLNmdVbjB1anpCeHh2aDhiVmY1VnhEZHJRMmRISnU1RnZKWVB4WXJ1YWlsZGh6dQpTV3RlRW9l\nc3IyNzN1MVZYbVo1SEdrMmVjMmd6YkVPdFExSGwyR043bTd2a2ZxTXlYaHRWZzZSVHZ4YlBDVyt6\nCjlOb3N5aFA1M0FxczdHVTh6UlNRUmQvSEl0YWdLSE9UVkFJdkk1bis2WGFIS0tWakpxTkFqTEh5\nUjhkaDgzZFcKcFNyc29STXJvZVlCRm1oQlBWK1FtQ1l4SGNvMk1LMUJBZ2Y1Z0FJVWJDUjdONkt4\ndTMrMURxaGxHbVlGd0s2OApJbTR5d0VWVDZBSHl0NWgzcDFKQ2xxL2FNNUlUbVA0L05xNVdFUCt0\nOXVYMFNaWTgvaUZVT09DUHpVclVmLzBKCk01YnJIcUJJWXMzQ2xQWnM3cUdZRXZEbVFEWG8rUVlM\nUjA1Q0JiZy90dHROd29mbktPanBYQ1NOZUVjTEYySHYKMkFDNS95ZzZpVTlSR1RLTGlxTm4rOE1G\nRGxHNmMwOTltQTNQV3orenlrajJYVFAvNVJRdVNISFMrdlgzcUVyOAozZkNyTGhQdVQzTnZTOFQw\nVlhCdDNPYTl0UVBPb2tUck5TZGxobEg4UFFTamY0dWlDZUoyNmYvTzFwSTZIOC9MCkNsdWdDOWhu\nbU8vVzJLVnBmTnR4dlhaeXFSWWRVdGUvbm9Yc2hwY2JIMEFhMmxQUFYxcnFiNElBZU1MUGFwMmQK\naG1rNTJzUFBDdVlVU2JNMjg1L2tTZ0VzRjI5QnNnSkltSURVcm14VGRpRnBPQWQrbFJGQUdvdXZ4\nQVVxaVFFYwpCQkFCQWdBR0JRSlBNd3NsQUFvSkVIRmhIVkkrMFY0SW5nUUgvaitZVUV3emVZdWZM\naTFIeGY2VC9nWFc1YTExCm81Mng2UTQ0NXFmTHJCcHpoSEwwVjJnbzF6cXN3YndQVEtEdXRjallC\nMDZmcjg4NVJMNEN1eTBuZXNuQVJaYUcKL1JZOUg1VWtFcU5WcWx0OHZzZjVEVWcvMXAyL2JIdnNG\nMGp5OGJ0WC9acFhUZUkzWndmZUw0OGdYNEpNRnhCZQpVN0E4MElCWmZ3WUVuV2dXeHZKUzFHS0c2\nU294UDRxUjRWaVpzcnJ2Y1E4ZFNrNENDaHBic01rbmM4eVl4eldrCktuZVMxMFMxKzBoVXdWT0tn\nbzlzSG93UnVKSGhwNXpEUDBsOXJTSkk2QzdMZmdnSGFhb0JzalhPbTNNNit3bGYKVEZsdmRKMDUw\nRVlMNlhHNmJFSFlVOFEyalF2U2JxQ3Y3RDNVcmkxTi9jNi8rNUs2SUpQL1p1d25YaktJUmdRUQpF\nUUlBQmdVQ1R6UlVad0FLQ1JEM0NnS1FiREFZRS92QUFKMFl6eFlxb3N3UjVMQjUxck9vR2diMVBz\nbzhDUUNnCmttd1ZJRGlPYnpZajF6MGdweVE0NVBDWEM0V0lSZ1FRRVFJQUJnVUNUelJVWndBS0NS\nRGNBN3FqMDBrcUt2dkEKQUowWGZRSVduNE1KbDRJV3hxeTVSZUtUL0ZaUm53Q2ZjZzBqV3NiUGhP\nbDI3c1ZOZldQanE1T1lFQkdJUmdRUQpFUUlBQmdVQ1R6UlV2Z0FLQ1JDYzdodHJCWnRaanF1QkFL\nQ1BGZkl0ZDhzWU5TNi84RUdlRHc5cGRCV0ozQUNnCnk2N083UnlGN0ljdCt5QWRPVEl1UjJWdGli\naUpBaHdFRUFFQ0FBWUZBazgwV3hNQUNna1FCbzNXdit4eUJ4RVEKWkJBQWo5VjMrUEJ5NjZVaTlG\nM2V3QnJ5YW92MGdsN09hcjdoalVrY3NXeHlJem5aQ2NOdXo3TDZpdHRIc3d0MAp1N084dVVhWWdi\neDVVVDlkYldRR3k0OVFwWGZNSEhlOFFQU3paQWsvcDZrTVgrdkN0bGEwbTUrb1NEV2RlWmhuCmlE\nOVpRNThoaG0wdklTdmxZcHY2K1hLYnJ2TDIwKzZ1MmhRUm9uRGlpRWRCWm5lTFdsK3ZOTjVaSDZG\nbjJyRlUKNTNVK2kvT1RuT2ltcVRMMUFDOHBXZ2RDTGhjZEZqSHhjcWlqcFBicWRScmRtTytTVkFY\ncGV2WGp3S2NSRDdXTgpJYVVGMFVqTDd4Wk9FY3UyVW95MzRPYXc2VUU5UC9jZXhNZThTdEpaOWw4\nRTFFaWM3RXpNcFo1eDRxbmwwb1QrCnlFaDdkSkNCemRsa1hRak9EQTEreDJPSW1Kdi9Yd2cwWHRW\nbmMydXF1aWlmd1BGR2RYRnpKcTlTZkhiTkVQMCsKR0FIZVNaZkdjUXhDMmRuVFFDMGpob01kajBP\ndG4vWUtRWm9HbGxpbUFOeTlOejlsZmFQU0U3b3ByOWxueHVJNwp1Wm9wT2NKUHNmbm9rRDJoZkNI\nLzBQOTJHU05hUmRDWFBvQUNHQUlrei9aSFE2YUcwQXVpcTZjOE83SXVSMHhFCldPVXljbmhmRlB2\nTjMxc1I0VHlUYjVtdU8zWjZLL1ZHakdzZjRjUndJLzBrREJBSkNPSUU5Ukk1VEt0NHN4eWMKMjVJ\nTHk3M1VubkZCdENsVERXa1I2SjNCblYwSWtrSVE4TVczQkRLTll1S0xFY1U4Sm94R0NaSGd5WFFC\nUHJuaQpEY3ZuQ09yM0w4ZDgvbzdwRXhhZFZiU2R6cXBZOTQ1WUpGaENDYko2OGxwODVjaUlSZ1FR\nRVFJQUJnVUNUeS8vCkhnQUtDUkM4S1JTMEZ4eXFTdjg0QUo5RmZvcnJaRUQ3YU5QTFBwaEx2dTZx\nZEdBbFBBQ2d5bllHamoyeWJ3OHoKSjZEbC9iSEp3WitxN0hTSVhnUVFFUWdBQmdVQ1R6SkU1d0FL\nQ1JCMWo2SXFZVDg2NUtzaEFQOVltN1prYllQRAphaHd5bFIxSzR3eUtZTUExTWlIRU5jWTF4N0xH\nK2c3RTBnRUFuUlI3QTFPZTROd0NuQ1BaU1pVdHMwS3BTWjRiCmppcHc1anMzM1k1NjNQT0lYZ1FR\nRVFnQUJnVUNUekx4dlFBS0NSQkZKam1JbTFOVlZtdHdBUDBmcjdwOE5XbmIKbWlwb3lsNjlxRFpi\nVXM1U0wrd2xvT2dyRXpBc05QMUNpZ0Q3QlZodm5FOTQvSWdSTnFZVFQ0OUdJYTQvcy9pWgptWkY5\nUkt6S2srOGhjYitKQWh3RUV3RUNBQVlGQWs4MGF6TUFDZ2tRclpiV3BzQWVQV2RTb3cvL1F4dEU5\nbnhQCkdET29zQVcyaVZKQWdkZmFvL0hUbGF3WGZNbTVCYUNUb2hEK2JrR2U2cmVsbDN1ZFlUVzA1\ncVU4NTFGUzNyNG4KV2F3R3BvTjhjUjlJZGJaSzRMdU1veUZIY0lVcVRzNjk0cDg2VkZmMWpWMW9v\nTFdDWVZLRnd6aHJ1SVgrZ2lMUQoxTnp1WktPejhBOU53ZVphaHlPTmtKQU01ZlliVFdrRXlDenFO\nbkI5aDFaYk5ZSnJrRXdjbXYwNm5XRC9KVzlVCkNFVkR5WUc2cFUvR3N1ak1vb2NEUFFlYkUvaDhX\nNkViWm9kWjlTSGNrQVB3T1VNRFdqMUdpbjJFYjNCRC9uU2MKVWIzU2xrMDBVc3h6QlA3dUlodHFr\nVThFRFkzcWtqUVVTNmNVTUdaVlR1RlNTTXMzMEVJRThWWUN4Q2k0UTdGNQoyVk02TFd6c0EzRFRk\nSDR5ZkRIYVFVYmlmUTlVM25zc3dOLzZ6a25tWm5FK1RHYnp2Z2QzUk1DM0hoWU5ZajJCClBhTVl2\nWVdGc2x0VjlwaGxlRC9PQWpSb2hScUVlQXVYSHRaZ2JuTGVBbXR5YlRkalJyUHhYY3dSd3lGMTRB\nTEIKdjA0N2Rvc2tReVJqYkhLcE4wVHpOa1lXM2RVQUR6UnJUWFVVaXRxZUpDellTS003WmxneHRw\ncFdEMHBKMjJpcApKYktiMWdTVENBUkNQVWhuZ1JONG5oOWdSOTNNT3MyMm1JT3AzTWpVT1ViWlNa\neHFKVTgvY0tVQXE4UzhkZm5MCnpmdklPNnBwK0ovNW1KOWdWdEluQ2xKMWFaYkVTVnhSR1EwMmVm\nVU5GZG95SSs5ZFAyNnV4bllRZmtNVjllemwKeTVYVUViREJCRUw1bzFKZnBrVTJuMktjSjRIdCtT\nYlpDbWFKQWh3RUV3RUNBQVlGQWs4MGNQY0FDZ2tRS1BVQgpsc1E0QXV0WkFoQUFncmhiVTQ5Mzgr\nWmc0UjRkd3BnREx5WDRYeE41MlE5ZnJkU2k4YjNSdzdqUXBSWDFRRUlZCk1pbWNodFRRNHZnQkc0\nd0JDWGVCWDZjMnRZaFVNOHNKSUtEUFA1SDQxQ0hVSFNWNlJnK0twQWJDMGYwNXN6aVIKU24zR3N2\ndTVNdWR2L0UwWTkrMjdBV3ltc0tZU2NIYm5aNUFnNCtWdFpHODA5Sm96alhYcDg2N3l6b1Vxb0pT\nOQp6OFNsTE1SdzRqbUpFdFJJL0V1a09VeGRXUFlvVHJsVUwvbkMyZXhheGdvY3RFTklKdU9aWjFS\nQnlNRVhBcVdhCmpmOUZZNktyaTVBcTFyRDFSd2NnYi9va2diZm5aTk1DN3d1MmNHWjgzUTRkMDU4\nd1p2V1FuU0FLa0JlS01GSlkKaXJnaVFEVnIxTEh4Zy9wdEpMQ1ovVXE0NWROUTNsQ09rVmNKZ2tR\nanI3MlMrZVJadjI1SG1zbWp1SkFxQVRZTgpvL1VKN1ZTVlVscUNoQ2ZEdEZTS1hOSHJEcFErQ0Vi\nVVdJdGFTUzJYSXVFMThvV0U3a21jdVBUejBMTU5oZmZhCnpGSFhtbEQxcGtpbUowMWlMZCtic0dx\nRk5NamdmZXA5bEM1Q3ZnWW9WdEZGVElWUFpQcmcwck5rSEdVamY4d2oKVHFaZjhjRHJ3OUREUDFH\nSU9ZUW05T3g0Sk1KVDdTallEZ3ZNNklPdWpQV0Nyd0cwYVpid3dwRzk5ZHZPbXpDRQpPYXZIYVpD\nOHNHSHl0UVpUZ0VzcFc3a2hTRXdWRHA1aE1EWlF1Qk9SM3dyRjM3ZEs1Rk5TNGo3TFVKWTJtdy84\nCnhoU1BaZHAxTm5wNlFsVWI5Vm5LUzllU2w0YThVc000Q2dDTDVqUXNYSUNxOHR1NCtXdWZxS3lK\nQWh3RUV3RUMKQUFZRkFrODBlTUVBQ2drUURtNmpqcE5IOEN4citnLytQYjlTalRPd1FKelV5bW92\naUhwV3YyRkZrcmF5T0lZMgpOUUpHUDRzWGVoODl2NDBFNURqanZNdnVQcEtSRklBc3YrTGd2Z1VV\nUEhYK2dVR3NBc3ltdXhVSUhVazE1T3lRCkVzMmo3TnhsTEdJTlNNMmsrTGJ4YXo4MEJ6dXFXeThI\nYWExbUdyTTZGMnRwUVNHckFvK1V1cnpKaStBNkhCaWIKTDJsazEraDlyZ2RLdXc0NnN4ZjJ1L00r\nU0taaTgzUnN4RkYxV2xJR1ZBbC9oRGt2aDBoZEx5WEh4NEZSRXc2bgpVTVJDQS9Ld3dyTUNLTDJ6\nM05xOWc3ZDJYd0VxZGdlUU5wSDZkNTd0bEpiTmF5bEt2Y0hOZ0wyL1Z2dnFKVXUwCkRQZFNKY0Zy\nWWN5bm1yWTBGV1NSREx5S0Mzc1NzOS91elFtWVhpbzZ5OTRVRVp0aTd2Z2s0cnJOclBIcFZudlIK\nUzJYMmNXKzdBdjUrL1FMb1NwbGZWK1pLcDE1bllPZC9BODQzM3ZDY0QrdjBxbkJpWTVuM2FPY0cz\nbTY3TEV6OQpvYTRzM2hPMVFyRWJSZHdOSHBEZzkzeTd5bnlCZzF1NEtvZDFEN2FQNFVHbjFIODRi\nU0ExY0E2eHlCeTRyakZaCjJLV2tTSHgvZXpQRHlCTmliYkVicnpQM3FxdktrVm1ON3NPc3JESG1x\neDFuay9EWWJLdWZndU5GWjdDQzJNbmUKbHNGVW9lUXpRaGUwc0JXYlhmSmZUajkzOWorN1dBOTZX\nOVN2bXF1aHp6SElBUHVIRzc2L1dGRS81M1M0Vno4bwo0ZXBTZkltS0UybkMwQkFSVGtGNkZVZjJu\nS0NNYWF1d0ZmQUxaeHhEVEw0TDVJR1BEMXNBV1R3YlI3RzkvWWR6CmlkQWpFTWZvbEpLSVJnUVNF\nUUlBQmdVQ1R6R0tLUUFLQ1JBYi9qTXlPTm9vUncwZUFKOW12bzFsS3lrVXBmd24KODJFTnVmNHJu\nZFN0VndDZldjVkJNUUw2cnlLZ1ZrMXVyVGFGYTBidWJEQ0pBaHdFRUFFQ0FBWUZBazgyVW1BQQpD\nZ2tRODZ4Sm1KclJNa1g5b0EvK0xwUUpnOVg5VDNXU2U3cllGbVlta1lGTjFTYzVmMEdlWm1KWDNu\nMGJERlcyCnZBRFM4YzB3bVpvZ2orUUxiMDNseExyd004MWxyb1oza2ViM2hYaWo0VFJzRU53Q094\nRUNYekF5dnVWZjhWNU8KUEZyQU93SGZJQzJHMGc2NHJiUTNUWEFqcGtxcUw4NGpmcjNaNXBjQjV5\nTEJoZUcrUldMR25tQVVaMlhLZnJScApqQTAzM0Y1MDRIU1JIQ091enRlVGFQdU1jRHJMN2pBMnJn\nbFNPdkVQWG9yc0VCRTU2dzZyckljQVRQTFV3SG11CnBtM3Yvd1duazhVN2ltOW5RdmVHOFNhRzNh\nK1VCbDdKZ0RRYmN3N1pOVmZtRUlCSUlpNytoSkxkYzZhTXh4UzkKZHFWQ3hDZzZBUGJkbHIxUkcv\nQ3QvVEhjWWkzVGNWTmtsNHpKM0FOY3dtS3BQaFVWb2l3QlJ4ZXJicko5c093OApKOHcvYXhpa0kz\nTXNqYjdaK3dTemhIN0lYaEFRMm02OG9CMnZ3RHJoS3FHREg2cTBpS3VSVnd5dWxyOWt2OWEvClFN\nVDlIZzJzM1dLV3RDa3pLZ09XcWIvSXhVRjE0QlVXamd1aVlnMCttZXRLN0tHNTc1RnFmQUd3YVF3\nYTdGRWoKUnEreStINGJqeVdvSk1TZUxHT0dlbm1Ed2N1SFJtZnlnZFZURWhqYzFLZUh4UUNvQ1M4\nek95V0tiNVZZTWt5YQpGVnZGeEN6dGFOc2NFSlFnTmorbkRuZEpZOHdKR3dJWm8vMzNuRVNzN1Nj\nWmpwRHoxYWdKU3o2Vzd1UlUycDdMClhUaUY1YmxwL2Z1Z2QyWk9DNkY1NnBLZlNsT3pDdExuZFNB\nM01HcnJnY3dSVzNKa0ZDdmo1dTRDTlBkb0FxdUoKQWh3RUVBRUNBQVlGQWs4M3Njc0FDZ2tRTTVZ\nVmlPSENHRVUzTUEvL2ZkWm1SaWZhSzc4Q2Fld3l5L1BzVzE4Mwp6d1g0L2V4ZTVrWnM3aEhJaEFF\ndFJDaDFqL3pVS0l4NUhUT3hZZ0FqNGY0L3F4a2hSbWxVQjdIeUsvMmtEc090CjdKWXl4b1hOU0pu\nZGpvcmIvVncvWUkyNUhWQ0J4ZVo3Yyt4THFwNlY4YThjQklSUXh0ZjRwMTFEbFNQd21BUjkKQ3Vx\nZDlrNnY4U1puQ3hNZkt5NUxaNnZKbTN3Tk8yUmRiL2JXakJNSHVyZXpNV3dnam1KRmNBdWo1S3dK\nenpuaAowcFovTms4UENJZE1tSGVVZmYzc0Znd2s5bzFIQ3VWcWZlV0VFazBaYWJhTzJudkd1dmpz\nb0ozSXFyQWE1MkErCmpZdzY1UGJsQUVtbWF1YVUwVlUyZW9DMHdsMjd6OUw4RmRqeGJpT0xNWGRL\nckxqNTNEcTZJbmxreGRYOSttU2wKb1dKelY2TlZWdjA4QUJBMWZobWVVYTNLNzFKb0R2MGdIbm1Y\ndjFTQWZLekNKUmdzbElVbDczSm5wc1NHNUJOcQpZZEtjWTdQMFlFVURmaW9DbnV6VFhCemlWUXdV\nOFdGWWhaT2JhOGhQaWZGRG5tWEJ1ZmtOSFlaenJzMktydmY0Ckg3NzB0bXg2V2N5WDlDTG5ZQXVZ\nSG5wcUpwVHg0V0xlaGNnaiswaFVkbnhia0tlYXNlZThrM0ppZXVDYVpUQTgKMjY0WGxZcjBQVmhO\nSlhtSDhoUkY0UDJhWG92QUJzL2dCTDhOUWVqMnh6YWF4OVRML01TaHpFWm82WU5GZ0pSegpnVHM3\nYjAwR3haNWxHVE1RczdMSzFlT01QUGt6YVRwVTIvNWY1cVhEbTU3YjJtRWVNWmswWEU1UG5VL0hK\nOVkyCnlSK0FJM2ZNalgwalYrSUNTNENKQWh3RUVnRUlBQVlGQWs4MzZvSUFDZ2tRWmpNQWlXeWQv\nN0xnbncvL2ZXaEkKeTlRN1pYTm13L0Y0UVptNi9yalJLQVpIN09ralZlcllpS2g1TXMwR21uZGYw\neVdBeE5kdHViaTZwN21IWk1iNQpaWEVqa2RYTWduQVF2aXFQMWxIbFdOUWJCQWNMam1uQlQyTzhq\nRTM1aU0yOU9zeC85V2dVekFBdEJCVHloQlE0CmxreXo5Yi9wLzFQQlNReE1aUnpBMGlmVmVGTzhU\nTGVxMGkwSEFReGJhOWMwUGozcTl6Q0JYekdrbE16eFMzRGQKS0p5NkRTNFA0Um41ZU0wRnQrRHIy\nTEtIa1BnYlpkQno1L21XbEZkRnZKQVcrUFBINWpZaEJrVkp6ZWpGN0UvWgpnOXJWR0tyT1VudUVp\nNnBhUi9yNXZPVDlUd2puRjJTZVgxVEwxNisxYVpLY01IL2FTQ2NIN2dtS0ZqankyRVovCjBpeC9G\neVMwQnZIRVE3RjhKd1pEcVRkVXNCL0Zzb09zblVLcTlrMUF6TlR0OGZQYklwcUtnUFU5OTNuZ3My\nQTMKaHpIbVJVUkJyWU9SM1NXeE9GTnJKbDdKUFJYWnJxVWhtM2RFamRZSVdiZ0Z4czhrQncxOXJy\nejlWOVRzM3FzYwpXcXpIbkNiMUVVMWozRGVabGVZNUd3SE51Sk54TmtRRFVxbC9yWmlVK244TlJk\nMkJlYUlGeTJOeVZ2ZzdzMmgvCkFnOWtzT2VJOG05VnVrcy92NVUrTDMxL3QraHA1YVNZVkc5WG1r\nZ0pTWnROT0Q1MzY1dTB5Q0pDa0xUUEFJSXQKUWJ3THcwejFRMnN0cUdEZzBKUUNoZEJzeDJ3MFFk\nbGloNWxhaXFYSFZzRDVzME5oY2NCNTA1SjkyUGtkdmYwbwpnZDJHTzNFUHF0bkJaQzBnUVRjenJz\neXdlZGNkVGxmajhGNUp0UjZKQWh3RUVBRUNBQVlGQWs4NEhFd0FDZ2tRClY1TU5xd3VHc0dlUUxB\nLy9UVkREZjkyUE1hUkgydVVsckt1aFpuZ0QyYVdUN2xnbWJLa3BiM1hYbEJNTlZQdkIKdHFISzIz\nSlZtK21YS0wwWTBiN3FkdHdHYWM2R3BSODhkcEVCVDlibUM0QmNQVk00UU94WFMwbDRpMzNtMXhS\nVgp4a3U0Q2RORWpHTGpMWjdBSTAxcVExeGVzR252Ync5UU1WdEhuQzRRN1lhdDdQYVBWbDcxTjNw\nYm96dTF4b2lkCkNCdHErY2twT1FPT2tjK3VUVDd1SnhOQW5mZmVSL2o2L3ZyNm42VzJpS1h4SmhF\nSDUxSUM2dVFTZ2tmVFgzODEKZkJCbjVkRHg2aGVocE9kQTREQTR0a3Y2QW9sVExmS0tRYnRRN2xw\najliejZIMFdSaFdxSDFVcTJsUnNRUmVySApqcTJqVzljODFNWDFkSmNWdmpXdkJDY1pDWFpLYjlz\nOTJTV1Fmbkk2aG9uQ3dZOHllbzNVTERsd05sUTQxck1OCmozcGlxQzE3ejRhVHBzazErS3R5aU9M\nSUh5RXhQSTZBYjBHb2pHeFBZVG1xRU92TUtEYXhHSGg4blVacU80bVQKbkNnWk5uNE1zK1FqQjY2\naDA1djlVREw4d3FTeHBzVlczU1VoeitORHNjRlJrZzdIQ09MUmF4Qjd3NHlkTDFVcApHRThPVk45\nMlhCa2oraGpwbW85ZXJVVjJ3SjY5LzNIajZiQkR2aitZUkhPcHF4RmdIMTkwdnAxQVZxV094WWVm\nCmU2UkxmZmhKOE9aaitmWDI4d1ZyTCtrRnpQRGZhZDJUVUM5U3J2K0RQQzYwbTl5T3ByWXc0MXhk\nOVYzQ1NPZEgKWFIyc3BYUHpTaGNKYnFNa0Rhb3pVV09mN0tYUDZTeWhubEw1c3diNlpWYzZVRkkv\nclZ4bHhVdS8zYWlKQWh3RQpFQUVDQUFZRkFrODdva1FBQ2drUTZIRGtHaCtuTzN0UUlBLy9YNlJv\nUGpzR1NaWWJuRHVpZksyU0d5M0tUV1NKCjFGQ1E3NmRjS3lGMUwrbjc1MHF5NktySFR1MjR1MXdN\naE1GL0tnMWJTWnFHRC95aDdoYUQ0L1V5Q3FMb0U0cXcKZDhPOGJVZVhxMXRYL0dYbDZhNHJiVG00\nZVNnRkpPUWtPUDRpVUpzZERxME1aak5Nb0dEVkxGZk9VVDBnQWN0bgphUVg1MUw2SWZDOTRzYkdu\nQ3dHOFhhZitNdk9MdHhneEFhc2RndjRTSGZhT3pKTUwwcTA4S29UZ0NzNEc3eHp4ClBjMzNIcUZP\nRHdPeHBKdGExMXBSTDJRcE82bWFWMHlidXBBSVdIKzcvL0N2dHo5aFFhS3ZJT2pnRFpHVmx0WUwK\nWVFkbkZvWUFwUGZJVGwwYmxUb0pjUXJvUzhReC9BL3JacXpMaW4wbk9PSFhEaEFmak9qME93dlBr\ndmh1TWFKUQpSVWxvZ1cvQ2VQaEJvUWFzaE5XSGJkcWpEd2FBcGV4VXg3OTZCYU5wMUk3T0xtbkdl\nTnRFcnFRTU5kYVFMNnFhClovV2w5bnRMV0Z6UWtRdmxLaERtV2Z0c2xqZnNSVmM2cm9KZ2E4YkxC\nbnBUZ2JBaHIybkNwMUY5QU9KRnhjY3IKVWxhSFV2MkhJZ0ZwV2dNc0hPMEIyQXcrc1dTVUR0bm54\nQnRXclNkZEFSOUU2WUpxU2RJeTlZaFIwdmVSL0FEQgpNV2RFVmF5TDFFK2JKV0UzdjFRRDBXUU1Z\nTkV2RHV2Q2t1Q29CY3pyb2IxWlpSVXBxWjlUWU1mL2lCOHlGWVNTCmxNMVJDM2VTSE5jd1h4SVV6\nZWc3b2lRMThuV2NPYkZvTXRzOEJZRUM5RlpsTkk2K2oxcHQ2cCt2MVhuckVDZlQKLzlMYng5M3Bt\nSG4rKzcrSVhnUVFFUWdBQmdVQ1R6VHlhZ0FLQ1JBWDduQ2JPRFJvdnp2eUFRQzlmaGVNLzh6Mgp5\nV0piaklmdGI1MXVyNXllZXJSS3lldytXc2VZa2ZqTnJBRDZBNFphMjRSbEJWNWVoa3RBc2pUeGQr\nTTl4N1RUCmcrRlpQbEVoejd1anFMcUlYZ1FRRVFnQUJnVUNUelR5ZVFBS0NSQTBtUEJhN3pNOGZz\nT2JBUUNnL3lDYlAraEgKRTlmSlJmemQwRDFzQWF1ak9ERStVZHZCWXVqNGZrSW1Jd0QvZExkSFpm\nV3I0TGt5VTkyOXNZTkRyRVg2VkwvUwphRmh1V1VxM1NvdU9haTJKQWh3RUVnRUNBQVlGQWs5QzA5\nMEFDZ2tRUjk1MlYwcExyaXhRSEEvN0JCSEt1eXJuCndIYTNmVFZlZTQzT0plRXZja2hxUzdoUG5H\nT0l1dHhETUZWZ0RuTndmWDN4V2FRZUlWNUxjaXU0ZDF0Y2g3aHAKTnVqeXRDVGZjdnhVcEhaY094\nQmhWL01YZnBwL09YNzJOckZrREExZCsyMFNwQkZVVHNTNUVOdGNGYUN3UTVZSwpQaEtWRjMzQnV1\nQmVCNHBFcW1FZ3VYVFZQZjBlZTUzYUZXeHhuZEZLU2Frc1A1aWg2dE1TS3RJWmN2ZGlvcksvClZN\nSEpUenppSThteVBnOEljK0UrOUU1QW14cWk4MDJqVXhDWkVteEc2U0VNNnVvbFZEb2ZKWTBOclJO\nRndDR1MKZlphN3dzK1I5S3VUWWNGTGMwdTBTbkdramdtZUJ4bnowU0dKd0RpWXM1a0ZRQldodXVk\nWnQ2d05ndWxOZW5OVgpoRnBTMEx5Ykc0a0ZqdHp5N1Ntc2diaDBFZTR4cG10Vm9QTHYya2w0QjQ1\nbFhwcGxlNXNDck13L0tHeHVFbVJwCnJzV2wvVTBrRFUweDBXT0kzNk1FZGFXRnNmVUdOdlBFWGgr\nWWNSTThUQS9nckNKaVU2UTg4SlVKVHZHcTRXbUUKVG11UUxjaDc4cHBLdXpVUjNwMWk0VTlSM1FI\nRGsvS1lkMzlURUQ1WS8xY1hiTFFOdy84d0MwS052bGZxZFVSawprU3NRaXZYUDJRalVrRDBYeGlD\nV3laZDg3L2dsVEVPK0VlU25GK3NLSUg2QTY5eG96S1Vpdnp0K2tGN0JxdVpXCkxzVktYSlF1QXdN\nak1aYUdOZW82OTBmcjRZWTc5NEpVQmw1Smd6NDdnUmt1ZVRYbHRJb2xOMks0eXk0M0NaTUMKdjFI\nTEFINlpBeU4vY3E2a01rNVZZcDY3QWdLanp2MlBtU0NJUmdRUUVRb0FCZ1VDVDBZbU5RQUtDUkMz\nVVJRSgovQlhiN0dzakFKMFJiekFkeFFnKzU2NG5sNmZvMi9SUkhlcmlkd0NnN1R4RGxkVGNody8v\nT0pCN3pRbWsyL3U5CkdqYUlSZ1FRRVFJQUJnVUNUMEVXNXdBS0NSQlc1LytLS0VESENHSVpBS0Q2\ndGRFVm83NUsvN3dZbTFPa0Q1Y2oKVXBUOTlnQ2d4bnpXb2JCbTJsMkJuVHFxbHI2c1Z4cG5mWjZJ\nUmdRUUVRSUFCZ1VDVDBFVzV3QUtDUkJqUXZMcwp2RnIydVdJWkFKOWhiZTAxZnJxMWl1d0RUVUVm\nbUJSdlJaSlVUUUNmVm5kRm1UZWZUWHRCdlgzUkxCMURsVVlHCmtyS0pBUndFRUFFQ0FBWUZBazlC\nRnVjQUNna1FaK2R5OElOUjRLOEFYUWY4Q2p3NGpqaGRkVHFMKzBMN1NVT00KKzBndGttamZNMlVN\nRWlNTUY2WkcyQ0kzMFBrODR2ZXllY09PN1VZVk5zYk92THJaWEkwZEs5dHpzMHlkRHZkMwo0emZx\nTVZLa0NielVIdVNQamVFMVY4MllMei9LNFhGalIrbnRUaTFNQmcyUFFtb2VELy9mL1NveUJMRFB0\nYTZHCjR3WkV4UC9aQkVKcUVoOTZaQmFIT05YVG9xRUxHcEJrM3VTSmNBUmI1THlNa1B4OFNBb25i\nQVd0RE1ubDM4N2wKS1Jvcjd3SnVRN2JibXlsQXNXN3BDc0Q3RllqZlNYMk5ONUxmeVZoM3krYU15\nVXpwVHpncWJaZVcvWlRJRFNFQwpZRS9WeDcrU0pwOFBPTG01Ryt5eU5XRnpSNjZ5YTdNRUQzdDlW\nemJKbFMrTlUvaW90eVpCMTNicTlkSCs5amZUCkxZa0JIQVFRQVFJQUJnVUNUMEVXNXdBS0NSQ296\naWltQVExdk9nQmRDQUNvU0ZJWEtvOGRGZUhnK24rYWdJWEEKL2ZTL2RLMkNYTHNueUlwajJ0ZS9l\nOG8wd01CaWdCcU8rc0lhU2pPU3B5eGIrK3VRcktyOXBYNzRVL2ZldVVqdQpHMTJodnVNOWZkcXRQ\nR1BIK3lKKy80KzcvVGVuTGpwYmxiMTdQZnNLeEl1WEg4Q3ZFREhOTWhEWnladEphV2VrCmVQM2U3\nZHU0bmNTWlhNRENlYXNNa0Fnbkg1NkF5cVp6SENOa3ZvSkFEMU5wQjdQYktWVVhNS2llck5obHJQ\nMS8KSXVqaWNrdVFMYmFUZDRIUUZUcEFhRGEzSW5WWThNZTVuRHpRdzVNdGpyVmthNjQxckJLTWVp\nVUVoYTYyeXo1eQpEWmZQaGRSWThKWUlFaU9mbmdNQVZ3MmRGUmFUMjZrUzJCMTVVc0xYSEVJZjBo\neVUxQXpnKy9SUjd2ZTBpRzRTCmlRSWNCQklCQ0FBR0JRSlBUQlgwQUFvSkVOR05Ua0dOWmFRdk5p\nRVAvaU5WbXJWc1BHM0NOekZoWWR1ZEpFQXEKdit2d1BSelZTYW9HR2xqK2RVSnRzMUFrR3BxVGZ0\nYXd1NWQ2aUprMTl4SEVlSW1hcDcxMnIvNDA3bGJDUS9QdQpVNmZEbGN5S2o1NmIyeFVaNkE3ZmVI\nb0NBQ01Yd09Cc0JiaFAzQ29IcVJDLzN4NTNwRFVuLzVlMEJrWDM3NVBFClZweldwam5kQVBnT1c1\nTlRsQzVDUE43bUhQK1dWWjJibXpoQ0twbmtmRUdWM3NwWVhBYnFxcW9OUW82LzJyWGgKMXlsVkxn\nYnZHMy96WW02bmxKbG1FSWx6V01kM0lGbU8zemJBM1UxTnUxdXI2VkRqNFp5UHFPOGo3TFVOZWQ1\nNApUS1pzQUNpS0F0Smo4cnlnWUN2QUlxRSs0ZnJPSkRobjJaOG42WkJCTDREK2xqZjNxblBXZXRB\nNS9VZUt5S3lQCmhUVDdhWFBZRFNneG1TeEN2WVU5QjBFYlVqdm1uVklLTk52cmU1TFJla1JYbmpX\nOW1jM1libTB4d2ZFR3FGZG0KMnlPczJzOWpWRENRcVd3bFkvYXFuaC9oM1NBeWxNYjUwOW5peTVE\nVkVKUkJwaDJFa1grcFUrT3J1Sk82aWlVVwpkYjFha3p1OWdITjF6NE04ZW8wclcrMHVKSmduMGRs\nOU4vTHBFaVIrSzRIbiszSWEzVFExdXNDMVEwRkhvV3JuCjZmZjBHZC9SZzJGbGFRU3IyWEl6U1R0\ndEJtbHFYbXpYd3N1MWh1UTdTMkVpQ0piNVNQOGZYRGgreGxZRm9uUzgKZHNLM2hablU5RUd0WGZQ\naGg3cWZ3QS9mT2ZIWnZoZUJhWFdzcXVKQmZPQ1BXUnRKSzJmbDMrRW00NElEQjhGRApwQi9OWEo5\nQUVpUGZQWVJYVWl4TmlRSWNCQk1CQWdBR0JRSlBYbml3QUFvSkVISmkyNzNRc2NpN1NWQVFBTlVC\nCk9aM21qVG9uUDZUK1hocTY5Nm9maWlUdnNzcXVHVzMwN0tJcGxHWUd0QkxMVkJ1UlJ4OTdMNjdn\ndUtDc0prTXYKK05USCtndXZBcXVsSWt3Smx1OE5kODVYZUJ2ZE9LQU9iZzI3MnRrWDkyWHNLRWdW\ndkRhYXZZYXpPckhrd2thMAo1NS85cUh2bkdVYndWYWNIRFBDVHBlb2hZWWNxSHdhRnVESGlDRmUr\nMEpoSE15TU02blJCMk55RUpoUnBHRHhuClpXWjJ5dUQ0TkhWdkZJSWp6SjRvSlVhY0xDS0h4Sld0\nQVpOVzB2L0VMa2FRU01lQldNMXNrbTIyUm1OYStxd1UKcDhBTEhraWxXek5zRjNCeTRBTDRBc09C\nUnAwOWJ6WnVTMm1IRkxiZDdqcWZwZitQRHlMMUhtZHk2REc2cjNvTwpTLzlJT1JNbUFHY3B5WGlw\nQ2pqcHFTbUNLdXlpTzZodjVOT1hjbG54LzhEQW5tcTd3QWdiTFBTQ3p5VUFlWXZCClROdy9FWUJQ\neGczSzcrVXBvOElpS3VaSGppWTdpVUZQUWRFZDJXaG1EL1VyRzVRdkg4Q25FMis1VlVpNmJKWXUK\nU0lQNjhSMTc1dGdWb0xJdEYzckRFUXNsdkQ5WTFiVU9pMnVrYmxzSDBOYzNGSlpsOHRNcWt5Yk5Q\nNkx4N2VPOAo3OEd3THhDNm5YQm1GUDlvcG5CbUtqeTdyd3AyM1N2QUlKbFlJeGZSOThCRnB3SkJL\na0dVQUFZTFR3UzdhMmhoCmtZejliK2Eybll4cEU3cGVCdi9YTXRoazg5bTl3c2pvTlU4Z00zMEh3\nL1NOUmQxWE5wT1dqbHJIaWJQZk1MNHIKSjBVV2sxTUlFb25GS041ZUJpZnQ1ODdQTHp1U3ZTZHJQ\nN25acDJCK2lRSWNCQk1CQWdBR0JRSlBkelkvQUFvSgpFRUFEdlhBQVNNaWlMWXdRQUp0RmZ1K1hx\nSHdBYjc5RmVhaEI2RVRGWnFYT3VDZXVkaWtXZ3M3Y2tuTjV4QWRKCkZRNEcxSHJRWUFhWDNjNEEr\nQWc5OUlhaWdRSVlGTmV6WmhyR0dVSzA3ay9LaC9CNUMwRFdSRFQyNUVETklzVTQKbHJRVGFIUGRE\ncUYrczBLcGo1b1Z4enpjQW5vVUM3K0NObjBJVU4xeEVhWnV0dG1SM3VFS3VYUStZcUttK0VGTAox\nOFdzYjJoUDdQSWFOdjcwUE9yTjNjVTM0QnBocWZ3eThuVmR5RHJENEJZdGIrM3d5aTh4QlVRekcv\nSGVOZjJlClZQZkhxUEVIUHBSMWRWRXczaXJ2UlVScko0R1FQL2RxR3VtQ3EyR1drbHJnVDh2UUla\nUnAvVy81QkJtUHY0TGwKaFluMFhrSlNkL2xqV0NIQ0M1cS8ycXRMbTVlRC9MWUhTWXNYZ1c2UWJq\nb0VaRDVGcWZQUGZ1RXk3UFQwaU8vegp0bElBNXRXVmV2NHB1MHRiZmhQSHFSbmlFU0RXcnVyengz\nNUhHRUdWR1IzRkFYZzM3UDQreFdBS1Q1UDkxSWJuCmZCQmZQeUI2TXoxODhxTkV5TVRLWjBzODNt\nZWpQbEJQMWM5SmY2UlBhMThRdktwNmRpQ3pkN1NMM0EwWFB3akoKa1NXSHBlWlRWbzEvN2dWOHo1\nRU5zWjNsTmxsL3cwVnB4ZUxoY3M3ZFVkdVdnZzkwZDJBbjZNcEU0WjVicGNqRApOS3Q1WnVkdTdE\ndi8vTTNpaG9RZzBlVnFsNk1KUURQakNBT1V6OWlQTjRwSFFpaWxhNVBiaHVCTTdWQUlvRkUvCjJp\nUFFXNTNkWXR6bnRSU3hyTDZHSFYvSVVDaVhGTkNCUWthalhuNk81bTZldURYT3NIS000ZE41V2Rx\nZmlRSWMKQkJNQkFnQUdCUUpQZHpaR0FBb0pFQWRHWFJqaWlwTVdmcUFRQUlCdTlaQ3JyM2tsNmpP\nSCtOekxyc2JBVFMyUgpIT0p3djBhUmN0Q0xaajBBTEV2SE15dElRYi9IY0UxMXRVNTJocmZGeTRR\nWGk1SGJ1bWRuRVMyOVEzUEY2MFdzCmJReDBUZkVpY1JiNDExTjNPbEJlWkp0UmVoWFIyYzJPL2ZY\nVnFOT0Evbk0wVVUxNGJSeG16dTVVWXNpZFhTN2MKMEVaTGxNNDdaWFhHZytIak05WGs0UVRhWWpk\nNlNQVjhkdHRDcll2L0FuYm9jR084Q0FLTEpjODFVcW91RGZHYgpMa3JmUDNTNWxtbXNwUnI5S1FO\nZUpTd3dLV0h5NWQydlVjVmFoS0hrWFBzVDJsanEzeFp4NFoyb2MzU1N2bTRECjlWWGZiNTlYRzBO\nbXI0b2ZpK3Irdi92WE10UFppTjI0SlNPSnVOYkVCTE9Dd0hlNDErUFM0OUcxTlEwc3B5SGoKZ1Zu\nVkt2MFVxTzVleWwxQjNYTzl3QXJpRlBBUEtvdDFuWlJNNVdXQlp1c1BRQ3JMUW8xeVJEWjcvblRm\nSjdISgplYUs1ZklmcWRwamNSSTdUMURXTjNtSktEUVc4MXFjemhBaW5FcUMxdWdQSjlkbGg0VG1i\nSVdoSWlndnlERU5mCmlzTCtSQlU2NXNvMTN6elhCcmNZVnRPL1I0a0RTcEd1dGhJN0t4TUJ0dGNt\nOXJaeTVySTgvNk5RUy9lTG5TVUsKclpheDRIcTBmZUtrQk0vWER0djM3b1BRaW9LM1YycEFUOUJm\nQzhzd0JJZmhNejlXYkVhNFAvY3ZIVE5sczdIVgp5azdwTFRaYVM0dERTTUdSUDJCb2kwWjdUd1Qw\nNE5EQWc0UWJpbGhuY203bEFjOGJLaEx1WFJXclpYeVFaa24vCmk3RElzYlF3MEdXQWdDdE1pUUlj\nQkJBQkFnQUdCUUpQZmYyaUFBb0pFSlpRaWM1cmxmaUNRT1FRQUljaEFDTGUKQWU4K1dXcXI3U2RW\nY1Y0TzVjS1F2UGlncitITXNnWTYwd0gycUVSWlJIZWQzZERybXBTSnFnYWZPSjVtV0IregpJYlBp\nTHI5d1FMcGM1aUxRZTc4bVNNQ2puTDdaclJXNlZGM1BJKzloQS9WbGxFbmZKaVFTc21GbE9lUWJJ\nNC8xClNoRlNLdU93ZTJNcGVuOWRUZHlpbVZ6ZEQrRmdOb1MwRGpUQkdhUERKbVF3UWt4eUNDMzBX\nTnNReno2WUprTDIKNGFUM0lkSElEOWdrZXE2UDV5RU5uRFh4Q2FIc0ZHb0JocitPVkZjYUJUVmw3\nYkg2bjJpSzlQeXEyRlJhcDdacAp1NmV6TjRoeWtaN1NKUXk1TFRORW8zaHdsc3BYNUlQa2RZUlNs\nTnBDbGhTQThUN3NhYWVDbUlkTGdiU2FtNXFQCmxsNmltRG02Tm1BVnZDajIrTGMzQXFzRlVtc25T\nMTJNTk5mQXJlUkJrZ05Mb0szU21CRUhDVi8reEdOdzR2a0cKSDVVQjMxbS9ra1QxODRhMHFGRC9y\neTNzL1d3endEOGE4UUU2OHFCZW4vWjl1Mm5MS0huNkN1bmdTVmZySGtsKwpBR1lwcG9KZE5PbmF4\nRUtXKyt2SEo5Nzk4V3J1RWVLTEV3SHo2emIvM0VSaVBEOU5kMmZaNFc0S3piTUh2RkdTCmpSejJk\nbS9LVWZkd0VZYVRINjVVL2kxMlRvVmVSbU1RdzJyVDhYdE0wRE9DNG9QYURrcTdLQ3MxbWh2bjBU\ndGQKWDdhTmRkZTRRYWRCTm9ONDlGTmNOZytnVUhoaFNnc3NobnkzZzlZaGpqd1pZdWxZVWMyNTJZ\nNkl2eDUycVgxQQo2ZHZha2NkdnRsZURlWmxuQ0Z5OVRPaWVpNjRaU0JKd0hsRUlpRVlFRWhFQ0FB\nWUZBaytHQk9nQUNna1E2QTh0Ck1FckJ3YXVlQUFDZ3hyckNqdUl4WllPeFNTUnR1OExPdUtWcDZp\nRUFuakM5UDZBNkJ3cm4yZk1GUEFMN29ZR00KZXVNcWlRRWNCQkFCQWdBR0JRSlA0a0VrQUFvSkVG\nV25CY2dBYmZKU1Zad0gvQXExZU4zZ3ZNdmFiYlhGaTNucQpiR0tUTGxZaFd6VTAwV0NEMU5rTmdM\nNm1TYXdNanBKU3FnY2ZqQTJPN00rQlZ1U3E4b043TlJENzlDQk1qYUJTClZXSGZkM2JoOW43amhz\ndCtzckxRRnhBQy9rbjc4eTM3VzJ3Y3RJSVVTb1lUU2t6Vzhnald1bk9jLzc2UXd3ZjQKYWI5akVY\nemE2d240VnRHSW5jbnZkRHBjckNxVDhRNERIbUpzSU1XcUVjWGJMMTNVYnpUaFN0VWp2dEJ0RzJR\nSwpvUnBmeFZIY0hxa0R4Q1R5MDF0dFBsZGNIbklJeFBIbHZlNTA0am45dnNQOC92SkliQW5VdTQ3\na1RvSzQrcnh5CjJtV3RjeU9kRWRDRjJVbTVHaHl0OUxZUTBEd01uTURraVdFeTlZbFRQSEFpUGVq\nNGZ1Z0oydzAxZmY4MTFIS3oKMEk2SkFoMEVFQUVDQUFZRkFrL3l4eVlBQ2drUTBnNk5tMXF0YzNt\nSFZoQUJBWHg4ajNHaEpNVDVsOG9SMmQzWQpDeHQ2SHk5NjRxaElMK2F6RWRZVitrOVdDQXl4SzFk\nckx3L0tvU0taTHl5R0ZGR2FTUFJuM0VQdUhnaDQxK0xpCjd5bDlsWk9qdUlUNnBiNWorM0cycUpT\nanRQdXRqS1FIdUN3VUxwcFF2TmF6ek45allVMEdRSWNOTnFDdExwYTMKa1N5elJBR3I2Z2JXZElj\nUXNiNm5NeE1PUVFrK1czWWVONHhmQTgzc2VRTHFWUThZcXArOVhVeHRDczdXclhSSQo0Mnh2cUtu\nTEt3RjN4cWluN1lQUVRjd1R1Smp5eHN6NEFDanlMZTZJaFZLZ0tpUXZKVHVjUzhHK05nNDhMWWNh\nCjRPUEdWeUptQTZNZnZaMmxvNkpVQmxvTm51azZndk95cjVGbEZaVGxVb1ZzSHlPenVlTTNPRjIw\nYUhQcnh6aUMKWW9sTnJ2ZG5JNWQyOGM3K1FWUHlKbEFFOUNqVVp4bHF2QkNOVWFzTmpHc3lYeVRl\nbEJ5SUZobmdVSWNsYUpXTwpNT3dmRVlVUk9vdkxUVEpybTRBcGx4anJVMkdVM1hEREJZMFdveGRX\ndkJiOVBJS1JyZWx0TzBxclZnY3I0MU9wCnJOT3BYVjlDL1ZoSzYxbmttWDhSKzRNWE90cW9OOHJo\nY3lxWFEyMVNDekNJNUxKNGpManhEY3JydU12Y1lCWFQKc1l4T1lkcENNTHkvUERaZEFvQjFVWDlI\nZXE3WFdKYkRhL2s1a3Q3d2JkeEtucTFGLzY0Nm1RNERYdFVCRHJZYwpiOU1GWDUyQzVHbmdQZkh1\nK3Y2VlNaTUVaSUdLQzBGODhPSTMwckNibW8vZEEyYzd2VFNiU29HUmJJdTJXNHpICjZYSDhHbjlj\nOE5Uc0NTMkhIMHIzaUZlWGlRSWNCQk1CQWdBR0JRSlBkelowQUFvSkVNQlVzRGlXQjdMdS9QUVEK\nQU05Qi9VUkZaV21uaEhvUkFTSGxBUktpRmFtSm95WnVJdVlJWTNHQnR2OC9QQUl4eFh1ZUp4WFpT\nZWlJemtVVQpIVmlqWEhNSm5Ba3llKzNLc3VQSzJrSEVkWEdsWkdaSXhGNi8wUm55SFVmNldYV3BX\nbXYxTGtjcXhHS3I5NHFJCklnQ2d0VXFOUGROOFlieU1SSGY3aHVxYlZqN1pXejV5WWJ4UUErUytV\nNVExb1FHck5yaWpNclBONGRaT3V3NXUKQ25vVW1PUE41a2tXTmk2WXFMUldJZnY0bTVCUE1BYVM3\neDEzMWhRYTg0eHRUVXkxT0VJaXIxWHEzRmVjV1FETQpRRENjYlY0ZWIvN3JsODF5OFhLVkRHdFVG\nbGxOUFVJN0o4UE9JVEl6SzVxTHlpcjVEMkY4MGk2dzJLb25wdXc4CjFxYTdUQWNCcjVOYnFQZDQr\nTEl5cFFIbGVVVnpxNnZyVloyZGpJYTRWMmhsN291K0x3K0grejh3WmtRMzExZ2IKVFFCSVh0eVZh\nc1FnUWNDRWx5dGRINSsvMm12R0FrdmNJY3MyZ09Pb2xlN3JkRGhzVm42c3B6Tks3U0dqVXFBUgpQ\nekhJYVZjb2lVdVpDdzFGYUpydTIxTTAzZFM4aGNGTWlhMGljUSsxQkVrUVZjUlhXS09odUlWemRv\nTmc4S2c4CjlXY3A4aVRZUUN4UmdBcW9tRk1BUE9nL29xOGdUeEhtZDg1U1ZhT0dObGFITkFHUy9p\neDBtMXlBT3lWazBrdzUKeXQ2ZWp3dlNWNjlROVFUM2d2NUdjTnJuK0h3VmduelplTFRXREttcWlk\nZThLYjZOTm1na2x6U0V5VHVLdVYyUQo0ZnYzZVhPeldHZ1JxZEFLVjlTM3R3bVhML1FmZXh2ZEJy\nTEdVUFNlNFZhSWlRRWNCQk1CQWdBR0JRSlFFcmx3CkFBb0pFSEFZMExKSzdKY3dhU3NIL2lySFJF\nVHRVZXladTkwaGtIUk5jM2xxdEYyS1J0WjJRMnVPU3JmTi94QTQKbzVMc1htOXZzQXVrL21rakIx\ndkVWSkV6MHV6cFlLd2ttdzdUUzlHSzMraVEzallFalN6eWY0c2Rtb2pCakxLMQpDZGxvTXpiSFJ4\ncDlJcGExc2J6RWRHV2VnbnZ5a3M5S0YyZmhOckMvSTFPeEV5OWRoZnNRa2p2c3JmNndVelRtCnc4\nS3hvMEIxYlliRUlJZXRWZWhxaXMxVjFKdlNMbDR4ejBXVFZ6TmpjRWpvRnpzaHJpVmJMQS80ejZl\ncVIvSFYKaWtGS2tPQ2RZOG9VNVM5Z3czR0R6ejc2MDdGZDJ0NDhBeVZVd2VKNjQvY1Y5YzhGKzl0\ndkxUNUtpWDRKUzdLegpkdk5vbmg2SVIxcU5CVmFDT0JQOEtRdm1WT0RHK0RvVVZscHZXcUd4RGt5\nSkFSd0VFQUVDQUFZRkFsQ2t0YUVBCkNna1FDTFdnOG1OdGFPbFQzZ2YvUjZiZFdVY3dRc29TYmF3\ndmgxM0hFR25oaGxJcGpPZnVLNG5TdndKbXRFeGIKNXNZTGV2Q0R4dlV6dDBDeDJ2a012UnFEZjhy\nRnlxNFRVSGNNeHZ6cXg0NnBaN1Y5RzA1V1NUaHFFbVdIVUM0VQpNNUdEY2RtOWVJYXJoczlseksx\nVkRrd1Q3QnkvWnBxY0hqUjR4eDdFQmVwSUt0L1lBNStjU0hDN2RoUlpOTDBGCkczMUJkakVmMWw0\nK3lyUVY5cUpYajdsYUlvY3dFZ2FjMWREeVJnSjRPN3lONWd4d0xHdEFyclcvRC9hcnlpalAKMDJ2\ndmJKU0hMYllTeEozUDFGV090OFRyWEVsUU5lM051SlN2MW5HYlNBK1JiUkJaaHFMSWdPY3RNL3lq\nK1NvOQpIQTNiZjFnK2NudXdJK0VqS040MVN2ZkRYOEMvRk9pLy9XUittMFVVd29rQkhBUVFBUUlB\nQmdVQ1VTYWZoUUFLCkNSREx3bUU4MTBWeUs2ZlBCLzlmUy9IeWFUM1crbXNINFZTbmE2SW5ad2J1\nZWNSM0NkZnc5NXlCQlhvM2ZQN1gKMmdUbVVrS0Jacm4zR3JCblJ0ZVFwMS81RG5SZHBrcHhJTm11\nNVg3WENpNy9JZSsyREI5dG1rY0hoK3d4U2kyQwpsSUw1MW44NjNVbDJLdjczVldpeXNmWlp2Tytw\nL3ZrZ2lieVVjZzRSeEdtYWt0Z0NydWEzbHN1V09RUEFtZGl5CkpYNVhLeTVteHh1R0dwZFgrN2kx\nUnJBY0ErUTB6ZFBYbGJYNGZLaW1hRXU0eFhDemhDNFVxdXJyUDhmUEJzam4KNDRMdlZLTzB6QzVR\nVi9WVWFJZm51TXlYbGJPOEhpbWJkSVlxbUtLRGNHLyt2SExBemladWRPbnJUdGplbDBlVwphODBM\nbFEzRTlXeUY1VThsWTJ4V0UwV0tySWlkempVSWdGTjhIWnZYaVFFY0JCQUJBZ0FHQlFKUXRSajZB\nQW9KCkVDYURrbE91dWlkWUVla0lBTG0vbkI0c1JGeGZVUmo2TXFpckgwWDB5eTBYdFBtSVpVUDVh\nYXhtRjBCRmxmTTMKR2JtSzhZWEhzbHZxbUZJUHFic0ZVOHZJUEY5RmZrajdRem5WYTdFQzA2OUNX\nOWtBWEFGNGVERENsOFpJZ0s3TAplY0t1TE9vZkw2U1RmcERyamxza3pmbWE3ZTNVNnlkdHlnb0pH\nMW5zc1ZNdWUxYllLQjVsUWVLcTlMa3ZCMTJ2CndrckVFVGt6Uk5WTGJiYVVyMEltME9sK2o5bWpR\naUpSOGptQm93elhad0kxb1NLc2tIcnlBT2VFdHdZeVRWeEoKcTN1TG5DcGtwNkZYYjJ3RWJiNXFo\nTFdraktUcXMxR3Jnckp5Mk8xSVNKVlhiRlJsa0ZWT3JSV2FuTlN0TGwxawo0TjE3dTZHMHJ2OGN1\nVTdURnRIcFN0dGpWTXdJc2F0YXFsV2JQbytKQWh3RUVBRUNBQVlGQWxDMThkUUFDZ2tRClNVOGh1\nZFo4VFNIMG5BLytMbUUyS2lRK3BMeHNZcWs2VHptcVB2ZVZlK2JjUis5dEtIOHpyMzNkYWd1bVln\nOWcKUUEzZmhkVndRaWx1THpESCtSR3FQU3FLK1ZQM0Y1RkZVYW9haldISkZFUzhkZEdqdUNKeHpT\nK2NtZTJ2OGJncwpEUmczM1ZnM0pnMmZZZThSYlU4MElyTThUeE9vM1hia2xaNjBMMlJjL1BrSkV2\nRmV0eFRkNkNjQ0JXR3N6TVczCkZ4VUdNQlE1dENFdnhoUVNqRGI5bjNORzh4MnMwYmJONkZrelp0\nSnRZYUtKbWJtVmxaRU9UU3hLb1JxcTM5ZUMKSVRDTEV6OUwwdVBXQ2cvaDlyMkFXdGZmc05rUkZz\nai9lV21HODJDNy80NGVnS1hKMDNHTVY2QVQ2OFN5dUM4WApWYXhxamwzMTlyK0l1dHQ4N1pMZXR1\nbHZkNnYzclduYVYvM0xHVFU2OWFhRnV0SmZhbFFsTkdoSVdUZ0NiaVYyCk1mejRTbTZKNGpVQjB3\nSDhwY2ovM2VodXNhbGkydTZsZEc5NzVmdERtZ3Q4TTIxaVNxQXoraFFLOExKUTdkRUQKeVNLVkdM\nL01hTUpMTDg4NUw2SDNRZjRwZXRXTUwvR01oMDZiclBTM01NRkNiaUtaeGtjNXRiREtEbFZXZjhX\ncgpwZEJ6bnRSN0hYNk91c0kxaGtmcmI0RlR0dndmSUk0NFQ0eDZOcno3Y0d0WUYwOTd2dkx1ai9O\neTBocXJYMzRpCnVKWS8wVU44dllRM3pFSTdwNE8wSmZVbTFoalp0bmdoNTNEdUk3VmYxdEdWWE9u\nZFFqMWMrRHFPYUxqZlBtUjgKOEV4bVoyNEpBaTVSZFlCejMvL3hyQ0M2eHR6dHgreFdxL0tEcTR5\nN1ptd1pNWmM3MndIQnJub3ZQNzZKQWh3RQpFQUVDQUFZRkFsQzE5aGdBQ2drUVNVOGh1ZFo4VFNG\nWnl3Ly9Xa2J1ODdOb0hTWStLVkZrdXRUUWpScS9HcUlLCjlQY3B0MXljTDlORmoyUWVmb2tHb0pk\nTGkxcmNwZUVZaktJbUJWRCtjLzRyVThoQXVpZCtYTG5vMEJaanZsVVIKejBWcE0yTVl6L1ltUnQw\nNFd4RFNnbHBsWktHS3Izb3RGZVBTVDNuVk0vZGtEWEhaVEk1NHVaZitxU2pZci96Qgp4L0ZsYVl4\nRkd3amp0c2YyUW8rSEcxd2s0d0lOQ1V5QkFlYW9JdlYvakd2MURkcFpRQkxBU1JBQmc1RVIra2JK\nCnNRZkR1Q3BLNTBGUnlSRUYwcWdjUms3aVJnVkZCT1VWeDZwVzRyalFRZDZUb3cxZ2NNdmhuMUM0\nbHpTUzhQSDgKMTdBWXEza21LYXRtNUJ0NDcrVTBpNU51UGsvQmp3QXJ4WHZZUnprYVJtSW4xMUNs\nOTRqdWRvNFZHeVl4RWpqSAovRnh4YXRNTkJ4TXhrMzlwNlRieWdWZE1PMmwxK1ZpQkFYZnVjdkFU\nbmFOa0ZqRGpaZDQyalFobzFaTnk5RysrCi95cmFSejQxRis1NTJldnE0UThGMWo5WVBlYmVZS25R\nTENCZEJlakdFNUEzUXRBYmpFWkJmcHhBV2JvSElGeFcKeEUzM0NJMXBZRHJ1QWtBQ00rS1djakIy\nbjM1Q2JVMitQb2dNQ3pWTTB3K0hPRUFibzhoUHV1dTdFWDVqZS95MQpycHRURDNjajBsVGMyVE81\ncFFNaUxDZXpTRENmc0RXNGRLakFCUkJLODhMbHpwenExWWFIVXdpN2I1WjFtRWJmCnNRcllFczhU\neE15TGs0blYyNHlnWWRrbnFhMEpRek8yb3g1dU5ucncrRGlsNm5lc3d4UGhBZkE4UWVVUmt4ZzEK\nWmRBN0hva3BaSkZqTG1TSkFod0VFd0VDQUFZRkFsQzFBQXdBQ2drUXROSmFIcG1abHBkU2VCQUFs\nbi9ySkVaSgpFKzU3UEJhb1VzNElnR0tGcFRobFlCMi9KM0NDUFhOTGJlS1BLcHZ0cEVvUFVLVEFZ\nUUpTZmY3QmEydnd1aGpGCjBCTytlcmpuZTZ4WUIvdUJXc2dmcjZNZXZPYlJBVTlLek1Gbmh3Zi81\nTDFnSkZvd1p1ekQyUFVseTZnT0FFUE8KaHo1VHRpUHp6RGNEc3FiMTJMY1NwVmEra3JLREVValZv\nbUxoeENBdUNWNEVUVVJsemRFYlU2MW90OUNvZisxbgprWkJuTXZUNGI5blluTkt6Uk5nUDA5UkRo\nTzUxWUNFZzE5RjZTZ0NZQ1hCeEFTSnczUW1xS2dMa1hEV1BsZHVMCllKNEhqeWpWOG96emxCVVc1\nZTdHS1ZFeHlGaGQ2bHgvZC9hVU82NG1xVEVRMWFmRUsyODJsaUo2aExBbEN0VUUKbXViNjlIY1dM\ncHRiUm42SEgvZ0ZZcDJxalRiM0RNVWhINzNydm5jOWVRVWNRaHpSREErV0lRUTN6d0ZpckUrTgpI\nYS9XbWxEMXN6MEdqY25tTGNucHBTL2lnZnNwVE5sbGUxKzYrRm1zTXMxRnhmL1hrcTdxTytZRjQv\nS0ptb2FhCkc3RmdsZ2UvYThJdE5iRjdrSjQ4eWprSFJ5S1MrUWxqTkMzQzdLOTJCWjBGN2RyY1pi\nSzZlZTY0T2JDQ2RvZnoKanpHUXRWYVFWd3hsazhNUmM0RVo3QUMrMTJhWFBnSWw4UTZBdUZ6Kysv\nbTJMb01uL01oTU5tLzBaU1ZxdGp0VQowaG1yVGJFRXAwWjF4Rm5lVHo1a1BCQTZxTm1jNEhKcW9s\nZ3djbjNaNTdYaW50YU5XTS8zd1VyeUwxaUxuK1JvCjFEbXFwbjhydjVJWE4zSzBEWk9YZjFyVEps\nMHBlWDQwTWFPSkFpQUVFQUVDQUFvRkFsRXBLMDhEQlFGNEFBb0oKRUk5Q0xLRTNKU1BnKzNRUC9q\nbkpPK2JBNFg1a1crdUMvK1lUVTFTTno1Y2RqbHNicWt6UjlrSUlmZGpyc2VUYgoyaElQcW1yWU9I\nWWJZRlBZcWU2QUhyOVBySDJaTkVnM0lTbTlZUFNUMlR0dkhQeXowMkJMemxDMm04b25wYjV4CnYz\nOUk2RzFVYXNBZkRla0I3YmxiTUliUGdHZE16ZGQvSGx5MUFaZWpGZExsLzhPUCtPeWMyZkpmZGVt\nVWxjem4KVzM4czJRa0JOaTh4aWRocWJMeHY5TEt0Tzh2akRkeUp2cGRROXhKc0lTTHN3VDNOTytm\nSzZ0NFlNZGRKNXlxSwpMcExDbzgyS0FQSDVzRlRXVkQxWmxNYzA3WGVvaGVxNFNDUS9idDBWY2Vl\nT1Q1dkM1QWVpR1BuTmVVZzZublNLCjhKdCtFajNZL1kyWEx0RjZPV1hSYk5WZFlBcUhjZ0tYblY0\ncGpuSmxDbmlxS0NSa1dWRXNqVjc5MG9LcDVPK3UKUHJTaUJaRGYwem56WkZQdkhESE5yYTdOVVFu\nUjd1TFRVNVdNY25ydW5SbDIySG12WS91RVZKaVR0aEx0RU5STQozd1NLVVRGQjFIb3NCb3BKUEI2\nOE4rL2R1WVJiWkJFQ052OGFMM08wT1F0b1F1LzRDQmFIWmRJQ0p5cnBoS2xmCmMvNUJ1aGxzWHFy\neGw2NFlKSzRoVmhpTkYvNzRCRGNYUVRNWitLRkxZSGJYWWZwZXhKMncrQW8zdWJSdVZqWm4KWnhY\nSDdLUjNLRG80VUk2SzNabXdUV1ZONVQ2OUdyVVUwb3NGMC9uRktGdDBKczUrM0Z6RDlkUU1NR21W\nYU9MSQpUTVJ5aFVrR00xNHZFOFRZdTBJTkpiL1NDcjlWemp4WFhjUGZEc3Q5UlVWUVkyM01EbGgz\nRElSNWlPQkRpUUVjCkJCQUJBZ0FHQlFKUlR5U1NBQW9KRUlTeTByVjdPT1hCb0FZSUFJSmU1Z2Y3\ndWF6TjlsUU91VEFhb1lUOEN2R1AKMzNhVXRNWHcydnUwOVlxM0plM2Qya2c2dTdHU3JaRDcwamZR\nYVhOR3V5ajlYbGxkZktDOStXbWFkSWVWWEJ4VwovVnl4Mi9SVFdtdlovRW1OWERqWDBkcnVQckZ0\nVkRaWVRkUUxrOE9GQ1EyTm1zemRZNXlwQjk2KzN2RHNRNXJsCjl6WTQ4TzAvT2pqL3gxL0szYTNN\nOXYwWVpIL1ZYRU53dzN5bUhmbm5JOWoyYzJrRVNpdUpzYmh3RW1XWENNVEEKbitvQ1FmVnVJL1Zm\nakYxRTRWZjVBNnlpQmM2c29ESzBkQm1hQ1dmYTdneFc2RUxyU3NSWHlsRjlIMnczN3BDSwpUUWFt\nN0hBVklkRFZrcDNEa2R5Y0NJUm9lVUl6ZEJ5MUJGZm5iN285UWVNZWdFZWpaalhLM1FJbG9UZUpB\nUndFCkV3RUNBQVlGQWxHelkzSUFDZ2tRRXRkV3djOE9hb0h1dlFmOUZ4NjJuNE5yWEllbHE5TTJa\nRXgwYXhMUCtPYWwKVXZSTWtIQkNpRjFxMWF0MlJNY2RzamprWkN1Ri9maTRONnp1L1l2S0V5UENE\nM3VsMVpENmhKLzR4RFAxU2drSgp2aHN0VkJKR1dONllRSUVGUjZOTHNVdFBnYnJ6ck1VOVo1WEZP\naEtha0dvVnRiSHMxSDQvdEJJL2Ricm1KbUIxClNHZTJZUnY3ekFmQ1YwRlJBZDFpbWZ0Qnh0eFlK\nRFJXQWhicGd1YWc0bWZyQjlpT2JGU0d3ckt1QWJ5MEFqZisKT0R0U2gwNWNSSE94cjhGUmVKZUVi\ndTkrbllNby9lN1QzYzVYS3VCL2NDZ1JwRkZuMERoemVhQi92QVQremMwdgpCcUVVUXc1TXdnS3lV\nSU84bjNnOFRBMnI3akRyRGtnN1JsZXRJUjh5RmVvajRLVkJEN0NMcW90bGJJaGVCQkFSCkNBQUdC\nUUpTZm9QaUFBb0pFS1FBbmRLY2MraHFZeVVCQUsrdFc1K2RZMkJPTTFuQmgvdjUzU3RqZVhWTXV3\ndEsKbHF3c3N6MkV3dWcyQVA5QUV1bkNUQmYwelRxVlF4VUJILzlhc2NkR1ZIZW93b3VDZzQyTkFx\nYnBnb2tCSEFRUQpBUUlBQmdVQ1VvQWRtZ0FLQ1JERkl4TlBGNzliT1R6NUIvOUlFNUtoQ2pZenps\nNHphYWdMdUptUmdvRHNKMGJzCkh0c2kyU3NoR242cXl5aHU0U0tLSmc0bm56ZHNrUUNka3RHVnZm\neFhBaWJXZWpvRXhzdjRLMjA3eDRzSTQxeDMKbzhaMlcvZlJxajN2NzVrYytYak0xOXoxQzJWdkJa\naVlFVHJNSDMwWlFFbTVSY1pqbTVuYXRPbm03aktCYkNmdwoyc2hkQmNuL3RNNnQwZlJsd3BYNzZr\nMXQrOVgyaEZ4bGZ2Z0U0RVFNR2VXWTFVaTU5alU4dnpHZUU4Z3VOMGp1CkpQNGZwcUduaHU5R2FC\nM1MySmJqSGY1cHVDWi9mbW9xZGxNVnVyTUljeXNRcTI1YmV6MmpGZk1meXQyekF3eTYKMHRLcEQz\nZUpyWmZtcHZkWHNZbHB3eDBMeFVseVZsNm9NbUlEdzhaQU1QaEhpRjZYczNHWURJTzhpUUljQkJB\nQgpBZ0FHQlFKU3dZMktBQW9KRUZNMzQ3ZGczc0YvcEhvUUFLZjJuSkZKNHpQSUxwbHk5d1pvWVRk\nUUNXLy94M1dXCkZyVmRjTmt5VEJkTDQ0R2RSZEpycjdTTXJjN0xHRTlxbnNzOVpBNjJyakR3aEpw\nY2tTQUpkUUhLQXFTTFpxMGwKR29Vek5xYWoxUkNrUkRHdzZRVHlzcVFKYWMxRTZmU1ptSlRUclhi\nZWlSc2NsOUg4Y0s4eTRJWEk0d25CS2hKNAozMkM3anNacDVXblgwUjBmdHJ0a2xCL0x5dXRYam8w\nZ3k4Z3FnTTF4Sktya1EwN3RtWVpMdmQrUEJKaVI5ZThCCk1JaTZFVkpLeFIvSEtoczB4TE9UNVJN\nT3E0aHlpZWw0azFodnF3d3VjeDR0QTM2Qnp3OW0wS2NRQlNoL0N1VHQKSThZVUxKTFlYSTNyRnk5\nYWMrQVFkdTlkb2xwWmdJQVFLYk9CV0I5S2FqSHBIYjdSSk95RmVFTlR0MGhFU213LwpnTFppTUNq\nUDl2WFVwblBvUk5LN25MU04rdHNZeVZmM1RPY3B5eUM1L0RIZWJWU3ZnU3ltUGJsRTVBSHJ2K2pt\nCktoeFpxN3ZUWTFGZGNqbTVMY1hEZUJZNzZnMnp2MU5LZGJneDZVUkJENFVRTkt2RkRFZXYwbHpW\nSHlVOHdSc3YKT0RweTUvbm5aMnlaallJM20wR3hUVndQSE4xYkx4bDdjS1g5M0ZFSFBuZHR4clJx\nL1NIMW10Q3Vjd1NjRC9vdApLN2hVL0x0MElabmNmWEVzcTZIaEM2WlpwaHlpWnNsSFNVL3daTTN4\nYzA4Y0ZtcWZzY213N1RPTWErVWt6cUs2CjhIc1Z6WDlaRHh5Wm4yazRXWmxyckpFQWNlK1lPNjF4\nTjU0Ulp5MnNWbnJhUmNXYUE5ODd2QUo4cjRubGY3ZWsKQVJDalJ1MmhJR0dzdVFJTkJFMGN4cGtC\nRUFDbFgvTzFaaHFzUWhkclhndEpUNWwyNFFrZEJpbEZkYXFJK2xkQwpZcUdndnl3N3Z4RmdQSDBa\nY1haTTlFVGhYenAzcVR2a1VRUHQweldMNlhqZndTUDlwZmYrV1BBWU1hYXRFUVhpCkVRTHBEbldG\nUWJzSEt5ZDR5V0tqWUZCTHVZeVlCNDhUajVpR1VMZnI5OEloQ0lHQ2V6eVFYdSsxeEV1eVhlUzYK\nNnp1Y24zY0krb3RzTFBSRkc3MUs4OTBGL0ozaFlxK1FWWENaUUFyRnhFQVQwNDNFQm4vMllvcGdm\nK1JpVVNlcgpTdU5YUlFyWXVGSlR3RWkrbjJjcEFEVmJSMDFTeHA1WEY5L201VXcyOVgzTUhCOXIr\nZDZ0c3ltWkJZQ2hidlpyCnRVVXpONWdJbjEzeDFjai8yaVFtVWttZlErSm1JSGRxSTc1RzRML2lp\nSmdSZUpzYW9hby9hekU1c2wyMW5PKysKZ0tpVUFod2lWM3FjY2N2R3hnUEhFd01POHhkOGVvakQv\ncXBHWm44WUNTT3hLbVRKa25hVnU1bmFaK04rRFVtaApjSysvTmRMcmpWV1l3RjA4eDloTTNhcFA1\nY2xsVHBPUER6SGl3NndHUWFHZDhXSGtSLy8xTmtxbzVvd202OHBZClE2SlUxeUlKNG1zYUdhVG9n\ncDI3bnFLS0VxS2VzeWJGQ0lwYmloc2hVY0toVGQ0b3ZScDZvK3BWWjNxUmhwYWoKSWR5Nm5XTzlI\nZnhwc21zODdTNTBUQWNseVRmejQyTUNmUkxCYUd2VnZKaXFZYXJkTFQxUnA4WUZTTUxlUC9WRwpQ\nL2NXaWRjS2IvU0VoWVhzaEsrbHMvdVFlUGdWS1FsY3U0N2ZZRTN1SloycUVCMFZkWDlUVEVMVXJj\nWThvbWROCjN0aUprUUFSQVFBQmlRSWZCQmdCQWdBSkJRSk5ITWFaQWhzTUFBb0pFTlhjS25uQzVL\nNlNTRjhQLzMyZ1VxaTAKUmdSWThPT1ZydDlLd3JRMGhtTE9CYlRVVzRtOVhvZzhsM1JVZGJOdDhX\nNHp5L0ltTGVwdVRHL2hXTVQ3ZnlCUgplN0JicElBVTJlUHdzMlFLY0xHVG94eGNrU09uRWxnK04w\nNXhTSVFYM3FIQVE0cWxuVkttVENtTjlTNG4vTG43CkwxRlpMZThZK3VWNjM2a28yZW9yQ3krL3d6\nVDJnM0lSb1A4RHhXbklXd2YyM3cvdjBqMHZyczFlRm9vT2VkZm4KU2FZZWZiYlNZWm1zVEZxUkVK\nZVNBcytSUkMvSjZsd3BvckExMXl5dXFDVlhVSlNaZFlCdE9ITUI0VDRpcXA3RApzRUJ5dm9rWWE1\nUGd5MVljYnRZVlhOZHlvdUNJTnJQdHBQaXdGUXJHb3MxVjl4UG9hWDJjWDZLYmtCMWZObE41Cnhm\nMmNRWXE0aHdHNTF4YnBOc2xycEw4QjJVRE4yWmRTQW1UMGhVM25UakFIVkpsTXhBcTh6R3lTekl6\nNlRtZHEKdTRLQXExRkdVajcyZTJrYzJ1WWkyL21ibzFBb0VWWGQzS1B6OTc0U1dFMXhYK0p2bEll\ncC8rN204QjhTZ1hpYwpXNWtLcy9UWGRHSXJ1ekJmKytiWHBNTGtkZ1JGTTE4amI5a1drOWRXZVpu\nbUhVSGJSZ1hCZEZOQkFoK20yWmJaCkt3TEdxTDNTVys0dXdxNWQvRTd6TFpqZ1AxY2twcXlJUWgx\nNk9hWk1wTmUzYkt1dFg1bXNja3Y3cVVwQjVYOEgKWU5sK3ZpRHA5ZndWSVJXZnRUemRJcVo0dy9p\nY3loTFAzMzBXRE9LM1dBc2U4UzQ0OEtnQURzTnNkZVBkUDM3bwpubm5PNEY4aEdONXpodkUzVWdL\nTGFpZ0lHNUZKSVhwSmdVSXNtUUlPQkUrK1Zya0JFQUVCaVNHNWVYZ004U1ovCjRabWRyeGJkemVU\ncUhrRmxFblJBVWlxbCtURjJMY2Z0YzBma3VmdC9TSi8vRkJHRkdBaUE1QUoxMkhHN0x0disKdkxN\nQk8zdldvTmlIMWZUVG9mb1VZS2k4cEpiQUdBRWwxTVIzVWZtZTRsVEcvam9NQW5tdEZpQzhrWTB6\nMFpqNQpVSEFCTnJ4SEIyS1JiTEtHa0lFUzNtNDZQSjFwc1pyekQwdkRkTUg4YmFIcm9KRnZxMlpk\ncWw3bzVxT1pZTW5tCmdUZkZkTGpBMFpnRHYrTGJJL2o2NWx0VHZmOHNiVm92QW0zMTV2OGs5Qklt\ndk9WeHdzZnFkaDh2SWRjTmx5QVcKRmsvSDJXRjQ3QS9sUkgvMzQ0WC9lSldwa1lOQ2lvU0s1YkNL\nNHJXQlVjU0k2ekU1RUszRDIvTkV3Unl3S1pWQwpNR2dhYnhBN3dwZG4xd2VERXVFbkN3aTF5Zk0v\nMnBpajFhaVhLNFk3dHNQN0tVeW9sQXdsNlR5eTVlRzdwOUdtClp6YXU2L25NbktBeVovcGxxem92\nemloK1huNXZSUXRremRWMVRjL01EZlBTUmx6OFltaTlSenRtTTFDb3VTN0UKWHd2Y2tSd1hoZ3FH\nL0ZGYWMzVDN1UlBBR1c4NnIrMm1Jb3NwdG9DcnFhN3hWdHhMaXVPRkZBRnFwTmV0ZVprOQpmVWUy\ndk1tOWw2eXpMUFVDL3ZwUUswVTd6ajBvVXN3RUxjTkNiaFYvUC9jUHAwQTR2TUFTeThkM01HSk03\nalk4CmVtYWZMcmJOM2JpUHppTmZlbWc3b1crcnRTb2kraWhPaWpWK3ViQ2RMTTNBQ2JOSVhJQkU3\nYXBXTmppS3gybW8Kc2h4K0VHcmQzNXY5bXRWZUxOUmtob28zTFltbmlJVUFFUUVBQWJRblUyMWhj\nbWtnVFdORFlYSjBhSGtnS0hSaApZbXhsZENrZ1BITnRZWEpwUUdsdGJXa3VhWE0raVFJY0JCTUJB\nZ0FHQlFKUHZsYldBQW9KRU5JT2padGFyWE41Ckt5Z1FBTUdPOURXNm1helErMmJvcGJQdzhGQnRu\najZZSHZYQVR2cXN4bmlXYkZIK285SVRmaHE3THpYUEd2SSsKUm1Sc3BWZ1N6alg1MFYyYkhvNVJj\ndk4xNHh0RzM0amhlcnJwaUxGMHpRcDNGYzMwcDkzWW1QTCtFOUlxaGxDWgpOZ0tyQTA0alpncGZr\nQ1NQSjQzM0pMYnNBSk5Qa3RBYW5RVURSL040elhlVVFkMFFqWTczU2pDQ1dad2w4WlJuCmwramhi\nY1F6MWRKbno1WWkrd3dhRTZMaW9wVjNqbmlpc29sYkNuU0kzeWM0TlRHMXJ4ekN1cC9ZL2dHa1hS\nM0EKcXI3WndwMDlTVTBLdzkvK01uTDlwSGEwQjRnd3lTUVZlVW5WakZWcXFXazVSaHlNYkJsSVFH\nNkFzYkZ6bWdpago4cXJXNGtya0EyRTVsa2RPRWJjVXMwSk10ckcvUnNPYlpkc2g3K05aWFcrdG0y\neEkwb292TTdmbDRCOFRDKy9IClhHY0ZjUnd6WnZQbFVVczlPQjh5MzJHTmdtT1E5U2pIanJ6S0ZH\nMnFZRXJCb2RCYmZMcnZqNEZYWllIZU1vbHoKM0tiak9LNFNJWE9RR3RKd1IraXY3bzJHVTcyTHhO\nQ1U2anoxaWFLNFZNZFdDWTVmdGFYU3JJYjdTc2hVYi9ENQpuQzJzc1YrdTBJclBHdERSZy9QT29K\nbEc1THdCcXpnam5YeFVBNW1uVSsvNDRBZmhtRlpGYTdENEpHWENFaks0CkI5aEZYNm43b2ZuRWhj\nM1dXVXpqR3I3Vjg0T204Vy9neUNMbnMwdmhNcDJBMXp1MWtra2xRdEl2TWk3aVlsSFgKTElTT2RO\neWtYL3R0SzQwQm9HQzhkZTVRcjV5aTFCRjRQTDdCUUYvUGh2dFpCL3pJaVFJeEJCTUJBZ0FhQlFK\nUAp2bGJXQXBzUEJvc0pDQWNEQWdTVkFnZ0RCSllDQXdFQUNna1EwZzZObTFxdGMzbGx1QkFCQVVj\ncDhyVjBwRjUrCk9BUlJnZ2RkL055WCt2YWppRm43MzduaG93bWtKVXpkb3FWRENpeklKSDkvdlNx\nc0duY0hXTG9JTTcvQ08wNnQKeFUxVHBVVVg5UDdwa1FMYUJqMlp4T3U3c1F1bURqK2NOY0NGWDhR\nR0orQUlyT0NSNmdCM3BrUWpDQnNjVkcrTQpuQnZGbTc5Z0EvZEJzRUNFOEE3L2dkTHFLNTJkekQv\nU0VzVzZiWFdwa2RYbkptVm9meVAwOHpKT2lEWjY1U0RGCmxmWW9CbTFkYURvZWRldXpLVGROL1B2\nNVRpS3RZVngyTERESWc4eGRyQlZvaEJmdnJUbDV3NElXL3FMbHNmVXIKZDVRczZOZnBKMGJveXNY\nS1ZjY2xHTEFNUkdNVGdkTzQ4L1pVK2xjdVlxRzBSMDhVZ1JnUWlleU1pZlJkYVBQcwowT3ZTVVBJ\nNE5xalVnNThBRmlpUlh1UWR5RHNLSnVQVUhRZTVON3JLc2xBK1FXSldQSGJ0dHlJU2IwY0ZtdDkz\nClo3OWVBQSttWjc3SzlVajRZM0J6dFRCazhVbit5L2VsTFMrbnMzeVpvZGJUSmt5UjhLU2E3SjhP\nMkJ2R211bXYKL1FiZXdKZndsaUZOR2dtb2pGaUNIUWxsOStWKzVKZlRiMkdrVGVORGN2RndBK3FH\nNHRyOU5wbmw1WkRhV1kxagprNnNzdXdyQ1ZvQlllQW1zL1FOWEluenFZL3FnQlJZRmhUTDZ6ZEFl\nakJHdzZQOGpNd3BGZ0NSREI0Tkh0aTdvClpsaDFvdkswTmpkajhhVS9tbXFIbHlDbGlqdWtCbStj\nZzlhcEphVi9IRko5VHFTVnQvYS9mZTRqM2xsK2htWFUKUmtxeEpReTJ3am9mcU1QL1FNbkUyODhm\nVW5zMkZtc1QKPTB1R2gKLS0tLS1FTkQgUEdQIFBVQkxJQyBLRVkgQkxPQ0stLS0tLQo=\n--=_719c52dffcf6edaa9d3b3c0248688168--\n\n\n"
  },
  {
    "path": "mailpile/tests/data/Maildir/cur/mailpile-1400069992.mbx",
    "content": "Return-Path: <bounces+hi=wigglebot.com@bounce-dyn.square.com>\nX-Original-To: reverse@heart.wigglecorp.com\nDelivered-To: reverse@heart.wigglecorp.com\nReceived: by heart.wigglecorp.com (Postfix, from userid 5001)\n\tid E2C476C9C2; Tue, 13 May 2014 18:16:24 -0700 (PDT)\nX-Spam-Checker-Version: SpamAssassin 3.3.2 (2011-06-06) on\n\theart.wigglecorp.com\nX-Spam-Level: \nX-Spam-Status: No, score=-1.9 required=5.0 tests=BAYES_00,HTML_MESSAGE,\n\tRCVD_IN_DNSWL_NONE,T_DKIM_INVALID autolearn=ham version=3.3.2\nReceived: from mtaout-090-ewr.square.com (mtaout-090-ewr.square.com [216.146.33.90])\n\tby heart.wigglecorp.com (Postfix) with ESMTP id 1DFDD6C83F\n\tfor <hi@wigglebot.com>; Tue, 13 May 2014 18:16:20 -0700 (PDT)\nDKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; s=201308-pay-dyn; d=square.com;\n h=Date:From:To:Cc:Message-ID:Subject:MIME-Version:Content-Type:Sender; i=demo@square.com;\n bh=zGLjgIBiSUnRmcU7eQ61Hy18vqc=;\n b=sR7k0/0x/E4a9tuMLMy//xPw3Vwdyuzp+/b+dGhfi2hxRYcLqWb9TCvz8L8OavfOHJkL9SRq7oYz\n   BTxGeEEwb8mWLu8AVk8RkfFsYdnfWsTUu/uW2eMbRZfkUPn20yXoQhduqOkOtCBG/xMik3zappb9\n   oM25cVk01i7MD9mHsZo=\nDKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=square.com;\n s=201308-pay; t=1400030175; i=demo@square.com;\n bh=m2lW3TrxcDXUiE8SbFgz8T7if500H4l/9gKFuKOl83E=;\n h=Date:From:To:Cc:Message-ID:Subject:MIME-Version:Content-Type:From:\n Content-Type;\n b=SaDYImGjdsnCmiCtbLQrOBwZdSHTIlGNIQcUedSvhYYOPIaMUlKoZXldJK7eBv6BH\n tfVNbGiZZFBxE4PhZ7lq/8r81Rm0au2e816Lig4f0XY3cz3UUlTHyW9skw7lgKRsbT\n krByvO+lN0o8WzcJ4zuctIE0fLBGY0OZhlW5NievvqHZCtPHk7vJGCuWSWoPXsVlNO\n yVsqj1eJXUvOTklB9oPzne9fMuVU+ROdjGO/sRmKYqKmXroeZw03sR3p1q/7Pke7f9\n JXdQHfu8Yn5XtUSJKyexRUCzQcfQ9YWYqs/V0iquxoHNLYPPyUVukoHxp4JQJaIyEu\n DR6SVCBquvuhg==\nDate: Wed, 14 May 2014 01:16:15 +0000 (UTC)\nFrom: The Square Team <demo@square.com>\nTo: hi@wigglebot.com\nCc: cash@square.com\nX-DynectEmail-Msg-Key: 20140514011619.FF8A06700036@mail6-12-ewr\nMessage-ID:  <f38s7nk064or5h7pjjwzsj4kt-a881f968-36a7-4af7-b027-a12d6b686326@square.com>\nSubject: Re: Here's $1\nMIME-Version: 1.0\nContent-Type: multipart/alternative;  boundary=\"----=_Part_31399_1772714090.1400030175154\"\nX-Square-Cash-Referrer-Token: C_apf4xbdgjbf39ema8ie8noen3\nX-Square-Message-ID:  <f38s7nk064or5h7pjjwzsj4kt-a881f968-36a7-4af7-b027-a12d6b686326@square.com>\nX-Square-Received: by cash; Wed, 14 May 2014 01:16:15 +0000\nAuto-Submitted: auto-replied\nX-Autoreply: yes\nX-Square-Cash-Template: tryItNow\nSender: demo@square.com\nX-DynectEmail-Msg-Hash:  +c7LZw7dwhJTgSQcZA2UskvtjfMRvKccJ3eLTW+ZNi0tOHFDJwRPcH/UgnThrmMFzDj31y9O/3TqMoq/CD2Q0Q2QeET6hCCDW8g8JMOmvIY=\nX-DynectEmail-X-Headers:  MHw3NzA5NjE6JTNDZjM4czduazA2NG9yNWg3cGpqd3pzajRrdC1hODgxZjk2OC0zNmE3LTRhZjctYjAyNy1hMTJkNmI2ODYzMjYlNDBzcXVhcmUuY29tJTNFOw==\nX-Feedback-ID: U3F1YXJlVk1UQXM=:36726:275572:dyn06\n\n------=_Part_31399_1772714090.1400030175154\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 7bit\n\nYour $1 is on its way.\n\nAll it takes is an ordinary email like this one to send cash. Just enter a dollar amount in the subject line and add cash@square.com to the Cc field.\n\nEnjoy,\n\nThe Square Team\n------=_Part_31399_1772714090.1400030175154\nContent-Type: multipart/related; \n\tboundary=\"----=_Part_31398_1983417104.1400030175154\"\n\n------=_Part_31398_1983417104.1400030175154\nContent-Type: text/html; charset=UTF-8\nContent-Transfer-Encoding: 7bit\n\n<html>\n <head></head>\n <body>\n  Your $1 is on its way.\n  <br />\n  <br />All it takes is an ordinary email like this one to send cash. Just enter a dollar amount in the subject line and add cash@square.com to the Cc field.\n  <br />\n  <br />Enjoy,\n  <br />\n  <br />The Square Team\n </body>\n</html>\n------=_Part_31398_1983417104.1400030175154--\n\n------=_Part_31399_1772714090.1400030175154--\n\nFrom MAILER-DAEMON Wed May 14 12:19:52 2014\nReturn-Path: <bounces+hi=wigglebot.com@bounce-dyn.square.com>\nX-Original-To: reverse@heart.wigglecorp.com\nDelivered-To: reverse@heart.wigglecorp.com\nReceived: by heart.wigglecorp.com (Postfix, from userid 5001)\n\tid D92DE6C9C1; Tue, 13 May 2014 18:16:26 -0700 (PDT)\nX-Spam-Checker-Version: SpamAssassin 3.3.2 (2011-06-06) on\n\theart.wigglecorp.com\nX-Spam-Level: \nX-Spam-Status: No, score=-1.6 required=5.0 tests=BAYES_00,DC_IMAGE_SPAM_TEXT,\n\tHTML_IMAGE_RATIO_06,HTML_MESSAGE,RCVD_IN_DNSWL_BLOCKED,T_DKIM_INVALID\n\tautolearn=no version=3.3.2\nReceived: from mtaout-088-ewr.square.com (mtaout-088-ewr.square.com [216.146.33.88])\n\tby heart.wigglecorp.com (Postfix) with ESMTP id 257C66C9BF\n\tfor <hi@wigglebot.com>; Tue, 13 May 2014 18:16:22 -0700 (PDT)\nDKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; s=201308-pay-dyn; d=square.com;\n h=Date:From:To:Message-ID:In-Reply-To:Subject:MIME-Version:Content-Type:Sender; i=cash@square.com;\n bh=n8uJdeJzYnfdSJuzS2CxC+Mtc3M=;\n b=mGVUARyX4keiTSukTuat+jBgkb36vJ4Wr/JH4/tRcK0ykMjXJeF5591Ewa1XnEhpYZ2udhJofjeE\n   vW0wFx8xU77u+tTQhif3E5UfWnDlHFCd768ASY2uft67XS87n24fRyowmS0ElyYjEt5qtZzXtr3i\n   2IID0R54T9/ytPWubVo=\nDKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=square.com;\n s=201308-pay; t=1400030180; i=cash@square.com;\n bh=UnSpjyrNunPWj7UR7n92moWlPsyua1QAB+gxcgXf7Hc=;\n h=Date:From:To:Message-ID:In-Reply-To:Subject:MIME-Version:\n Content-Type:From:Content-Type;\n b=Q9Ms61s6qzx5xhAuV5rT7JOL1cqY3hIczHMjjXIgRKhwTqIT11Hb4pPfJZLayL4eA\n 1kGARliKisWDO26DQqoPKVyN+7rsHUMYz5dn0rvU2+CEJJJFF79fUCCtXLTEJ/j58T\n Qg5wDkFoJ7N8MBhZNZFNQnSSnoAKWrWUyVp7RetZ2Vm6TTz57HRzqEF/5pZtgbx7pb\n dBR9AbDd9wEY7Zl7yNSttC07EL4gnzRB8n/OWQhJnp+gF+6yKgS0z3oxm6JKwEnENA\n n8QQuq+2Gl38I6Btp4NrZcNslUnkOK78HXTYjb9Zqi7gBXYxJ0rt5KcdlWm7IPBoIf\n fzJ3/iZw8SruA==\nDate: Wed, 14 May 2014 01:16:20 +0000 (UTC)\nFrom: Square Cash <cash@square.com>\nTo: hi@wigglebot.com\nX-DynectEmail-Msg-Key: 20140514011621.94D806700038@mail6-12-ewr\nMessage-ID:  <bqr3om0kmor203jvzmdpx79vb-fdef1c6b-2d87-4cd1-ad72-eb6e440278d4@square.com>\nIn-Reply-To:  <f38s7nk064or5h7pjjwzsj4kt-a881f968-36a7-4af7-b027-a12d6b686326@square.com>\nSubject: Re: Here's $1\nMIME-Version: 1.0\nContent-Type: multipart/alternative;  boundary=\"----=_Part_61073_141710048.1400030180530\"\nX-Square-Message-ID:  <bqr3om0kmor203jvzmdpx79vb-fdef1c6b-2d87-4cd1-ad72-eb6e440278d4@square.com>\nX-Square-Received: by cash; Wed, 14 May 2014 01:16:20 +0000\nAuto-Submitted: auto-replied\nX-Autoreply: yes\nX-Square-Cash-Template: recipientLinkRequired\nSender: cash@square.com\nX-DynectEmail-Msg-Hash:  +c7LZw7dwhJTgSQcZA2UskvtjfMRvKccwx8KB1ukvaiXOcCzgQYYET/scCge6kJI3qlhhMmNfsD655YUBBuitwk7Jn5VjlM4f/WYpp7O7SI=\nX-DynectEmail-X-Headers:  MHw3NzA5NjE6JTNDYnFyM29tMGttb3IyMDNqdnptZHB4Nzl2Yi1mZGVmMWM2Yi0yZDg3LTRjZDEtYWQ3Mi1lYjZlNDQwMjc4ZDQlNDBzcXVhcmUuY29tJTNFOw==\nX-Feedback-ID: U3F1YXJlVk1UQXM=:36726:155511:dyn06\n\n------=_Part_61073_141710048.1400030180530\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 7bit\n\nThe Square Team sent you $1. To deposit, visit this URL in your web browser:\n\nhttps://square.com/cash/payments/RANDOMDIGITSHERE\n\nBy depositing, you agree to the Square Cash terms and conditions:\n\nhttps://squareup.com/legal/cash-ua\n\nTo reject, visit this URL in your browser:\n\nhttps://square.com/cash/payments/RANDOMDIGITSHERE?action=reject\n\n------=_Part_61073_141710048.1400030180530\nContent-Type: multipart/related; \n\tboundary=\"----=_Part_61072_577381436.1400030180529\"\n\n------=_Part_61072_577381436.1400030180529\nContent-Type: text/html; charset=UTF-8\nContent-Transfer-Encoding: quoted-printable\n\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org=\n/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n<html>\n <head>=20\n  <meta charset=3D\"utf-8\" />=20\n  <meta name=3D\"viewport\" content=3D\"initial-scale=3D1.0, maximum-scale=3D1=\n.0\" /> =20\n  <style type=3D\"text/css\">\n    .email {\n      text-decoration: none;\n      color: #90de7c;\n      cursor: text;\n    }\n\n    p {\n      margin: 1em 0;\n    }\n  </style> =20\n </head>=20\n <body>=20\n  <!--[if !mso]><!-->=20\n  <div style=3D\"font-size:0;max-height:0;overflow:hidden;display:none\">\n   The Square Team sent you $1\n  </div>=20\n  <!--<![endif]-->=20\n  <table width=3D\"100%\" cellspacing=3D\"0\" cellpadding=3D\"0\" border=3D\"0\">=\n=20\n   <tbody>\n    <tr>\n     <td height=3D\"20\"> <img src=3D\"http://square-production.s3.amazonaws.c=\nom/images/email/alpha.gif\" width=3D\"1\" height=3D\"20\" style=3D\"display:block=\ndisplay: block;\" /> </td>\n    </tr>=20\n    <tr>\n     <td align=3D\"center\">=20\n      <div id=3D\"view-on-the-web\" style=3D\"font-size: 14px;  font-family: H=\nelvetica Neue, Helvetica, Arial, sans-serif;  font-weight: 300;  color: #99=\n9999;  letter-spacing: 0.25px;\">=20\n       <a href=3D\"https://square.com/cash/email/bqr3om0kmor203jvzmdpx79vb-f=\ndef1c6b-2d87-4cd1-ad72-eb6e440278d4\" style=3D\"color: #999999;  text-decorat=\nion: none;\">Having trouble viewing this email?</a>=20\n      </div> </td>\n    </tr>=20\n    <tr>\n     <td height=3D\"20\"> <img src=3D\"http://square-production.s3.amazonaws.c=\nom/images/email/alpha.gif\" width=3D\"1\" height=3D\"20\" style=3D\"display:block=\ndisplay: block;\" /> </td>\n    </tr>=20\n    <tr>\n     <td align=3D\"center\">=20\n      <table align=3D\"center\" id=3D\"container\" border=3D\"0\" cellpadding=3D\"=\n0\" cellspacing=3D\"0\" bgcolor=3D\"#16a500\" width=3D\"300\" style=3D\"-webkit-bor=\nder-radius: 10px;  -moz-border-radius: 10px;  -ms-border-radius: 10px;  -o-=\nborder-radius: 10px;  border-radius: 10px;  overflow: hidden;  table-layout=\n: fixed;  letter-spacing: 0.25px;\">=20\n       <tbody>\n        <tr>=20\n         <td width=3D\"300\" height=3D\"277\" background=3D\"cid:image-c63065ff-=\n0226-4b99-8968-1b52010d717c@square.com\">=20\n          <table width=3D\"300\" height=3D\"277\" background=3D\"data:image/png;=\nbase64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEVBAMAAACoJsUjAAAAJFBMVEUAAAAWpQAWpQAi=\nqQAxrgJAtBpPui9dv0FsxVOZ14jN7MX///+ZeuycAAAAAXRSTlMAQObYZgAABCZJREFUeNrt3E1=\nME0EUB/C3S4GWU1WiBy/ogeANvw7eMB6QG2oC9FZiYiCIJkaIUUOIYoRoDEEN6EFXT4BGORm8ID=\ndupp4AP5ArnDgYvtruulO27S5dSADLe5H/JG1n2hR+mZl9M7OzW20fSUw6gQUWWGCBBRZYYIEFF=\nlibpgBrnTw8ki1Y0x3ZQkGIkRU54ypopfEpEY2o13rLF2X0raKNy7qgw03Hkbh5MhvSueBbQXFr=\n2SeHKA/Wf3wk2in0xh4P68XVluY80LfAAgusnGQ5D2HhdKkOjQjW3mEFfXICjkR9WH4jWjJYpkz=\nW6sZl1toa8ZY/COlbg7/dTTjlUmrYQgcLLLDAAgsssMACCyywwAILLLDAAgsssMACCyywwAILLP=\n6Uny306koi8xEHq8RYjG70WdFlyt2J5m/ELpF9K1LGx6ra8JPgBa4uH2g7bveugV9+vTrQ9w8qf=\nFu3iBQ9T91xEjp87s9szoe3nCa03u9yIwaehZ3c/uactqyo5Aqn7eFMVmta172LbnBF+aC7PvSb=\n3j/XFeZi3fOUDnpu1qkuIyZWsfrP5qcoLY4tqLL7QsjiRrah+qRSdRv280CLOg4D2eoqeMo3g1C=\nKqVgqG7+94AmsV8J8LLsNzU4nn2i145ORiQ1nnYg1svusYnJf/7UyQpMxJ1+Yjg2TDCx1f8JMtv=\nguma45euA04Xwn8bBcI048ks7VOLHBfExMLMOvcZ3YYI3OCprL6+nYMG1IWmLUOx0r0cG38vGZA=\nn5zOtZLprilrln1mbv8XHsZHWdk+YzHq6mO/t0gRtYhn/cVKHmXbfBJqiE56tuKVg/fYj+hRufz=\nvq04EWM8B6EGPL0/9/3X872cp0bG1NOBwdr17/+4ynrGZjk1J9VzWAliZdF15zXURXlL22AtOWO=\nxVj5QJYhFdxYyq9daQax4a9qlRWrlsGilJT2n0hrK5LAo3v7Vf1XNyyLqjvquqrlZanRca8lLwl=\ngr7V9S52iEsYj6X6leH5XGotGP/nNV7iXGsB3BSuWxkiP52QjZ6YJszOfnCQSwTJmsBBHJrC2JL=\nJLJ0sWw2tyFgM/vALCwak6Veb8vghVs9AyCR/PT67fKKuwjqnCVo7k3/HKwrLD9nc5MUW1pzApg=\nJdR5hmOZhdh9Z/XP3reeqClWk+NqtisrGZPAWlZtpjerKFHeo7Yt5mTErdQ5W+20QSVdamvY6pX=\nBWh73FOdmZbDohRtiXpMy+CQz5yDUnoWgcxCZtb41YeSHta2JeLy9/oTd3a2Zz+MkiEU0NLTZFU=\nmYBoJFO1h7LtahEcECCyywwAILLLDAAgsssMACCyywwAILLLDAAmuLCb9qDRZYYIEFFlhggQUWW=\nGCBBRZYYIEFFlhggQUWWGCBBRZYYIEFFlhggQUWWGCBBRZYYO1d1l+YoNJYmTim2QAAAABJRU5E=\nrkJggg=3D=3D\" border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\">\n           <tbody>\n            <tr>\n             <td> <img width=3D\"300\" height=3D\"277\" style=3D\"width:300px;/*=\nFor iCloud*/display: block;\" src=3D\"cid:image-dd72b895-de9b-4559-9756-6067b=\n47f64b5@square.com\" alt=3D\"$1\" /></td>=20\n            </tr>\n           </tbody>\n          </table></td>=20\n        </tr>=20\n        <tr>=20\n         <td align=3D\"center\">=20\n          <table align=3D\"center\" border=3D\"0\" cellpadding=3D\"0\" cellspacin=\ng=3D\"0\" width=3D\"220\">=20\n           <tbody>\n            <tr>\n             <td align=3D\"center\" id=3D\"title\" style=3D\"color: white;  font=\n-family: Helvetica Neue, Helvetica, Arial, sans-serif;  font-size: 20px;  f=\nont-weight: 400;  line-height: 1.4;\">\n              <div style=3D\"overflow: hidden;\">\n               The Square Team sent you $1\n              </div></td>\n            </tr>=20\n            <tr>\n             <td height=3D\"15\"> <img src=3D\"http://square-production.s3.ama=\nzonaws.com/images/email/alpha.gif\" width=3D\"1\" height=3D\"15\" style=3D\"displ=\nay:blockdisplay: block;\" /> </td>\n            </tr>=20\n            <tr>\n             <td align=3D\"center\" class=3D\"secondary\" style=3D\"font-family:=\n Helvetica Neue, Helvetica, Arial, sans-serif;  font-size: 16px;  line-heig=\nht: 1.4;  font-weight: 300;color: #90de7c;\">\n              <div style=3D\"overflow: hidden;\">\n               You can deposit this cash in your bank account.\n              </div></td>\n            </tr>=20\n            <tr>\n             <td height=3D\"25\"> <img src=3D\"http://square-production.s3.ama=\nzonaws.com/images/email/alpha.gif\" width=3D\"1\" height=3D\"25\" style=3D\"displ=\nay:blockdisplay: block;\" /> </td>\n            </tr>=20\n            <tr>\n             <td height=3D\"50\" bgcolor=3D\"#34bd0a\" class=3D\"button\" align=\n=3D\"center\" valign=3D\"middle\" style=3D\"display: block;  -webkit-border-radi=\nus: 4px;  -moz-border-radius: 4px;  -ms-border-radius: 4px;  -o-border-radi=\nus: 4px;  border-radius: 4px;  letter-spacing: 0.5px;\"> <a class=3D\"button\"=\n href=3D\"https://square.com/cash/payments/RANDOMDIGITSHERE\" style=\n=3D\"color: white;  font-size: 20px;  font-weight: 400;  font-family: Helvet=\nica Neue, Helvetica, Arial, sans-serif;  text-decoration: none;  display: i=\nnline-block;  width: 220px;  text-align: center;  line-height: 50px;  -webk=\nit-text-size-adjust: none;  letter-spacing: 0.5px;\">Deposit Cash</a> </td>\n            </tr>=20\n            <tr>\n             <td height=3D\"20\"> <img src=3D\"http://square-production.s3.ama=\nzonaws.com/images/email/alpha.gif\" width=3D\"1\" height=3D\"20\" style=3D\"displ=\nay:blockdisplay: block;\" /> </td>\n            </tr>=20\n            <tr>\n             <td align=3D\"center\">=20\n              <div id=3D\"minor-button\" class=3D\"secondary\" style=3D\"overflo=\nw: hidden;font-family: Helvetica Neue, Helvetica, Arial, sans-serif;  font-=\nsize: 16px;  line-height: 1.4;  font-weight: 300;color: #90de7c;\">\n               <a href=3D\"https://square.com/cash/payments/f3j2xavdvc79p3vs=\nu70sudpc1?action=3Dreject\" style=3D\"font-family: Helvetica Neue, Helvetica,=\n Arial, sans-serif;  font-size: 16px;  line-height: 1.4;  font-weight: 300;=\ncolor: #90de7c;\">Reject</a>\n              </div> </td>\n            </tr>=20\n           </tbody>\n          </table> </td>=20\n        </tr>=20\n        <tr>\n         <td height=3D\"40\"> <img src=3D\"http://square-production.s3.amazona=\nws.com/images/email/alpha.gif\" width=3D\"1\" height=3D\"40\" style=3D\"display:b=\nlockdisplay: block;\" /> </td>\n        </tr>=20\n       </tbody>\n      </table> </td>\n    </tr>=20\n    <!-- Referral footer -->=20\n    <!-- END Referral footer -->=20\n    <tr>\n     <td>=20\n      <table align=3D\"center\" border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D=\n\"0\" width=3D\"280\">=20\n       <tbody>\n        <tr>\n         <td height=3D\"20\"> <img src=3D\"http://square-production.s3.amazona=\nws.com/images/email/alpha.gif\" width=3D\"1\" height=3D\"20\" style=3D\"display:b=\nlockdisplay: block;\" /> </td>\n        </tr>=20\n        <tr>=20\n         <td align=3D\"center\" id=3D\"external-footer\" style=3D\"font-size: 16=\npx;  text-align: center;  line-height: 1.4;  font-family: Helvetica Neue, H=\nelvetica, Arial, sans-serif;  font-weight: 300;  color: #999999;  letter-sp=\nacing: 0.25px;\"> By depositing, you agree to these <a href=3D\"https://squar=\neup.com/legal/cash-ua\" style=3D\"color: #999999;\">terms and conditions</a>. =\n</td>=20\n        </tr>=20\n       </tbody>\n      </table> </td>\n    </tr>=20\n   </tbody>\n  </table>\n  <table class=3D\"first-time-footer\" width=3D\"100%\" border=3D\"0\" cellpaddin=\ng=3D\"0\" cellspacing=3D\"0\">=20\n   <tbody>\n    <tr>\n     <td height=3D\"50\" style=3D\"font-family: Helvetica Neue, Helvetica, Ari=\nal, sans-serif;  letter-spacing: 0.25px;\"></td>\n    </tr>=20\n    <tr>\n     <td bgcolor=3D\"#16a500\" align=3D\"center\" valign=3D\"middle\" style=3D\"fo=\nnt-family: Helvetica Neue, Helvetica, Arial, sans-serif;  letter-spacing: 0=\n.25px;\">=20\n      <table width=3D\"280\" cellspacing=3D\"0\" cellpadding=3D\"0\" border=3D\"0\"=\n class=3D\"footer-block\" style=3D\"display: inline-block;  vertical-align: mi=\nddle;\">=20\n       <tbody>\n        <tr>\n         <td rowspan=3D\"6\" width=3D\"20\" style=3D\"font-family: Helvetica Neu=\ne, Helvetica, Arial, sans-serif;  letter-spacing: 0.25px;\"></td>\n         <td height=3D\"40\" style=3D\"font-family: Helvetica Neue, Helvetica,=\n Arial, sans-serif;  letter-spacing: 0.25px;\"></td>\n         <td rowspan=3D\"6\" width=3D\"20\" style=3D\"font-family: Helvetica Neu=\ne, Helvetica, Arial, sans-serif;  letter-spacing: 0.25px;\"></td>\n        </tr>=20\n        <tr>=20\n         <td class=3D\"title\" align=3D\"left\" style=3D\"color: white;  font-fa=\nmily: Helvetica Neue, Helvetica, Arial, sans-serif;  font-size: 20px;  font=\n-weight: 400;  line-height: 1.4;font-family: Helvetica Neue, Helvetica, Ari=\nal, sans-serif;  letter-spacing: 0.25px;\"> About Square Cash </td>=20\n        </tr>=20\n        <tr>\n         <td height=3D\"10\" style=3D\"font-family: Helvetica Neue, Helvetica,=\n Arial, sans-serif;  letter-spacing: 0.25px;\"></td>\n        </tr>=20\n        <tr>=20\n         <td class=3D\"secondary footer\" align=3D\"left\" style=3D\"font-family=\n: Helvetica Neue, Helvetica, Arial, sans-serif;  font-size: 16px;  line-hei=\nght: 1.4;  font-weight: 300;color: white;font-family: Helvetica Neue, Helve=\ntica, Arial, sans-serif;  letter-spacing: 0.25px;color: #90de7c;\"> Square C=\nash makes sending and requesting money as easy as composing an email. All i=\nt takes is a debit card to send and receive with your bank account =E2=80=\n=93 all&nbsp;for&nbsp;free. </td>=20\n        </tr>=20\n        <tr>\n         <td height=3D\"10\" style=3D\"font-family: Helvetica Neue, Helvetica,=\n Arial, sans-serif;  letter-spacing: 0.25px;\"></td>\n        </tr>=20\n        <tr>=20\n         <td style=3D\"font-size:16px;font-family: Helvetica Neue, Helvetica=\n, Arial, sans-serif;  letter-spacing: 0.25px;\" align=3D\"left\"> <a href=3D\"h=\nttps://square.com/cash\" style=3D\"color: #90de7c\">Learn More</a> </td>=20\n        </tr>=20\n       </tbody>\n      </table>\n      <table width=3D\"300\" cellspacing=3D\"0\" cellpadding=3D\"0\" border=3D\"0\"=\n class=3D\"footer-block\" style=3D\"display: inline-block;  vertical-align: mi=\nddle;\">=20\n       <tbody>\n        <tr>=20\n         <td rowspan=3D\"4\" width=3D\"20\" style=3D\"font-family: Helvetica Neu=\ne, Helvetica, Arial, sans-serif;  letter-spacing: 0.25px;\"></td>\n         <td colspan=3D\"3\" height=3D\"45\" style=3D\"font-family: Helvetica Ne=\nue, Helvetica, Arial, sans-serif;  letter-spacing: 0.25px;\"></td>\n         <td rowspan=3D\"6\" width=3D\"20\" style=3D\"font-family: Helvetica Neu=\ne, Helvetica, Arial, sans-serif;  letter-spacing: 0.25px;\"> </td>\n        </tr>=20\n        <tr>=20\n         <td valign=3D\"middle\" height=3D\"50\" align=3D\"left\" style=3D\"font-f=\namily: Helvetica Neue, Helvetica, Arial, sans-serif;  letter-spacing: 0.25p=\nx;\"> <img src=3D\"cid:image-51be63f9-05c3-49ef-8268-e937035bbe29@square.com\"=\n style=3D\"width:30px;display: block;\" width=3D\"30\" height=3D\"32\" /></td>=20\n         <td width=3D\"10\" style=3D\"font-family: Helvetica Neue, Helvetica, =\nArial, sans-serif;  letter-spacing: 0.25px;\"></td>=20\n         <td valign=3D\"middle\" align=3D\"left\" class=3D\"title with-icon\" sty=\nle=3D\"color: white;  font-family: Helvetica Neue, Helvetica, Arial, sans-se=\nrif;  font-size: 20px;  font-weight: 400;  line-height: 1.4;font-size: 18px=\n;font-family: Helvetica Neue, Helvetica, Arial, sans-serif;  letter-spacing=\n: 0.25px;\"> Safe to use. </td>=20\n        </tr>=20\n        <tr>=20\n         <td valign=3D\"middle\" height=3D\"50\" align=3D\"left\" style=3D\"font-f=\namily: Helvetica Neue, Helvetica, Arial, sans-serif;  letter-spacing: 0.25p=\nx;\"> <img src=3D\"cid:image-97de2d61-9082-46fa-b927-32b6151aaa9d@square.com\"=\n style=3D\"width:30px;display: block;\" width=3D\"30\" height=3D\"30\" /></td>=20\n         <td style=3D\"font-family: Helvetica Neue, Helvetica, Arial, sans-s=\nerif;  letter-spacing: 0.25px;\"></td>=20\n         <td valign=3D\"middle\" align=3D\"left\" class=3D\"title with-icon\" sty=\nle=3D\"color: white;  font-family: Helvetica Neue, Helvetica, Arial, sans-se=\nrif;  font-size: 20px;  font-weight: 400;  line-height: 1.4;font-size: 18px=\n;font-family: Helvetica Neue, Helvetica, Arial, sans-serif;  letter-spacing=\n: 0.25px;\"> Free to send and receive. </td>=20\n        </tr>=20\n        <tr>=20\n         <td valign=3D\"middle\" height=3D\"50\" align=3D\"left\" style=3D\"font-f=\namily: Helvetica Neue, Helvetica, Arial, sans-serif;  letter-spacing: 0.25p=\nx;\"> <img src=3D\"cid:image-399bc086-a392-46b6-b70c-cbbab92bac55@square.com\"=\n style=3D\"width:30px;display: block;\" width=3D\"30\" height=3D\"30\" /></td>=20\n         <td style=3D\"font-family: Helvetica Neue, Helvetica, Arial, sans-s=\nerif;  letter-spacing: 0.25px;\"></td>=20\n         <td valign=3D\"middle\" align=3D\"left\" class=3D\"title with-icon\" sty=\nle=3D\"color: white;  font-family: Helvetica Neue, Helvetica, Arial, sans-se=\nrif;  font-size: 20px;  font-weight: 400;  line-height: 1.4;font-size: 18px=\n;font-family: Helvetica Neue, Helvetica, Arial, sans-serif;  letter-spacing=\n: 0.25px;\"> Direct to your bank. </td>=20\n        </tr>=20\n       </tbody>\n      </table>=20\n      <table width=3D\"260\" cellspacing=3D\"0\" cellpadding=3D\"0\" border=3D\"0\"=\n>=20\n       <tbody>\n        <tr>\n         <td height=3D\"40\" style=3D\"font-family: Helvetica Neue, Helvetica,=\n Arial, sans-serif;  letter-spacing: 0.25px;\"></td>\n        </tr>=20\n        <tr>=20\n         <td align=3D\"center\" style=3D\"font-family: Helvetica Neue, Helveti=\nca, Arial, sans-serif;  letter-spacing: 0.25px;\">\n          <div id=3D\"first-time-footer-copyright\" style=3D\"text-align: cent=\ner; color: #ffffff;font-size: 16px;  font-family: Helvetica Neue, Helvetica=\n, Arial, sans-serif;\">\n            &copy; Square Inc.\n          </div> </td>=20\n        </tr>=20\n        <tr>\n         <td height=3D\"40\" style=3D\"font-family: Helvetica Neue, Helvetica,=\n Arial, sans-serif;  letter-spacing: 0.25px;\"></td>\n        </tr>=20\n       </tbody>\n      </table> </td>\n    </tr>=20\n   </tbody>\n  </table> =20\n  <img src=3D\"https://square.com/cash/pixel-open/bqr3om0kmor203jvzmdpx79vb-=\nfdef1c6b-2d87-4cd1-ad72-eb6e440278d4\" height=3D\"1\" width=3D\"1\" style=3D\"dis=\nplay: block;\" /> =20\n </body>\n</html>\n------=_Part_61072_577381436.1400030180529\nContent-Type: image/png\nContent-Transfer-Encoding: base64\nContent-ID: <image-c63065ff-0226-4b99-8968-1b52010d717c@square.com>\nContent-Disposition: INLINE\n\niVBORw0KGgoAAAANSUhEUgAAASwAAAEVBAMAAACoJsUjAAAAJFBMVEUAAAAWpQAWpQAiqQAxrgJA\ntBpPui9dv0FsxVOZ14jN7MX///+ZeuycAAAAAXRSTlMAQObYZgAABCZJREFUeNrt3E1ME0EUB/C3\nS4GWU1WiBy/ogeANvw7eMB6QG2oC9FZiYiCIJkaIUUOIYoRoDEEN6EFXT4BGORm8IDdupp4AP5Ar\nnDgYvtruulO27S5dSADLe5H/JG1n2hR+mZl9M7OzW20fSUw6gQUWWGCBBRZYYIEFFlibpgBrnTw8\nki1Y0x3ZQkGIkRU54ypopfEpEY2o13rLF2X0raKNy7qgw03Hkbh5MhvSueBbQXFr2SeHKA/Wf3wk\n2in0xh4P68XVluY80LfAAgusnGQ5D2HhdKkOjQjW3mEFfXICjkR9WH4jWjJYpkzW6sZl1toa8ZY/\nCOlbg7/dTTjlUmrYQgcLLLDAAgsssMACCyywwAILLLDAAgsssMACCyywwAILLP6Uny306koi8xEH\nq8RYjG70WdFlyt2J5m/ELpF9K1LGx6ra8JPgBa4uH2g7bveugV9+vTrQ9w8qfFu3iBQ9T91xEjp8\n7s9szoe3nCa03u9yIwaehZ3c/uactqyo5Aqn7eFMVmta172LbnBF+aC7PvSb3j/XFeZi3fOUDnpu\n1qkuIyZWsfrP5qcoLY4tqLL7QsjiRrah+qRSdRv280CLOg4D2eoqeMo3g1CKqVgqG7+94AmsV8J8\nLLsNzU4nn2i145ORiQ1nnYg1svusYnJf/7UyQpMxJ1+Yjg2TDCx1f8JMtvguma45euA04Xwn8bBc\nI048ks7VOLHBfExMLMOvcZ3YYI3OCprL6+nYMG1IWmLUOx0r0cG38vGZAn5zOtZLprilrln1mbv8\nXHsZHWdk+YzHq6mO/t0gRtYhn/cVKHmXbfBJqiE56tuKVg/fYj+hRufzvq04EWM8B6EGPL0/9/3X\n872cp0bG1NOBwdr17/+4ynrGZjk1J9VzWAliZdF15zXURXlL22AtOWOxVj5QJYhFdxYyq9daQax4\na9qlRWrlsGilJT2n0hrK5LAo3v7Vf1XNyyLqjvquqrlZanRca8lLwlgr7V9S52iEsYj6X6leH5XG\notGP/nNV7iXGsB3BSuWxkiP52QjZ6YJszOfnCQSwTJmsBBHJrC2JLJLJ0sWw2tyFgM/vALCwak6V\neb8vghVs9AyCR/PT67fKKuwjqnCVo7k3/HKwrLD9nc5MUW1pzApgJdR5hmOZhdh9Z/XP3reeqClW\nk+NqtisrGZPAWlZtpjerKFHeo7Yt5mTErdQ5W+20QSVdamvY6pXBWh73FOdmZbDohRtiXpMy+CQz\n5yDUnoWgcxCZtb41YeSHta2JeLy9/oTd3a2Zz+MkiEU0NLTZFUmYBoJFO1h7LtahEcECCyywwAIL\nLLDAAgsssMACCyywwAILLLDAAmuLCb9qDRZYYIEFFlhggQUWWGCBBRZYYIEFFlhggQUWWGCBBRZY\nYIEFFlhggQUWWGCBBRZYYO1d1l+YoNJYmTim2QAAAABJRU5ErkJggg==\n------=_Part_61072_577381436.1400030180529\nContent-Type: image/png\nContent-Transfer-Encoding: base64\nContent-ID: <image-dd72b895-de9b-4559-9756-6067b47f64b5@square.com>\nContent-Disposition: INLINE\n\n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\n------=_Part_61072_577381436.1400030180529\nContent-Type: image/png\nContent-Transfer-Encoding: base64\nContent-ID: <image-51be63f9-05c3-49ef-8268-e937035bbe29@square.com>\nContent-Disposition: INLINE\n\niVBORw0KGgoAAAANSUhEUgAAADwAAABACAYAAABGHBTIAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ\nbWFnZVJlYWR5ccllPAAAAyNpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdp\nbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6\neD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1\nMTQ4MSwgMjAxMy8wMy8xMy0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJo\ndHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlw\ndGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAv\nIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RS\nZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpD\ncmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChNYWNpbnRvc2gpIiB4bXBNTTpJbnN0YW5j\nZUlEPSJ4bXAuaWlkOkVBNjg2MTc5MkZBNzExRTNCQ0Y1OTQ3RDQ4MTgzQjJCIiB4bXBNTTpEb2N1\nbWVudElEPSJ4bXAuZGlkOkVBNjg2MTdBMkZBNzExRTNCQ0Y1OTQ3RDQ4MTgzQjJCIj4gPHhtcE1N\nOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RUE2ODYxNzcyRkE3MTFFM0JD\nRjU5NDdENDgxODNCMkIiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6RUE2ODYxNzgyRkE3MTFF\nM0JDRjU5NDdENDgxODNCMkIiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94Onht\ncG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4MVoyyAAAC/klEQVR42uyaz29NQRTH72382Ajqd9GI\nldoQIULbhLCiRLDhsZJI/AGi/4Ol5RMLyUvtJFoaC69VVBfS+NGNrQRh4UfQNFV6fU/eiHqZuZ3n\nnTu/zEk+izf33nnznTPnzI970yzLkv/JFhj4j/XgKOgG28AGsFRc+wLegAnwEAyAt4W2hjxcEF1g\nAPzM9I3u7QedRbWriEpXgL6sOZsFFdDK3b6UOYZ3gFugnam+VyIcJrgayCl4OxgBy5ij7hPYxyW6\nhalRa8BgjtgpUAY9YC11NFgkRkKPuDaleLYV3AGrXEpa/TnxeBW0adTRJu5V2U1XktYhRQOnQekf\n6iuJZ2V20AXBo4osW2qiztOijnq7Z1twh8ITZYaOLCs6cnMz9TabtI5Jyj6DXob00ivq+mtWAcdt\nZuluSVlFTCUc01FFUt5pU/BWSdldxjlYVleHTcEbJWXPGAU/VWxGrK20ZA+n3Psbye8WWx62YanN\nIe2dRcFRsNoW+9juRh+kLd1F8BxM5mRVTmT2XeyPLzXa8Y1MSzTn3hYbfZeMhB8GrzkFk2cfg52O\nhiYtUPYIz7MM6fMOi/19lnaBM4bPeJCAT3EO6a9gieOCJ3XaqCvYl/cxKdeQjguPKDgKluaB6+BA\nUju4p4P1veAKmDEW5IaS1iw4C24orlMnDDKsz51JWuUcsWRD4HJIHt4Fxue5Zwt4WbSHTQleCT7O\nc89CnbWwL0NaJzZXh5Sl92vcczKkGKbYpDcGqjcSm8ATBi87M6TpbcEDkbzqG3gCPDI1pE1vHt6D\ndXN+02dTH5I/nzEFt3kYrvv9Q3g+2LX0iKSsGrLgIduCTcYwnSq2K+LundhQBBXD93M6sxrikB5u\ncKh7LzhPVDXEGDaix7Usbd2i4Ci4ZtMeaPnGKXjcA8EvOAX3eSBYq4260xJt40bBbkfF0uFBV6Jx\nvq3rYdrGHQFjDoodE22b4fTwXE+fA6Wk9unDcksi6Stb+s6EzrqvCYckRQiO83AUHAVHwVGwSfsl\nwADqBTnGvkkd+AAAAABJRU5ErkJggg==\n------=_Part_61072_577381436.1400030180529\nContent-Type: image/png\nContent-Transfer-Encoding: base64\nContent-ID: <image-97de2d61-9082-46fa-b927-32b6151aaa9d@square.com>\nContent-Disposition: INLINE\n\niVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ\nbWFnZVJlYWR5ccllPAAABDpJREFUeNrsm2tIVEEUx6+PosxHpRYSFGloZoKkVpQ9PlQU9IKoJIug\noof2DouIKIsIITAKow9FkJagFVIfIoqKsCjKvkSkYllEGD00zSDR3P4Hj3RZXHfu7pndu+mBH3sX\nLjP7v3Nn5sx/ZoMcDofRnyLY6GcxIPh/j1CNZQ8BWWAmSAaJYAwYyfV2gibwCdSBN+AxqAK/df2o\nIOFBi0SuBGvAXP5uNUjsQ3AVVEiLlxI8HOwFuSBa8Pd9A8XgNPghUiIJ9oIQkAeaHHqDyt8Kgr38\nvV61cBIoBRk+HHOegRzw1tejNPXRFz4WSzENvASrfSn4MLdsuJ9mlkhQBvJ90YeLHPaKk1b7sJWb\nCxz2jHwdg1YOv8Z2DBKwClyTmocngud+7LMq0QLSVUZvd4NWCLhic7EUUeAyNaC3gvPAlABZF8wA\nm7x5pUeABn56gRJfQTxo86SFd2kUG6Sp3FiwzZMWHgY+citLjqbVnB5uB+dBGsgUXpc3gnGgw0ri\nkSM8Vz4Ak1zUNR5cAl2C9S2zmnjcEay8kldV7pKCbNAuVGeFFcHhoEOo4l8gzlR2AjjL18VgulPd\nu4Xq/QkGqwpeKNi6103lpvIDcPB3B7/GhYDGkjC+XyqyehPcm6c1S3AAqTFdkxsS1stovZ9ng3Th\n5WYW+2NuTbxk4ZG5J5r6uG+LhikqSXUeThSsNMF0XcgLEF9tdSSqCo4TrHQpGMvXrWAdmMDfqzUL\njlMVHCFYaRi7E+aM7R1/ZrD4Y6BZg+AI1cRDR9SCRX3MwTHgtnCdLaoGQIfGHQky4ErYZw5y6s9U\n5z0wR9DTjlXJpZvZWNcZJPYDiz9lMtkz2GyQiAZeObntw40+GkVpMDsEnhr/tmTI+v0iuIhQGrTq\nNIj742a+zNTwwOtUBdcICqXusRhku7nvu+l6tIYsr89ReongSJnsZPN2OeXSFGWme+J159K9CY4C\nnUKVHnQqezYo5+tbINdp6VgkuFoKtbIevi9UMT24jYom+QLBZWm5q3pcWSslQv2IbN4LnG25ytHJ\nTjoAbgrO/6VWPa1I9rQihVdOlHg8ATvBUZAC5gubhR55WgZvVOkKQ2PZezzdW4oB7/mV6xe+NOWi\nBUZgxZG+xLrbeaAYxP1ucgCIreKFR5c3gilS2TwfamOxzeyJNbi7UcXxfwV22FgstdgGFbGqgiku\nghM2FUznwyrVH4/6cQF6/c/Z7LhDgc4zHj2ij9tAaBfvUhi6BfdA+XGbn8S2ghX+OImXwjlrmg/7\nK6Wl60G9pwV4sy/7mp2Kfew56wxKgjbz9km9NwVJnaaN5qmLFgWSm+jkb9Fp2jOG0Gla6fPSZLwv\nB2vBPM7UrAadj77LS8oboF3ULtX4rxZaWtJO5FSj+6wXmXWjjO4jUBHcDSjv/Qxq2YN6xC5mwJyI\nt30M/KtlQPCA4MCOvwIMALRqfCmpqxXQAAAAAElFTkSuQmCC\n------=_Part_61072_577381436.1400030180529\nContent-Type: image/png\nContent-Transfer-Encoding: base64\nContent-ID: <image-399bc086-a392-46b6-b70c-cbbab92bac55@square.com>\nContent-Disposition: INLINE\n\niVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ\nbWFnZVJlYWR5ccllPAAAAOxJREFUeNrs2rEJAkEQheFbPQMzS7EMMw3EIgxExDasQQsQE7GOw0gz\nS7hQLjjWN3KBBYjCzD/wuEk/5thd2E055yJS9YpgBRgwYMCAAQMG/HPwQNkqd6VVspO0nWmj9A2a\ndJYu9T0rE+fDvShTA5t+F+SPXhu4UjMOAq4MXKsZBQE/o63Sw3DbUvnRJ+fWzEkLMGBfi1ZmwoAB\nAwYMGDBgwIABAwYMGDBgwIABA/53vS/THoHANwMfAoH3dj9sbx+Oysw59qQsbML28GOurJSr0jhC\nNp1paVizJh6IAwYMGDBgwIABA/5OvQQYAPu/YgTnDZhFAAAAAElFTkSuQmCC\n------=_Part_61072_577381436.1400030180529--\n\n------=_Part_61073_141710048.1400030180530--\n"
  },
  {
    "path": "mailpile/tests/data/Maildir/cur/no-subject-1400067892.mbx",
    "content": "From nobody Fri Mar  7 15:17:43 2014\nContent-Type: text/plain; charset=UTF-8\nMIME-Version: 1.0\nFrom: ohcheeou@test.local\nTo: another@test.local\n\nTest\n"
  },
  {
    "path": "mailpile/tests/data/Maildir/new/.gitkeep",
    "content": ""
  },
  {
    "path": "mailpile/tests/data/Maildir/tmp/.gitkeep",
    "content": ""
  },
  {
    "path": "mailpile/tests/data/contacts/README",
    "content": "contacttest.vcf, contacttest.ldif and contacttest.mork are from:\n https://wiki.mozilla.org/Examples_of_various_Address_Book_formats\n\n"
  },
  {
    "path": "mailpile/tests/data/contacts/contacttest.ldif",
    "content": "dn: cn=Douglas Adams,mail=Douglas@douglasadams.com\nobjectclass: top\nobjectclass: person\nobjectclass: organizationalPerson\nobjectclass: inetOrgPerson\nobjectclass: mozillaAbPersonObsolete\ngivenName: Douglas\nsn: Adams\ncn: Douglas Adams\nmail: Douglas@douglasadams.com\nmodifytimestamp: 0Z\nmozillaNickname: Douglas_adams\nhomePhone: (858) 555-0042\nmobile: (858) 555-4200\nstreet: 42 some street\nl: some city\npostalCode: 00042\nc: England\nworkurl: http://www.douglasadams.com\n"
  },
  {
    "path": "mailpile/tests/data/contacts/contacttest.mork",
    "content": "// \n< <(a=c)> // (f=iso-8859-1)\n (B8=Custom4)(B9=Notes)(BA=LastModifiedDate)(BB=RecordKey)\n (BC=AddrCharSet)(BD=LastRecordKey)(BE=ns:addrbk:db:table:kind:pab)\n (BF=ListName)(C0=ListNickName)(C1=ListDescription)\n (C2=ListTotalAddresses)(C3=LowercaseListName)\n (C4=ns:addrbk:db:table:kind:deleted)\n (80=ns:addrbk:db:row:scope:card:all)\n (81=ns:addrbk:db:row:scope:list:all)\n (82=ns:addrbk:db:row:scope:data:all)(83=FirstName)(84=LastName)\n (85=PhoneticFirstName)(86=PhoneticLastName)(87=DisplayName)\n (88=NickName)(89=PrimaryEmail)(8A=LowercasePrimaryEmail)\n (8B=SecondEmail)(8C=PreferMailFormat)(8D=PopularityIndex)\n (8E=AllowRemoteContent)(8F=WorkPhone)(90=HomePhone)(91=FaxNumber)\n (92=PagerNumber)(93=CellularNumber)(94=WorkPhoneType)(95=HomePhoneType)\n (96=FaxNumberType)(97=PagerNumberType)(98=CellularNumberType)\n (99=HomeAddress)(9A=HomeAddress2)(9B=HomeCity)(9C=HomeState)\n (9D=HomeZipCode)(9E=HomeCountry)(9F=WorkAddress)(A0=WorkAddress2)\n (A1=WorkCity)(A2=WorkState)(A3=WorkZipCode)(A4=WorkCountry)\n (A5=JobTitle)(A6=Department)(A7=Company)(A8=_AimScreenName)\n (A9=AnniversaryYear)(AA=AnniversaryMonth)(AB=AnniversaryDay)\n (AC=SpouseName)(AD=FamilyName)(AE=DefaultAddress)(AF=Category)\n (B0=WebPage1)(B1=WebPage2)(B2=BirthYear)(B3=BirthMonth)(B4=BirthDay)\n (B5=Custom1)(B6=Custom2)(B7=Custom3)>\n<(80=0)>\n{1:^80 {(k^BE:c)(s=9)} \n [1:^82(^BD=0)]}\n@$${1{@\n<(8E=1)(81=Douglas)(82=Adams)(83=Douglas Adams)(84\n   =Douglas@douglasadams.com)(85=douglas@douglasadams.com)(86\n   =Douglas_adams)(87=(858\\) 555-0042)(88=(858\\) 555-4200)(89\n   =42 some street)(8A=some city)(8B=00042)(8C=England)(8D\n   =http://www.douglasadams.com)>\n{-1:^80 {(k^BE:c)(s=9)} \n [1:^82(^BD=1)]\n [-1(^83^81)(^84^82)(^87^83)(^89^84)(^8A^85)(^88^86)(^90^87)(^93^88)\n   (^9F^89)(^A1^8A)(^A3^8B)(^A4^8C)(^B0^8D)(^BB=1)]}\n@$$}1}@\n"
  },
  {
    "path": "mailpile/tests/data/contacts/contacttest.vcf",
    "content": "BEGIN:VCARD\nVERSION:3.0\nN:Adams;Douglas;;;\n FN:Douglas Adams\nNICKNAME:Douglas_adams\nEMAIL;type=INTERNET;type=HOME;type=pref:Douglas@douglasadams.com\nTEL;type=CELL:(858) 555-4200\nTEL;type=HOME;type=pref:(858) 555-0042\nitem1.ADR;type=WORK;type=pref:;;42 some street;some city;some state;00042;England\nitem1.X-ABADR:us\nitem2.URL;type=pref:http\\://www.douglasadams.com\nitem2.X-ABLabel:_$!<HomePage>!$_\nX-ABUID:46BA1516-8EA6-4687-8838-A7EC4F190D01\\:ABPerson\nEND:VCARD\n"
  },
  {
    "path": "mailpile/tests/data/gpg-keyring/testing.gpg.pub",
    "content": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: GnuPG v1.4.14 (GNU/Linux)\n\nmI0EUtAPXgEEAK6KUVSqSE9lvWDYuIoNURj4K5Zq0GDW7FPnZHbSCYprs+vdKSz1\nTeUS4DaAN2h2NRUo8e+r64KcnHNR+JXcYHvv9WmhzbAa5AZH6jPyPcd+CqDG53V4\nahFbzdTppSzQBkfgBnRq6PAnKjynJxtEipcEQv8m+43iuOQCjP8bBblrABEBAAG0\nOk1BSUxQSUxFIFRFU1RJTkcgKERvIE5vdCBUcnVzdCkgPHRlYW0rdGVzdGluZ0Bt\nYWlscGlsZS5pcz6IuAQTAQIAIgUCUtAPXgIbAwYLCQgHAwIGFQgCCQoLBBYCAwEC\nHgECF4AACgkQPZVbXXhIJS+N6gP9ENBX6/+jI9yIgi1E2/WMI4CfGNG5DD2B0+Hw\nQvowEB8wxlfFUuGN3V1CNyt21oweVNSQ5OdWCbQqqovhqGyCW/TSLQaNiMXhJYAG\nRu6cGzlO0muMhv9338a4DMVANqm9Q/IrmXk2ZrIBwMZQqkRhrlXmdc6rLAfZ3FFY\nMIRPnPe4jQRS0A9eAQQA3Kc35DexhSGP+wUhFnKY/Fqrr0F8yEhEMMGCs0qzYli6\nn/zT/6C88DrggSa3nEqmrd2FdBO+dm73AcxdnCT+/TJMIOAfKwkF87rrxSHwOCrK\nw3CQQ7xpabHvs9hWFx11FJ4T6sphZsiD+Xs1dQR0ZjRhbTNUR/F/h+Igz+HWUIUA\nEQEAAYifBBgBAgAJBQJS0A9eAhsMAAoJED2VW114SCUvozMD/05QFso5Nb5k8vG+\nqEMS1hLyhjZiAin94jNDaFi9URac8NVcmdNT8nL1Uklo906Zhwjd6HIOeK+FNoFA\nz335XBWWW/RHQh25OMTzIgzMmgiV4VeU/5BYnwVgdAN/uTkDtzUXG/kG9SVjxrak\n44rNc/cqfXi4xioE6dvtNAuYlBhp\n=xBVV\n-----END PGP PUBLIC KEY BLOCK-----\n"
  },
  {
    "path": "mailpile/tests/data/pgp-data/buildexamples.py",
    "content": "from __future__ import print_function\nimport os\nimport email\nimport mailbox\nfrom subprocess import Popen, PIPE\n\ndef getSourceFiles():\n    return os.listdir(\"sources\")\n    \ndef getProcesses():\n    return [\"--armor --sign\", \"--armor --clearsign\", \"--recipient 0x5AB5B329 --armor --encrypt\",\n            \"--sign\", \"--recipient 0x5AB5B329 --encrypt\"]\n\ndef runPGP(input, params):\n    print(\"Running PGP\")\n    params = params.split(\" \")\n    params.insert(0, \"../gpg-keyring/\")\n    params.insert(0, \"--home\")\n    params.insert(0, \"gpg\")\n    pr = Popen(params, stdin=PIPE, stdout=PIPE)\n    pr.stdin.write(input)\n    pr.stdin.close()\n    pr.wait()\n    m = pr.stdout.read()\n    return m\n\ndef genExamples():\n    output = mailbox.mbox(\"output.mbox\")\n    for source in getSourceFiles():\n\tcontents = open(\"sources/\" + source, \"r\").read()\n\tlanguage, charset = source.split(\".\")\n        for process in getProcesses():\n            print(\"Creating %s mail with %s encoding and %s PGP\" % (language,\n                  charset, process))\n            string = runPGP(contents, process)\n            e = email.message_from_string(string)\n            e.set_charset(charset)\n            e[\"from\"] = \"sender@test.mailpile.is\"\n            e[\"to\"] = \"recipient@test.mailpile.is\"\n            output.add(e)\n\n    output.close()\n\nif __name__ == \"__main__\":\n    genExamples()\n"
  },
  {
    "path": "mailpile/tests/data/pgp-data/sources/ar.iso-8859-6",
    "content": "       .           .\n"
  },
  {
    "path": "mailpile/tests/data/pgp-data/sources/ar.utf-8",
    "content": "يولد جميع الناس أحرارًا متساوين في الكرامة والحقوق. وقد وهبوا عقلاً وضميرًا وعليهم أن يعامل بعضهم بعضًا بروح الإخاء.\n"
  },
  {
    "path": "mailpile/tests/data/pgp-data/sources/ar.windows-1256",
    "content": "       .           .\n"
  },
  {
    "path": "mailpile/tests/data/pgp-data/sources/cn.utf-8",
    "content": "人人生而自由,在尊严和权利上一律平等。他们赋有理性和良心,并应以兄弟关系的精神相对待。"
  },
  {
    "path": "mailpile/tests/data/pgp-data/sources/gr.iso-8859-7",
    "content": "          .      ,         ."
  },
  {
    "path": "mailpile/tests/data/pgp-data/sources/gr.utf-8",
    "content": "Ολοι οι άνθρωποι γεννιούνται και ίσοι στην αξιοπρέπεια και στα δικαιώματα. Είναι προικισμένοι με λογική και συνείδηση, και οφείλουν να συμπεριφέρονται μεταξύ τους με πνεύμα αδελφοσύνης."
  },
  {
    "path": "mailpile/tests/data/pgp-data/sources/gr.windows-1253",
    "content": "          .      ,         ."
  },
  {
    "path": "mailpile/tests/data/pgp-data/sources/ru.koi8-ru",
    "content": "          .               .\n"
  },
  {
    "path": "mailpile/tests/data/pgp-data/sources/ru.utf-8",
    "content": "Все люди рождаются свободными и равными в своем достоинстве и правах. Они наделены разумом и совестью и должны поступать в отношении друг друга в духе братства.\n"
  },
  {
    "path": "mailpile/tests/data/pgp-data/sources/ru.windows-1251",
    "content": "          .               .\n"
  },
  {
    "path": "mailpile/tests/data/pgp-data/sources/vi.utf-8",
    "content": "Tất cả mọi người sinh ra đều được tự do và bình đẳng về nhân phẩm và quyền. Mọi con người đều được tạo hóa ban cho lý trí và lương tâm và cần phải đối xử với nhau trong tình bằng hữu."
  },
  {
    "path": "mailpile/tests/data/pub.key",
    "content": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: GnuPG v1\n\n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\n6c4Vzx1HxD7MadRhrubJtFE=\n=AQD0\n-----END PGP PUBLIC KEY BLOCK-----\n"
  },
  {
    "path": "mailpile/tests/data/tests.mbx",
    "content": "From kde-isl+owner@example.com Mon Sep 16 14:55:07 2013\nReturn-path: <kde-isl+owner@example.com>\nEnvelope-to: bre@localhost\nDelivery-date: Mon, 16 Sep 2013 14:55:07 +0000\nReceived: from localhost ([::1] helo=hottie)\n\tby hottie with esmtp (Exim 4.80)\n\t(envelope-from <kde-isl+owner@example.com>)\n\tid 1VLaCg-0000WS-Km\n\tfor bre@localhost; Mon, 16 Sep 2013 14:55:06 +0000\nDelivered-To: bjarni.runar@gmail.com\nReceived: from gmail-pop.l.google.com [173.194.71.109]\n\tby hottie with POP3 (fetchmail-6.3.21)\n\tfor <bre@localhost> (single-drop); Mon, 16 Sep 2013 14:55:06 +0000 (GMT)\nReceived: by 10.58.13.104 with SMTP id g8csp66176vec;\n        Sun, 15 Sep 2013 17:58:58 -0700 (PDT)\nX-Received: by 10.220.88.13 with SMTP id y13mr1467434vcl.20.1379293137579;\n        Sun, 15 Sep 2013 17:58:57 -0700 (PDT)\nDomainKey-Status: bad format\nReceived-SPF: softfail (google.com: best guess record for domain of transitioning kde-isl+owner@example.com does not designate <unknown> as permitted sender)\nReceived: by 10.230.42.14 with POP3 id q14mf4145517vbe.39;\n        Sun, 15 Sep 2013 17:58:57 -0700 (PDT)\nX-Gmail-Fetch-Info: bre@example.com 1 example.com 110 bre\nReceived: from localhost.localdomain (localhost.localdomain [127.0.0.1])\n\tby example.com (8.12.11.20060308/8.12.11) with ESMTP id r8G0R3Ag013323\n\tfor <bre@example.com>; Mon, 16 Sep 2013 00:27:03 GMT\nDate: Mon, 16 Sep 2013 00:27:03 GMT\nMessage-Id: <201309160027.r8G0R3Ag013323@example.com>\nMime-Version: 1.0\nContent-Type: text/plain; charset=iso-8859-1\nFrom: test&test <kde-isl+askrift@example.com>\nSubject: Moderation request [kde-isl] <bang>\nX-Loop: kde-isl\nStatus: RO\nContent-Length: 7195\nLines: 147\n\nVinsamlegast lestu yfir eftirfarandi skilabo.  Ef  telur a samrmast\nmarkmium \"kde-isl\" skaltu svara essu brfi me eftirfarandi\nskipun  annars auri lnu  meginmli brfsins:\n\n\tkjsa 523650573407581c06e\n\nEa, ef  vilt samykkja brfi n ess a f kvittun:\n\n\tegjandi kjsa 523650573407581c06e\n\nFresta skilabo: \n> To: kde-isl@example.com\n> From: תԱ <il@park.4.cn>\n> Subject: м6ÿkde-isl\n> \n> ҵĸ߲߱Դ\n> ôв߾ǡã쵼ͷ\n> \n> м6ÿ\n> \n> 2컥ʽѵ   15䰸\n> Աӣв൱ڶ֣һõĶ֣ɱɻ򣻶ֲλҲҪɳ\n> \n> 20130926-27 \n> 20130928-29 \n> 20131109-10 \n> 20131116-17 Ϻ\n> 20131123-24 \n> \n> γ̷ã4800Ԫ/죬һһٴۣһշ3600ԪϷѡͼȣ\n> ڿζҵܡžܡвԱεġרҵ˲ת͵ġһ߹Чġ\n> ߲ԼԤԱ\n> \n> ѯ绰020-80560638   0755-61287552  021-51602856  Ҫżظtuixin2013@126.comţ\n> -----------------------------------------------------------------------------------------------\n> ѦӺ\n> ѵר\n> 廪ѧܲðƸʦ\n> ʱ⻪ʦų\n> йְҵЭ\n> йְҵѵѧԺ\n> κ춹Ź\n> νտмŹ\n> νԹʻ³\n> ѵҵＯšһš¶ࡢн֡춹šżšԶš켯šӽҩҵ\n> šϲ̩ïʣɽγĻơơൺͨݹС㽭꼯š̳\n> 鼯šߺ֡Ϸ͡ǵӡɽѧԺֹ֡йСʿҩҵ\n> ͨʵҵܹ˾е缯šսͨšǷزչâšйѧоԺ ȡ\n> Св㾭ձ磩вѧ磬ִ̣Ŷӡձ磩  \n> --------------------------------------------------------------------------------\n> ΪʲôҪѵ6ÿΣ\n> йҵвɲܶǰ·ҡԭҵǸɡ֣ʱ컯Ƶλãҵһ\n> ֣¡һ˰һ̯ãɫתס\n> ѦӺʦҺҵвɲİĿγ̺üˡѵصʵѦӺʦвɲ\n> ٵġ¡̸вɲٵġˡ˾ˣͬˣҲˣвɲǸ˴򽻵\n> ǻΪǸµıǿΪʧܣвɲʧܡ\n> ѦӺʦĿγ̣һЩְǱǱǹ˾ƶҵģҲǴѧｲڵģǱ򣬲\n> Ǳ¡Ϊˣп˾ʶ㣬ͬ㣬㣬ְĲ˳չ򣬼ʹɾ죬Ҳ\n> пⲻֺá衣\n> Ҫвɲγ̸˴𰸡\n>                     ԶŶ³  Ϊв㾭\n> --------------------------------------------------------------------------------\n> һÿ  ȷԼҵĶλ\n> 1.ҵĺṹ߲ҪоҪжвҪִ\n> 2.Ϊʲôв㣨ʲôִ֣λ\n> 3.вѹأ˾Ͽɡ֧ͬ֡Ƴ磩\n> 4.вȺ䡢С֮\n> 5.вһߣжΣԱۣϰֻ¥\n> 6.в㲻ͬ׶εĶλ\n>         ۣ쵼в㡰аôɵģ\n> Դôͳֺã\n> ڶÿ  εõ쵼Ͽ \n> 1.쵼ǶԵģִУ쵼һʱһӣڶͨ\n> 2.쵼Ƿǣ£ϴ´£\n> 3.ά쵼ţ˺󣨳߶߶\n> 4.˵ϣ㱨̸ʾ˵\n> 5.쵼ѡ⣺˼ʴԶԼ\n> 6.쵼ˣڵûл˾ûкˣûл˾ûִ\n>   ۣǴСģܴ죬һս¾Ǻã\n> Աչϰ巢ŭһвɲô죿\n> ÿ  νп粿Э \n> 1.ϧԵΪͬԱгͻûгͻûиƣ\n> 2.أӵһڶӾøУøоɰܣ\n> 3.ߵ£͵ˣǲǺþ\n> 4.˼գڷ£ˣ\n> 5.ˣõ壬㣨Эáأְҵ˱زٵ\n> ۣ\n> ЭӦԹ˾ڲϵ֮\n> ְȨĲŲҵʣô죿\n> ÿ   ε  \n> 1.ǮҪΨһ̸нˮߣ̸нˮǺߣ\n> 2.֮ŪΪ˭䱧Թнˮ٣ָλֵͣ\n> 3.˵뷨Լ˵\n> ˵ɴҵ\n> 4.ͷ٣ӲǮı￪ʼҪ˷ܣ˷ܼ\n> 5.΢ŽһдӹԳĹߣӦǼ֣\n>          ۣԱԸͻԷǮòñ\n> ̻ƽ𼾽ˣԱҸô죿\n> ¹800Ĵѧײʹ˾ʧ3δ\n> ÿ   ιܺòżЧ   \n> 1.ɫת죨õĹ߾Ǻý\n> 2.֣ޡ·ޡˡ ̫練˼Ϊɶ̲ã\n> 3.۽Ч̸Ϊ½ۣ£ӽۣǸԱΪķ\n> 4.ץסؼʲô͵õʲôȷһԱ\n> 5.Ŀƹ̲ܿƽĿ꼨Ч̽յƣ\n> 6.ʹ׷֣ӱʹࣨʲĴʣĴ\n> ۣ󻻽̯ôٳЧ\n> Ա˽̺ô©ô£\n> пˣԱΧ ô죿\n> ÿ   δŶ   \n> 1.ŶΪӢۻ䣿˵ŬʵĿ꣩\n> 2.ϷҲ˵淨ϷΪ˽ƽ⡢Ч⣩\n> 3.ͬ۹ãһӪ찲ȫСУԱ̬⣬ǹߵΣ\n> 4.˫£һץƶȣһץĻǾ   \n> ӣѧУǼͥҵĻ \n> ˵Ķ˵ˣ\n> 5.ҵࣺԱѵģѵǵڶ\n> ۣԡ\n> ôӦԡֳӲ\n> ӱõԱ\"ζ\"ô?\n> --------------------------------------------------------------------------------\n> м6ÿΡִ봫020-62351156\n> \n>    λ  ƣ_______________________________________________________\n> \n>    㣺 Ϻ \n> \n> ϵˣ______________绰:________________:________________\n> \n> ʼ______________    :_________   _________Ԫ\n> \n>   ˣ___________  ְ ____________   _____________\n> \n>   ˣ___________  ְ ____________   _____________\n> \n>   ˣ___________  ְ ____________   _____________\n> \n>   ˣ___________  ְ ____________   _____________\n> \n> ʽѡ򡰡̡ 1ֽ 2ת 3\n> ====================================================================================\n> ע:ѱִش˾Ϊȷ,ٴε绰020-80560638ȷ!\n> \n> \n\n-- \nAnOmY!\n\nFrom bounce-mc.us2_2315386.626585-bre=example.com@mail71.us2.mcsv.net Mon Sep 16 14:57:46 2013\nReturn-path: <bounce-mc.us2_2315386.626585-bre=example.com@mail71.us2.mcsv.net>\nEnvelope-to: bre@localhost\nDelivery-date: Mon, 16 Sep 2013 14:57:46 +0000\nReceived: from localhost ([::1] helo=hottie)\n\tby hottie with esmtp (Exim 4.80)\n\t(envelope-from <bounce-mc.us2_2315386.626585-bre=example.com@mail71.us2.mcsv.net>)\n\tid 1VLaDH-0000WS-9N\n\tfor bre@localhost; Mon, 16 Sep 2013 14:55:43 +0000\nDelivered-To: bjarni.runar@gmail.com\nReceived: from gmail-pop.l.google.com [173.194.71.109]\n\tby hottie with POP3 (fetchmail-6.3.21)\n\tfor <bre@localhost> (single-drop); Mon, 16 Sep 2013 14:55:43 +0000 (GMT)\nReceived: by 10.58.13.104 with SMTP id g8csp88159vec;\n        Mon, 16 Sep 2013 05:31:02 -0700 (PDT)\nX-Received: by 10.58.211.227 with SMTP id nf3mr5380141vec.20.1379334661893;\n        Mon, 16 Sep 2013 05:31:01 -0700 (PDT)\nDomainKey-Status: good\nReceived-SPF: pass (google.com: domain of bounce-mc.us2_2315386.626585-bre=example.com@mail71.us2.mcsv.net designates 173.231.139.71 as permitted sender) client-ip=173.231.139.71;\nReceived: by 10.220.241.204 with POP3 id lf12mf646056vcb.9;\n        Mon, 16 Sep 2013 05:31:01 -0700 (PDT)\nX-Gmail-Fetch-Info: bre@example.com 1 example.com 110 bre\nReceived: from mail71.us2.mcsv.net (mail71.us2.mcsv.net [173.231.139.71])\n\tby example.com (8.12.11.20060308/8.12.11) with ESMTP id r8GBsJu9029942\n\tfor <bre@example.com>; Mon, 16 Sep 2013 11:54:20 GMT\nDKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; s=k1; d=mail71.us2.mcsv.net;\n h=Subject:From:Reply-To:To:Date:Message-ID:List-Unsubscribe:Sender:Content-Type:MIME-Version; i=no-reply=3Dexample.com@mail71.us2.mcsv.net;\n bh=u+QXUC6y09NZ67omxLcXE8rGGCY=;\n b=s3pZDnuRsE/B/IRQtbBPJOATokU0EUpqoRd9NOgtyLBhjRLEqfxmVF6JIO9tFztNYb1cyjx6+w3u\n   zfmCzLeNchOStST9df2AspC6ypSUaGLFrCgtU2KIVo28Smn6DcA6vdGOfVZ2mce2uifqIy1ECxZM\n   idsk87CdqM3MaiygP9w=\nDomainKey-Signature: a=rsa-sha1; c=nofws; q=dns; s=k1; d=mail71.us2.mcsv.net;\n b=pCg3E4g73wFqfv5Xfi9gRjA4y9qCQXaXTs5o3mMbQyY8v/BIhX6ak2u+OEasKZ57Qjaiww0fhSdx\n   EAzLZhDZaV25JI4g2T+e3npDO3D/rRSfBBdHLbtrlShkLKAufMwGF3O2i6lOsbj0AnI/hGkqe62H\n   1Se5MUjYAsZ6WNX+vck=;\nReceived: from (127.0.0.1) by mail71.us2.mcsv.net (PowerMTA(TM) v3.5r16) id h6romo174gsk for <bre@example.com>; Mon, 16 Sep 2013 11:54:18 +0000 (envelope-from <bounce-mc.us2_2315386.626585-bre=example.com@mail71.us2.mcsv.net>)\nSubject: =?utf-8?Q?=E2=9C=88=20Northern=20Lights=20Extravaganza=3A=20Iceland=20from=20DKK599=2F=C2=A369=2F=E2=82=AC96?=\nFrom: \"=?utf-8?q?Bjarni_R=C3=BAnar_Einarsson?=\" <no-reply@example.com>\nReply-To: =?utf-8?Q?WOW=20club?= <no-reply@example.com>\nTo: \"=?utf-8?b?U8O6c2FubmEgR2VzdHNkw7N0dGly?=\" <bre@example.com>\nDate: Mon, 16 Sep 2013 11:54:18 +0000\nMessage-ID: <cfa47c79110f45a8f68fab75b1f1636935c.20130916115400@mail71.us2.mcsv.net>\nX-Mailer: MailChimp Mailer - **CIDdb82fef6841f1636935c**\nX-Campaign: mailchimpcfa47c79110f45a8f68fab75b.db82fef684\nX-campaignid: mailchimpcfa47c79110f45a8f68fab75b.db82fef684\nX-Report-Abuse: Please report abuse for this campaign here: http://www.mailchimp.com/abuse/abuse.phtml?u=cfa47c79110f45a8f68fab75b&id=db82fef684&e=1f1636935c\nX-MC-User: cfa47c79110f45a8f68fab75b\nx-accounttype: pd\nIn-Reply-To: <201309160027.r8G0R3Ag013323@example.com>\nList-Unsubscribe: <mailto:unsubscribe-cfa5b-db84-15c@mailin1.us2.mcsv.net?subject=unsubscribe>, <http://example.us2.list-manage.com/unsubscribe?u=cfa75b&id=994&e=1f1c&c=db4>\nSender: \"WOW club\" <no-reply=example.com@mail71.us2.mcsv.net>\nx-mcda: FALSE\nContent-Type: multipart/alternative; boundary=\"_----------=_MCPart_2065307511\"\nMIME-Version: 1.0\nX-BRE-Whitelisted: procmail (no-reply@example.com)\nStatus: RO\nContent-Length: 92962\nLines: 2147\n\nThis is a multi-part message in MIME format\n\n--_----------=_MCPart_2065307511\nContent-Type: text/plain; charset=\"utf-8\"; format=\"fixed\"\nContent-Transfer-Encoding: quoted-printable\n\nOh hello there=2C\n\nHey guess what? The latest WOW newsletter is out:\nhttp://us2.campaign-archive1.com/?u=3Dcfa47c79110f45a8f68fab75b&id=3Ddb82f=\nef684&e=3D1f1636935c\n\n- - - - - - - -\n\nHighlighs:\n- Iceland this autumn for very little indeed.\n  Paris > Reykjavik from only 104 euros\n  London > Reykjav=C3=ADk from only 69 pounds\n  Berlin > Reykjav=C3=ADk from only 96 euros\n  Copenhagen > Reykjav=C3=ADk from only DKK599\n- Also: Northern Lights tours.\n- And finally: The ten most popular Iceland tours.\n\nMore on the web:\nhttp://us2.campaign-archive1.com/?u=3Dcfa47c79110f45a8f68fab75b&id=3Ddb82f=\nef684&e=3D1f1636935c\n\n\n- - -\n\nTime to say goodbye?\n\n--_----------=_MCPart_2065307511\nContent-Type: text/html; charset=\"utf-8\"\nContent-Transfer-Encoding: quoted-printable\n\n<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/xh=\ntml1/DTD/xhtml1-transitional.dtd\">\n\n\n<html>\n    <head>\n        <meta http-equiv=3D\"Content-Type\" content=3D\"text/html; charset=3D=\nUTF-8\">\n=09=09<meta name=3D\"author\" content=3D\"Design and template made for WOW by=\n Takk Takk in West-Germany=2C using brains and fancy machines\">\n=09=09<link rel=3D\"shortcut icon\" href=3D\"http://www.wowiceland.co.uk/site=\ns/all/themes/example/favicon.ico\" type=3D\"image/x-icon\">\n=09=09<link rel=3D\"image_src\" href=3D\"http://www.takktakk.com/forbeingagre=\natclient/wow/template/i/wow-plane-210.png\">\n=09=09<meta name=3D\"title\" content=3D\"=E2=9C=88 Northern Lights Extravagan=\nza: Iceland from DKK599/=C2=A369/=E2=82=AC96\">\n=09=09<meta name=3D\"description\" content=3D\"It's not a newsletter=2C it's=\n a WOWsletter.\">\n=09=09\n=09=09<!-- Facebook sharing information tags -->\n        <meta property=3D\"og:title\" content=3D\"=E2=9C=88 Northern Lights E=\nxtravaganza: Iceland from DKK599/=C2=A369/=E2=82=AC96\">\n        <meta property=3D\"og:description\" content=3D\"It's not a newsletter=\n=2C it's a WOWsletter.\">\n        <meta property=3D\"og:image\" content=3D\"http://www.takktakk.com/for=\nbeingagreatclient/wow/template/i/wow-plane-210.png\">\n\n        <title>=E2=9C=88 Northern Lights Extravaganza: Iceland from DKK599=\n/=C2=A369/=E2=82=AC96</title>\n\n\n\n\n=09\n<style type=3D\"text/css\">\n=09=09#outlook a{\n=09=09=09padding:0;\n=09=09}\n=09=09body{\n=09=09=09width:100% !important;\n=09=09}\n=09=09.ReadMsgBody{\n=09=09=09width:100%;\n=09=09}\n=09=09.ExternalClass{\n=09=09=09width:100%;\n=09=09}\n=09=09body{\n=09=09=09-webkit-text-size-adjust:none;\n=09=09}\n=09=09body{\n=09=09=09margin:0;\n=09=09=09padding:0;\n=09=09}\n=09=09img{\n=09=09=09border:0;\n=09=09=09height:auto;\n=09=09=09line-height:100%;\n=09=09=09outline:none;\n=09=09=09text-decoration:none;\n=09=09}\n=09=09table td{\n=09=09=09border-collapse:collapse;\n=09=09}\n=09=09#backgroundTable{\n=09=09=09height:100% !important;\n=09=09=09margin:0;\n=09=09=09padding:0;\n=09=09=09width:100% !important;\n=09=09}\n=09=09td.left{\n=09=09=09text-align:left;\n=09=09}\n=09=09td.right{\n=09=09=09text-align:right;\n=09=09}\n=09=09.purple{\n=09=09=09color:#99248D;\n=09=09}\n=09=09.blue{\n=09=09=09color:#40c7f4 !important;\n=09=09}\n=09=09.green{\n=09=09=09color:#5dc3ad !important;\n=09=09}\n=09=09.yellow{\n=09=09=09color:#ffdc01 !important;\n=09=09}\n=09=09.black{\n=09=09=09color:#000;\n=09=09}\n=09=09.twitter{\n=09=09=09color:#4b9cc8 !important;\n=09=09}\n=09=09.facebook{\n=09=09=09color:#3B5998 !important;\n=09=09}\n=09=09.bold{\n=09=09=09font-weight:bold;\n=09=09}\n=09=09h1=2C.h1{\n=09=09=09color:#99248D;\n=09=09=09display:block;\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C 'Lucida Gra=\nnde'=2C sans-serif !important;\n=09=09=09font-size:34px;\n=09=09=09font-weight:bold;\n=09=09=09line-height:100%;\n=09=09=09margin-top:0px;\n=09=09=09margin-right:0;\n=09=09=09margin-bottom:20px;\n=09=09=09margin-left:0;\n=09=09=09text-align:left;\n=09=09}\n=09=09h2=2C.h2{\n=09=09=09color:#000 !important;\n=09=09=09font-weight:bold;\n=09=09=09font-size:17px;\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C 'Lucida Gra=\nnde'=2C sans-serif !important;\n=09=09=09line-height:21px;\n=09=09=09margin-top:0px;\n=09=09=09margin-right:0;\n=09=09=09margin-bottom:20px;\n=09=09=09margin-left:0;\n=09=09=09text-align:left;\n=09=09}\n=09=09.fancybuttontableouter{\n=09=09=09background-image:url('http://www.takktakk.com/forbeingagreatclien=\nt/wow/template/i/hnappur.png');\n=09=09=09background-repeat:repeat-x;\n=09=09=09background-color:#33a9e5;\n=09=09=09border-radius:5px;\n=09=09=09border:1px solid #28C;\n=09=09=09height:32px;\n=09=09=09word-wrap:break-word;\n=09=09=09text-align:center;\n=09=09=09margin-top:20px;\n=09=09=09margin-bottom:24px;\n=09=09}\n=09=09.fancybuttontableinner{\n=09=09=09font-family:'Helvetica Neue'=2CHelvetica=2CArial=2Csans-serif;\n=09=09}\n=09=09.fancybuttonlink=2C.fancybuttonlink a:link=2C.fancybuttonlink a:visi=\nted{\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif=\n !important;\n=09=09=09font-size:16px;\n=09=09=09line-height:16px;\n=09=09=09font-weight:bold;\n=09=09=09color:white;\n=09=09=09text-align:center;\n=09=09=09text-decoration:none;\n=09=09=09text-shadow:0px -1px #666;\n=09=09=09text-transform:uppercase;\n=09=09}\n=09=09.smallbuttontableouter{\n=09=09=09background-image:url('http://www.takktakk.com/forbeingagreatclien=\nt/wow/template/i/hnappur.png');\n=09=09=09background-repeat:repeat-x;\n=09=09=09background-color:#33a9e5;\n=09=09=09border-radius:5px;\n=09=09=09border:1px solid #28C;\n=09=09=09height:20px;\n=09=09=09word-wrap:break-word;\n=09=09=09text-align:right;\n=09=09}\n=09=09.smallbuttontableinner{\n=09=09=09font-family:'Helvetica Neue'=2CHelvetica=2CArial=2Csans-serif;\n=09=09}\n=09=09.smallbuttonlink=2C.smallbuttonlink a:link=2C.smallbuttonlink a:visi=\nted{\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif=\n !important;\n=09=09=09font-size:12px;\n=09=09=09line-height:12px;\n=09=09=09font-weight:bold;\n=09=09=09color:white;\n=09=09=09text-align:center;\n=09=09=09text-decoration:none;\n=09=09=09text-shadow:0px -1px #666;\n=09=09=09text-transform:uppercase;\n=09=09}\n=09=09#preheader{\n=09=09=09background-color:#99248D;\n=09=09=09border-bottom:#5CC4F0 4px solid;\n=09=09}\n=09=09td.preheader{\n=09=09=09color:#fff;\n=09=09=09font-size:9px;\n=09=09=09font-family:'Lucida sans Unicode'=2C 'Lucida Grande'=2C 'Lucida G=\nrande'=2C Helvetica=2C Arial=2C sans-serif !important;\n=09=09=09line-height:100%;\n=09=09}\n=09=09td.preheader a{\n=09=09=09color:#fff;\n=09=09=09text-decoration:underline;\n=09=09}\n=09=09#header{\n=09=09=09background-color:#fff;\n=09=09=09border-bottom:#ddd 1px solid;\n=09=09}\n=09=09td.header{\n=09=09=09color:#666;\n=09=09=09font-size:13px;\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C 'Lucida Gra=\nnde'=2C sans-serif !important;\n=09=09=09line-height:100%;\n=09=09=09font-weight:bold;\n=09=09=09vertical-align:middle;\n=09=09}\n=09=09#opening{\n=09=09=09background-color:#fff;\n=09=09=09border-bottom:#ddd 1px solid;\n=09=09}\n=09=09.openingtext{\n=09=09=09color:#99248D;\n=09=09=09display:block;\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C 'Lucida Gra=\nnde'=2C sans-serif !important;\n=09=09=09font-size:34px;\n=09=09=09font-weight:bold;\n=09=09=09line-height:100% !important;\n=09=09=09margin-top:0px;\n=09=09=09margin-right:0;\n=09=09=09margin-bottom:20px;\n=09=09=09margin-left:0;\n=09=09=09text-align:left;\n=09=09}\n=09=09#tocbox{\n=09=09=09background-color:#f3ede6;\n=09=09=09border-bottom:#ddd 1px solid;\n=09=09}\n=09=09#tocbox ul{\n=09=09=09margin-bottom:1em !important;\n=09=09=09padding:0 !important;\n=09=09=09list-style-type:none !important;\n=09=09=09text-align:center !important;\n=09=09}\n=09=09#tocbox ul li{\n=09=09=09display:inline !important;\n=09=09=09padding:0 4px !important;\n=09=09}\n=09=09#mctoc a{\n=09=09=09margin:1em 0 !important;\n=09=09=09font-weight:normal !important;\n=09=09=09font-size:12px !important;\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C 'Lucida Gra=\nnde'=2C sans-serif !important !important;\n=09=09=09line-height:14px !important;\n=09=09=09text-transform:uppercase !important;\n=09=09=09color:#333 !important;\n=09=09=09text-decoration:underline !important;\n=09=09=09display:inline !important;\n=09=09=09list-style-type:none !important;\n=09=09=09text-align:center !important;\n=09=09}\n=09=09#mctoc{\n=09=09=09margin:1em 0 !important;\n=09=09=09font-weight:normal !important;\n=09=09=09font-size:12px !important;\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C 'Lucida Gra=\nnde'=2C sans-serif !important !important;\n=09=09=09line-height:14px !important;\n=09=09=09text-transform:uppercase !important;\n=09=09=09color:#333 !important;\n=09=09=09text-decoration:underline !important;\n=09=09=09display:inline !important;\n=09=09=09list-style-type:none !important;\n=09=09=09text-align:center !important;\n=09=09}\n=09=09td.toc p{\n=09=09=09margin:1em 0;\n=09=09=09font-weight:bold;\n=09=09=09font-size:15px;\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C 'Lucida Gra=\nnde'=2C sans-serif !important;\n=09=09=09line-height:18px;\n=09=09=09text-align:center;\n=09=09=09color:#000;\n=09=09=09text-transform:uppercase;\n=09=09}\n=09=09td.toc a{\n=09=09=09margin:1em 0;\n=09=09=09font-weight:normal;\n=09=09=09font-size:12px;\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C 'Lucida Gra=\nnde'=2C sans-serif !important;\n=09=09=09line-height:14px;\n=09=09=09text-transform:uppercase;\n=09=09=09color:#333;\n=09=09=09text-decoration:underline;\n=09=09}\n=09=09.multicitybox{\n=09=09=09background-color:#fff;\n=09=09=09border-bottom:#ddd 1px solid;\n=09=09=09padding-bottom:20px;\n=09=09=09padding-top:20px;\n=09=09}\n=09=09td.multicityunit{\n=09=09=09padding-top:20px;\n=09=09=09padding-bottom:0px;\n=09=09}\n=09=09.multicityunit p{\n=09=09=09margin-top:0;\n=09=09=09margin-bottom:5px;\n=09=09=09font:normal 18px Helvetica=2C Arial=2C sans-serif;\n=09=09=09line-height:18px;\n=09=09=09color:#333;\n=09=09=09text-align:center;\n=09=09}\n=09=09.from{\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif;\n=09=09=09text-align:center;\n=09=09=09font-size:12px;\n=09=09=09color:#333;\n=09=09=09font-weight:bold;\n=09=09}\n=09=09.city{\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif;\n=09=09=09text-align:center;\n=09=09=09font-size:26px;\n=09=09=09line-height:1em;\n=09=09=09font-weight:bold;\n=09=09}\n=09=09.packagedescription{\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif=\n !important;\n=09=09=09font-size:12px;\n=09=09=09margin:3px 0 9px;\n=09=09=09text-align:center;\n=09=09=09font-weight:bold;\n=09=09}\n=09=09.starting_at{\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif;\n=09=09=09text-align:center;\n=09=09=09font-size:16px;\n=09=09}\n=09=09.price{\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif;\n=09=09=09text-align:center;\n=09=09=09font-size:22px;\n=09=09=09line-height:22px;\n=09=09=09font-weight:bold;\n=09=09=09margin-top:0;\n=09=09=09margin-bottom:8px;\n=09=09}\n=09=09#continuedbox{\n=09=09=09background-color:#fff;\n=09=09=09border-bottom:#ddd 1px solid;\n=09=09=09padding-bottom:30px;\n=09=09=09padding-top:30px;\n=09=09}\n=09=09td.continued p{\n=09=09=09margin-bottom:1em;\n=09=09=09font-style:normal;\n=09=09=09font-size:16px;\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif=\n !important;\n=09=09=09line-height:21px;\n=09=09=09color:#333;\n=09=09}\n=09=09td.continued a{\n=09=09=09color:#99249d;\n=09=09}\n=09=09.continuedmaintext{\n=09=09=09margin-bottom:30px;\n=09=09=09font-style:normal;\n=09=09=09font-size:16px;\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif=\n !important;\n=09=09=09line-height:21px;\n=09=09=09color:#333;\n=09=09}\n=09=09.continuedmaintext a{\n=09=09=09color:#99249d;\n=09=09}\n=09=09.continuedmaintext img{\n=09=09=09padding-left:10px;\n=09=09=09padding-bottom:10px;\n=09=09}\n=09=09.itembox{\n=09=09=09background-color:#fff;\n=09=09=09border-bottom:#ddd 1px solid;\n=09=09}\n=09=09td.item{\n=09=09=09padding-top:40px;\n=09=09=09padding-bottom:40px;\n=09=09}\n=09=09td.item p{\n=09=09=09margin-bottom:1em;\n=09=09=09font-style:normal;\n=09=09=09font-size:16px;\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif=\n !important;\n=09=09=09line-height:21px;\n=09=09=09color:#333;\n=09=09}\n=09=09td.item a{\n=09=09=09color:#99249d;\n=09=09}\n=09=09.itemmaintext{\n=09=09=09margin-bottom:30px;\n=09=09=09font-style:normal;\n=09=09=09font-size:16px;\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif=\n !important;\n=09=09=09line-height:21px;\n=09=09=09color:#333;\n=09=09}\n=09=09.itemmaintext a{\n=09=09=09color:#99249d;\n=09=09}\n=09=09.itemmaintext img{\n=09=09=09padding-left:10px;\n=09=09=09padding-bottom:10px;\n=09=09}\n=09=09.airplanebox{\n=09=09=09padding-bottom:40px;\n=09=09=09border-bottom:#ddd 1px solid;\n=09=09=09background-color:#fff;\n=09=09}\n=09=09td.airplane{\n=09=09=09padding-top:40px;\n=09=09=09margin-bottom:16px;\n=09=09=09font-family:'Helvetica Neue'=2CHelvetica=2CArial=2Csans-serif !im=\nportant;\n=09=09=09color:#40c7f4;\n=09=09=09font-size:36px;\n=09=09=09line-height:36px;\n=09=09}\n=09=09.airplanetext{\n=09=09=09margin-bottom:12px;\n=09=09}\n=09=09.wowshoutbox{\n=09=09=09border-bottom:#ddd 1px solid;\n=09=09}\n=09=09td.wowshout{\n=09=09=09padding-top:40px;\n=09=09=09padding-bottom:40px;\n=09=09}\n=09=09.wowshoutblue{\n=09=09=09margin-bottom:1em;\n=09=09=09font-weight:normal;\n=09=09=09font-size:48px !important;\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif=\n !important;\n=09=09=09line-height:40px !important;\n=09=09=09letter-spacing:-1px;\n=09=09=09margin:0 0 0 0;\n=09=09=09color:#40c7f4 !important;\n=09=09}\n=09=09.wowshoutblue a{\n=09=09=09color:#40c7f4 !important;\n=09=09=09text-decoration:none !important;\n=09=09}\n=09=09.wowshoutgreen{\n=09=09=09margin-bottom:1em;\n=09=09=09font-weight:normal;\n=09=09=09font-size:48px !important;\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif=\n !important;\n=09=09=09line-height:40px !important;\n=09=09=09letter-spacing:-1px;\n=09=09=09margin:0 0 0 0;\n=09=09=09color:#5dc3ad !important;\n=09=09}\n=09=09.wowshoutgreen a{\n=09=09=09color:#5dc3ad !important;\n=09=09=09text-decoration:none !important;\n=09=09}\n=09=09.wowshoutyellow{\n=09=09=09margin-bottom:1em;\n=09=09=09font-weight:normal;\n=09=09=09font-size:48px !important;\n=09=09=09font-family:'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif=\n !important;\n=09=09=09line-height:40px !important;\n=09=09=09letter-spacing:-1px;\n=09=09=09margin:0 0 0 0;\n=09=09=09color:#ffdc01 !important;\n=09=09}\n=09=09.wowshoutyellow a{\n=09=09=09color:#ffdc01 !important;\n=09=09=09text-decoration:none !important;\n=09=09}\n=09=09#ancillarybox{\n=09=09=09border-bottom:#ddd 1px solid;\n=09=09=09padding-bottom:0px;\n=09=09}\n=09=09td.ancillary{\n=09=09=09padding-top:40px;\n=09=09=09padding-bottom:0px;\n=09=09}\n=09=09.ancillary p{\n=09=09=09margin:0;\n=09=09=09font-family:'Helvetica Neue'=2CHelvetica=2CArial=2Csans-serif !im=\nportant;\n=09=09=09font-weight:bold;\n=09=09=09font-size:24px;\n=09=09=09color:#aaa;\n=09=09}\n=09=09#socialbox{\n=09=09=09border-bottom:#ddd 1px solid;\n=09=09=09padding-bottom:0px;\n=09=09}\n=09=09td.social{\n=09=09=09padding-top:40px;\n=09=09=09padding-bottom:0px;\n=09=09}\n=09=09.social p{\n=09=09=09margin:0;\n=09=09=09font-family:'Helvetica Neue'=2CHelvetica=2CArial=2Csans-serif !im=\nportant;\n=09=09=09font-weight:bold;\n=09=09=09font-size:24px;\n=09=09=09color:#aaa;\n=09=09=09padding-top:0px;\n=09=09=09padding-bottom:40px;\n=09=09}\n=09=09#closingbox{\n=09=09=09padding-bottom:40px;\n=09=09}\n=09=09td.closing{\n=09=09=09padding-top:40px;\n=09=09=09margin-bottom:16px;\n=09=09=09font-family:'Helvetica Neue'=2CHelvetica=2CArial=2Csans-serif !im=\nportant;\n=09=09=09color:#40c7f4;\n=09=09=09font-size:36px;\n=09=09=09line-height:36px;\n=09=09}\n=09=09.closingtext{\n=09=09=09margin-bottom:12px;\n=09=09}\n=09=09#prefooter{\n=09=09=09background-color:#99248D;\n=09=09=09border-bottom:#5CC4F0 4px solid;\n=09=09=09border-top:#5CC4F0 4px solid;\n=09=09}\n=09=09td.prefooter{\n=09=09=09color:#fff;\n=09=09=09font-size:10px;\n=09=09=09font-weight:bold;\n=09=09=09font-family:'Lucida sans Unicode'=2C 'Lucida Grande'=2C 'Lucida G=\nrande'=2C Helvetica=2C Arial=2C sans-serif !important;\n=09=09=09line-height:16px;\n=09=09=09height:20px;\n=09=09}\n=09=09#footer{\n=09=09=09background-color:#fff;\n=09=09}\n=09=09td.footer p{\n=09=09=09color:#333;\n=09=09=09font-size:11px;\n=09=09=09font-family:'Lucida sans Unicode'=2C 'Lucida Grande'=2C Helvetica=\n=2C Arial=2C sans-serif !important;\n=09=09=09line-height:13px;\n=09=09=09font-weight:normal;\n=09=09=09vertical-align:middle;\n=09=09}\n=09=09td.footer a{\n=09=09=09text-decoration:none;\n=09=09=09color:#99248D;\n=09=09=09font-weight:bold;\n=09=09}\n</style></head>\n\n\n<body style=3D\"margin: 0;padding: 0;background-color: #fff;-webkit-text-si=\nze-adjust: none;width: 100% !important;\" bgcolor=3D\"#ffffff\" background=3D=\n\"#ffffff\">\n\n\n<!-- PREHEADER -->\n\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" width=3D\"100%\" id=\n=3D\"preheader\" style=3D\"background-color: #99248D;border-bottom: #5CC4F0 4=\npx solid;\">\n=09<tr>\n=09=09<td style=3D\"border-collapse: collapse;\">\n=09=09=09<table border=3D\"0\" cellpadding=3D\"10\" cellspacing=3D\"0\" width=3D=\n\"610\" align=3D\"center\">\n=09=09=09=09<tr>\n=09=09=09=09=09<td width=3D\"383\" class=3D\"preheader left\" style=3D\"border-=\ncollapse: collapse;text-align: left;color: #fff;font-size: 9px;line-height=\n: 100%;font-family: 'Lucida sans Unicode'=2C 'Lucida Grande'=2C 'Lucida Gr=\nande'=2C Helvetica=2C Arial=2C sans-serif !important;\">\n=09=09=09=09=09=09<div>Iceland this autumn for less &middot; Packages=2C p=\nackages=2C packages!<br>\nThe ten most popular Iceland tours &middot; Hotels=2C cars and stuff.</div=\n>\n=09=09=09=09=09</td>\n=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></td>\n=09=09=09=09=09<td width=3D\"187\" class=3D\"preheader right\" style=3D\"border=\n-collapse: collapse;text-align: right;color: #fff;font-size: 9px;line-heig=\nht: 100%;font-family: 'Lucida sans Unicode'=2C 'Lucida Grande'=2C 'Lucida=\n Grande'=2C Helvetica=2C Arial=2C sans-serif !important;\">\n=09=09=09=09=09<!--\n -->\n=09=09=09=09=09=09<div>\n                        =09No pictures?<br><a href=3D\"http://us2.campaign-=\narchive1.com/?u=3Dcfa47c79110f45a8f68fab75b&id=3Ddb82fef684&e=3D1f1636935c\"=\n target=3D\"_blank\" style=3D\"color:#fff; text-decoration:underline;\">Click=\n here to see them</a>.\n                        </div>\n                    <!--\n -->\n                    </td>\n=09=09=09=09</tr>\n=09=09=09</table>\n=09=09</td>\n=09</tr>\n</table>\n\n\n\n\n<!-- {Main Area Begins} -->\n\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" width=3D\"100%\">\n=09<tr>\n=09=09<td style=3D\"border-collapse: collapse;\">\n\n\n\n\n<!-- HEADER -->\n\n=09=09=09<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" width=3D\"=\n630\" align=3D\"center\" id=3D\"header\" style=3D\"background-color: #fff;border=\n-bottom: #ddd 1px solid;\">\n=09=09=09=09<tr>\n=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></td>\n=09=09=09=09=09<td width=3D\"265\" height=3D\"75\" class=3D\"header left\" style=\n=3D\"border-collapse: collapse;text-align: left;color: #666;font-size: 13px=\n;line-height: 100%;font-weight: bold;vertical-align: middle;font-family: '=\nHelvetica Neue'=2C Helvetica=2C Arial=2C 'Lucida Grande'=2C sans-serif !im=\nportant;\"><span class=3D\"purple\" style=3D\"color: #99248D;\">WOW air Iceland=\n</span><br><span>16 September 2013</span></td>\n=09=09=09=09=09<td width=3D\"60\" style=3D\"border-collapse: collapse;\"><a ta=\nrget=3D\"_blank\" href=3D\"http://example.us2.list-manage.com/track/click?u=3D=\ncfa47c79110f45a8f68fab75b&id=3D2c7c127dc2&e=3D1f1636935c\"><img src=3D\"htt=\np://wow.is/wowklubbur/logo-is.gif\" border=3D\"0\" alt=3D\"WOW\" width=3D\"60\" s=\ntyle=3D\"border: 0;height: auto;line-height: 100%;outline: none;text-decora=\ntion: none;\"></a></td>\n=09=09=09=09=09<td width=3D\"265\" height=3D\"75\" class=3D\"header right\" styl=\ne=3D\"border-collapse: collapse;text-align: right;color: #666;font-size: 13=\npx;line-height: 100%;font-weight: bold;vertical-align: middle;font-family:=\n 'Helvetica Neue'=2C Helvetica=2C Arial=2C 'Lucida Grande'=2C sans-serif !=\nimportant;\">\n=09=09=09=09=09=09You foll<span class=3D\"purple\" style=3D\"color: #99248D;\"=\n>wow</span>?<br>\n=09=09=09=09=09=09<a target=3D\"_blank\" href=3D\"http://example.us2.list-mana=\nge.com/track/click?u=3Dcfa47c79110f45a8f68fab75b&id=3Ddf62b29172&e=3D=\n1f1636935c\"><img title=3D\"Be our friend on Facebook\" width=3D\"15\" height=3D\"14=\n\" style=3D\"margin-top: 3px;color: white;background-color: #3B5998;border:=\n 0;height: auto;line-height: 100%;outline: none;text-decoration: none;\" al=\nt=3D\"F\" src=3D\"http://www.takktakk.com/forbeingagreatclient/wow/newsletter=\n/i/wow-icon-fb.png\"></a>\n=09=09=09=09=09=09<a target=3D\"_blank\" href=3D\"http://example.us2.list-mana=\nge.com/track/click?u=3Dcfa47c79110f45a8f68fab75b&id=3D578c1f94e7&e=3D=\n1f1636935c\"><img title=3D\"Follow us on Pinterest\" width=3D\"15\" height=3D\"14\" s=\ntyle=3D\"margin-top: 3px;color: white;background-color: #C92228;border: 0;h=\neight: auto;line-height: 100%;outline: none;text-decoration: none;\" alt=3D=\n\"P\" src=3D\"http://www.takktakk.com/forbeingagreatclient/wow/newsletter/i/w=\now-icon-pinterest.png\"></a>\n=09=09=09=09=09=09<a target=3D\"_blank\" href=3D\"http://example.us2.list-mana=\nge.com/track/click?u=3Dcfa47c79110f45a8f68fab75b&id=3Db330f756e0&e=3D=\n1f1636935c\"><img title=3D\"See our Instagrams\" width=3D\"15\" height=3D\"14\" style=\n=3D\"margin-top: 3px;color: white;background-color: #e7d1c3;border: 0;heigh=\nt: auto;line-height: 100%;outline: none;text-decoration: none;\" alt=3D\"P\"=\n src=3D\"http://www.takktakk.com/forbeingagreatclient/wow/newsletter/i/wow-=\nicon-instagram.jpg\"></a>\n=09=09=09=09=09=09<a target=3D\"_blank\" href=3D\"http://example.us2.list-mana=\nge1.com/track/click?u=3Dcfa47c79110f45a8f68fab75b&id=3Da151234d13&e=3D=\n1f1636935c\"><img title=3D\"Read our hipster magazine\" width=3D\"15\" height=3D\"1=\n4\" style=3D\"margin-top: 3px;color: white;background-color: #99248D;border:=\n 0;height: auto;line-height: 100%;outline: none;text-decoration: none;\" al=\nt=3D\"W\" src=3D\"http://www.takktakk.com/forbeingagreatclient/wow/template/i=\n/wow-icon-blog.png\"></a>\n=09=09=09=09=09</td>\n=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></td>\n=09=09=09=09</tr>\n=09=09=09</table>\n\n\n\n=09=09=09<!-- OPENING -->\n\n <table width=3D\"650\" border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" ali=\ngn=3D\"center\" style=3D\"border-bottom: 1px #ddd solid\">\n    <tr>\n      <td background=3D\"http://www.departmentoficelandicthings.com/wow/i/w=\now-front-650a.jpg\" bgcolor=3D\"#99248D\" width=3D\"650\" height=3D\"400\" align=\n=3D\"center\" valign=3D\"top\" style=3D\"border-collapse: collapse;\">\n        <!--[if gte mso 9]>\n          <v:rect xmlns:v=3D\"urn:schemas-microsoft-com:vml\" fill=3D\"true\"=\n stroke=3D\"false\" style=3D\"width:650px;height:400px;\">\n          <v:fill type=3D\"tile\" src=3D\"http://www.departmentoficelandicthi=\nngs.com/wow/i/wow-front-650a.jpg\" color=3D\"#0ba7b3\" />\n          <v:textbox inset=3D\"0=2C0=2C0=2C0\">\n        <![endif]-->\n        <table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" align=3D\"l=\neft\">\n          <tr height=3D\"65\">\n            <td style=3D\"border-collapse: collapse;\">\n            </td>\n          </tr>\n          <tr valign=3D\"top\">\n            <td width=3D\"30\" style=3D\"border-collapse: collapse;\">\n            </td>\n            <td style=3D\"border-collapse: collapse;\">\n              <table width=3D\"100%\" border=3D\"0\" cellpadding=3D\"0\" cellspa=\ncing=3D\"0\" align=3D\"left\">\n                <tr>\n                  <td style=3D\"font-family: Helvetica=2C Arial=2C sans-ser=\nif;color: white;font-weight: 100;font-size: 48px;line-height: 42px;border-=\ncollapse: collapse;\" align=3D\"left\">\n\n                  <div class=3D\"airplanetext\" style=3D\"line-height: 36px;m=\nargin-bottom: 12px;\"><br>\n<span style=3D\"color: #ffdc01; font-weight: bold; font-size: 36px; line-he=\night: 36px;\">Iceland this autumn?<br>\n<span class=3D\"blue\" style=3D\"color: #40c7f4 !important;\">Flights starting=\n at<br>\nDKK599=2C &pound;69 and &euro;96</span></span></div>\n=09=09=09=09  </td>\n                 </tr>\n                <tr height=3D\"20\">\n                  <td style=3D\"border-collapse: collapse;\">\n                  </td>\n                </tr>\n                <tr>\n                  <td style=3D\"font-family: Helvetica=2C Arial=2C sans-ser=\nif;color: white;font-size: 18px;line-height: 21px;font-weight: bold;border=\n-collapse: collapse;\" align=3D\"left\">\n                    From today until 15 December.<br>The cheapest seats go=\n first!\n                  </td>\n                </tr>\n                <tr height=3D\"20\">\n                  <td style=3D\"border-collapse: collapse;\">\n                  </td>\n                </tr>\n                <tr>\n                  <td style=3D\"border-collapse: collapse;\">\n=09=09=09=09=09=09\n=09=09=09=09=09=09<div mc:hideable=3D\"hideable_1\" mchideable=3D\"hideable_1=\n\">\n=09=09=09=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\"=\n class=3D\"fancybuttontableouter\" style=3D\"background-image: url(http://www=\n=2Etakktakk.com/forbeingagreatclient/wow/template/i/hnappur.png);background-=\nrepeat: repeat-x;background-color: #33a9e5;border-radius: 5px;border: 1px=\n solid #28C;height: 32px;word-wrap: break-word;text-align: center;margin-t=\nop: 20px;margin-bottom: 24px;\">\n=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09<td align=3D\"center\" style=3D\"padding-left: 10p=\nx;padding-right: 10px;border-collapse: collapse;\">=09=09=09=09=09=09=09=09=\n=09=09=09=09\n=09=09=09=09=09=09=09=09=09=09<table cellspacing=3D\"0\" cellpadding=3D\"0\" b=\norder=3D\"0\" class=3D\"fancybuttontableinner\" style=3D\"font-family: 'Helveti=\nca Neue'=2CHelvetica=2CArial=2Csans-serif;\">\n=09=09=09=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09=09=09=09<td height=3D\"32\" class=3D\"fancybut=\ntonlink\" style=3D\"border-collapse: collapse;font-size: 16px;line-height: 1=\n6px;font-weight: bold;color: white;text-align: center;text-decoration: non=\ne;text-shadow: 0px -1px #666;text-transform: uppercase;font-family: 'Helve=\ntica Neue'=2C Helvetica=2C Arial=2C sans-serif !important;\"><span><a href=\n=3D\"http://example.us2.list-manage2.com/track/click?u=3Dcfa47c79110f45a8f68=\nfab75b&id=3D455175f589&e=3D1f1636935c\" target=3D\"_blank\" style=3D\"font-si=\nze: 16px;line-height: 16px;font-weight: bold;color: white;text-align: cent=\ner;text-decoration: none;text-shadow: 0px -1px #666;text-transform: upperc=\nase;font-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !imp=\nortant;\">Click to book &rarr;</a></span></td>\n=09=09=09=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09=09=09</td>\n=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09</table>\n=09=09=09=09=09=09</div>\n                   </td>\n                </tr>\n              </table>\n            </td>\n          </tr>\n        </table>\n        <!--[if gte mso 9]>\n          </v:textbox>\n          </v:rect>\n        <![endif]-->\n      </td>\n    </tr>\n  </table>\n\n\n<!-- OPENING --\n\n=09=09=09<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" width=3D\"=\n630\" align=3D\"center\" id=3D\"opening\">\n=09=09=09=09<tr>\n=09=09=09=09=09<td width=3D\"20\"></td>\n=09=09=09=09=09<td width=3D\"370\" style=3D\"padding-top: 30px; padding-botto=\nm: 0px;\">\n=09=09=09=09=09=09<div mc:edit=3D\"opening\" class=3D\"openingtext\">\n=09=09=09=09=09=09=09<span class=3D\"purple\" style=3D\"font-weight: bold;\">I=\nceland this autumn?</span> <span class=3D\"black\" style=3D\"font-weight: nor=\nmal;\">Flights from <strong>DKK599=2C &pound;67</strong> and <strong>&euro;=\n83.</strong></span>\n=09=09=09=09=09=09</div>\n=09=09=09=09=09=09\n=09=09=09=09=09=09<div mc:hideable=3D\"\">\n=09=09=09=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\"=\n class=3D\"fancybuttontableouter\">\n=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09<td align=3D\"center\" style=3D\"padding-left:10px=\n;padding-right:10px;\">=09=09=09=09=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09=09=09<table cellspacing=3D\"0\" cellpadding=3D\"0\" b=\norder=3D\"0\" class=3D\"fancybuttontableinner\">\n=09=09=09=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<td height=3D\"32\" class=3D\"fancy=\nbuttonlink\"><span mc:edit=3D\"main_call_to_action_1\"><a href=3D\"http://www.=\nwowiceland.co.uk/\" target=3D\"_blank\">Click to book &rarr;</a></span></td>\n=09=09=09=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09=09=09</td>\n=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09</table>\n=09=09=09=09=09=09</div>\n=09=09=09=09=09</td>\n=09=09=09=09=09<td width=3D\"10\">\n=09=09=09=09=09</td>\n=09=09=09=09=09<td width=3D\"210\">\n=09=09=09=09=09=09=09<a target=3D\"_blank\" href=3D\"http://us2.campaign-arch=\nive1.com/?u=3Dcfa47c79110f45a8f68fab75b&id=3Ddb82fef684&e=3D1f1636935c\"><img=\n src=3D\"http://www.takktakk.com/forbeingagreatclient/wow/newsletter/i/wow-=\nnordurljos.jpg\" width=3D\"210\" height=3D\"210\" style=3D\"max-width:210px; bac=\nkground-color: #ccc; padding: 0; margin: 0;\" alt=3D\"No picture? Click here=\n\" align=3D\"right\" mc:label=3D\"opening_image\" mc:edit=3D\"opening_image\"></a=\n>\n=09=09=09=09=09</td>=09=09=09=09=09\n=09=09=09=09=09<td width=3D\"20\"></td>\n=09=09=09=09</tr>\n=09=09=09</table>\n\n\n\n\n=09<!-- MULTICITY X 3 -->\n=09\n=09\n=09=09=09<div mc:hideable=3D\"hideable_2\" mchideable=3D\"hideable_2\">\n=09=09=09=09<table align=3D\"center\" border=3D\"0\" cellspacing=3D\"0\" cellpad=\nding=3D\"0\" width=3D\"630\" class=3D\"multicitybox\" style=3D\"border-bottom: no=\nne;margin-bottom: 0 !important;background-color: #fff;padding-bottom: 20px=\n;padding-top: 20px;\">\n=09=09=09=09=09<tr valign=3D\"top\">\n=09=09=09=09=09=09<td width=3D\"128\" class=3D\"multicityunit\" style=3D\"borde=\nr: 1px #99248D dashed;border-collapse: collapse;padding-top: 20px;padding-=\nbottom: 0px;\">\n=09=09=09=09=09=09=09<div class=3D\"from\" style=3D\"font-family: 'Helvetica=\n Neue'=2C Helvetica=2C Arial=2C sans-serif;text-align: center;font-size: 1=\n2px;color: #333;font-weight: bold;\">To Reykjavik from</div>\n=09=09=09=09=09=09=09<div class=3D\"city blue\" style=3D\"font-family: 'Helve=\ntica Neue'=2C Helvetica=2C Arial=2C sans-serif;text-align: center;font-siz=\ne: 26px;line-height: 1em;font-weight: bold;color: #40c7f4 !important;\"><sp=\nan class=3D\"purple\" style=3D\"color: #99248D;\">Paris</span></div>\n=09=09=09=09=09=09=09<div class=3D\"packagedescription\" style=3D\"font-size:=\n 12px;margin: 3px 0 9px;text-align: center;font-weight: bold;font-family:=\n 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !important;\">Until 1=\n5 Dec</div>\n=09=09=09=09=09=09=09<div class=3D\"price purple\" style=3D\"color: #99248D;f=\nont-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif;text-alig=\nn: center;font-size: 22px;line-height: 22px;font-weight: bold;margin-top:=\n 0;margin-bottom: 8px;\">&euro;104*</div>\n=09=09=09=09=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"=\n0\" class=3D\"fancybuttontableouter\" align=3D\"center\" style=3D\"background-im=\nage: url(http://www.takktakk.com/forbeingagreatclient/wow/template/i/hnapp=\nur.png);background-repeat: repeat-x;background-color: #33a9e5;border-radiu=\ns: 5px;border: 1px solid #28C;height: 32px;word-wrap: break-word;text-alig=\nn: center;margin-top: 20px;margin-bottom: 24px;\">\n=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09<td align=3D\"center\" style=3D\"padding-left:=\n 10px;padding-right: 10px;border-collapse: collapse;\">=09=09=09=09=09=09=\n=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09=09=09=09<table cellspacing=3D\"0\" cellpadding=3D\"0=\n\" border=3D\"0\" class=3D\"fancybuttontableinner\" style=3D\"font-family: 'Helv=\netica Neue'=2CHelvetica=2CArial=2Csans-serif;\">\n=09=09=09=09=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<td height=3D\"32\" class=3D\"fancy=\nbuttonlink\" style=3D\"border-collapse: collapse;font-size: 16px;line-height=\n: 16px;font-weight: bold;color: white;text-align: center;text-decoration:=\n none;text-shadow: 0px -1px #666;text-transform: uppercase;font-family: 'H=\nelvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !important;\"><span><a h=\nref=3D\"http://example.us2.list-manage.com/track/click?u=3Dcfa47c79110f45a8f=\n68fab75b&id=3Dce674451af&e=3D1f1636935c\" target=3D\"_blank\" style=3D\"font-=\nsize: 16px;line-height: 16px;font-weight: bold;color: white;text-align: ce=\nnter;text-decoration: none;text-shadow: 0px -1px #666;text-transform: uppe=\nrcase;font-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !i=\nmportant;\">Book</a></span></td>\n=09=09=09=09=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09=09=09=09</td>\n=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09</table>=09=09=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09</td>\n=09\n=09=09=09=09=09=09<td width=3D\"19\" style=3D\"border-right: 1px #ddd solid;b=\norder-collapse: collapse;\"></td>\n=09=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></=\ntd>\n=09\n=09=09=09=09=09=09<td width=3D\"127\" class=3D\"multicityunit\" style=3D\"borde=\nr: 1px #99248D dashed;border-collapse: collapse;padding-top: 20px;padding-=\nbottom: 0px;\">\n=09=09=09=09=09=09=09<div class=3D\"from\" style=3D\"font-family: 'Helvetica=\n Neue'=2C Helvetica=2C Arial=2C sans-serif;text-align: center;font-size: 1=\n2px;color: #333;font-weight: bold;\">To Reykjavik from</div>\n=09=09=09=09=09=09=09<div class=3D\"city blue\" style=3D\"font-family: 'Helve=\ntica Neue'=2C Helvetica=2C Arial=2C sans-serif;text-align: center;font-siz=\ne: 26px;line-height: 1em;font-weight: bold;color: #40c7f4 !important;\"><sp=\nan class=3D\"purple\" style=3D\"font-size: 18px;color: #99248D;\">Copenhagen</=\nspan></div>\n=09=09=09=09=09=09=09<div class=3D\"packagedescription\" style=3D\"font-size:=\n 12px;margin: 3px 0 9px;text-align: center;font-weight: bold;font-family:=\n 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !important;\">Until 1=\n5 Dec</div>\n=09=09=09=09=09=09=09<div class=3D\"price purple\" style=3D\"color: #99248D;f=\nont-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif;text-alig=\nn: center;font-size: 22px;line-height: 22px;font-weight: bold;margin-top:=\n 0;margin-bottom: 8px;\">DKK599*</div>\n=09=09=09=09=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"=\n0\" class=3D\"fancybuttontableouter\" align=3D\"center\" style=3D\"background-im=\nage: url(http://www.takktakk.com/forbeingagreatclient/wow/template/i/hnapp=\nur.png);background-repeat: repeat-x;background-color: #33a9e5;border-radiu=\ns: 5px;border: 1px solid #28C;height: 32px;word-wrap: break-word;text-alig=\nn: center;margin-top: 20px;margin-bottom: 24px;\">\n=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09<td align=3D\"center\" style=3D\"padding-left:=\n 10px;padding-right: 10px;border-collapse: collapse;\">=09=09=09=09=09=09=\n=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09=09=09=09<table cellspacing=3D\"0\" cellpadding=3D\"0=\n\" border=3D\"0\" class=3D\"fancybuttontableinner\" style=3D\"font-family: 'Helv=\netica Neue'=2CHelvetica=2CArial=2Csans-serif;\">\n=09=09=09=09=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<td height=3D\"32\" class=3D\"fancy=\nbuttonlink\" style=3D\"border-collapse: collapse;font-size: 16px;line-height=\n: 16px;font-weight: bold;color: white;text-align: center;text-decoration:=\n none;text-shadow: 0px -1px #666;text-transform: uppercase;font-family: 'H=\nelvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !important;\"><span><a h=\nref=3D\"http://example.us2.list-manage.com/track/click?u=3Dcfa47c79110f45a8f=\n68fab75b&id=3D6a55c4b7b1&e=3D1f1636935c\" target=3D\"_blank\" style=3D\"font-=\nsize: 16px;line-height: 16px;font-weight: bold;color: white;text-align: ce=\nnter;text-decoration: none;text-shadow: 0px -1px #666;text-transform: uppe=\nrcase;font-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !i=\nmportant;\">Book</a></span></td>\n=09=09=09=09=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09=09=09=09</td>\n=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09</table>=09=09=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09</td>=09\n=09=09=09=09=09=09<td width=3D\"19\" style=3D\"border-right: 1px #ddd solid;b=\norder-collapse: collapse;\"></td>\n=09=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></=\ntd>\n=09\n=09=09=09=09=09=09<td width=3D\"128\" class=3D\"multicityunit\" style=3D\"borde=\nr: 1px #99248D dashed;border-collapse: collapse;padding-top: 20px;padding-=\nbottom: 0px;\">\n=09=09=09=09=09=09=09<div class=3D\"from\" style=3D\"font-family: 'Helvetica=\n Neue'=2C Helvetica=2C Arial=2C sans-serif;text-align: center;font-size: 1=\n2px;color: #333;font-weight: bold;\">To Reykjavik from</div>\n=09=09=09=09=09=09=09<div class=3D\"city blue\" style=3D\"font-family: 'Helve=\ntica Neue'=2C Helvetica=2C Arial=2C sans-serif;text-align: center;font-siz=\ne: 26px;line-height: 1em;font-weight: bold;color: #40c7f4 !important;\"><sp=\nan class=3D\"purple\" style=3D\"color: #99248D;\">London</span></div>\n=09=09=09=09=09=09=09<div class=3D\"packagedescription\" style=3D\"font-size:=\n 12px;margin: 3px 0 9px;text-align: center;font-weight: bold;font-family:=\n 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !important;\">Until 1=\n5 Dec</div>\n=09=09=09=09=09=09=09<div class=3D\"price purple\" style=3D\"color: #99248D;f=\nont-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif;text-alig=\nn: center;font-size: 22px;line-height: 22px;font-weight: bold;margin-top:=\n 0;margin-bottom: 8px;\">&pound;69*</div>\n=09=09=09=09=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"=\n0\" class=3D\"fancybuttontableouter\" align=3D\"center\" style=3D\"background-im=\nage: url(http://www.takktakk.com/forbeingagreatclient/wow/template/i/hnapp=\nur.png);background-repeat: repeat-x;background-color: #33a9e5;border-radiu=\ns: 5px;border: 1px solid #28C;height: 32px;word-wrap: break-word;text-alig=\nn: center;margin-top: 20px;margin-bottom: 24px;\">\n=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09<td align=3D\"center\" style=3D\"padding-left:=\n 10px;padding-right: 10px;border-collapse: collapse;\">=09=09=09=09=09=09=\n=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09=09=09=09<table cellspacing=3D\"0\" cellpadding=3D\"0=\n\" border=3D\"0\" class=3D\"fancybuttontableinner\" style=3D\"font-family: 'Helv=\netica Neue'=2CHelvetica=2CArial=2Csans-serif;\">\n=09=09=09=09=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<td height=3D\"32\" class=3D\"fancy=\nbuttonlink\" style=3D\"border-collapse: collapse;font-size: 16px;line-height=\n: 16px;font-weight: bold;color: white;text-align: center;text-decoration:=\n none;text-shadow: 0px -1px #666;text-transform: uppercase;font-family: 'H=\nelvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !important;\"><span><a h=\nref=3D\"http://example.us2.list-manage2.com/track/click?u=3Dcfa47c79110f45a8=\nf68fab75b&id=3D86c1f8563c&e=3D1f1636935c\" target=3D\"_blank\" style=3D\"font=\n-size: 16px;line-height: 16px;font-weight: bold;color: white;text-align: c=\nenter;text-decoration: none;text-shadow: 0px -1px #666;text-transform: upp=\nercase;font-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !=\nimportant;\">Book</a></span></td>\n=09=09=09=09=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09=09=09=09</td>\n=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09</table>=09=09=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09</td>=09=09=09=09=09=09\n=09=09=09=09=09=09<td width=3D\"19\" style=3D\"border-right: 1px #ddd solid;b=\norder-collapse: collapse;\"></td>\n=09=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></=\ntd>\n=09\n=09=09=09=09=09=09<td width=3D\"127\" class=3D\"multicityunit\" style=3D\"borde=\nr: 1px #99248D dashed;border-collapse: collapse;padding-top: 20px;padding-=\nbottom: 0px;\">\n=09=09=09=09=09=09=09<div class=3D\"from\" style=3D\"font-family: 'Helvetica=\n Neue'=2C Helvetica=2C Arial=2C sans-serif;text-align: center;font-size: 1=\n2px;color: #333;font-weight: bold;\">To Reykjavik from</div>\n=09=09=09=09=09=09=09<div class=3D\"city blue\" style=3D\"font-family: 'Helve=\ntica Neue'=2C Helvetica=2C Arial=2C sans-serif;text-align: center;font-siz=\ne: 26px;line-height: 1em;font-weight: bold;color: #40c7f4 !important;\"><sp=\nan class=3D\"purple\" style=3D\"color: #99248D;\">Berlin</span></div>\n=09=09=09=09=09=09=09<div class=3D\"packagedescription\" style=3D\"font-size:=\n 12px;margin: 3px 0 9px;text-align: center;font-weight: bold;font-family:=\n 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !important;\">Until 1=\n5 Dec</div>\n=09=09=09=09=09=09=09<div class=3D\"price purple\" style=3D\"color: #99248D;f=\nont-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif;text-alig=\nn: center;font-size: 22px;line-height: 22px;font-weight: bold;margin-top:=\n 0;margin-bottom: 8px;\">&euro;96*</div>\n=09=09=09=09=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"=\n0\" class=3D\"fancybuttontableouter\" align=3D\"center\" style=3D\"background-im=\nage: url(http://www.takktakk.com/forbeingagreatclient/wow/template/i/hnapp=\nur.png);background-repeat: repeat-x;background-color: #33a9e5;border-radiu=\ns: 5px;border: 1px solid #28C;height: 32px;word-wrap: break-word;text-alig=\nn: center;margin-top: 20px;margin-bottom: 24px;\">\n=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09<td align=3D\"center\" style=3D\"padding-left:=\n 10px;padding-right: 10px;border-collapse: collapse;\">=09=09=09=09=09=09=\n=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09=09=09=09<table cellspacing=3D\"0\" cellpadding=3D\"0=\n\" border=3D\"0\" class=3D\"fancybuttontableinner\" style=3D\"font-family: 'Helv=\netica Neue'=2CHelvetica=2CArial=2Csans-serif;\">\n=09=09=09=09=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<td height=3D\"32\" class=3D\"fancy=\nbuttonlink\" style=3D\"border-collapse: collapse;font-size: 16px;line-height=\n: 16px;font-weight: bold;color: white;text-align: center;text-decoration:=\n none;text-shadow: 0px -1px #666;text-transform: uppercase;font-family: 'H=\nelvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !important;\"><span><a h=\nref=3D\"http://example.us2.list-manage.com/track/click?u=3Dcfa47c79110f45a8f=\n68fab75b&id=3D3f7fca9a90&e=3D1f1636935c\" target=3D\"_blank\" style=3D\"font-=\nsize: 16px;line-height: 16px;font-weight: bold;color: white;text-align: ce=\nnter;text-decoration: none;text-shadow: 0px -1px #666;text-transform: uppe=\nrcase;font-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !i=\nmportant;\">Book</a></span></td>\n=09=09=09=09=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09=09=09=09</td>\n=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09</table>=09=09=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09</td>\n=09=09=09=09\n=09=09=09=09=09</tr>\n=09=09=09=09</table>\n=09=09=09=09\n=09=09=09=09<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" width=\n=3D\"630\" align=3D\"center\" id=3D\"opening\" style=3D\"background-color: #fff;b=\norder-bottom: #ddd 1px solid;\">\n=09=09=09=09=09<tr>\n=09=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></=\ntd>\n=09=09=09=09=09=09<td width=3D\"590\" style=3D\"padding-top: 10px;padding-bot=\ntom: 30px;border-collapse: collapse;\">\n=09=09=09=09=09=09=09<div class=3D\"openingtext\" style=3D\"text-align: cente=\nr;color: #99248D;display: block;font-size: 34px;font-weight: bold;margin-t=\nop: 0px;margin-right: 0;margin-bottom: 20px;margin-left: 0;font-family: 'H=\nelvetica Neue'=2C Helvetica=2C Arial=2C 'Lucida Grande'=2C sans-serif !imp=\nortant;line-height: 100% !important;\"><span class=3D\"black\" style=3D\"font-=\nweight: normal;font-size: 26px;line-height: 26px;color: #000;\">Not in thes=\ne cities?<br>\n<strong>See if we fly to Reykjavik from your&nbsp;city.</strong></span> </=\ndiv>\n=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09<div mc:hideable=3D\"hideable_3\" mchideable=3D\"hideabl=\ne_3\">\n=09=09=09=09=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"=\n0\" class=3D\"fancybuttontableouter\" align=3D\"center\" style=3D\"background-im=\nage: url(http://www.takktakk.com/forbeingagreatclient/wow/template/i/hnapp=\nur.png);background-repeat: repeat-x;background-color: #33a9e5;border-radiu=\ns: 5px;border: 1px solid #28C;height: 32px;word-wrap: break-word;text-alig=\nn: center;margin-top: 20px;margin-bottom: 24px;\">\n=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09<td align=3D\"center\" style=3D\"padding-left:=\n 10px;padding-right: 10px;border-collapse: collapse;\">=09=09=09=09=09=09=\n=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09=09=09=09<table cellspacing=3D\"0\" cellpadding=3D\"0=\n\" border=3D\"0\" class=3D\"fancybuttontableinner\" style=3D\"font-family: 'Helv=\netica Neue'=2CHelvetica=2CArial=2Csans-serif;\">\n=09=09=09=09=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<td height=3D\"32\" class=3D\"fancy=\nbuttonlink\" style=3D\"border-collapse: collapse;font-size: 16px;line-height=\n: 16px;font-weight: bold;color: white;text-align: center;text-decoration:=\n none;text-shadow: 0px -1px #666;text-transform: uppercase;font-family: 'H=\nelvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !important;\"><span><a h=\nref=3D\"http://example.us2.list-manage.com/track/click?u=3Dcfa47c79110f45a8f=\n68fab75b&id=3D73f8f1e574&e=3D1f1636935c\" target=3D\"_blank\" style=3D\"font-=\nsize: 16px;line-height: 16px;font-weight: bold;color: white;text-align: ce=\nnter;text-decoration: none;text-shadow: 0px -1px #666;text-transform: uppe=\nrcase;font-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !i=\nmportant;\">View route map &rarr;</a></span></td>\n=09=09=09=09=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09=09=09=09</td>\n=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09</div>\n=09=09=09=09=09=09</td>\n=09=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></=\ntd>\n=09=09=09=09=09</tr>\n=09=09=09=09</table>\n=09=09=09</div>\n=09\n\n\n<!-- CONTINUED --\n\n=09=09<div mc:hideable=3D\"\">\n=09=09=09<table align=3D\"center\" border=3D\"0\" cellspacing=3D\"0\" cellpaddin=\ng=3D\"0\" width=3D\"630\" id=3D\"continuedbox\">\n=09=09=09=09<tr valign=3D\"top\">\n=09=09=09=09=09<td width=3D\"20\"></td>\n=09=09=09=09=09<td width=3D\"590\" class=3D\"continued\">\n=09=09=09=09=09=09<div mc:edit=3D\"continuedmaintext\" class=3D\"continuedmai=\nntext\">\n=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09<strong>London =C3=AD j=C3=BAn=C3=AD=2C j=C3=BAl=C3=\n=AD og =C3=A1g=C3=BAst fr=C3=A1 12.900 kr.</strong><br><br>\n=09=09=09=09=09=09=09<strong>B=C3=B3ka=C3=B0u</strong><br>N=C3=BAna! Til s=\n=C3=B6lu til mi=C3=B0n=C3=A6ttis e=C3=B0a =C3=BEanga=C3=B0 til s=C3=A6ti k=\nl=C3=A1rast.<br><br>\n=09=09=09=09=09=09=09<strong>Og sm=C3=A1a letri=C3=B0?</strong> Ver=C3=B0=\n er fyrir flug a=C3=B0ra lei=C3=B0 me=C3=B0 sk=C3=B6ttum; b=C3=B3kunargjal=\nd og t=C3=B6skugjald ekki innifali=C3=B0. Takmarka=C3=B0ur s=C3=A6tafj=C3=\n=B6ldi og valdar dagsetningar; fyrstir koma=2C fyrstir f=C3=A1.\n\n=09=09=09=09=09=09</div>\n\n=09=09=09=09=09=09<div mc:hideable=3D\"\">\n=09=09=09=09=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"=\n0\" class=3D\"fancybuttontableouter\">\n=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09<td align=3D\"center\" style=3D\"padding-left:1=\n0px;padding-right:10px;\">=09=09=09=09=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09=09=09=09<table cellspacing=3D\"0\" cellpadding=3D\"0=\n\" border=3D\"0\" class=3D\"fancybuttontableinner\">\n=09=09=09=09=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<td height=3D\"32\" class=3D\"fancy=\nbuttonlink\"><span mc:edit=3D\"main_call_to_action_2\"><a href=3D\"http://wow.=\nis/\" target=3D\"_blank\">Book now &rarr;</a></span></td>\n=09=09=09=09=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09=09=09=09</td>\n=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09</div>\n=09=09=09=09=09</td>\n=09=09=09=09=09<td width=3D\"20\">\n=09=09=09=09</td></tr>\n=09=09=09</table>\n=09=09</div>\n\n\n\n\n<!-- ANCILLARY -->\n\n=09=09<div mc:hideable=3D\"hideable_4\" mchideable=3D\"hideable_4\">\n=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" align=3D\"=\ncenter\" width=3D\"630\" bgcolor=3D\"#f3ede6\" id=3D\"ancillarybox\" style=3D\"bor=\nder-bottom: #ddd 1px solid;padding-bottom: 0px;\">\n=09=09=09=09<tr>\n=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></td>\n=09=09=09=09=09<td width=3D\"590\" align=3D\"center\" class=3D\"ancillary\" styl=\ne=3D\"border-collapse: collapse;padding-top: 40px;padding-bottom: 0px;\">\n=09=09=09=09=09=09<img src=3D\"http://www.takktakk.com/forbeingagreatclient=\n/wow/newsletter/i/wow-hotels.png\" align=3D\"right\" width=3D\"155\" height=3D\"=\n190\" style=3D\"border: 0;height: auto;line-height: 100%;outline: none;text-=\ndecoration: none;\">\n=09=09=09=09=09=09<p style=3D\"margin: 0;font-weight: bold;font-size: 24px;=\ncolor: #aaa;font-family: 'Helvetica Neue'=2CHelvetica=2CArial=2Csans-serif=\n !important;\">\n=09=09=09=09=09=09=09<span class=3D\"black\" style=3D\"color: #000;\">Everythi=\nng you need.</span><br><br>\n=09=09=09=09=09=09=09=09<span>Good places <a target=3D\"_blank\" class=3D\"pu=\nrple\" href=3D\"http://example.us2.list-manage.com/track/click?u=3Dcfa47c7911=\n0f45a8f68fab75b&id=3D9c75fe6bdb&e=3D1f1636935c\" style=3D\"color: #99248D;\"=\n>to sleep</a>=2C<br>\n=09=09=09=09=09=09=09=09fast cars <a target=3D\"_blank\" class=3D\"blue\" href=\n=3D\"http://example.us2.list-manage.com/track/click?u=3Dcfa47c79110f45a8f68f=\nab75b&id=3Dc69d138f6b&e=3D1f1636935c\" style=3D\"color: #40c7f4 !important;=\n\">to drive</a>=2C<br>\n=09=09=09=09=09=09=09=09and fun sights <a target=3D\"_blank\" class=3D\"green=\n\" href=3D\"http://example.us2.list-manage1.com/track/click?u=3Dcfa47c79110f4=\n5a8f68fab75b&id=3D9bb55ee2a7&e=3D1f1636935c\" style=3D\"color: #5dc3ad !imp=\nortant;\">to see</a>.</span>\n=09=09=09=09=09=09</p>\n=09=09=09=09=09</td>\n=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></td>\n=09=09=09=09</tr>\n=09=09=09</table>\n=09=09</div>\n\n\n\n\n\n\n=09<!-- MULTICITY X 3 -->\n=09\n=09\n=09=09=09<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" width=3D\"=\n630\" align=3D\"center\" id=3D\"opening\" style=3D\"border-bottom: none;\">\n=09=09=09=09=09<tr>\n=09=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></=\ntd>\n=09=09=09=09=09=09<td width=3D\"590\" style=3D\"padding-top: 5px;padding-bott=\nom: 10px;border-collapse: collapse;\">\n=09=09=09=09=09=09=09<div class=3D\"openingtext\" style=3D\"font-size: 24px;l=\nine-height: 24px;padding-bottom: 0;margin-bottom: 20px;padding-top: 30px;c=\nolor: #99248D;display: block;font-weight: bold;margin-top: 0px;margin-righ=\nt: 0;margin-left: 0;text-align: left;font-family: 'Helvetica Neue'=2C Helv=\netica=2C Arial=2C 'Lucida Grande'=2C sans-serif !important;\">Flying in fro=\nm Copenhagen? <span class=3D\"black\" style=3D\"font-weight: normal;color: #0=\n00;\">Here are <strong>three packages to Iceland&nbsp;</strong><strong>this=\n autumn.</strong></span></div>\n=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09<!--<div mc:hideable>\n=09=09=09=09=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"=\n0\" class=3D\"fancybuttontableouter\">\n=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09<td align=3D\"center\" style=3D\"padding-left:1=\n0px;padding-right:10px;\">=09=09=09=09=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09=09=09=09<table cellspacing=3D\"0\" cellpadding=3D\"0=\n\" border=3D\"0\" class=3D\"fancybuttontableinner\">\n=09=09=09=09=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<td height=3D\"32\" class=3D\"fancy=\nbuttonlink\"><span mc:edit=3D\"main_call_to_action_1\"><a href=3D\"http://www.=\nwowiceland.co.uk/\" target=3D\"_blank\">Click to book &rarr;</a></span></td>\n=09=09=09=09=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09=09=09=09</td>\n=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09</div>-->\n=09=09=09=09=09=09</td>\n=09=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></=\ntd>\n=09=09=09=09=09</tr>\n=09=09=09=09</table>\n=09=09=09=09\n=09=09=09=09<div mc:hideable=3D\"hideable_5\" mchideable=3D\"hideable_5\">\n=09=09=09=09<table align=3D\"center\" border=3D\"0\" cellspacing=3D\"0\" cellpad=\nding=3D\"0\" width=3D\"630\" class=3D\"multicitybox\" style=3D\"border-bottom: no=\nne;margin-bottom: 0 !important;background-color: #fff;padding-bottom: 20px=\n;padding-top: 20px;\">\n=09=09=09=09=09<tr valign=3D\"top\">\n=09=09=09=09=09=09<td width=3D\"20\" style=3D\"background-color: white;border=\n-collapse: collapse;\"></td>\n=09=09=09=09=09=09\n=09=09=09=09=09=09<td width=3D\"177\" class=3D\"multicityunit\" style=3D\"borde=\nr-collapse: collapse;padding-top: 20px;padding-bottom: 0px;\">\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<a href=3D\"http://example.us2.lis=\nt-manage.com/track/click?u=3Dcfa47c79110f45a8f68fab75b&id=3D257194f9c1&e=\n=3D1f1636935c\" target=3D\"_blank\"><img src=3D\"http://www.takktakk.com/forb=\neingagreatclient/wow/newsletter/i/wow-IS1.jpg\" width=3D\"177\" height=3D\"177=\n\" style=3D\"padding-bottom: 5px;border: 0;height: auto;line-height: 100%;ou=\ntline: none;text-decoration: none;\"></a><div class=3D\"from\" style=3D\"font-=\nfamily: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif;text-align: c=\nenter;font-size: 12px;color: #333;font-weight: bold;\">From Copenhagen</div=\n>\n=09=09=09=09=09=09=09<div class=3D\"city blue\" style=3D\"font-size: 18px;lin=\ne-height: 18px;font-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans=\n-serif;text-align: center;font-weight: bold;color: #40c7f4 !important;\">Im=\nagine Peace Weekend</div>\n=09=09=09=09=09=09=09<div class=3D\"packagedescription\" style=3D\"font-size:=\n 12px;margin: 3px 0 9px;text-align: center;font-weight: bold;font-family:=\n 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !important;\">Return=\n flight=2C 2 nights in a hotel=2C Northern Lights Boat Tour</div>\n=09=09=09=09=09=09=09<div class=3D\"price purple\" style=3D\"color: #99248D;f=\nont-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif;text-alig=\nn: center;font-size: 22px;line-height: 22px;font-weight: bold;margin-top:=\n 0;margin-bottom: 8px;\">DKK1999*</div>\n=09=09=09=09=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"=\n0\" class=3D\"fancybuttontableouter\" align=3D\"center\" style=3D\"background-im=\nage: url(http://www.takktakk.com/forbeingagreatclient/wow/template/i/hnapp=\nur.png);background-repeat: repeat-x;background-color: #33a9e5;border-radiu=\ns: 5px;border: 1px solid #28C;height: 32px;word-wrap: break-word;text-alig=\nn: center;margin-top: 20px;margin-bottom: 24px;\">\n=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09<td align=3D\"center\" style=3D\"padding-left:=\n 10px;padding-right: 10px;border-collapse: collapse;\">=09=09=09=09=09=09=\n=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09=09=09=09<table cellspacing=3D\"0\" cellpadding=3D\"0=\n\" border=3D\"0\" class=3D\"fancybuttontableinner\" style=3D\"font-family: 'Helv=\netica Neue'=2CHelvetica=2CArial=2Csans-serif;\">\n=09=09=09=09=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<td height=3D\"32\" class=3D\"fancy=\nbuttonlink\" style=3D\"border-collapse: collapse;font-size: 16px;line-height=\n: 16px;font-weight: bold;color: white;text-align: center;text-decoration:=\n none;text-shadow: 0px -1px #666;text-transform: uppercase;font-family: 'H=\nelvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !important;\"><a href=3D=\n\"http://example.us2.list-manage1.com/track/click?u=3Dcfa47c79110f45a8f68fab=\n75b&id=3Df6e77c4878&e=3D1f1636935c\" target=3D\"_blank\" style=3D\"font-size:=\n 16px;line-height: 16px;font-weight: bold;color: white;text-align: center;=\ntext-decoration: none;text-shadow: 0px -1px #666;text-transform: uppercase=\n;font-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !import=\nant;\">Book</a></td>\n=09=09=09=09=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09=09=09=09</td>\n=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09</table>=09=09=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09</td>\n=09\n=09=09=09=09=09=09<td width=3D\"19\" style=3D\"border-right: 1px #ddd solid;b=\norder-collapse: collapse;\"></td>\n=09=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></=\ntd>\n=09\n=09=09=09=09=09=09<td width=3D\"177\" class=3D\"multicityunit\" style=3D\"borde=\nr-collapse: collapse;padding-top: 20px;padding-bottom: 0px;\">\n=09=09=09=09=09=09=09<a href=3D\"http://example.us2.list-manage1.com/track/c=\nlick?u=3Dcfa47c79110f45a8f68fab75b&id=3D7e38b1717a&e=3D1f1636935c\" target=\n=3D\"_blank\"><img src=3D\"http://www.takktakk.com/forbeingagreatclient/wow/n=\newsletter/i/wow-IS3.jpg\" width=3D\"177\" height=3D\"177\" style=3D\"padding-bot=\ntom: 5px;border: 0;height: auto;line-height: 100%;outline: none;text-decor=\nation: none;\"></a><div class=3D\"from\" style=3D\"font-family: 'Helvetica Neu=\ne'=2C Helvetica=2C Arial=2C sans-serif;text-align: center;font-size: 12px;=\ncolor: #333;font-weight: bold;\">From Copenhagen</div>\n=09=09=09=09=09=09=09<div class=3D\"city blue\" style=3D\"font-size: 18px;lin=\ne-height: 18px;font-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans=\n-serif;text-align: center;font-weight: bold;color: #40c7f4 !important;\">Bl=\nue Lagoon<br>City Break</div>\n=09=09=09=09=09=09=09<div class=3D\"packagedescription\" style=3D\"font-size:=\n 12px;margin: 3px 0 9px;text-align: center;font-weight: bold;font-family:=\n 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !important;\">Return=\n flight=2C 3 nights in a hotel=2C Trip to the Blue Lagoon</div>\n=09=09=09=09=09=09=09<div class=3D\"price purple\" style=3D\"color: #99248D;f=\nont-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif;text-alig=\nn: center;font-size: 22px;line-height: 22px;font-weight: bold;margin-top:=\n 0;margin-bottom: 8px;\">DKK2155*</div>\n=09=09=09=09=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"=\n0\" class=3D\"fancybuttontableouter\" align=3D\"center\" style=3D\"background-im=\nage: url(http://www.takktakk.com/forbeingagreatclient/wow/template/i/hnapp=\nur.png);background-repeat: repeat-x;background-color: #33a9e5;border-radiu=\ns: 5px;border: 1px solid #28C;height: 32px;word-wrap: break-word;text-alig=\nn: center;margin-top: 20px;margin-bottom: 24px;\">\n=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09<td align=3D\"center\" style=3D\"padding-left:=\n 10px;padding-right: 10px;border-collapse: collapse;\">=09=09=09=09=09=09=\n=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09=09=09=09<table cellspacing=3D\"0\" cellpadding=3D\"0=\n\" border=3D\"0\" class=3D\"fancybuttontableinner\" style=3D\"font-family: 'Helv=\netica Neue'=2CHelvetica=2CArial=2Csans-serif;\">\n=09=09=09=09=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<td height=3D\"32\" class=3D\"fancy=\nbuttonlink\" style=3D\"border-collapse: collapse;font-size: 16px;line-height=\n: 16px;font-weight: bold;color: white;text-align: center;text-decoration:=\n none;text-shadow: 0px -1px #666;text-transform: uppercase;font-family: 'H=\nelvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !important;\"><a href=3D=\n\"http://example.us2.list-manage1.com/track/click?u=3Dcfa47c79110f45a8f68fab=\n75b&id=3Dbcb6094df1&e=3D1f1636935c\" target=3D\"_blank\" style=3D\"font-size:=\n 16px;line-height: 16px;font-weight: bold;color: white;text-align: center;=\ntext-decoration: none;text-shadow: 0px -1px #666;text-transform: uppercase=\n;font-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !import=\nant;\">Book</a></td>\n=09=09=09=09=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09=09=09=09</td>\n=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09</table>=09=09=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09</td>=09\n=09=09=09=09=09=09<td width=3D\"19\" style=3D\"border-right: 1px #ddd solid;b=\norder-collapse: collapse;\"></td>\n=09=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></=\ntd>\n=09\n=09=09=09=09=09=09<td width=3D\"176\" class=3D\"multicityunit\" style=3D\"borde=\nr-collapse: collapse;padding-top: 20px;padding-bottom: 0px;\">\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<a href=3D\"http://example.us2.lis=\nt-manage1.com/track/click?u=3Dcfa47c79110f45a8f68fab75b&id=3Dc5c790fe22&e=\n=3D1f1636935c\" target=3D\"_blank\"><img src=3D\"http://www.takktakk.com/forb=\neingagreatclient/wow/newsletter/i/wow-IS2.jpg\" width=3D\"177\" height=3D\"177=\n\" style=3D\"padding-bottom: 5px;border: 0;height: auto;line-height: 100%;ou=\ntline: none;text-decoration: none;\"></a><div class=3D\"from\" style=3D\"font-=\nfamily: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif;text-align: c=\nenter;font-size: 12px;color: #333;font-weight: bold;\">From Copenhagen</div=\n>\n=09=09=09=09=09=09=09<div class=3D\"city blue\" style=3D\"font-size: 18px;lin=\ne-height: 18px;font-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans=\n-serif;text-align: center;font-weight: bold;color: #40c7f4 !important;\">No=\nrthern Lights Adventure Holiday</div>\n=09=09=09=09=09=09=09<div class=3D\"packagedescription\" style=3D\"font-size:=\n 12px;margin: 3px 0 9px;text-align: center;font-weight: bold;font-family:=\n 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !important;\">Return=\n flight=2C 4 nights in a hotel=2C Northern Lights Tour</div>\n=09=09=09=09=09=09=09<div class=3D\"price purple\" style=3D\"color: #99248D;f=\nont-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif;text-alig=\nn: center;font-size: 22px;line-height: 22px;font-weight: bold;margin-top:=\n 0;margin-bottom: 8px;\">DKK2199*</div>\n=09=09=09=09=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"=\n0\" class=3D\"fancybuttontableouter\" align=3D\"center\" style=3D\"background-im=\nage: url(http://www.takktakk.com/forbeingagreatclient/wow/template/i/hnapp=\nur.png);background-repeat: repeat-x;background-color: #33a9e5;border-radiu=\ns: 5px;border: 1px solid #28C;height: 32px;word-wrap: break-word;text-alig=\nn: center;margin-top: 20px;margin-bottom: 24px;\">\n=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09<td align=3D\"center\" style=3D\"padding-left:=\n 10px;padding-right: 10px;border-collapse: collapse;\">=09=09=09=09=09=09=\n=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09=09=09=09<table cellspacing=3D\"0\" cellpadding=3D\"0=\n\" border=3D\"0\" class=3D\"fancybuttontableinner\" style=3D\"font-family: 'Helv=\netica Neue'=2CHelvetica=2CArial=2Csans-serif;\">\n=09=09=09=09=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<td height=3D\"32\" class=3D\"fancy=\nbuttonlink\" style=3D\"border-collapse: collapse;font-size: 16px;line-height=\n: 16px;font-weight: bold;color: white;text-align: center;text-decoration:=\n none;text-shadow: 0px -1px #666;text-transform: uppercase;font-family: 'H=\nelvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !important;\"><a href=3D=\n\"http://example.us2.list-manage.com/track/click?u=3Dcfa47c79110f45a8f68fab7=\n5b&id=3D8107f7bdc8&e=3D1f1636935c\" target=3D\"_blank\" style=3D\"font-size:=\n 16px;line-height: 16px;font-weight: bold;color: white;text-align: center;=\ntext-decoration: none;text-shadow: 0px -1px #666;text-transform: uppercase=\n;font-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !import=\nant;\">Book</a></td>\n=09=09=09=09=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09=09=09=09</td>\n=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09</table>=09=09=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09</td>\n=09=09=09=09=09=09\n=09=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></=\ntd>=09=09=09=09\n=09=09=09=09=09</tr>\n=09=09=09=09</table>\n=09=09=09=09\n=09=09=09=09<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" width=\n=3D\"630\" align=3D\"center\" id=3D\"opening\">\n=09=09=09=09=09<tr>\n=09=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></=\ntd>\n=09=09=09=09=09=09<td width=3D\"590\" style=3D\"padding-top: 5px;padding-bott=\nom: 60px;border-collapse: collapse;\">\n=09=09=09=09=09=09=09<div class=3D\"openingtext\" style=3D\"font-size: 24px;l=\nine-height: 24px;padding-bottom: 0;margin-bottom: 20px;padding-top: 10px;c=\nolor: #99248D;display: block;font-weight: bold;margin-top: 0px;margin-righ=\nt: 0;margin-left: 0;text-align: left;font-family: 'Helvetica Neue'=2C Helv=\netica=2C Arial=2C 'Lucida Grande'=2C sans-serif !important;\">\n=09=09=09=09=09=09=09=09<span class=3D\"blue\" style=3D\"color: #40c7f4 !impo=\nrtant;\">Not in Copenhagen?</span> <span style=3D\"font-weight: normal;color=\n: #000;\" class=3D\"black\">We also have great packages for visitors flying i=\nn from <a target=3D\"_blank\" style=3D\"font-weight: bold; color: #99248D;\" h=\nref=3D\"http://example.us2.list-manage2.com/track/click?u=3Dcfa47c79110f45a8=\nf68fab75b&id=3Dac314bd1aa&e=3D1f1636935c\">Paris</a>=2C <a target=3D\"_blan=\nk\" style=3D\"font-weight: bold; color: #99248D;\" href=3D\"http://example.us2.=\nlist-manage.com/track/click?u=3Dcfa47c79110f45a8f68fab75b&id=3D885062cca5&=\ne=3D1f1636935c\">London</a> and <a target=3D\"_blank\" style=3D\"font-weight:=\n bold; color: #99248D;\" href=3D\"http://example.us2.list-manage.com/track/cl=\nick?u=3Dcfa47c79110f45a8f68fab75b&id=3D5eee181315&e=3D1f1636935c\">Berlin<=\n/a>.</span>\n=09=09=09=09=09=09=09</div>\n=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09<!--<div mc:hideable>\n=09=09=09=09=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"=\n0\" class=3D\"fancybuttontableouter\">\n=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09<td align=3D\"center\" style=3D\"padding-left:1=\n0px;padding-right:10px;\">=09=09=09=09=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09=09=09=09<table cellspacing=3D\"0\" cellpadding=3D\"0=\n\" border=3D\"0\" class=3D\"fancybuttontableinner\">\n=09=09=09=09=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<td height=3D\"32\" class=3D\"fancy=\nbuttonlink\"><span mc:edit=3D\"main_call_to_action_1\"><a href=3D\"http://www.=\nwowiceland.co.uk/\" target=3D\"_blank\">Click to book &rarr;</a></span></td>\n=09=09=09=09=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09=09=09=09</td>\n=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09</div>-->\n=09=09=09=09=09=09</td>\n=09=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></=\ntd>\n=09=09=09=09=09</tr>\n=09=09=09=09</table>\n=09=09=09</div>\n=09\n\n\n<!-- ITEM 2 --\n\n=09<!-- Item 2 Variation TEXT LEFT PICTURE RIGHT --\n=09\n=09=09<div mc:repeatable=3D\"item2\" mc:variant=3D\"Text left=2C picture righ=\nt\">\n=09=09=09<table align=3D\"center\" border=3D\"0\" cellspacing=3D\"0\" cellpaddin=\ng=3D\"0\" width=3D\"630\" class=3D\"itembox\">\n=09=09=09=09<tr valign=3D\"top\">\n=09=09=09=09=09<td width=3D\"20\"></td>\n=09=09=09=09=09<td width=3D\"360\" class=3D\"item\">\n=09=09=09=09=09=09<h1 mc:edit=3D\"item2v1mainhead\">The snappy text-left hea=\ndline goes here</h1>\n=09=09=09=09=09=09<h2 mc:edit=3D\"item2v1mainsubhead\">The descriptive subhe=\nadline goes here.</h2>\n=09=09=09=09=09=09<div mc:edit=3D\"item2v1maintext\" class=3D\"itemmaintext\">\n=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09Lorem Ipsum is simply dummy text of the printing a=\nnd typesetting industry. Lorem Ipsum has been the industry's standard dumm=\ny text ever since the 1500s=2C when an unknown printer took a galley of ty=\npe.<br><br>\n=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09It has survived not only <a target=3D\"_blank\" href=\n=3D\"http://www.takktakk.is/\">Find the best prices here</a>=2C but also the=\n leap into electronic typesetting=2C remaining essentially unchanged.<br><=\nbr>\n=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09It was popularised in the 1960s with <span style=\n=3D\"BACKGROUND: #FBF17A\">the release of Letraset</span> sheets containing=\n Lorem Ipsum passages=2C and more recently with desktop publishing softwar=\ne like Aldus PageMaker including versions of Lorem Ipsum.\n\n=09=09=09=09=09=09</div>\n\n=09=09=09=09=09=09<div mc:hideable=3D\"\">\n=09=09=09=09=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"=\n0\" class=3D\"fancybuttontableouter\">\n=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09<td align=3D\"center\" style=3D\"padding-left:1=\n0px;padding-right:10px;\">=09=09=09=09=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09=09=09=09<table cellspacing=3D\"0\" cellpadding=3D\"0=\n\" border=3D\"0\" class=3D\"fancybuttontableinner\">\n=09=09=09=09=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<td height=3D\"32\" class=3D\"fancy=\nbuttonlink\"><span mc:edit=3D\"item2v1_call_to_action\"><a href=3D\"http://www=\n=2Ewowiceland.co.uk/\" target=3D\"_blank\">Insert link here &rarr;</a></span></=\ntd>\n=09=09=09=09=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09=09=09=09</td>\n=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09</div>\n=09=09=09=09=09</td>\n=09=09=09=09=09<td width=3D\"20\"></td>\n=09=09=09=09=09<td width=3D\"210\" class=3D\"item\"><img src=3D\"http://www.tak=\nktakk.com/forbeingagreatclient/icelandexpress/newsletter/i/citybreak.jpg\"=\n mc:label=3D\"item2v1_image\" mc:edit=3D\"item2v1_image\" style=3D\"max-width:2=\n10px;\"></td>=09=09=09=09=09\n=09=09=09=09=09<td width=3D\"20\"></td>\n=09=09=09=09</tr>\n=09=09=09</table>\n=09=09</div>\n=09=09\n=09=09=09=09\n\n\n\n=09<!-- Item 2 Variation PICTURE LEFT TEXT RIGHT --\n=09\n=09=09<div mc:repeatable=3D\"item2\" mc:variant=3D\"Picture left=2C text righ=\nt\">\n=09=09=09<table align=3D\"center\" border=3D\"0\" cellspacing=3D\"0\" cellpaddin=\ng=3D\"0\" width=3D\"630\" class=3D\"itembox\">\n=09=09=09=09<tr valign=3D\"top\">\n=09=09=09=09=09<td width=3D\"20\"></td>\n=09=09=09=09=09<td width=3D\"210\" class=3D\"item\"><img src=3D\"http://www.tak=\nktakk.com/forbeingagreatclient/wow/newsletter/i/fyrirtaekjasamningur-210.p=\nng\" mc:label=3D\"item2v2_image\" mc:edit=3D\"item2v2_image\" style=3D\"max-widt=\nh:210px;\"></td>=09=09=09=09=09\n=09=09=09=09=09<td width=3D\"20\"></td>\n=09=09=09=09=09<td width=3D\"360\" class=3D\"item\">\n=09=09=09=09=09=09<a id=3D\"fyrirtaekjasamningar\"></a><h1 mc:edit=3D\"item2v=\n2mainhead\">Gamanfer=C3=B0ir pakkar</h1>\n=09=09=09=09=09=09<h2 mc:edit=3D\"item2v2mainsubhead\">Fyrirt=C3=A6kja=C3=BE=\nj=C3=B3nusta WOW b=C3=BD=C3=B0ur fast ver=C3=B0 og meiri sveigjanleika.</h=\n2>\n=09=09=09=09=09=09<div mc:edit=3D\"item2v2maintext\" class=3D\"itemmaintext\">\n=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09Af hverju a=C3=B0 borga meira? Hj=C3=A1 WOW getur=\n fyrirt=C3=A6ki=C3=B0 =C3=BEitt fengi=C3=B0 hagst=C3=A6=C3=B0an samning me=\n=C3=B0 f=C3=B6stum fer=C3=B0akostna=C3=B0i =C3=A1 bestu kj=C3=B6rum.<br><b=\nr>\n=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09=C3=8D sumar flj=C3=BAgum vi=C3=B0 13 sinnum =C3=\n=AD viku til London=2C 10 til Kaupmannahafnar=2C 6 til Par=C3=ADsar og til=\n 10 annarra sta=C3=B0a =C3=AD Evr=C3=B3pu.<br><br>\n=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09<span style=3D\"BACKGROUND: #FBF17A\">Fast ver=C3=B0=\n=2C tv=C3=A6r lei=C3=B0ir: 19.900 e=C3=B0a 29.900</span>.\n\n=09=09=09=09=09=09</div>\n\n=09=09=09=09=09=09<div mc:hideable=3D\"\">\n=09=09=09=09=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"=\n0\" class=3D\"fancybuttontableouter\">\n=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09<td align=3D\"center\" style=3D\"padding-left:1=\n0px;padding-right:10px;\">=09=09=09=09=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09=09=09=09<table cellspacing=3D\"0\" cellpadding=3D\"0=\n\" border=3D\"0\" class=3D\"fancybuttontableinner\">\n=09=09=09=09=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<td height=3D\"32\" class=3D\"fancy=\nbuttonlink\"><span mc:edit=3D\"item2v2_call_to_action\"><a href=3D\"http://wow=\n=2Eis/fyrirtaekjathjonusta\" target=3D\"_blank\">S=C3=A6kja um fyrirt=C3=A6kjas=\namning &rarr;</a></span></td>\n=09=09=09=09=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09=09=09=09</td>\n=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09</div>\n=09=09=09=09=09</td>\n=09=09=09=09=09<td width=3D\"20\"></td>\n=09=09=09=09</tr>\n=09=09=09</table>\n=09=09</div>\n\n=09=09\n=09=09\n=09=09\n=09=09\n<!-- Item 2 Variation FULL WIDTH -->\n=09\n=09=09<div mc:repeatable=3D\"repeat_1\" mc:variant=3D\"Full width\" mc:repeati=\nndex=3D\"0\" mc:hideable=3D\"hideable_repeat_1_1\" mchideable=3D\"hideable_repe=\nat_1_1\">\n=09=09=09<table align=3D\"center\" border=3D\"0\" cellspacing=3D\"0\" cellpaddin=\ng=3D\"0\" width=3D\"630\" class=3D\"itembox\" style=3D\"border-bottom: none;paddi=\nng-bottom: 30px;background-color: #fff;\">\n=09=09=09=09<tr valign=3D\"top\">\n=09=09=09=09=09<td width=3D\"630\" class=3D\"item\" style=3D\"padding-bottom: 0=\npx;padding-top: 20px;border-collapse: collapse;\">\n=09=09=09=09=09=09<div style=3D\"text-align: center;\"><img src=3D\"http://ww=\nw.takktakk.com/forbeingagreatclient/wow/newsletter/i/wow-moments-en.jpg\" a=\nlt=3D\"\" border=3D\"0\" style=3D\"margin: 0;padding: 0;max-width: 630px;border=\n: 0;height: auto;line-height: 100%;outline: none;text-decoration: none;\"><=\n/div>\n=09=09=09=09=09</td>\n=09=09=09=09</tr>\n=09=09=09</table>\n\n=09=09=09<table align=3D\"center\" border=3D\"0\" cellspacing=3D\"0\" cellpaddin=\ng=3D\"0\" width=3D\"630\" class=3D\"itembox\" style=3D\"background-color: #fff;bo=\nrder-bottom: #ddd 1px solid;\">\n=09=09=09=09<tr valign=3D\"top\">\n=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></td>\n=09=09=09=09=09<td width=3D\"590\" class=3D\"item\" style=3D\"padding-top: 0 !i=\nmportant;margin-top: 0;border-collapse: collapse;padding-bottom: 40px;\">\n=09=09=09=09=09=09<h1 style=3D\"color: #99248D;display: block;font-size: 34=\npx;font-weight: bold;line-height: 100%;margin-top: 0px;margin-right: 0;mar=\ngin-bottom: 20px;margin-left: 0;text-align: left;font-family: 'Helvetica N=\neue'=2C Helvetica=2C Arial=2C 'Lucida Grande'=2C sans-serif !important;\">T=\nake a moment</h1>\n=09=09=09=09=09=09<h2 style=3D\"font-weight: bold;font-size: 17px;line-heig=\nht: 21px;margin-top: 0px;margin-right: 0;margin-bottom: 20px;margin-left:=\n 0;text-align: left;color: #000 !important;font-family: 'Helvetica Neue'=\n=2C Helvetica=2C Arial=2C 'Lucida Grande'=2C sans-serif !important;\">We as=\nked you to send us photos of your WOW moments in Iceland. Here they are=2C=\n all 551 of&nbsp;them.</h2>\n<!--=09=09=09=09=09=09<div mc:edit=3D\"item2v3maintext\" class=3D\"itemmainte=\nxt\">\n=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09 =09We\n=09=09=09=09=09=09</div>\n-->\n=09=09=09=09=09=09<div mc:hideable=3D\"hideable_repeat_1_2\" mchideable=3D\"h=\nideable_repeat_1_2\">\n=09=09=09=09=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"=\n0\" class=3D\"fancybuttontableouter\" style=3D\"background-image: url(http://w=\nww.takktakk.com/forbeingagreatclient/wow/template/i/hnappur.png);backgroun=\nd-repeat: repeat-x;background-color: #33a9e5;border-radius: 5px;border: 1p=\nx solid #28C;height: 32px;word-wrap: break-word;text-align: center;margin-=\ntop: 20px;margin-bottom: 24px;\">\n=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09<td align=3D\"center\" style=3D\"padding-left:=\n 10px;padding-right: 10px;border-collapse: collapse;\">=09=09=09=09=09=09=\n=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09=09=09=09<table cellspacing=3D\"0\" cellpadding=3D\"0=\n\" border=3D\"0\" class=3D\"fancybuttontableinner\" style=3D\"font-family: 'Helv=\netica Neue'=2CHelvetica=2CArial=2Csans-serif;\">\n=09=09=09=09=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<td height=3D\"32\" class=3D\"fancy=\nbuttonlink\" style=3D\"border-collapse: collapse;font-size: 16px;line-height=\n: 16px;font-weight: bold;color: white;text-align: center;text-decoration:=\n none;text-shadow: 0px -1px #666;text-transform: uppercase;font-family: 'H=\nelvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !important;\"><span><a h=\nref=3D\"http://example.us2.list-manage.com/track/click?u=3Dcfa47c79110f45a8f=\n68fab75b&id=3De26962115a&e=3D1f1636935c\" target=3D\"_blank\" style=3D\"color=\n: white;font-size: 16px;line-height: 16px;font-weight: bold;text-align: ce=\nnter;text-decoration: none;text-shadow: 0px -1px #666;text-transform: uppe=\nrcase;font-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !i=\nmportant;\">Your photos from Iceland &rarr;</a></span></td>\n=09=09=09=09=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09=09=09=09</td>\n=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09</div>\n=09=09=09=09=09</td>\n=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></td>\n=09=09=09=09</tr>\n=09=09=09</table>\n=09=09</div>\n\n\n=09=09\n=09=09\n=09=09\n\n<!-- Item 2 Variation FULL WIDTH --\n=09\n=09=09<div mc:repeatable=3D\"item2\" mc:variant=3D\"Full width\">\n=09=09=09<table align=3D\"center\" border=3D\"0\" cellspacing=3D\"0\" cellpaddin=\ng=3D\"0\" width=3D\"630\" class=3D\"itembox\" style=3D\"border-bottom: none; padd=\ning-bottom: 30px;\">\n=09=09=09=09<tr valign=3D\"top\">\n=09=09=09=09=09<td width=3D\"630\" class=3D\"item\" style=3D\"padding-bottom: 0=\npx; padding-top: 20px;\">\n=09=09=09=09=09=09<img src=3D\"http://www.takktakk.com/forbeingagreatclient=\n/wow/newsletter/i/wow-bcnwide.jpg\" width=3D\"630\" height=3D\"337\" align=3D\"c=\nenter\" mc:label=3D\"item2v3a_image\" mc:edit=3D\"item2v3_image\" style=3D\"max-=\nwidth:630px;\">\n=09=09=09=09=09</td>\n=09=09=09=09</tr>\n=09=09=09</table>\n\n=09=09=09<table align=3D\"center\" border=3D\"0\" cellspacing=3D\"0\" cellpaddin=\ng=3D\"0\" width=3D\"630\" class=3D\"itembox\">\n=09=09=09=09<tr valign=3D\"top\">\n=09=09=09=09=09<td width=3D\"20\"></td>\n=09=09=09=09=09<td width=3D\"590\" class=3D\"item\" style=3D\"padding-top: 0 !i=\nmportant; margin-top: 0;\">\n=09=09=09=09=09=09<h1 mc:edit=3D\"item2v3mainhead\">La Rambla=2C flamenco=2C=\n La Sagrada Familia=2C Parc G=C3=BCell=2C tapas=2C Sitges=2C Cava og Monts=\nerrat.</h1>\n=09=09=09=09=09=09<h2 mc:edit=3D\"item2v3mainsubhead\">Allt um Barcelona hj=\n=C3=A1 vinum okkar =C3=AD WOW magazine.</h2>\n=09=09=09=09=09=09<div mc:edit=3D\"item2v3maintext\" class=3D\"itemmaintext\">\n=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09 =09Lorem Ipsum is simply dummy text of the printing=\n and typesetting industry. Lorem Ipsum has been the industry's standard du=\nmmy text ever since the 1500s=2C when an unknown printer took a galley of=\n type\n=09=09=09=09=09=09</div>\n\n=09=09=09=09=09=09<div mc:hideable=3D\"\">\n=09=09=09=09=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"=\n0\" class=3D\"fancybuttontableouter\">\n=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09<td align=3D\"center\" style=3D\"padding-left:1=\n0px;padding-right:10px;\">=09=09=09=09=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09=09=09=09<table cellspacing=3D\"0\" cellpadding=3D\"0=\n\" border=3D\"0\" class=3D\"fancybuttontableinner\">\n=09=09=09=09=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<td height=3D\"32\" class=3D\"fancy=\nbuttonlink\"><span mc:edit=3D\"item2v3_call_to_action\"><a href=3D\"http://www=\n=2Ewow.is/barcelona\" target=3D\"_blank\">Lestu um Barcelona &rarr;</a></span><=\n/td>\n=09=09=09=09=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09=09=09=09</td>\n=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09</div>\n=09=09=09=09=09</td>\n=09=09=09=09=09<td width=3D\"20\"></td>\n=09=09=09=09</tr>\n=09=09=09</table>\n=09=09</div>\n\n\n=09=09\n=09=09\n=09=09\n<!-- Item 2 Variation FULL WIDTH IMAGE WITHOUT TEXT--\n=09\n=09=09<div mc:repeatable=3D\"item2\" mc:variant=3D\"Full-width-image-no-text\"=\n>\n=09=09=09<table align=3D\"center\" border=3D\"0\" cellspacing=3D\"0\" cellpaddin=\ng=3D\"0\" width=3D\"630\" class=3D\"itembox\" style=3D\"padding-bottom: 40px;\">\n=09=09=09=09<tr valign=3D\"top\">\n=09=09=09=09=09<td width=3D\"630\" class=3D\"item\" style=3D\"padding-bottom: 0=\npx; padding-top: 20px;\">\n=09=09=09=09=09=09<img src=3D\"http://www.takktakk.com/forbeingagreatclient=\n/wow/newsletter/i/wow-bcnwide.jpg\" width=3D\"630\" height=3D\"337\" align=3D\"c=\nenter\" mc:label=3D\"item2v3a_image\" mc:edit=3D\"item2v4_image\" style=3D\"max-=\nwidth:630px;\">\n=09=09=09=09=09</td>\n=09=09=09=09</tr>\n=09=09=09</table>\n=09=09</div>\n\n\n=09=09\n=09=09\n=09=09\n<!-- SOCIAL -->\n\n=09=09<div mc:hideable=3D\"hideable_6\" mchideable=3D\"hideable_6\">\n=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" align=3D\"=\ncenter\" width=3D\"630\" bgcolor=3D\"#f3ede6\" id=3D\"socialbox\" style=3D\"border=\n-bottom: #ddd 1px solid;padding-bottom: 0px;\">\n=09=09=09=09<tr>\n=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></td>\n=09=09=09=09=09<td width=3D\"590\" align=3D\"center\" class=3D\"social\" style=\n=3D\"border-collapse: collapse;padding-top: 40px;padding-bottom: 0px;\">\n=09=09=09=09=09=09<img src=3D\"http://www.takktakk.com/forbeingagreatclient=\n/wow/newsletter/i/wow-freyja2.png\" align=3D\"left\" width=3D\"87\" height=3D\"1=\n87\" style=3D\"border: 0;height: auto;line-height: 100%;outline: none;text-d=\necoration: none;\"><p style=3D\"margin: 0;font-weight: bold;font-size: 24px;=\ncolor: #aaa;padding-top: 0px;padding-bottom: 40px;font-family: 'Helvetica=\n Neue'=2CHelvetica=2CArial=2Csans-serif !important;\"><span style=3D\"color:=\n #000;\">Look=2C we're all over the place.</span><br><br>Be our friend on <=\na target=3D\"_blank\" class=3D\"facebook\" href=3D\"http://example.us2.list-mana=\nge.com/track/click?u=3Dcfa47c79110f45a8f68fab75b&id=3D2646ba2312&e=3D=\n1f1636935c\" style=3D\"color: #3B5998 !important;\">Facebook</a>=2C<br>follow us=\n on <a style=3D\"color: #C92228;\" target=3D\"_blank\" href=3D\"http://example.u=\ns2.list-manage2.com/track/click?u=3Dcfa47c79110f45a8f68fab75b&id=3D5be9e2d=\na90&e=3D1f1636935c\">Pinterest</a>=2C<br>and read our <a target=3D\"_blank\"=\n class=3D\"purple\" href=3D\"http://example.us2.list-manage.com/track/click?u=\n=3Dcfa47c79110f45a8f68fab75b&id=3Daacf017a7d&e=3D1f1636935c\" style=3D\"col=\nor: #99248D;\">fancy magazine</a>.</p>\n=09=09=09=09=09</td>\n=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></td>\n=09=09=09=09</tr>\n=09=09=09</table>\n=09=09</div>\n=09\n\n\n\n\n=09=09\n=09=09\n=09=09\n<!-- CLOSING -->\n\n=09=09<div mc:hideable=3D\"hideable_7\" mchideable=3D\"hideable_7\">\n=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"0\" width=3D\"=\n630\" align=3D\"center\" id=3D\"closingbox\" style=3D\"padding-bottom: 40px;\">\n=09=09=09=09<tr>\n=09=09=09=09=09<td width=3D\"20\" style=3D\"border-collapse: collapse;\"></td>\n=09=09=09=09=09<td width=3D\"400\" class=3D\"closing\" style=3D\"border-collaps=\ne: collapse;padding-top: 40px;margin-bottom: 16px;color: #40c7f4;font-size=\n: 36px;line-height: 36px;font-family: 'Helvetica Neue'=2CHelvetica=2CArial=\n=2Csans-serif !important;\">\n=09=09=09=09=09=09<div class=3D\"closingtext\" style=3D\"margin-bottom: 12px;=\n\"><span class=3D\"purple bold\" style=3D\"color: #99248D;font-weight: bold;\">=\nPlan ahead.</span>&nbsp;Check out the <strong>10 most popular tours</stron=\ng> in Iceland.</div>=09\n=09=09=09=09=09=09=09=09=09\n=09=09=09=09=09=09<div mc:hideable=3D\"hideable_8\" mchideable=3D\"hideable_8=\n\">\n=09=09=09=09=09=09=09<table cellpadding=3D\"0\" cellspacing=3D\"0\" border=3D\"=\n0\" class=3D\"fancybuttontableouter\" style=3D\"background-image: url(http://w=\nww.takktakk.com/forbeingagreatclient/wow/template/i/hnappur.png);backgroun=\nd-repeat: repeat-x;background-color: #33a9e5;border-radius: 5px;border: 1p=\nx solid #28C;height: 32px;word-wrap: break-word;text-align: center;margin-=\ntop: 20px;margin-bottom: 24px;\">\n=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09<td align=3D\"center\" style=3D\"padding-left:=\n 10px;padding-right: 10px;border-collapse: collapse;\">=09=09=09=09=09=09=\n=09=09=09=09=09=09\n=09=09=09=09=09=09=09=09=09=09=09<table cellspacing=3D\"0\" cellpadding=3D\"0=\n\" border=3D\"0\" class=3D\"fancybuttontableinner\" style=3D\"font-family: 'Helv=\netica Neue'=2CHelvetica=2CArial=2Csans-serif;\">\n=09=09=09=09=09=09=09=09=09=09=09=09<tbody>\n=09=09=09=09=09=09=09=09=09=09=09=09=09<tr>\n=09=09=09=09=09=09=09=09=09=09=09=09=09=09<td height=3D\"32\" class=3D\"fancy=\nbuttonlink\" style=3D\"border-collapse: collapse;font-size: 16px;line-height=\n: 16px;font-weight: bold;color: white;text-align: center;text-decoration:=\n none;text-shadow: 0px -1px #666;text-transform: uppercase;font-family: 'H=\nelvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !important;\"><span><a h=\nref=3D\"http://example.us2.list-manage.com/track/click?u=3Dcfa47c79110f45a8f=\n68fab75b&id=3Daff82e7870&e=3D1f1636935c\" target=3D\"_blank\" style=3D\"font-=\nsize: 16px;line-height: 16px;font-weight: bold;color: white;text-align: ce=\nnter;text-decoration: none;text-shadow: 0px -1px #666;text-transform: uppe=\nrcase;font-family: 'Helvetica Neue'=2C Helvetica=2C Arial=2C sans-serif !i=\nmportant;\">Take a look &rarr;</a></span></td>\n=09=09=09=09=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09=09=09=09=09</td>\n=09=09=09=09=09=09=09=09=09</tr>\n=09=09=09=09=09=09=09=09</tbody>\n=09=09=09=09=09=09=09</table>\n=09=09=09=09=09=09</div>\n=09=09=09=09=09</td>\n=09=09=09=09=09<td width=3D\"210\" class=3D\"closing\" valign=3D\"top\" style=3D=\n\"border-collapse: collapse;padding-top: 40px;margin-bottom: 16px;color: #4=\n0c7f4;font-size: 36px;line-height: 36px;font-family: 'Helvetica Neue'=2CHe=\nlvetica=2CArial=2Csans-serif !important;\">\n=09=09=09=09=09=09<img src=3D\"http://www.takktakk.com/forbeingagreatclient=\n/wow/template/i/wow-plane-210.png\" border=3D\"none\" width=3D\"210\" height=3D=\n\"69\" style=3D\"padding-top: 60px;padding-bottom: 20px;border: 0;height: aut=\no;line-height: 100%;outline: none;text-decoration: none;\">\n=09=09=09=09=09</td>\n=09=09=09=09</tr>\n=09=09=09</table>\n=09=09</div>\n\n\n\n\n<!-- {Main Area Ends} -->\n\n=09=09</td>\n=09</tr>\n</table>\n\n\n\n\n<!-- PREFOOTER -->\n\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" width=3D\"100%\" id=\n=3D\"prefooter\" style=3D\"background-color: #99248D;border-bottom: #5CC4F0 4=\npx solid;border-top: #5CC4F0 4px solid;\">\n=09<tr>\n=09=09<td style=3D\"border-collapse: collapse;\">\n=09=09=09<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" width=3D\"=\n590\" align=3D\"center\">\n=09=09=09=09<tr>\n=09=09=09=09=09<td width=3D\"590\" class=3D\"prefooter\" style=3D\"border-colla=\npse: collapse;color: #fff;font-size: 10px;font-weight: bold;line-height: 1=\n6px;height: 20px;font-family: 'Lucida sans Unicode'=2C 'Lucida Grande'=2C=\n 'Lucida Grande'=2C Helvetica=2C Arial=2C sans-serif !important;\">\n=09=09=09=09=09=09<div>Our CEO politely requests that you read this newsle=\ntter again.</div>\n=09=09=09=09=09</td>\n=09=09=09=09</tr>\n=09=09=09</table>\n=09=09</td>\n=09</tr>\n</table>\n\n\n\n<!-- FOOTER -->\n\n<table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" width=3D\"100%\" id=\n=3D\"footer\" style=3D\"background-color: #fff;\">\n=09<tr>\n=09=09<td style=3D\"border-collapse: collapse;\">\n=09=09=09<table border=3D\"0\" cellpadding=3D\"20\" cellspacing=3D\"0\" width=3D=\n\"630\" align=3D\"center\">\n=09=09=09=09<tr>\n=09=09=09=09=09<td class=3D\"footer\" style=3D\"border-collapse: collapse;\">\n=09=09=09=09=09=09<img src=3D\"http://www.takktakk.com/forbeingagreatclient=\n/wow/template/i/wow-stamp.png\" align=3D\"right\" height=3D\"153\" width=3D\"160=\n\" style=3D\"border: 0;height: auto;line-height: 100%;outline: none;text-dec=\noration: none;\">\n=09=09=09=09=09=09<p style=3D\"color: #333;font-size: 11px;line-height: 13p=\nx;font-weight: normal;vertical-align: middle;font-family: 'Lucida sans Uni=\ncode'=2C 'Lucida Grande'=2C Helvetica=2C Arial=2C sans-serif !important;\">=\n<strong>Oh=2C hi there.</strong> This email is sent to <a href=3D\"mailto:=\nbre@example.com\" style=3D\"text-decoration: none;color: #99248D;font-weight=\n: bold;\">bre@example.com</a> by WOW air=2C Katr=C3=ADnart=C3=BAni 12=2C 1=\n05 Reykjavik=2C Iceland=2C because at some point you subscribed to our new=\nsletter or=2C more likely=2C the newsletter of Iceland Express=2C which is=\n now a part of WOW air. Confused? Well=2C that's how it goes.</p>=09=09=09=\n=09=09=09\n=09=09=09=09=09=09<p style=3D\"color: #333;font-size: 11px;line-height: 13p=\nx;font-weight: normal;vertical-align: middle;font-family: 'Lucida sans Uni=\ncode'=2C 'Lucida Grande'=2C Helvetica=2C Arial=2C sans-serif !important;\">=\n<strong>Talk to us</strong> &mdash; but please don't reply to this email.=\n It's sent by very busy and important people who are <span>busy arguing ab=\nout how to spell Reykjav&iacute;k.</span><br>&rarr; <a target=3D\"_blank\" h=\nref=3D\"http://example.us2.list-manage.com/track/click?u=3Dcfa47c79110f45a8f=\n68fab75b&id=3Db57cfe5507&e=3D1f1636935c\" style=3D\"text-decoration: none;c=\nolor: #99248D;font-weight: bold;\">Click here for better ways to contact us=\n</a>.</p>\n=09=09=09=09=09=09<p style=3D\"color: #333;font-size: 11px;line-height: 13p=\nx;font-weight: normal;vertical-align: middle;font-family: 'Lucida sans Uni=\ncode'=2C 'Lucida Grande'=2C Helvetica=2C Arial=2C sans-serif !important;\">=\n<strong>The small print.</strong> <span>Unless stated otherwise=2C prices=\n are for flights=2C one-way=2C inclusive of tax and charges. An asterisk*=\n indicates the lowest price available at the time of sending. Package tour=\n prices are based on return flights and price per person for twin/double r=\noom hotel accommodation with two sharing unless otherwise stated. Booking=\n fee is additional. The cost of checked-in luggage is not included unless=\n otherwise stated. So=2C yeah.</span></p>\n=09=09=09=09=09=09<p style=3D\"color: #333;font-size: 11px;line-height: 13p=\nx;font-weight: normal;vertical-align: middle;font-family: 'Lucida sans Uni=\ncode'=2C 'Lucida Grande'=2C Helvetica=2C Arial=2C sans-serif !important;\">=\n<strong>The even smaller print.</strong> <span>Even though the people writ=\ning this are really=2C really smart=2C they're not robots. They're only hu=\nman=2C and sometimes they make misteakes and speling erors. If any of thos=\ne have crept into this newsletter=2C they're really sorry.</span></p>\n=09=09=09=09=09=09<p style=3D\"color: #333;font-size: 11px;line-height: 13p=\nx;font-weight: normal;vertical-align: middle;font-family: 'Lucida sans Uni=\ncode'=2C 'Lucida Grande'=2C Helvetica=2C Arial=2C sans-serif !important;\">=\n<strong>The smallest print of all.</strong> <span>This newsletter is hand-=\nmade from locally sourced ingredients using brains and fancy machines.</sp=\nan></p>\n=09=09=09=09=09=09<p style=3D\"color: #333;font-size: 11px;line-height: 13p=\nx;font-weight: normal;vertical-align: middle;font-family: 'Lucida sans Uni=\ncode'=2C 'Lucida Grande'=2C Helvetica=2C Arial=2C sans-serif !important;\">=\na>.</p>\n=09=09=09=09=09</td>\n=09=09=09=09</tr>\n=09=09=09</table>\n=09=09</td>\n=09</tr>\n</table>\n\n\n\n\n<img src=3D\"http://example.us2.list-manage.com/track/open.php?u=3Dcfa47c791=\n10f45a8f68fab75b&id=3Ddb82fef684&e=3D1f1636935c\" height=3D\"1\" width=3D\"1\"=\n></body>\n</html>\n--_----------=_MCPart_2065307511--\n\nFrom bre@slinky  Fri Jan 10 15:32:16 2014\nReturn-Path: <bre@slinky>\nX-Original-To: bre@slinky\nDelivered-To: bre@slinky\nReceived: by slinky (Postfix, from userid 1000)\n\tid DBB6A462C7; Fri, 10 Jan 2014 15:32:15 +0000 (GMT)\nDate: Fri, 10 Jan 2014 15:32:15 +0000\nFrom: Bjarni Runar Einarsson <bre@slinky>\nTo: Bjarni Runar Einarsson <bre@slinky>\nCc: Nobody <nobody@nowhere.com>\nSubject: Testing encryption, raw ISO-8859-1: \nMessage-ID: <20140110153215.GA12247@slinky>\nMIME-Version: 1.0\nContent-Type: multipart/encrypted; protocol=\"application/pgp-encrypted\";\n\tboundary=\"3V7upXqbjpZ4EhLz\"\nContent-Disposition: inline\nUser-Agent: Mutt/1.5.21 (2010-09-15)\nContent-Length: 956\nLines: 28\n\n\n--3V7upXqbjpZ4EhLz\nContent-Type: application/pgp-encrypted\nContent-Disposition: attachment\n\nVersion: 1\n\n--3V7upXqbjpZ4EhLz\nContent-Type: application/octet-stream\nContent-Disposition: attachment; filename=\"msg.asc\"\n\n-----BEGIN PGP MESSAGE-----\nVersion: GnuPG v1.4.14 (GNU/Linux)\n\nhIwDPobfLEGaNZgBBADRFKHhXFgLhMfQgEgFtGal1yxy4Up+TJTp9JcnxudA1cR0\nlpK02mHowJ8dsKTPIEa21Z5bLjVmkF1tPMZt5HbrmoD9U25GPSsvIZ6yDB8tX1F9\ntl1pNEy+kKWh/XvuK8yXAbuG00KPuFfl6om1eeKlJVwFtJ9b9HmkZZUckiZCgtLA\neAFIoV9OLe/HJrkofuvYmQk/k+K4+f7oDJ9i6VpdD1j3G+nQ/L3iH0dJeXq8vIhl\nswpByx+fxEjUHL4sFoZ9UocPoqVnXllMDIi0O5pAuCquauPEc7gwjrmbyU8wHAyC\nsXbAFU0K60dVMdQ8+crYAPnaENjxxMN4Y4h1gYVfTD6fPRjgd9u3Fm6qAdH+hpb0\ncMaqdBOcDNxklcZYxdFXpPWj3zUZMDH+b0wsVzpk3snL9dUOteuSX568w4N9p66V\nNepwSnpZc42+tpphGso0dNK1A11CSmfbdpwCb88w5jt5NIcjt/anu704ViRhxQmO\nH9+os+AeAmbJeGfZe5AbTYW+nTjuW2tYz+IY9u7MyHBKjVvLXHhwMRC6mxtSD7sE\nB20WcSPIvkc5vjC1ydcGKe8T6OlLmwN1Jg==\n=n4kr\n-----END PGP MESSAGE-----\n\n--3V7upXqbjpZ4EhLz--\n\nFrom bre@slinky  Fri Jan 10 15:32:54 2014\nReturn-Path: <bre@slinky>\nX-Original-To: bre@slinky\nDelivered-To: bre@slinky\nReceived: by slinky (Postfix, from userid 1000)\n\tid CD259462C7; Fri, 10 Jan 2014 15:32:54 +0000 (GMT)\nDate: Fri, 10 Jan 2014 15:32:54 +0000\nFrom: Bjarni Runar Einarsson <bre@slinky>\nTo: Bjarni Runar Einarsson <bre@slinky>\nSubject: Testing signatures, raw UTF-8: áéíóúýþæöðÁÉÍÓÚÝÞÆÖÐ\nMessage-ID: <20140110153254.GB12247@slinky>\nMIME-Version: 1.0\nContent-Type: multipart/signed; micalg=pgp-sha256;\n\tprotocol=\"application/pgp-signature\"; boundary=\"MW5yreqqjyrRcusr\"\nContent-Disposition: inline\nUser-Agent: Mutt/1.5.21 (2010-09-15)\nStatus: RO\nContent-Length: 524\nLines: 23\n\n\n--MW5yreqqjyrRcusr\nContent-Type: text/plain; charset=us-ascii\nContent-Disposition: inline\n\nThis should be a signed message, hello world!\n\nLa la la.\n\n\n--MW5yreqqjyrRcusr\nContent-Type: application/pgp-signature; name=\"signature.asc\"\nContent-Description: Digital signature\n\n-----BEGIN PGP SIGNATURE-----\nVersion: GnuPG v1.4.14 (GNU/Linux)\n\niF4EAREIAAYFAlLQEqYACgkQyX+Mu/zuJ9uv7QD8CoJdy8WOV3K9V1bq4eA20Pi2\n3OyhpkKwTxqnIzg2MPYA/14AcCr4XGZnsv82kT3swgwp4OUaly4INrI0ZF4c9PSK\n=E6Dj\n-----END PGP SIGNATURE-----\n\n--MW5yreqqjyrRcusr--\n\nFrom bounce+300849.3b3-team+testing=mailpile.is@mailgun.org  Fri Jan 10 15:46:41 2014\nReturn-Path: <bounce+300849.3b3-team+testing=mailpile.is@mailgun.org>\nX-Original-To: team+testing@mailpile.is\nDelivered-To: mailpile@mailpile.is\nReceived: by mailpile.is (Postfix)\n\tid CCD9E62B43; Fri, 10 Jan 2014 15:46:41 +0000 (GMT)\nDelivered-To: team+testing@mailpile.is\nX-Greylist: delayed 649 seconds by postgrey-1.34 at mailpile.is; Fri, 10 Jan 2014 15:46:41 GMT\nReceived: from mail-luna33.mailgun.org (unknown [173.193.210.33])\n\tby mailpile.is (Postfix) with ESMTP id 8D02962B41\n\tfor <team+testing@mailpile.is>; Fri, 10 Jan 2014 15:46:41 +0000 (GMT)\nDKIM-Signature: a=rsa-sha256; v=1; c=relaxed/relaxed; d=mailgun.org; q=dns/txt; s=mg;\n t=1389368791; h=Content-Type: Subject: Mime-Version: From: Date:\n Content-Transfer-Encoding: Message-Id: Content-Description: To: Sender;\n bh=w9y0jpP3J9rFxp3YhCYyRUXNifnnXT2jbUyFAnCMQiM=; b=RRzKTgZXbqLdg49QzdkIwNNA9iNeHAs/Emv7+n+iQjWTFgeYgqN8b4uU/1MFxmtt3XIipUMv\n fAXhAfmHW0R/0Xw3/kStof67LqkfIZGqnFlMFqkv6s7JKp/rKZE21RuSEqbrzVKABj3LuhgV\n BBp1OdEr3nQr2Si5c2ywwzFCdWs=\nDomainKey-Signature: a=rsa-sha1; c=nofws; d=mailgun.org; s=mg; q=dns;\n h=Content-Type: Subject: Mime-Version: From: Date:\n Content-Transfer-Encoding: Message-Id: Content-Description: To: Sender;\n b=GMWCrZncXf33IJPX9NSgzlxGoj8x6nxX+SOil7QY+dOTAbyEkkopiZmQ2EpR1p9kirIS6T\n rCyReqHR+Vtix03T18AlxYTTN7m1nakzH5BFkoS1ioE39Qta0QvQeoYSZVcdCC2CXPJYK6Vk\n L1oNnPj+Z0/e9eg7tGZVscNjHG2JA=\nReceived: from [192.168.1.94] (dsl-ls-105-150.du.vortex.is\n [213.190.105.150]) by mxa.mailgun.org with ESMTP id 52d0136b.7721378-in3;\n Fri, 10 Jan 2014 15:36:11 -0000 (UTC)\nContent-Type: multipart/encrypted; boundary=\"Apple-Mail=_C42BC4E0-7957-4A17-83D3-1DC7E8F0D09C\"; protocol=\"application/pgp-encrypted\"\nSubject: Email That Is Encrypted\nMime-Version: 1.0 (Mac OS X Mail 7.1 \\(1827\\))\nX-Pgp-Agent: GPGMail (null)\nFrom: Brennan Novak <hi@brennannovak.com>\nDate: Fri, 10 Jan 2014 15:36:04 +0000\nContent-Transfer-Encoding: 7bit\nMessage-Id: <E8E0E57A-CA67-4929-BEF3-8A45F9CB77B9@brennannovak.com>\nContent-Description: OpenPGP encrypted message\nTo: team+testing@mailpile.is\nX-Mailer: Apple Mail (2.1827)\nX-Mailgun-Sid: WyI5OTA5MyIsICJ0ZWFtK3Rlc3RpbmdAbWFpbHBpbGUuaXMiLCAiM2IzIl0=\nSender: hi@brennannovak.com\nContent-Length: 1980\nLines: 44\n\n--Apple-Mail=_C42BC4E0-7957-4A17-83D3-1DC7E8F0D09C\nContent-Transfer-Encoding: 7bit\nContent-Type: application/pgp-encrypted\nContent-Description: PGP/MIME Versions Identification\n\nVersion: 1\n\n--Apple-Mail=_C42BC4E0-7957-4A17-83D3-1DC7E8F0D09C\nContent-Transfer-Encoding: 7bit\nContent-Disposition: inline;\n\tfilename=encrypted.asc\nContent-Type: application/octet-stream;\n\tname=encrypted.asc\nContent-Description: OpenPGP encrypted message\n\n-----BEGIN PGP MESSAGE-----\nComment: GPGTools - https://gpgtools.org\n\nhQEMA7oNcZ+2CxZ7AQf/RHWq8dwnMbVMQ2Jneq4awHBh4vj/gLtMz416snP/cVjs\nDQT+DYBIbt5DiocO+1UR2MuDGGWem4zxoLfMBIKJ1XFzWtNMtNj03GZpLMEtXSRW\n84t2AdjcOm+Dz1RjKxzDiqNeM/1LBW80LXwKXMYhCaaOlo1hv92ZyiCDzrJ76198\ntB4qxycsjOpdA2gnALtdD7WTtUFkgk/nRJctJi4rKoOQQ5AsAaOVKuk7UiVT8a+n\n04DyvOY0ugzgGuNK7Y5Sd8p9+SPc5XFyRl6S9PPi3Uol/jvwo0bzzpQH4F01Krld\nFEEtR8y0Zpn9ceK+0P5ZpWnI2K/Co9S+tytpsiYAo4SMAz6G3yxBmjWYAQP/d0yq\nce7S4yLJLEIbvlQ1UOXTkHUJO3zrzgMTEu1GSKqUTZcqxhNz7kGITod7GQm5EKEY\nIWKdPzYrk6lfCjtZp6ffki7q8kVJ+Sb4jppVZcEAEiT01YuC/po8zBqkh9xnB1+3\nFwnS5aLhSRq45J1tNoq8I+JexBHbEtgj4Z0olmjS6QH7hAtXiiw15INwirqCNeAX\n+BQXMypknxYAOnjofGvIhw1CJRfWKFH7uXAr6knkMg0PrVhEGunxZzYr0NfsKc3Z\nKDY5QFkDhAvxXjOCEne5Z+OkHYB3kaWW4+fRYhI8OS39J3y5CzOxEE2w/R6BwB+2\nT1VvRadI4qz07pqwi8h9ABCOGHiC8xmZTv+P6zGib9WVdeGQ7shQVQ21AtraTW8S\nTfH0RwaMJ6EBckefoUFBKVG+9+P4NtXFPvPAkHPTl1H6opfeAUrZq/bp6IXELWWc\n3C00r0WEz1RNx96YIRVZnHxXO933xINVGsmnEWOcFfVWkeMGmKE7U0sTIk2+yORb\nFvMTMviK/G0KdSz8QCJLfo3wEj85oDrYLAuKqaU96J4I6W9GhgYfe+2PIr9BRAKk\nXSYX1sjdrWC85lRh1mQ2+aeAJPn+no2Iu/vUEspltBOMpPGdyVHmhYOgZAmlKaTG\nRX12lskzKj1c/NWeVAggp1ZQwNwWJDwQepybR7OygY01qdSKWAu7FS+HO91BLMvs\nulM9Er3W9o/Lw/WJil0lJZdC0ec2dAXhBBTrLUz40Q21XoKJl4qZfkomH9w8qezq\njFTbr78Qrnk2IB25dKYleiKf7mLkmWbBsAq05UNVQW8Q5iqvxVkMPsXpTizt5xX2\nlDsALoR6Uizh/R5nzPrYYBMBl2D0XbLSb+3PF8Vf7TDu3CGI1RZuwzckp5rdSxMn\nkyUUNUYz92VwRH7/fWrIPk0M8ZTe71vw07ob9IRs8x4RrtpFSsSZ6tdCJlYl5bzY\na4I/uYMdvUgx839B1NdbWA==\n=7ar8\n-----END PGP MESSAGE-----\n\n--Apple-Mail=_C42BC4E0-7957-4A17-83D3-1DC7E8F0D09C--\n\nFrom bounce+300849.3b3-team+testing=mailpile.is@mailgun.org  Fri Jan 10 15:46:43 2014\nReturn-Path: <bounce+300849.3b3-team+testing=mailpile.is@mailgun.org>\nX-Original-To: team+testing@mailpile.is\nDelivered-To: mailpile@mailpile.is\nReceived: by mailpile.is (Postfix)\n\tid CF83F62B41; Fri, 10 Jan 2014 15:46:41 +0000 (GMT)\nDelivered-To: team+testing@mailpile.is\nReceived: from mail-luna33.mailgun.org (unknown [173.193.210.33])\n\tby mailpile.is (Postfix) with ESMTP id 9238062B42\n\tfor <team+testing@mailpile.is>; Fri, 10 Jan 2014 15:46:41 +0000 (GMT)\nDKIM-Signature: a=rsa-sha256; v=1; c=relaxed/relaxed; d=mailgun.org; q=dns/txt; s=mg;\n t=1389368791; h=From: Content-Type: Subject: Message-Id: Date: To:\n Mime-Version: Sender; bh=5PW2doCUCt4awqmrAOdtv5U3UOSJkdD6BXc8cX8/27w=;\n b=SGS2ac4EiHOjiL0bLxrbh5mltPINh7olvHPfImlZmEZHfyAnz+y03JTCQDKdJKixtcVvIoD2\n 4uo7J0g0q2BEOQFxOlWKneod0JkwdtDvXilmfr6zQuytjRU+Og5R07vE4Fhh14Pq5Jps8Sq8\n XrLcIB/GqJUrTC0V2rAGGeshg84=\nDomainKey-Signature: a=rsa-sha1; c=nofws; d=mailgun.org; s=mg; q=dns;\n h=From: Content-Type: Subject: Message-Id: Date: To: Mime-Version:\n Sender;\n b=JIPVJtH6RS2uUOk6/YM9FgftGLZL4EPN++401Dh0TWXP820Em+HXpAnKlPT8DzJQjki0Id\n LOjaYvvE+8VyClld28k2HCMP82uGDi3/6G5O9t7rQTDCfwQoil7mDkuU7MbYvyyMduLRqoWi\n YImsHshiwVl793tcrFZs3PTK2/1to=\nReceived: from [192.168.1.94] (dsl-ls-105-150.du.vortex.is\n [213.190.105.150]) by mxa.mailgun.org with ESMTP id 52d01345.67bcf80-in2;\n Fri, 10 Jan 2014 15:35:33 -0000 (UTC)\nFrom: Brennan Novak <hi@brennannovak.com>\nContent-Type: multipart/signed; boundary=\"Apple-Mail=_D07FF770-A934-422D-9AE3-53E5CF4BF1D6\"; protocol=\"application/pgp-signature\"; micalg=\"pgp-sha512\"\nSubject: Email That Is Signed\nMessage-Id: <5D16ABA3-6E8D-4B09-9D75-62C933C6C485@brennannovak.com>\nDate: Fri, 10 Jan 2014 15:35:29 +0000\nTo: team+testing@mailpile.is\nMime-Version: 1.0 (Mac OS X Mail 7.1 \\(1827\\))\nX-Mailer: Apple Mail (2.1827)\nX-Mailgun-Sid: WyI5OTA5MyIsICJ0ZWFtK3Rlc3RpbmdAbWFpbHBpbGUuaXMiLCAiM2IzIl0=\nSender: hi@brennannovak.com\nContent-Length: 4192\nLines: 93\n\n--Apple-Mail=_D07FF770-A934-422D-9AE3-53E5CF4BF1D6\nContent-Type: multipart/alternative;\n\tboundary=\"Apple-Mail=_E6D99D2A-4B4C-4D4A-8B34-46853042D7F9\"\n\n\n--Apple-Mail=_E6D99D2A-4B4C-4D4A-8B34-46853042D7F9\nContent-Transfer-Encoding: 7bit\nContent-Type: text/plain;\n\tcharset=us-ascii\n\nYo Bjarni, \n\nBrennan here, sending this message to ya for testing purposes only!!!\n\nThis message has a signature, but is not encrypted.\n\nCheers,\n\nBN\n\n\n--Apple-Mail=_E6D99D2A-4B4C-4D4A-8B34-46853042D7F9\nContent-Transfer-Encoding: quoted-printable\nContent-Type: text/html;\n\tcharset=us-ascii\n\n<html><head><meta http-equiv=3D\"Content-Type\" content=3D\"text/html =\ncharset=3Dus-ascii\"></head><body style=3D\"word-wrap: break-word; =\n-webkit-nbsp-mode: space; -webkit-line-break: after-white-space;\">Yo =\nBjarni,&nbsp;<div><br></div><div>Brennan here, sending this message to =\nya for testing purposes only!!!</div><div><br></div><div>This message =\nhas a signature, but is not encrypted.</div><br><div =\napple-content-edited=3D\"true\">\n<div style=3D\"color: rgb(0, 0, 0); font-family: Helvetica;  font-style: =\nnormal; font-variant: normal; font-weight: normal; letter-spacing: =\nnormal; line-height: normal; orphans: 2; text-align: -webkit-auto; =\ntext-indent: 0px; text-transform: none; white-space: normal; widows: 2; =\nword-spacing: 0px; -webkit-text-size-adjust: auto; =\n-webkit-text-stroke-width: 0px; word-wrap: break-word; =\n-webkit-nbsp-mode: space; -webkit-line-break: after-white-space; \"><div =\nstyle=3D\"color: rgb(0, 0, 0); font-family: Helvetica;  font-style: =\nnormal; font-variant: normal; font-weight: normal; letter-spacing: =\nnormal; line-height: normal; orphans: 2; text-align: -webkit-auto; =\ntext-indent: 0px; text-transform: none; white-space: normal; widows: 2; =\nword-spacing: 0px; -webkit-text-size-adjust: auto; =\n-webkit-text-stroke-width: 0px; word-wrap: break-word; =\n-webkit-nbsp-mode: space; -webkit-line-break: after-white-space; \"><div =\nstyle=3D\"color: rgb(0, 0, 0); font-family: Helvetica;  font-style: =\nnormal; font-variant: normal; font-weight: normal; letter-spacing: =\nnormal; line-height: normal; orphans: 2; text-align: -webkit-auto; =\ntext-indent: 0px; text-transform: none; white-space: normal; widows: 2; =\nword-spacing: 0px; -webkit-text-size-adjust: auto; =\n-webkit-text-stroke-width: 0px; word-wrap: break-word; =\n-webkit-nbsp-mode: space; -webkit-line-break: after-white-space; \"><div =\nstyle=3D\"color: rgb(0, 0, 0); font-family: Helvetica;  font-style: =\nnormal; font-variant: normal; font-weight: normal; letter-spacing: =\nnormal; line-height: normal; orphans: 2; text-align: -webkit-auto; =\ntext-indent: 0px; text-transform: none; white-space: normal; widows: 2; =\nword-spacing: 0px; -webkit-text-size-adjust: auto; =\n-webkit-text-stroke-width: 0px; word-wrap: break-word; =\n-webkit-nbsp-mode: space; -webkit-line-break: after-white-space; =\n\"><div>Cheers,</div><div><br></div><div>BN</div><div><br></div></div></div=\n></div></div></div></body></html>=\n\n--Apple-Mail=_E6D99D2A-4B4C-4D4A-8B34-46853042D7F9--\n\n--Apple-Mail=_D07FF770-A934-422D-9AE3-53E5CF4BF1D6\nContent-Transfer-Encoding: 7bit\nContent-Disposition: attachment;\n\tfilename=signature.asc\nContent-Type: application/pgp-signature;\n\tname=signature.asc\nContent-Description: Message signed with OpenPGP using GPGMail\n\n-----BEGIN PGP SIGNATURE-----\nComment: GPGTools - https://gpgtools.org\n\niQIcBAEBCgAGBQJS0BNBAAoJELgSf1EkRiQh+JQQAIh2aIfWNPQupHM27NW1p8Hl\nCuiw9Vsf04k+Zr/FTZQhNuE/UWG45XpHmsm7C3oQnz6PU/BTh+MRzb2r61SkBQDq\n1RqMPnwJOqdiao34DVifmORHPJSHIpRSmANex2bOMxpah2Dac+1qfU42DMFN6w6c\nqR8kuKz9uod13e60FrLVzvmP7XmcbxlQEFhjB/evGUzi097jhpkqIbUWgyjNwdw/\n8VwgIR9dS85xBkrFigFOXBf85OmwctthAkiWwwzyxhvit+dueHELgF20HJjZsNS9\nq8SLbuhC1tG6bx2xFOX0/MXr7028UTLcxlSozW6I+h0jvYQ03RdWohqnOnzg5DDZ\nK1n8VdygW0jtufjjk5+61x5waRZ0UMZWJfLzF2jZwT3V8+Oeke57zmKKm/qB+vtT\nNbnJILcP30klEZPnkNJsi2L9ZacBHe5MgvXEo6dRXE9n20zhQBwGSRmMl/zHQleF\nDW2RZhOgJUc1YO6ogjmTtsuOxe3UsyfC9QuvAzUqI1b1Alc8LcrbH63eUc1gAJSG\nYD5k+TMJpEzZeIg1KcLiCXhgu4z/NptMCjG+EnZYqlCUJvI/E7z3oezbc4IMwgWA\nwdgo2BnAw2QmkHdQDP+/2XhMc661ms3/77Y2rdE3PoZodfzelJZl4IhZpBTSnWsq\nh4U0yea2S7khSE92tO1f\n=YkzP\n-----END PGP SIGNATURE-----\n\n--Apple-Mail=_D07FF770-A934-422D-9AE3-53E5CF4BF1D6--\n\nFrom bre@slinky  Fri Jan 10 15:53:36 2014\nReturn-Path: <bre@slinky>\nX-Original-To: bre@slinky\nDelivered-To: bre@slinky\nReceived: by slinky (Postfix, from userid 1000)\n\tid E7871462C7; Fri, 10 Jan 2014 15:53:35 +0000 (GMT)\nDate: Fri, 10 Jan 2014 15:53:35 +0000\nFrom: Bjarni Runar Einarsson <bre@slinky>\nTo: Bjarni Runar Einarsson <bre@slinky>\nSubject: Inline encryption test\nMessage-ID: <20140110155335.GA12916@slinky>\nMIME-Version: 1.0\nContent-Type: text/plain; charset=us-ascii\nContent-Disposition: inline\nUser-Agent: Mutt/1.5.21 (2010-09-15)\nContent-Length: 585\nLines: 16\n\nThis is a message which is partially encrypted, and partially not.\n\n-----BEGIN PGP MESSAGE-----\nVersion: GnuPG v1.4.14 (GNU/Linux)\n\nhIwDPobfLEGaNZgBBACQ7h3M2n3V8IkjW7WNC0o9ZhTd5ggtmIm3JlD5BMtGnB3l\neL0JDNeQdW6gKiws+K7JnvniknhVRKqVVJF+XpASaxfcS1J52Fb+mrYng8lwUVFd\nOlHBJIMMf6wluHLLd0S1xhLibuC77+Rh4hg2hR790heecURlxyTVSRWb5c3YL9J8\nAQaLY3qsSSsb2foi9ASMU256rvm8dWcsLqsWBXwNgRgEASZT3lmCPhx6y1+s5zzv\nZF/lsOc0ssOOs6aOCdcLlU1e7swmDUI60Dteo2kgpQgtDIDQxa7e5J5/2Pd/BYN4\nYIC3qNe8fqErjT5dm/UJHBRd8H8hMREbBhlGGw==\n=tKWd\n-----END PGP MESSAGE-----\n\nYay, that is so neat and cool and funky and awesome.\n\n\nFrom barnaby@woots  Fri Jan 17 17:28:01 2014\nReturn-Path: <barnaby@woots>\nX-Original-To: team+testing@mailpile.is\nDelivered-To: mailpile@mailpile.is\nReceived: by mailpile.is (Postfix)\n\tid 1AC816A94A; Fri, 17 Jan 2014 17:28:01 +0000 (GMT)\nDelivered-To: team+testing@mailpile.is\nX-Greylist: delayed 465 seconds by postgrey-1.34 at mailpile.is; Fri, 17 Jan 2014 17:28:01 GMT\nReceived: from csmtp12.one.com (csmtp12.one.com [195.47.247.112])\n\tby mailpile.is (Postfix) with ESMTPS id 06F596A949\n\tfor <team+testing@mailpile.is>; Fri, 17 Jan 2014 17:28:01 +0000 (GMT)\nReceived: from [192.168.43.206] (mobile-out-229-219.siminn.is [194.105.229.219])\n\tby csmtp12.one.com (Postfix) with ESMTPA id DF89F4000B957\n\tfor <team+testing@mailpile.is>; Fri, 17 Jan 2014 17:19:56 +0000 (UTC)\nReceived: from [192.168.43.206] (mobile-out-229-219.siminn.is [194.105.229.219])\n\t(using TLSv1 with cipher AES128-SHA)\n\tby 0.0.0.0:587 (trex/4.8.87);\n\tFri, 17 Jan 2014 17:19:58 GMT\nContent-Type: multipart/encrypted; boundary=\"Apple-Mail=_FEEA9815-A6CE-42D3-B415-492B16ADEBAD\"; protocol=\"application/pgp-encrypted\";\nSubject: PGP Testing Email\nMime-Version: 1.0 (Mac OS X Mail 7.1 \\(1827\\))\nX-Pgp-Agent: GPGMail (null)\nFrom: Barnaby Walters <barnaby@woots>\nDate: Fri, 17 Jan 2014 17:19:50 +0000\nReferences: <201309160027.r8G0R3Ag013323@example.com>,\n <cfa47c79110f45a8f68fab75b1f1636935c.20130916115400@mail71.us2.mcsv.net>\nContent-Transfer-Encoding: 7bit\nMessage-Id: <52906F15-18A6-456A-A442-97667D27E4C0@woots>\nContent-Description: OpenPGP encrypted message\nTo: team+testing@mailpile.is\nX-Mailer: Apple Mail (2.1827)\nContent-Length: 2224\nLines: 48\n\nThis is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)\n--Apple-Mail=_FEEA9815-A6CE-42D3-B415-492B16ADEBAD\nContent-Transfer-Encoding: 7bit\nContent-Type: application/pgp-encrypted\nContent-Description: PGP/MIME Versions Identification\n\nVersion: 1\n\n--Apple-Mail=_FEEA9815-A6CE-42D3-B415-492B16ADEBAD\nContent-Transfer-Encoding: 7bit\nContent-Disposition: inline;\n\tfilename=encrypted.asc\nContent-Type: application/octet-stream;\n\tname=encrypted.asc\nContent-Description: OpenPGP encrypted message\n\n-----BEGIN PGP MESSAGE-----\nComment: GPGTools - http://gpgtools.org\n\nhQEMA46GztMrK7tlAQgArcg/CefSxYhTpURlCL66KVU3kD1BZRuFT6i2nrwwOnHh\nxKob+M2ywcNIykAKqWhI8QEfAT7ZsSocSrDM5JFNV4zCYl7NFYzqOuWfluioxbRU\nSQpm74VSEvpELRi4S66wiKi6xMSnThZhjHxEIGNCRK3WTzgeooMHu3ZsJEnZoqsV\nsB++w94Q0Dc8o2aRiEu79QY5icBEZVsaHWC5fP7xmLOkb/8KjlK9Lx0cKVTloQrK\nf/3Xt1C1cqZix2gT5o/ph4WYOh8G3ChHDs4urE5cMpoIvh5SU2jWpm9s1ybFzFXb\nUv3L0TR2vO6t3YkFoKFB6uuQDBzt0F3Pc/n1v6UN04SMAz6G3yxBmjWYAQP/QGc0\noJJisIE+1vqFzl1soB2V94132lCzZYxUXfhCN3rA1Dv7ebuaC2sax+3t7eM48gsG\ny7TFr8oY9UsRf4s2k7zzqTCZporqLakBclVp3r2CEUGR2a43ONaGKtDMq++aODqH\nq9ZWayMsTulh7lr3SOLHamRJ0QfQ4jYCLOOtW4DS6QHTx70MUC3hxdoeerwsqXRI\nasvkRQswjCjXsx0Cc8+9WCY+kzc4jkX+HnIecfKiblZMo8v7o12mnrBZ6jOse3m4\nO6OhEgDphTKIrDNAUcOsbNdwdUl5TgHxu3FqgukwTFgIREalnzaApYoN7mWMF6pY\nCWQDFNG8P+BlSl4f1ZC3w5lXcUIZ+aXp11LE6cdIyhPtX+ZbWf3QUHGgJZI+uFUJ\ni9FRK+30FN1hjBcSqdqaBQ7aRAjjQ48eGf9IM0xjPgi70yJ/zEE0Uo60+8Df+hLv\n+YVmmvrE9Tb9478/5JYL6XS9X4Vv2ezChmrN6S2d+RB2SH7E3Lb8O37MMOxqTYBN\n5STJjCiPqmi223ZRYpns4gu81EFbC0SctU9+YvZnP7Jo4/JH5cjFuOtxpN7QZrb+\ndz+Z8D2a/6SXn8kRcHi4BfyG+UMLgk8q6QMKBZ2nH/VjFQo6zcBtf7VmSLKd2pPp\nHMGMy9lg71P3xkrjIYk89WRAZr9o6eKwECCxX7nVSF+eIjztfcPc7LS0ZZJ6zqDs\n4IUTjTmgnRr6psFFP/BNzZhiz1pEEMMHzhjas1hl0agZJrN1VQzGCEBlEUMozbOS\nnRaeT2MAkNeyy5715Cghf3IrQB3Zeu2NUoAc3tlyHqSyvw9ed5cWpje9YaEP7JU7\nZtWnrGqtT2HpMqZTdW/LwCYrwIK2i8GPb0skM5KkiXdUBaG+UE2E9wpoIZJBKbuh\nvJIGwOs9shZGI+TG2wOoQ8nEqYRY4u6HyTVhlc0pcq9NSnQDtUkaosCbL9hJC8IT\nBnrcfkBU6+p6bjgWMyg9kalBoeByAff6HlX6N6z0OpEjt8DpOjRkHob3bRK7TuwB\nmt1PpraeojiurY0mtPAY4fqyNeGoy6tqf+OExRf7YXnxcT3cTMNkrUVhzKrilO9/\n8FmY/KtDp2hdM7a3WuG/CmOKFq4tyXuoyzLooweMan1iAj903SASM75jQ+Y23fsf\ngGcS1XaPCA==\n=GrUC\n-----END PGP MESSAGE-----\n\n--Apple-Mail=_FEEA9815-A6CE-42D3-B415-492B16ADEBAD--\n\nFrom bjarni@woots  Fri Jan 17 17:28:01 2014\nReturn-Path: <linux-usb-owner@vger>\nFrom:  ATA NOO <Noa@fujihamfist>\nTo: People <people@linux>,\n    \"linux-usb@vger\" <linux-usb@vger.kernel.org>\nCC: fx <MIA.Tadao@jifu>\nSubject: RE: [PATCH v3 00/11] usbip: features to USB over WebSocket\nThread-Topic: [PATCH v3 00/11] usbip: features to USB over WebSocket\nThread-Index: AQHQgI4ttWgT3k26+ZNRhh3DyZ1iCKMAgAATgBA=\nDate:   Tue, 28 Apr 2015 09:35:25 +0000\nMessage-ID: <HKNPR060B22EA62DA9D841BBB9E80@HKNPR06Mrd06.prod.outlook.com>\nReferences: <14300967-8006-1-git-send-email-nobuo.iwata@fujihamfist>\nIn-Reply-To: <553F3@sumsing>\nAccept-Language: ja-JP, en-US\nContent-Language: ja-JP\nContent-Type: text/plain; charset=\"utf-8\"\nContent-Transfer-Encoding: base64\nMIME-Version: 1.0\nX-OriginatorOrg: fujihamfist\nSender: linux-usb-owner@vger\nPrecedence: bulk\nList-ID: <linux-usb.vger>\nX-Mailing-List: linux-usb@vger\n\nSGVsbG8sDQoNCj4gQXMgZmFyIGFzIEkgdW5kZXJzdGFuZCB5b3VyIGRlc2lnbiB5b3UgaGF2ZSBr\nZXJuZWwgc3R1YiBkcml2ZXIgd2hpY2ggDQo+IGlzIHNlbmRpbmcgYW5kIHJlY2VpdmluZyBkYXRh\nIHZpYSBzb2NrZXQgZmQgcmVjZWl2ZWQgZnJvbSB1c2Vyc3BhY2UuDQo+IE5vdyBhZnRlciB0aGlz\nIHNlcmllcyB5b3UgYXJlIGV4cG9ydGluZyBhbGwgbWVzc2FnZXMgIHRvIHVzZXJzcGFjZQ0KPiB3\naGVyZSBkYWVtb24gaXMgc2VuZGluZyB0aGVtIHVzaW5nIHdlYiBzb2NrZXRzLiBBbSBJIHJpZ2h0\nPw0KDQpZZXMuDQoNCj4gSSBkb24ndCBzZWUgd2hhdCBhcmUgdGhlIGJlbmVmaXRzIG9mIHN1Y2gg\na2VybmVsIGRyaXZlcj8NCj4gQ291bGRuJ3QgeW91IGp1c3Qgc2ltcGx5IHVzZSBsaWJ1c2IgaW4g\neW91ciBkYWVtb24gYW5kIGRvIGV2ZXJ5dGhpbmcNCj4gZnJvbSB1c2Vyc3BhY2U/IFN1Y2ggc29s\ndXRpb24gY291bGQgYmUgYmVuZWZpY2lhbCBmb3Igb2xkZXIga2VybmVsDQo+IGJlY2F1c2UgeW91\nIGRvbid0IG5lZWQgdG8gYmFja3BvcnQgeW91ciBwYXRjaGVzIGJ1dCBzaW1wbHkgdXNlIHlvdXIN\nCj4gZGFlbW9uIHdoaWNoIHdpbGwgYmUgY29tcGF0aWJsZSB3aXRoIG1vc3Qga2VybmVsIHZlcnNp\nb25zIGFzIGxpYnVzYiBpcw0KPiB3b3JraW5nIHdpdGggdGhlbS4NCg0KU29ycnkgcmVwZWF0aW5n\nIGNvbW1lbnQgaW4gVjEgdGhyZWFkLg0KSSBoYWQgdG8gd3JpdGUgdGhpcyB0byBjaGFuZ2UgbG9n\nLg0KDQpUaGVyZSBhcmUgMiByZWFzb25zLg0KDQoxKSBBcHBsaWNhdGlvbih2aGNpX2hjZCkgc2lk\nZSBpcyBhbHNvIG5lZWRlZA0KDQpJbiBteSB1bmRlcnN0YW5kaW5nLCB1c2JmcyBwcm92aWRlcyBm\ndW5jdGlvbnMgdG8gY29udHJvbCBVU0IgZGV2aWNlcy4gDQpTbyBkZXZpY2UodXNiaXBfaG9zdCkg\nc2lkZSBjYW4gYmUgZG9uZSBieSB1c2JmcyBidXQgDQphcHBsaWNhdGlvbih2aGNpX2hjZCkgc2lk\nZSBjYW5ub3QuIE15IHBhdGNoIGNvdmVycyBib3RoIA0KZGV2aWNlKHVzYmlwX2hvc3QpIGFuZCBh\ncHBsaWNhdGlvbih2aGNpX2hjZCkgc2lkZS4gDQpBbmQgaXQgaXMgcXVpdGUgdGhlIHNhbWUgaW4g\nYm90aCBzaWRlIGFuZCB3b3JrcyBzeW1tZXRyaWNhbGx5Lg0KDQoyKSBNYWludGFpbmFiaWxpdHkN\nCg0KVXNiZnMgcHJvdmlkZXMgc2ltaWxhciBpbnRlcmZhY2VzIHdoaWNoIFVTQiBjb3JlIHByb3Zp\nZGVzIHRvIFVTQiBob3N0IA0KZHJpdmVzLiBUbyB1c2UgdGhlIGludGVyZmFjZXMgaW4gdXNlciBz\ncGFjZSwgaW1wbGVtZW50YXRpb24gd2hpY2ggYXJlIA0KaW5jbHVkZWQgaW4gc29tZSBwb3J0aW9u\ncyBvZiB1c2JpcF9ob3N0LmtvIGFuZCB1c2JpcF9jb3JlIHNob3VsZCBiZSANCmNvcGllZCB0byB1\nc2Vyc3BhY2UuIEF0IHRoZSBzYW1lIHRpbWUsIHV0aWxpdGllcyBtdXN0IGJlIG1vZGlmaWVkIA0K\nYmVjYXVzZSBpbnRlcmZhY2VzIHVzZWQgYmV0d2VlbiB0aGUgdXRpbGl0aWVzIGFuZCB0aGUga2Vy\nbmVsIG1vZHVsZXMgDQooc3lzZnMpIHdpbGwgYmUgY2hhbmdlZCB0byBmdW5jdGlvbiBjYWxscyBp\nbiB1c2Vyc3BhY2UuDQoNCkZvciBleGFtcGxlLCBJJ2QgbGlrZSB0byBicmVhayBkb3duIHVzYmlw\nX2hvc3Qua28gYXMgYmVsb3cuDQooYSkgc3VibWl0cyBhbmQgY2FuY2VscyBVUkJzIHRvIFVTQiBj\nb3JlDQooYikgY29yZSBwYXJ0OiByZWNlaXZlcyBhbmQgaGFuZGxlIFVSQnMsIG1hbmFnZSBzdWJt\naXR0ZWQgVVJCcywgZXRjLg0KKGMpIHByb3ZpZGVzIGZ1bmN0aW9ucyB0byB1dGlsaXRpZXMgdmlh\nIHN5c2ZzDQooZCkgY2FsbHMgdXNiaXBfY29tbW9uJ3MgZnVuY3Rpb25zDQooZSkgY2FsbHMga2Vy\nbmVsIGZ1bmN0aW9ucw0KDQpUbyBtb3ZlIGl0IHRvIHVzZXIgc3BhY2UsDQooYSkgcmVwbGFjZSB3\naXRoIHVzYmZzIGNhbGxzDQooYikgY29weSB0aGUgY29yZSBwYXJ0DQooYykgbW9kaWZ5IHRvIGlu\ndGVyZmFjZSBpbnNpZGUgdXNlcnNwYWNlIG9yIHVzZSB1c2JmcyBkaXJlY3RseQ0KKGQpIHBvcnQg\nc29tZSBwb3J0aW9uIG9mIHVzYmlwX2NvcmUNCihlKSByZXBsYWNlIHdpdGggc3lzdGVtY2FsbHMg\nYW5kIGxpYnJhcmllcy4NCg0KVGhlbiwgdG8gdXNlIHVzYmZzIChhKSwgYW5vdGhlciB1c2JpcF9o\nb3N0IGxpa2UgcHJvZ3JhbSB3aGljaCBoYXMgc2FtZSANCmZvciAoYikgYW5kIGRpZmZlcmVudCBp\nbiAoYyksIChkKSBhbmQgKGUpLiBCeSAoYyksIHV0aWxpdGllcyBzaG91bGQgYmUgDQpjaGFuZ2Vk\nIHVubGVzcyBzeXNmcyBlbXVsYXRpb24gaXMgbm90IHByb3ZpZGVkLg0KDQpJIHRoaW5rIGl0J3Mg\nYmV0dGVyIHRvIHVzZSB0aGUga2VybmVsIG1vZHVsZXMgYXMtaXMuIFN0cmljdGx5LCBpdCdzIA0K\nYWxtb3N0IGFzLWlzIGJlY2F1c2UgSSBwdXQgYSBzbWFsbCBjb2RlIHRvIG1ha2UgcmVwbGFjZWFi\nbGUgDQprZXJuZWxfc2VuZG1zZygpIGFuZCBrZXJuZWxfcmVjdm1zZygpLg0KDQpBcyBhIHJlZmVy\nZW5jZSwgSSBzdG9yZWQgbXkgcHJvdG90eXBlIGluY2x1ZGluZyB1c2Vyc3BhY2UgdXNiaXBfaG9z\ndCANCndpdGggbGlidXNiKG5vdCBzeXNmcyBidXQgYSBwb3J0YWJsZSB3cmFwcGVyIG9mIHN5c2Zz\nKSBpbiBzdGFnaW5nL3VzYmlwIA0Kb2YgbGludXggMy4xNC4yLiBJdCBzdGlsbCBuZWVkcyByZWZh\nY3RvcmluZy4NCmh0dHBzOi8vZHJpdmUuZ29vZ2xlLmNvbS9kcml2ZS9mb2xkZXJzLzBCeG51V0JX\nX3RCOU5mbEZEWTFobGNWQlJORWQ0WnpCMg0KVkZKM09USTBSRUZHZWxOQlYyeFhUSFJOYzBsUmVH\nSmxhVFJHZERRLzBCeG51V0JXX3RCOU5mamhhYnpWTVNuWjZWR3hyVFhCDQp3TkVFMWRGSllkRU52\nYUMxSU1VZzFaRzFrVFU5aU9VTjFPRXBHVWxVDQoNCkluIHRoZSBwcm90b3R5cGUsIGxpYnNyYy9z\ndHViX21haW4uYywgc3R1Yl9kZXYuYywgc3R1Yl9yeC5jIGFuZCANCnN0dWJfdHguYyBhcmUgcG9y\ndGluZ3Mgb2YgdXNiaXBfaG9zdC4gc3R1Yl9jb21tb24uYyBhbmQgc3R1Yl9ldmVudC5jIGlzIA0K\ndXNiaXBfY29yZS4gTWFjcm8gVVNFX0xJQlVTQiBpbiB1dGlsaXRpZXMgZGVub3RlcyBwb3J0aW9u\ncyB0byBiZSANCm1vZGlmaWVkIGluIHV0aWxpdGllcy4NCg0KTXkgcGF0Y2ggd29ya3MgaW4gYm90\naCBob3N0IGFuZCB2aGNpIHNpZGUgdXNpbmcgZXhpc3Rpbmcga2VybmVsIG1vZHVsZXMuDQoNClRo\nYW5rIHlvdSBmb3IgeW91ciBjb21tZW50LA0KDQpuLml3YXRhDQovLw0K\n--\nTo unsubscribe from this list: send the line \"unsubscribe linux-usb\" in\nthe body of a message to majordomo@vger\nMore majordomo info at  http://vger/majordomo-info.html\n"
  },
  {
    "path": "mailpile/tests/gui/__init__.py",
    "content": "try:\n    from selenium import webdriver\n    from selenium.common.exceptions import WebDriverException, StaleElementReferenceException\n    from selenium.common.exceptions import NoSuchElementException\n    from selenium.webdriver.common.by import By\n    from selenium.webdriver.support.wait import WebDriverWait\n    from selenium.webdriver.support import expected_conditions as EC\nexcept ImportError:\n    pass\n\nfrom mailpile.httpd import HttpWorker\nfrom mailpile.tests import MailPileUnittest, get_shared_mailpile\n\nfrom mailpile.safe_popen import MakePopenUnsafe\n\nMakePopenUnsafe()\n\n\nclass ElementHasClass(object):\n    def __init__(self, locator_tuple, class_name):\n        self.locator = locator_tuple\n        self.class_name = class_name\n\n    def __call__(self, driver):\n        try:\n            e = driver.find_element(self.locator[0], self.locator[1])\n            return self.class_name in e.get_attribute('class')\n        except (NoSuchElementException, StaleElementReferenceException):\n            return False\n\n\nclass ElementHasNotClass(object):\n    def __init__(self, locator_tuple, class_name):\n        self.locator = locator_tuple\n        self.class_name = class_name\n\n    def __call__(self, driver):\n        try:\n            e = driver.find_element(self.locator[0], self.locator[1])\n            return self.class_name not in e.get_attribute('class')\n        except (NoSuchElementException, StaleElementReferenceException):\n            return True\n\n\nclass SeleniumScreenshotOnExceptionAspecter(type):\n    \"\"\"Wraps all methods starting with *test* with a screenshot aspect.\n\n      The screenshot file is named *methodname*_screenshot.png.\n\n      Notes:\n        This class defines a type that has to be used as a metaclass:\n\n         >>> class Foobar()\n         ...    __metaclass__ = SeleniumScreenshotOnExceptionAspecter\n         ...\n         ...    def take_screenshot(self, filename):\n         ...        # take screenshot\n         ...        pass\n\n         The class has to provide a take_screenshot(filename) method\n\n      Attributes:\n        none\n    \"\"\"\n\n    def __new__(mcs, name, bases, dict):\n        for key, value in dict.items():\n            if (hasattr(value, \"__call__\")\n                and key != \"__metaclass__\"\n                and key.startswith('test')):\n                dict[key] = SeleniumScreenshotOnExceptionAspecter.wrap_method(\n                    value)\n        return super(SeleniumScreenshotOnExceptionAspecter,\n                     mcs).__new__(mcs, name, bases, dict)\n\n    @classmethod\n    def wrap_method(mcs, method):\n        \"\"\"Wraps method with a screenshot on exception aspect.\"\"\"\n        # method name has to start with test, otherwise unittest runner\n        # won't detect it\n        def test_call_wrapper_method(*args, **kw):\n            \"\"\"The wrapper method\n\n              Notes:\n                The method name has to start with *test*, otherwise the\n                unittest runner won't detect is as a test method\n\n              Args:\n                *args: Variable argument list of original method\n                **kw: Arbitrary keyword arguments of the original method\n\n              Returns:\n                The result of the original method call\n            \"\"\"\n            try:\n                results = method(*args, **kw)\n            except:\n                test_self = args[0]\n                filename = '%s_screenshot.png' % method.__name__\n                test_self.take_screenshot(filename)\n                raise\n\n            return results\n\n        return test_call_wrapper_method\n\n\nclass MailpileSeleniumTest(MailPileUnittest):\n    \"\"\"Base class for all selenium GUI tests\n\n\n        Attributes:\n            DRIVER (WebDriver): The webdriver instance\n\n        Examples:\n\n        >>> class Sometest(MailpileSeleniumTest):\n        ...\n        ...     def test_something(self):\n        ...         self.go_to_mailpile_home()\n        ...         self.take_screenshot('screen.png')\n        ...         self.dump_source_to('source.html')\n        ...\n        ...         self.navigate_to('Contacts')\n        ...\n        ...         self.driver.save_screenshot('screen2.png')\n        ...         self.assertIn('Contacts', self.driver.title)\n    \"\"\"\n    __metaclass__ = SeleniumScreenshotOnExceptionAspecter\n\n    DRIVER = None\n    http_worker = None\n\n    def __init__(self, *args, **kwargs):\n        MailPileUnittest.__init__(self, *args, **kwargs)\n\n    def setUp(self):\n        self.driver = MailpileSeleniumTest.DRIVER\n\n    def tearDown(self):\n        #        try:\n        #            self.driver.close()\n        #        except WebDriverException:\n        #            pass\n        pass\n\n    @classmethod\n    def _get_mailpile_sspec(cls):\n        (_, _, config, _) = get_shared_mailpile()\n        return (config.sys.http_host, config.sys.http_port, config.sys.http_path)\n\n    @classmethod\n    def _get_mailpile_url(cls):\n        return 'http://%s:%s/%s' % cls._get_mailpile_sspec()\n\n    @classmethod\n    def _start_web_server(cls):\n        if not MailpileSeleniumTest.http_worker:\n            (mp, session, config, _) = get_shared_mailpile()\n            sspec = MailpileSeleniumTest._get_mailpile_sspec()\n            MailpileSeleniumTest.http_worker = config.http_worker = HttpWorker(session, sspec)\n            config.http_worker.start()\n\n    @classmethod\n    def _start_selenium_driver(cls):\n        if not MailpileSeleniumTest.DRIVER:\n            driver = webdriver.PhantomJS()  # or add to your PATH\n            driver.set_window_size(1280, 1024)  # optional\n            driver.implicitly_wait(5)\n            driver.set_page_load_timeout(5)\n            MailpileSeleniumTest.DRIVER = driver\n\n    @classmethod\n    def _stop_selenium_driver(cls):\n        if MailpileSeleniumTest.DRIVER:\n            try:\n                MailpileSeleniumTest.DRIVER.quit()\n                MailpileSeleniumTest.DRIVER = None\n            except WebDriverException:\n                pass\n\n    @classmethod\n    def setUpClass(cls):\n        return  # FIXME: Test disabled\n\n        MailpileSeleniumTest._start_selenium_driver()\n        MailpileSeleniumTest._start_web_server()\n\n    @classmethod\n    def _stop_web_server(cls):\n        if MailpileSeleniumTest.http_worker:\n            (mp, _, config, _) = get_shared_mailpile()\n            mp._config.http_worker = None\n            MailpileSeleniumTest.http_worker.quit()\n            MailpileSeleniumTest.http_worker = MailpileSeleniumTest.http_worker = None\n\n    @classmethod\n    def tearDownClass(cls):\n        return  # FIXME: Test disabled\n\n        MailpileSeleniumTest._stop_web_server()\n        MailpileSeleniumTest._stop_selenium_driver()\n\n    def go_to_mailpile_home(self):\n        self.driver.get('%s/in/inbox' % self._get_mailpile_url())\n\n    def take_screenshot(self, filename):\n        try:\n            self.driver.save_screenshot(filename)  # save a screenshot to disk\n        except WebDriverException:\n            pass\n\n    def dump_source_to(self, filename):\n        with open(filename, 'w') as out:\n            out.write(self.driver.page_source.encode('utf8'))\n\n    def navigate_to(self, name):\n        contacts = self.find_element_by_xpath(\n            '//a[@alt=\"%s\"]/span' % name)\n        self.assertTrue(contacts.is_displayed())\n        contacts.click()\n\n    def submit_form(self, form_id):\n        form = self.driver.find_element_by_id(form_id)\n        form.submit()\n\n    def fill_form_field(self, field, text):\n        input_field = self.driver.find_element_by_name(field)\n        input_field.send_keys(text)\n\n    def assert_link_with_text(self, text):\n        try:\n            self.driver.find_element_by_link_text(text)\n        except NoSuchElementException:\n            raise AssertionError\n\n    def click_element_with_link_text(self, text):\n        try:\n            self.driver.find_element_by_link_text(text).click()\n        except NoSuchElementException:\n            raise AssertionError\n\n    def click_element_with_id(self, element_id):\n        self.driver.find_element_by_id(element_id).click()\n\n    def click_element_with_class(self, class_name):\n        self.driver.find_element_by_class_name(class_name).click()\n\n    def page_title(self):\n        return self.driver.title\n\n    def find_element_by_id(self, id):\n        return self.driver.find_element_by_id(id)\n\n    def find_element_containing_text(self, text):\n        return self.driver.find_element_by_xpath(\"//*[contains(.,'%s')]\" % text)\n\n    def find_element_by_xpath(self, xpath):\n        return self.driver.find_element_by_xpath(xpath)\n\n    def find_element_by_class_name(self, class_name):\n        return self.driver.find_element_by_class_name(class_name)\n\n    def assert_text(self, text):\n        self.find_element_containing_text(text)\n\n    def wait_until_element_is_visible(self, element_id):\n        self.wait_until_element_is_visible_by_locator((By.ID, element_id))\n\n    def wait_until_element_is_visible_by_locator(self, locator_tuple):\n        wait = WebDriverWait(self.driver, 10)\n        wait.until(EC.visibility_of_element_located(locator_tuple))\n\n    def wait_until_element_is_invisible_by_locator(self, locator_tuple):\n        wait = WebDriverWait(self.driver, 10)\n        wait.until(EC.invisibility_of_element_located(locator_tuple))\n\n    def wait_until_element_has_class(self, locator_tuple, class_name):\n        self.wait_for_element_condition(ElementHasClass(locator_tuple, class_name))\n\n    def wait_until_element_has_not_class(self, locator_tuple, class_name):\n        self.wait_for_element_condition(ElementHasNotClass(locator_tuple, class_name))\n\n    def wait_for_element_condition(self, expected_conditions):\n        wait = WebDriverWait(self.driver, 10)\n        wait.until(expected_conditions)\n"
  },
  {
    "path": "mailpile/tests/gui/test_contacts.py",
    "content": "from mailpile.tests.gui import MailpileSeleniumTest\n\n\nclass ContactsGuiTest(MailpileSeleniumTest):\n    def test_add_new_contact(self):\n        return  # FIXME: Test disabled\n\n        self.go_to_mailpile_home()\n        self.navigate_to('Contacts')\n\n        self.click_element_with_class('btn-activity-contact_add')\n\n        self.fill_form_field('name', 'Foo Bar')\n        self.fill_form_field('email', 'foo.bar@test.local')\n        self.submit_form('form-contact-add')\n\n        self.navigate_to('Contacts')\n\n        # we now should find a contact with name Foo Bar\n        self.assert_link_with_text('Foo Bar')\n"
  },
  {
    "path": "mailpile/tests/gui/test_mail.py",
    "content": "from mailpile.tests.gui import MailpileSeleniumTest\n\n\nclass MailGuiTest(MailpileSeleniumTest):\n    def test_read_mail(self):\n        return  # FIXME: Test disabled\n\n        self.go_to_mailpile_home()\n\n        self.wait_until_element_is_visible('pile-message-8')\n        self.click_element_with_link_text('Bjarni R. Einarsson, you have '\n                                          'new followers on Twitter!')\n\n        self.wait_until_element_is_visible('content-view')\n        self.assertEqual(\"Bjarni R. Einarsson, you have new followers \"\n                         \"on Twitter! | None's mailpile\", self.page_title())\n        self.assert_text('Samuel Faunt')\n"
  },
  {
    "path": "mailpile/tests/gui/test_tags.py",
    "content": "try:\n    from selenium.webdriver.common.by import By\nexcept ImportError:\n    pass\n\nfrom mailpile.tests.gui import MailpileSeleniumTest\n\n\nclass TagGuiTest(MailpileSeleniumTest):\n    def test_mark_read_unread(self):\n        return  # FIXME: Test disabled\n\n        self.go_to_mailpile_home()\n        self.wait_until_element_is_visible('pile-message-2')\n        self._assert_element_has_class('pile-message-2', 'in_new')\n        self._toggle_tag_bar()\n        self._click_on_visible_element_with_class_name('bulk-action-read')\n        self._assert_element_not_class('pile-message-2', 'in_new')\n        self._toggle_tag_bar()\n        self.wait_until_element_is_invisible_by_locator((By.CLASS_NAME, 'bulk-action-read'))\n        self._toggle_tag_bar()\n        self._click_on_visible_element_with_class_name('bulk-action-unread')\n        self._assert_element_has_class('pile-message-2', 'in_new')\n        self._toggle_tag_bar()\n        self.wait_until_element_is_invisible_by_locator((By.CLASS_NAME, 'bulk-action-unread'))\n\n    def _click_on_visible_element_with_class_name(self, class_name):\n        self.wait_until_element_is_visible_by_locator((By.CLASS_NAME, class_name))\n        unread_btn = self.find_element_by_class_name(class_name)\n        unread_btn.click()\n\n    def _toggle_tag_bar(self):\n        checkbox = self.find_element_by_xpath('//*[@id=\"pile-message-2\"]/td[6]/input')\n        checkbox.click()\n        return checkbox\n\n    def _assert_element_has_class(self, element_id, class_name):\n        self.wait_until_element_has_class((By.ID, element_id), class_name)\n\n    def _assert_element_not_class(self, element_id, class_name):\n        self.wait_until_element_has_not_class((By.ID, element_id), class_name)\n"
  },
  {
    "path": "mailpile/tests/test_command.py",
    "content": "import unittest\nimport os\nfrom mock import patch\n\nimport mailpile\nfrom mailpile.commands import Action as action\nfrom mailpile.tests import MailPileUnittest\n\n\nclass TestCommands(MailPileUnittest):\n    def test_index(self):\n        res = self.mp.rescan()\n        self.assertEqual(res.as_dict()[\"status\"], 'success')\n\n    def test_search(self):\n        # A random search must return results in less than 0.2 seconds.\n        res = self.mp.search(\"foo\")\n        self.assertLess(float(res.as_dict()[\"elapsed\"]), 0.2)\n\n    def test_optimize(self):\n        res = self.mp.optimize()\n        self.assertEqual(res.as_dict()[\"result\"], True)\n\n    def test_set(self):\n        self.mp.set(\"prefs.num_results=1\")\n        results = self.mp.search(\"twitter\")\n        self.assertEqual(results.result['stats']['count'], 1)\n\n    def test_unset(self):\n        self.mp.unset(\"prefs.num_results\")\n        results = self.mp.search(\"twitter\")\n        self.assertEqual(results.result['stats']['count'], 4)\n\n    def test_add(self):\n        res = self.mp.add(\"mailpile/tests/data/tests.mbx\")\n        self.assertEqual(len(res.as_dict()[\"result\"][\"added\"]), 1)\n\n    def test_add_mailbox_already_in_pile(self):\n        res = self.mp.add(\"mailpile/tests/data/tests.mbx\")\n        self.assertEqual(res.as_dict()[\"result\"], False)\n\n    def test_add_mailbox_no_such_directory(self):\n        res = self.mp.add(\"wut?\")\n        self.assertEqual(res.as_dict()[\"result\"], False)\n\n    def test_output(self):\n        res = self.mp.output(\"json\")\n        self.assertEqual(res.as_dict()[\"result\"], {'output': 'json'})\n\n    def test_help(self):\n        res = self.mp.help()\n        self.assertEqual(len(res.result), 3)\n\n    def test_help_variables(self):\n        res = self.mp.help_variables()\n        self.assertGreater(len(res.result['variables']), 1)\n\n    def test_help_with_param_search(self):\n        res = self.mp.help('search')\n        self.assertEqual(res.result['pre'], 'Search your mail!')\n\n    def test_help_urlmap_as_text(self):\n        res = self.mp.help_urlmap()\n        self.assertEqual(len(res.result), 1)\n        self.assertGreater(res.as_text(), 0)\n\n    def test_crypto_policy_action(self):\n        res = self.mp.crypto_policy(\"foobar\")\n        self.assertEqual(res.as_dict()[\"message\"],\n            u'The encryption policy for these recipients is: best-effort')\n        self.assertEqual(res.as_dict()[\"result\"]['crypto-policy'],\n                         'best-effort')\n\n    def test_reply_no_subject(self):\n        mid = self.mp.search('from:ohcheeou').result['data']['metadata']\\\n                                             .values()[0]['mid']\n        # Just checks it does not crash\n        res = self.mp.reply('ephemeral', '=%s' % mid)\n        self.assertEqual(res.status, 'success')\n        self.assertEqual(res.result['data']['metadata'].values()[0]['subject'],\n                         'Re:')\n\n    def test_reply_subjects_on_first_msg(self):\n        # Subject: Verb. Target. Outcome.'\n        mid = self.mp.search('subject:Verb').result['data']['metadata']\\\n                                            .values()[0]['mid']\n        res = self.mp.reply('ephemeral', '=%s' % mid)\n        self.assertEqual(res.result['data']['metadata'].values()[0]['subject'],\n                         'Re: Verb. Target. Outcome.')\n\n    def test_reply_subject_on_reply_msg(self):\n        # Subject: Re: Here's $1\n        mid = self.mp.search('subject:Here').result['data']['metadata']\\\n                                            .values()[0]['mid']\n        res = self.mp.reply('ephemeral', '=%s' % mid)\n        self.assertEqual(res.result['data']['metadata'].values()[0]['subject'],\n                         \"Re: Here's $1\")\n\n    def test_reply_subject_on_fw_msg(self):\n        # Subject: Fw: About shrubberies\n        mid = self.mp.search('shrubberies').result['data']['metadata']\\\n                                           .values()[0]['mid']\n        res = self.mp.reply('ephemeral', '=%s' % mid)\n        self.assertEqual(res.result['data']['metadata'].values()[0]['subject'],\n                         \"Re: Fw: About shrubberies\")\n\n    def test_fwd_subject_on_re_msg(self):\n        # Subject: Re: Here's $1\n        mid = self.mp.search('subject:Here').result['data']['metadata']\\\n                                            .values()[0]['mid']\n        res = self.mp.forward('ephemeral', '=%s' % mid)\n        self.assertEqual(res.result['data']['metadata'].values()[0]['subject'],\n                         \"Fwd: Re: Here's $1\")\n\n    def test_fw_subject_on_fw_msg(self):\n        # Subject: Fw: A question shrubberies\n        mid = self.mp.search('shrubberies').result['data']['metadata']\\\n                                           .values()[0]['mid']\n        res = self.mp.forward('ephemeral', '=%s' % mid)\n        self.assertEqual(res.result['data']['metadata'].values()[0]['subject'],\n                         \"Fw: About shrubberies\")\n\n\nclass TestCommandResult(MailPileUnittest):\n    def test_command_result_as_dict(self):\n        res = self.mp.help_splash()\n        self.assertGreater(len(res.as_dict()), 0)\n\n    def test_command_result_as_text(self):\n        res = self.mp.help_splash()\n        self.assertGreater(res.as_text(), 0)\n\n    def test_command_result_as_text_for_boolean_result(self):\n        res = self.mp.rescan()\n        self.assertEquals(res.result['messages'], 0)\n        self.assertEquals(res.result['mailboxes'], 0)\n        self.assertEquals(res.result['vcards'], 0)\n\n    def test_command_result_non_zero(self):\n        res = self.mp.help_splash()\n        self.assertTrue(res)\n\n    def test_command_result_as_json(self):\n        res = self.mp.help_splash()\n        self.assertGreater(res.as_json(), 0)\n\n    def test_command_result_as_html(self):\n        res = self.mp.help_splash()\n        self.assertGreater(res.as_html(), 0)\n\n\nclass TestTagging(MailPileUnittest):\n    def test_addtag(self):\n        pass\n\n\nclass TestGPG(MailPileUnittest):\n    def test_key_search(self):\n        gpg_result = {\n            \"D13C70DA\": {\n                \"uids\": [\n                    {\n                        \"email\": \"smari@mailpile.is\"\n                    }\n                ]\n            }\n        }\n\n        with patch('mailpile.commands.GnuPG') as gpg_mock:\n            gpg_mock.return_value.search_key.return_value = gpg_result\n\n            res = action(self.mp._session, \"crypto/gpg/searchkey\", \"D13C70DA\")\n            email = res.result[\"D13C70DA\"][\"uids\"][0][\"email\"]\n            self.assertEqual(email, \"smari@mailpile.is\")\n            gpg_mock.return_value.search_key.assert_called_with(\"D13C70DA\")\n\n    def test_key_receive(self):\n        gpg_result = {\n            \"updated\": [\n                {\n                    \"fingerprint\": \"08A650B8E2CBC1B02297915DC65626EED13C70DA\"\n                }\n            ]\n        }\n\n        with patch('mailpile.commands.GnuPG') as gpg_mock:\n            gpg_mock.return_value.recv_key.return_value = gpg_result\n\n            res = action(self.mp._session, \"crypto/gpg/receivekey\", \"D13C70DA\")\n            self.assertEqual(res.result[0][\"updated\"][0][\"fingerprint\"],\n                             \"08A650B8E2CBC1B02297915DC65626EED13C70DA\")\n            gpg_mock.return_value.recv_key.assert_called_with(\"D13C70DA\")\n\n    def test_key_import(self):\n        res = action(self.mp._session, \"crypto/gpg/importkey\",\n                     os.path.join('mailpile', 'tests', 'data', 'pub.key'))\n        self.assertEqual(res.result[\"results\"][\"count\"], 1)\n\n    def test_nicknym_get_key(self):\n        pass\n\n    def test_nicknym_refresh_key(self):\n        pass\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "mailpile/tests/test_config.py",
    "content": "import unittest\nimport os\nimport mailpile\nimport mailpile.config.validators as validators\n\nfrom nose.tools import raises\nfrom mailpile.tests import MailPileUnittest\n\n\nclass TestConfig(MailPileUnittest):\n\n    #\n    # config._BoolCheck should convert common yes/no strings into boolean values\n    #\n    def test_BoolCheck_trues(self):\n        for t in [\"yes\", \"true\", \"on\", \"1\", True]:\n            res = validators.BoolCheck(t)\n            self.assertEqual(res, True)\n\n    def test_BoolCheck_falses(self):\n        for f in [\"no\", \"false\", \"off\", \"0\", False]:\n            res = validators.BoolCheck(f)\n            self.assertEqual(res, False)\n\n    def test_BoolCheck_exception(self):\n        for ex in [\"wiggle\", \"\"]:\n            self.assertRaises(ValueError, lambda: validators.BoolCheck(ex))\n\n    #\n    # config._RouteProtocolCheck should verify that the protocol is actually a protocol, and strip and lowercase it\n    #\n    def test_RouteProtocolCheck_valid(self):\n        valid_protos = [\n            [\"SMTP\", \"smtp\"],\n            [\"smtptls \", \"smtptls\" ],\n            [\"smtpSSL\", \"smtpssl\"],\n            [\" local \", \"local\"]\n        ]\n        for v in valid_protos:\n            res = validators.RouteProtocolCheck(v[0])\n            self.assertEqual(res, v[1])\n\n    def test_RouteProtocolCheck_invalid(self):\n        invalid_protos = [\"http\", \"scp\", \"ssh\", \"spam\"]\n        for i in invalid_protos:\n            self.assertRaises(ValueError, lambda: validators.RouteProtocolCheck(i))\n\n    #\n    # config._HostNameValid should verify that a string is a valid hostname, returning a bool\n    #\n    def test_HostNameValid_ipv4(self):\n        ipv4_addrs = [\n          \"127.0.0.1\",\n          \"172.16.254.180\"\n        ]\n        for ipv4 in ipv4_addrs:\n            res = validators.HostNameValid(ipv4)\n            self.assertEqual(res, True)\n\n    def test_HostNameValid_ipv6(self):\n        ipv6_addrs = [\n          \"2001:cdba::3257:9651\",\n          \"ff02::9\",\n          \"::1\"\n        ]\n        for ipv6 in ipv6_addrs:\n            res = validators.HostNameValid(ipv6)\n            self.assertEqual(res, True)\n\n    def test_HostNameValid_hostname(self):\n        hostnames = [\n          \"localhost\",\n          \"foo.bar\",\n          \"eggs.foo.br\",\n          \"spam.eggs.foo.bar\"\n        ]\n        for hname in hostnames:\n            res = validators.HostNameValid(hname)\n            self.assertEqual(res, True)\n\n\n    #\n    # config._HostNameCheck should verify that a string is a valid hostname\n    #\n    def test_HostNameCheck_valid(self):\n        valids = [\n          \"127.0.0.1\",\n          \"localhost\",\n          \"ff02::9\"\n        ]\n        for v in valids:\n            res = validators.HostNameCheck(v)\n            self.assertEqual(res, v)\n\n    def test_HostNameCheck_invalid(self):\n        invalid_hostnames = [\n          \"\",\n          \" \",\n          \"127.0.0.17889\",\n          \"12.2\",\n          \"my.9\",\n          \"25:25:16\",\n          \"hello.com?q=45\",\n          \" a \",\n          \" mysite.com\",\n          \"20.20.280.1\",\n          \"8.999.89.11.23.34\",\n          \"/some/path\",\n          \"asdf::/12\"\n        ]\n        for invalid in invalid_hostnames:\n            self.assertRaises(ValueError, lambda: validators.HostNameCheck(invalid))\n\n    def test_HostNameCheck_non_socket_errors_still_raised(self):\n        self.assertRaises(NameError, lambda: validators.HostNameCheck(asdf))\n\n    #\n    # config._SlugCheck should verify that a string is a valid url slug\n    #\n    def test_SlugCheck_valid(self):\n        valid_slugs = [\"_Foo-bar.7\", \"foobar\", \"spam-eggs\", \"_\"]\n        for v in valid_slugs:\n            res = validators.SlugCheck(v)\n            self.assertEqual(res, v.lower())\n\n    def test_SlugCheck_invalid(self):\n        invalid_slugs = [\"url/path\", \"Bad Slug\"]\n        for nv in invalid_slugs:\n            self.assertRaises(ValueError, lambda: validators.SlugCheck(nv))\n\n    #\n    # config._SlashSlugCheck should act like _SlugCheck bug allow slashes\n    #\n    def test_SlashSlugCheck(self):\n        valids = [\"some/path\", \"a/very/long/path\"]\n        for v in valids:\n            res = validators.SlashSlugCheck(v)\n            self.assertEqual(res, v.lower())\n\n    #\n    # config._B36Check should verify that a string is a valid base-36 integer\n    #\n    def test_B36Check(self):\n        valids = [\"aa\", \"10\",\"AA\"]\n        for v in valids:\n            res = validators.B36Check(v)\n            self.assertEqual(res, v.lower())\n\n    def test_B36Check(self):\n        invalids = [\"=\", \".\", \"~12\", \"1278@\"]\n        for i in invalids:\n            self.assertRaises(ValueError, lambda: validators.B36Check(i))\n\n    #\n    # config._PathCheck should verify that a string is a valid and existing path and make it absolute\n    # skipped for windows (should be added later)\n    #\n\n    @unittest.skipIf(os.name == 'nt', \"testing skipped in windows\")\n    def test_PathCheck_valid(self):\n        valid_paths = {\n          \"posix\" : [\n            [\"/etc/../\", \"/\"]\n          ]\n        }\n        for v in valid_paths[os.name]:\n            res = validators.PathCheck(v[0])\n            self.assertEqual(res, v[1])\n\n    @unittest.skipIf(os.name == 'nt', \"testing skipped in windows\")\n    def test_PathCheck_invalid(self):\n        invalid_paths = {\n          \"posix\" : [\"/asdf/asdf/asdf\", \"\"]\n        }\n        for i in invalid_paths[os.name]:\n            self.assertRaises(ValueError, lambda: validators.PathCheck(i))\n\n    #\n    # config._FileCheck should verify that a string is an existing file and make it absolute\n    # skipped for windows (should be added later)\n    #\n\n    @unittest.skipIf(os.name == 'nt', \"testing skipped in windows\")\n    def test_FileCheck_valid(self):\n      valid_paths = {\n        \"posix\" : [\n          [\"/etc/../etc/group\", \"/etc/group\" ]\n        ]\n      }\n      for v in valid_paths[os.name]:\n          res = validators.FileCheck(v[0])\n          self.assertEqual(res, v[1])\n\n    @unittest.skipIf(os.name == 'nt', \"testing skipped in windows\")\n    def test_FileCheck_invalid(self):\n        invalid_paths = {\n          \"posix\" : [\"/etc\", \"/\", \"laksh09hahs--x\"]\n        }\n        for i in invalid_paths[os.name]:\n            self.assertRaises(ValueError, lambda: validators.FileCheck(i))\n\n    #\n    # config._DirCheck should verify that a string is an existing directory and make it absolute\n    # skipped for windows (should be added later)\n    #\n\n    @unittest.skipIf(os.name == 'nt', \"testing skipped in windows\")\n    def test_DirCheck_valid(self):\n        valid_paths = {\n          \"posix\" : [\n            [\"/etc/../\", \"/\"]\n          ]\n        }\n        for v in valid_paths[os.name]:\n            res = validators.DirCheck(v[0])\n            self.assertEqual(res, v[1])\n\n    @unittest.skipIf(os.name == 'nt', \"testing skipped in windows\")\n    def test_DirCheck_invalid(self):\n        invalid_paths = {\n          \"posix\" : [ \"/etc/group\" ]\n        }\n        for i in invalid_paths[os.name]:\n            self.assertRaises(ValueError, lambda: validators.DirCheck(i))\n\n    #\n    # config._NewPathCheck should verify that a string is path to an existing directory and make it absolute\n    # skipped for windows (should be added later)\n    #\n\n    @unittest.skipIf(os.name == 'nt', \"testing skipped in windows\")\n    def test_NewPathCheck_valid(self):\n        valid_paths = {\n          \"posix\" : [\n            [\"/etc/temp.txt\", \"/etc/temp.txt\"],\n            [\"/etc/../magic\", \"/magic\"]\n          ]\n        }\n        for v in valid_paths[os.name]:\n            res = validators.NewPathCheck(v[0])\n            self.assertEqual(res, v[1])\n\n    @unittest.skipIf(os.name == 'nt', \"testing skipped in windows\")\n    def test_NewPathCheck_invalid(self):\n        invalid_paths = {\n          \"posix\" : [ \"/some/random/path/\", \"/etc/asdf/tmp.txt\" ]\n        }\n        for i in invalid_paths[os.name]:\n            self.assertRaises(ValueError, lambda: validators.NewPathCheck(i))\n\n    #\n    # config._UrlCheck should verify that a string is a valid url\n    #\n    def test_UrlCheck_valid(self):\n        valid_urls = [\n          \"http://site.io\",\n          \"https://localhost\",\n          \"git://github.com/user/repo.git\",\n          \"magnet:?xt=urn:sha1:1C6HTVCWBTRNJ9V4XNAE52SJUQCZO5D\",\n        ]\n        for v in valid_urls:\n            res = validators.UrlCheck(v)\n            self.assertEqual(res, v)\n\n    def test_UrlCheck_invalid(self):\n        invalid_urls = [\n          \"obvious\",\n          \"\",\n          \" \",\n          \".com\",\n          \"/just/a/path\",\n        ]\n        for i in invalid_urls:\n            self.assertRaises(ValueError, lambda: validators.UrlCheck(i))\n\n    #\n    #config._EmailCheck should verify that an email address has an @ symbol\n    #\n    def test_EmailCheck_valid(self):\n        valid_emails = [\n          '\"This is a valid email!\"@believe.it',\n          'this.address(has-a-comment)@crazy-but-true.com',\n          '\"ABC\\@def\"@valid-email.com',\n          '\\$A12345@valid-email.com',\n          '!def!xyz%abc@valid-email.com',\n          '_somename@valid-email.com',\n          '\"Some\\\\Body\"@valid-email.com'\n        ]\n\n        for v in valid_emails:\n            res = validators.EmailCheck(v)\n            self.assertEqual(res, v)\n\n    def test_EmailCheck_invalid(self):\n        invalid_emails = [\n          \"invalidEmail\",\n          \"\",\n          \"@invalid-email\",\n          \"@\"\n        ]\n\n        for i in invalid_emails:\n            self.assertRaises(ValueError, lambda: validators.EmailCheck(i))\n\n    def test_GPGKeyCheck_valid(self):\n        valid_fingerprints = [\n          'User@Foo.com',\n          '1234 5678 abcd EF00',\n          '12345678'\n        ]\n\n        res = validators.GPGKeyCheck(valid_fingerprints[0])\n        self.assertEqual(res, 'User@Foo.com')\n\n        res = validators.GPGKeyCheck(valid_fingerprints[1])\n        self.assertEqual(res, '12345678ABCDEF00')\n\n        res = validators.GPGKeyCheck(valid_fingerprints[2])\n        self.assertEqual(res, '12345678')\n\n    def test_GPGKeyCheck_invalid(self):\n        invalid_fingerprints = [\n          '123456789',                                             # length of key not 8 or 16 or 40\n          'B906 8A28 15C4 F859  6F9F 47C1 3F3F ED73 5179',         # length is 36 i.e not 40\n          'zzzz zzzz zzzz zzzz zzzz  zzzz zzzz zzzz zzzz zzzz' # contains invalid character z characters should be within a-f\n        ]\n\n        for i in invalid_fingerprints:\n            self.assertRaises(ValueError, lambda: validators.GPGKeyCheck(i))\n"
  },
  {
    "path": "mailpile/tests/test_crypto_policy.py",
    "content": "from __future__ import print_function\nfrom mailpile.vcard import MailpileVCard, VCardLine\nfrom mailpile.tests import MailPileUnittest\n\nVCARD_CRYPTO_POLICY = 'X-MAILPILE-CRYPTO-POLICY'\n\n\nclass CryptoPolicyBaseTest(MailPileUnittest):\n    def setUp(self):\n        self.config.vcards.clear()\n        pass\n\n    def _add_vcard(self, full_name, email):\n        card = MailpileVCard(VCardLine(name='fn', value=full_name),\n                             VCardLine(name='email', value=email))\n        self.config.vcards.add_vcards(card)\n        return card\n\n\nclass UpdateCryptoPolicyForUserTest(CryptoPolicyBaseTest):\n    def test_args_are_checked(self):\n        self.assertEqual('error',\n            self.mp.crypto_policy_set().as_dict()['status'])\n        self.assertEqual('error',\n            self.mp.crypto_policy_set('one arg').as_dict()['status'])\n\n    def test_policies_are_validated(self):\n        self._add_vcard('Test', 'test@test.local')\n\n        for policy in ['default', 'none', 'sign', 'sign-encrypt',\n                       'encrypt', 'best-effort']:\n            r = self.mp.crypto_policy_set('test@test.local', policy)\n            print('%s' % r.as_dict())\n            self.assertEqual('success', r.as_dict()['status'])\n\n        for policy in ['anything', 'else']:\n            r = self.mp.crypto_policy_set('test@test.local', policy).as_dict()\n            self.assertEqual('error', r['status'])\n            self.assertEqual('Policy has to be one of none|sign|encrypt'\n                             '|sign-encrypt|best-effort|default', r['message'])\n\n    def test_vcard_has_to_exist(self):\n        res = self.mp.crypto_policy_set('test@test.local', 'sign').as_dict()\n        self.assertEqual('error', res['status'])\n        self.assertEqual('No vCard for email test@test.local!', res['message'])\n\n    def test_vcard_is_updated(self):\n        vcard = self._add_vcard('Test', 'test@test.local')\n        for policy in ['none', 'sign', 'encrypt']:\n            self.mp.crypto_policy_set('test@test.local', policy)\n            self.assertEqual(policy, vcard.get(VCARD_CRYPTO_POLICY).value)\n\n\nclass CryptoPolicyForUserTest(CryptoPolicyBaseTest):\n    def test_no_email_provided(self):\n        res = self.mp.crypto_policy().as_dict()\n        self.assertEqual('error', res['status'])\n\n    def test_no_msg_with_email_(self):\n        res = self.mp.crypto_policy('undefined@test.local').as_dict()\n        self.assertEqual('success', res['status'])\n        self.assertEqual('best-effort', res['result']['crypto-policy'])\n\n    def test_with_signed_email(self):\n        res = self.mp.crypto_policy('signer@test.local').as_dict()\n        self.assertEqual('success', res['status'])\n        self.assertEqual('best-effort', res['result']['crypto-policy'])\n\n    def test_with_encrypted_email(self):\n        res = self.mp.crypto_policy('encrypter@test.local').as_dict()\n        self.assertEqual('success', res['status'])\n        self.assertEqual('best-effort', res['result']['crypto-policy'])\n\n    def test_vcard_overrides_mail_history(self):\n        vcard = self._add_vcard('Encrypter', 'encrypter@test.local')\n        vcard.add(VCardLine(name=VCARD_CRYPTO_POLICY, value='sign'))\n\n        res = self.mp.crypto_policy('encrypter@test.local').as_dict()\n\n        self.assertEqual('success', res['status'])\n        self.assertEqual('sign', res['result']['crypto-policy'])\n"
  },
  {
    "path": "mailpile/tests/test_eventlog.py",
    "content": "import unittest\nimport mailpile\nimport re\nimport json\nimport os\nimport threading\nimport time\n\nfrom nose.tools import raises\nfrom mailpile.tests import MailPileUnittest\n\n\nEVENT_ID_RE = re.compile(\"[a-f0-9]{8}-[a-f0-9]{5}-[a-f0-9]+\")\n\nmailpile_root = os.path.join(os.path.dirname(__file__), \"..\", \"..\")\nmailpile_tmp  = os.path.join(mailpile_root, \"mailpile\", \"tests\", \"data\", \"tmp\")\n\nclass TestEventlog(MailPileUnittest):\n\n    def setUp(self):\n        pass\n\n    def tearDown(self):\n        pass\n\n    #\n    # eventlog.NewEventId should generate unique event ids\n    #\n    # FIXME: threading?\n    def test_NewEventId(self):\n        events = []\n        for i in range(100):\n            eid = mailpile.eventlog.NewEventId()\n            self.assertFalse(events.__contains__(eid))\n            self.assertIsNotNone( EVENT_ID_RE.match(eid) )\n            events.append(eid)\n\n\n    #\n    # eventlog._ClassName should return the class name of an object and remove mailpile from its inheritance hierarchy\n    #\n    def test_ClassName(self):\n        evt = mailpile.eventlog.Event()\n        str_klass_name = str(evt.__class__)\n        kn = mailpile.eventlog._ClassName(evt)\n        self.assertGreater(str_klass_name.find(\"mailpile\"), 0)\n        self.assertGreater(kn.find(\"eventlog.Event\"), 0)\n        self.assertEqual(kn.find(\"mailpile\"), -1)\n\n    def test_ClassName_unicode(self):\n        evt = mailpile.eventlog.Event()\n        u_klass_name = unicode(evt.__class__)\n        kn = mailpile.eventlog._ClassName(u_klass_name)\n        self.assertGreater( u_klass_name.find(\"mailpile\"), 0)\n        self.assertGreater( kn.find(\"eventlog.Event\"), 0)\n        self.assertEqual( kn.find(\"mailpile\"), -1)\n\n\n    #\n    # eventlog.Event should create an event for a given object with the following attributes:\n    #   event_id\n    #   ts\n    #   date\n    #   message\n    #   data\n    #   private_data\n    #   flags\n    #   source\n    #\n\n    def test_event_Parse(self):\n        evt_id = mailpile.eventlog.NewEventId()\n        msg = \"test: A Test Event Message\"\n        cmd = \".commands.Load\"\n        data = { \"test\" : \"test data\" }\n        date = \"Sun, 27 Apr 2014 14:32:08 -0000\"\n        evt_string =[date, evt_id, \"c\", msg, cmd, data, {}]\n        json_data = json.dumps(evt_string)\n        e = mailpile.eventlog.Event.Parse(json_data)\n        self.assertEqual( e.data, data)\n        self.assertEqual( e.event_id, evt_id )\n        self.assertEqual( e.source, cmd )\n        self.assertEqual( e.message, msg )\n\n    def test_event_Parse_invalid(self):\n        e = mailpile.eventlog.Event.Parse(\"all exceptions are caught\")\n        self.assertEqual(e.__class__, mailpile.eventlog.Event)\n\n    def test_event_as_dict(self):\n        cmd = self.mp.help()\n        evt = mailpile.eventlog.Event(source=cmd, data={ 'name' : 'testing'}, message=\"hello world\", private_data={ 'x' : 22})\n        res = evt.as_dict()\n        self.assertEqual(res['message'], \"hello world\")\n        self.assertEqual(res['data'], { 'name' : 'testing'})\n        self.assertIsNotNone( EVENT_ID_RE.match(res['event_id']) )\n        self.assertEqual(res['private_data'], { 'x' : 22})\n        self.assertIsNotNone( res['flags'] )\n        self.assertIsNotNone( res['source'] )\n        self.assertIsNotNone( res['date'] )\n        self.assertTrue( isinstance(res['ts'], float) )\n\n    def test_event_as_dict_no_private(self):\n        cmd = self.mp.help()\n        evt = mailpile.eventlog.Event(source=cmd, data={ 'name' : 'testing'}, message=\"hello world\", private_data={ 'x' : 22})\n        res = evt.as_dict(private=False)\n        self.assertFalse( \"private_data\" in res )\n\n\n    def test_event_as_json(self):\n        cmd = self.mp.help()\n        evt = mailpile.eventlog.Event(source=cmd, data={ 'name' : 'testing'}, message=\"hello world\", private_data={ 'x' : 22})\n        json_res = evt.as_json()\n        res = json.loads(json_res)\n        self.assertEqual(res['message'], \"hello world\")\n        self.assertEqual(res['data'], { 'name' : 'testing'})\n        self.assertIsNotNone( EVENT_ID_RE.match(res['event_id']) )\n        self.assertEqual(res['private_data'], { 'x' : 22})\n        self.assertIsNotNone( res['flags'] )\n        self.assertIsNotNone( res['source'] )\n        self.assertIsNotNone( res['date'] )\n        self.assertTrue( isinstance(res['ts'], float) )\n\n    def test_event_as_json_no_private(self):\n        cmd = self.mp.help()\n        evt = mailpile.eventlog.Event(source=cmd, data={ 'name' : 'testing'}, message=\"hello world\", private_data={ 'x' : 22})\n        json_res = evt.as_json(private=False)\n        res = json.loads(json_res)\n        self.assertFalse( \"private_data\" in res )\n\n    #\n    # Event.as_html() should return a html string containing the date and message\n    #\n    def test_event_as_html(self):\n        cmd = self.mp.help()\n        evt = mailpile.eventlog.Event(source=cmd, data={ 'name' : 'testing'}, message=\"hello world\", private_data={ 'x' : 22})\n        raw_html_private = evt.as_html()\n        raw_html_public  = evt.as_html(False)\n        self.assertGreater(raw_html_private.find(evt.date), 0)\n        self.assertGreater(raw_html_private.find(evt.message), 0)\n        self.assertGreater(raw_html_public.find(evt.date), 0)\n        self.assertGreater(raw_html_public.find(evt.message), 0)\n\n    #\n    # EventLog should be written encrypted to disk, rotated every N lines and stored in RAM\n    #\n    def test_eventlog(self):\n        rotate_lines = 100\n        evt_log = mailpile.eventlog.EventLog(mailpile_tmp,\n                                             lambda: False,\n                                             lambda: False, rotate_lines)\n        self.assertEqual(len(evt_log._events), 0)\n        evt = mailpile.eventlog.Event(source=self.mp.help(), data={},\n                                      message=\"test-event\")\n        evt_log.log_event(evt)\n        self.assertEqual(len(evt_log._events), 1)\n"
  },
  {
    "path": "mailpile/tests/test_keylookup.py",
    "content": "from mock import patch\nimport datetime\n\nfrom mailpile.tests import MailPileUnittest\nfrom mailpile.tests import get_shared_mailpile\n\nfrom mailpile.plugins.keylookup import lookup_crypto_keys, KeyserverLookupHandler\nfrom mailpile.plugins.keylookup.email_keylookup import *\nfrom mailpile.plugins.keylookup.dnspka import *\n\nGPG_MOCK_RETURN = {\n    '08A650B8E2CBC1B02297915DC65626EED13C70DA': {\n        'uids': [{'comment': '', 'name': 'Mailpile!', 'email': 'test@mailpile.is'}],\n        'keysize': '4096',\n        'keytype_name': 'RSA',\n        'created': datetime.datetime(2014, 6, 22, 2, 37, 23),\n        'fingerprint': '08A650B8E2CBC1B02297915DC65626EED13C70DA',\n    }\n}\n\nfpr = \"08A650B8E2CBC1B02297915DC65626EED13C70DA\"\nurl = \"https://mailpile.is/gpgkey.gpg\"\nDNSPKA_MOCK_RETURN = [{\n    'typename': 'TXT', 'name': 'test._pka.mailpile.is',\n    'data': ['v=pka1;fpr=%s;uri=%s' % (fpr, url)],\n}]\n\n\n\nclass KeylookupBaseTest(MailPileUnittest):\n    def setUp(self):\n        pass\n\n\nclass KeylookupDNSPKILookup(KeylookupBaseTest):\n    def test_lookup_dnspki(self):\n        with patch('DNS.Request') as dns_mock:\n            dns_mock.return_value.req.return_value.answers = DNSPKA_MOCK_RETURN\n            d = DNSPKALookupHandler(None, {})\n            res = d.lookup('test@mailpile.is')\n\n        self.assertIsNotNone(res)\n        self.assertEqual(res[fpr][\"fingerprint\"], fpr)\n        self.assertEqual(res[fpr][\"url\"], url)\n\n\nclass KeylookupPGPKeyserverLookup(KeylookupBaseTest):\n    def test_lookup_pgpkeyserver(self):\n        with patch('mailpile.crypto.gpgi.GnuPG.search_key') as gpg_mock:\n            gpg_mock.return_value = GPG_MOCK_RETURN\n            d = KeyserverLookupHandler(None, {})\n            res = d.lookup('test@mailpile.is')\n        self.assertIsNotNone(res)\n\n\nclass KeylookupEmailLookup(KeylookupBaseTest):\n    def test_lookup_emailkeys(self):\n        m = get_shared_mailpile()[0]\n        d = EmailKeyLookupHandler(m._session, {})\n        res = d.lookup('smari@mailpile.is')\n        self.assertIsNotNone(res)\n\n\nclass KeylookupOverallTest(KeylookupBaseTest):\n    def test_lookup(self):\n        with patch('DNS.Request') as dns_mock, patch('mailpile.crypto.gpgi.GnuPG.search_key') as gpg_mock:\n            gpg_mock.return_value = GPG_MOCK_RETURN\n            dns_mock.return_value.req.return_value.answers = DNSPKA_MOCK_RETURN\n\n            m = get_shared_mailpile()[0]\n            res = lookup_crypto_keys(m._session, 'smari@mailpile.is')\n\n        self.assertIsNotNone(res)\n        self.assertEqual(type(res), list)\n        for r in res:\n            self.assertEqual(type(r), dict)\n\n"
  },
  {
    "path": "mailpile/tests/test_mail_generator.py",
    "content": "# -*- coding: utf-8 -*-\r\nimport unittest\r\nimport mailpile\r\n\r\nfrom mailpile.tests import MailPileUnittest\r\n\r\nimport mailpile.mailutils.generator as mail_generator\r\n\r\nclass TestMailGenerator(MailPileUnittest):\r\n\r\n    def test_is8bitstring(self):\r\n        res = mail_generator._is8bitstring(u'\\xc4')\r\n        self.assertEqual(res, True)\r\n        for input_types_generating_false in [1, \"a\"]:\r\n            res = mail_generator._is8bitstring(input_types_generating_false)\r\n            self.assertEqual(res, False)\r\n            \r\n    def test_make_boundary(self):\r\n        for input_types in [None, \"abc\"]:\r\n            res = mail_generator._make_boundary()\r\n            self.assertEqual(len(res), 17 + mail_generator._width)\r\n            self.assertEqual(res[:15], '===============')\r\n            self.assertEqual(res[15:-2].isdigit(), True)\r\n            self.assertEqual(res[-2:], '==')\r\n"
  },
  {
    "path": "mailpile/tests/test_mailutils.py",
    "content": "import unittest\n\nimport mailpile\nfrom mailpile.tests import MailPileUnittest\nfrom mailpile.mailutils.header import decode_header\n\n\nclass TestCommands(MailPileUnittest):\n    def test_decode_header_no_encoding(self):\n        res = decode_header(\"olmsted\")\n        self.assertEqual(res, [('olmsted', None)])\n"
  },
  {
    "path": "mailpile/tests/test_performance.py",
    "content": "import unittest\nfrom nose.tools import assert_equal, assert_less\n\nfrom mailpile.tests import get_shared_mailpile, MailPileUnittest\n\n\ndef checkSearch(postinglist_kb, query):\n    class TestSearch(object):\n        def __init__(self):\n            self.mp = get_shared_mailpile()[0]\n            self.mp.set(\"sys.postinglist_kb=%s\" % postinglist_kb)\n            self.mp.set(\"prefs.num_results=50\")\n            self.mp.set(\"prefs.default_order=rev-date\")\n            results = self.mp.search(*query)\n            assert_less(float(results.as_dict()[\"elapsed\"]), 0.2)\n    return TestSearch\n\n\ndef test_generator():\n    postinglist_kbs = [126, 62, 46, 30]\n    search_queries = ['http', 'bjarni', 'ewelina', 'att:pdf',\n                      'subject:bjarni', 'cowboy', 'unknown', 'zyxel']\n    for postinglist_kb in postinglist_kbs:\n        for search_query in search_queries:\n            yield checkSearch(postinglist_kb, [search_query])\n"
  },
  {
    "path": "mailpile/tests/test_plugin.py",
    "content": "import shutil\nimport os\nfrom os import path\nfrom mailpile.tests import MailPileUnittest\nimport json\n\n\nclass PluginTest(MailPileUnittest):\n    def setUp(self):\n        plugins = os.path.join(self.config.workdir, 'plugins')\n        shutil.rmtree(plugins, ignore_errors=True)\n        os.mkdir(plugins)\n\n        self.plugin_dir = plugins\n\n    def tearDown(self):\n        shutil.rmtree(self.plugin_dir)\n\n    def test_plugin_is_discovered(self):\n        self._create_manifest('some_plugin', self._create_manifest_json('some_plugin'))\n        self.config.plugins.discover([self.plugin_dir])\n\n        self.assertTrue('some_plugin' in self.config.plugins.available())\n\n    def test_code_files_are_loaded_in_order(self):\n        # given\n        def create_python_file_add_stmt(number, filename):\n            self._create_code_file('from mailpile.plugins.order import add\\nadd(%d)' % number, path.join(order_plugin_path, filename))\n\n        order_plugin_path = path.join(self.plugin_dir, 'order')\n        os.mkdir(order_plugin_path)\n\n        create_python_file_add_stmt(1, 'first.py')\n        create_python_file_add_stmt(2, 'second.py')\n        create_python_file_add_stmt(3, 'third.py')\n\n        self._create_code_file('x = []\\ndef add(value):\\n\\tx.append(value)\\n', path.join(order_plugin_path, 'order.py'))\n\n        manifest = self._create_manifest_json('order')\n        manifest['code']['python'] = ['order.py', 'first.py', 'second.py', 'third.py']\n\n        self._create_manifest('order', manifest)\n\n        #when\n        self.config.plugins.discover([self.plugin_dir])\n\n        try:\n            self.mp.plugins_load('order')\n            from mailpile.plugins.order import x\n            #then\n            self.assertEqual([1, 2, 3], x)\n        finally:\n            self.mp.plugins_disable('order')\n\n    def _create_manifest(self, name, json_data):\n        order_plugin = os.path.join(self.plugin_dir, name)\n        if not os.path.exists(order_plugin):\n            os.mkdir(order_plugin)\n        manifest_file = os.path.join(order_plugin, 'manifest.json')\n\n        with open(manifest_file, 'w') as fp:\n            json.dump(json_data, fp)\n\n    def _create_manifest_json(self, name):\n        doc = dict()\n        doc['name'] = name\n        doc['author'] = 'Some Author'\n        doc['code'] = {\n            'python': [],\n            'javascript': [],\n            'css': []\n        }\n\n        return doc\n\n    def _create_code_file(self, content, filename):\n        with open(filename, 'w') as fp:\n            fp.write(content)\n"
  },
  {
    "path": "mailpile/tests/test_search.py",
    "content": "from __future__ import print_function\nimport unittest\nfrom nose.tools import assert_equal, assert_less\n\nfrom mailpile.tests import get_shared_mailpile\n\n\ndef checkSearch(query, expected_count=1):\n    class TestSearch(object):\n        def __init__(self):\n            self.mp = get_shared_mailpile()[0]\n            results = self.mp.search(*query)\n            try:\n                assert_equal(results.result['stats']['count'], expected_count)\n                assert_less(float(results.as_dict()[\"elapsed\"]), 0.2)\n            except:\n                print('BAD RESULT:\\n%s' % results.as_text())\n                raise\n    TestSearch.description = \"Searching for %s\" % str(query)\n    return TestSearch\n\n\ndef test_generator():\n    # All mail\n    yield checkSearch(['all:mail'], 13)\n    # Full match\n    yield checkSearch(['brennan'])\n    # Partial match\n    yield checkSearch(['agirorn'])\n    # Subject\n    yield checkSearch(['subject:emerging'])\n    # From\n    yield checkSearch(['from:twitter'], 2)\n    # From date\n    yield checkSearch(['dates:2013-09-17', 'feministinn'])\n    # with attachment\n    #  - Note: this differs from mailpile-test.py because we do not have the\n    #          keys required to decrypt, so encrypted mail => attachment.\n    yield checkSearch(['has:attachment'], 5)\n    # In attachment name\n    yield checkSearch(['att:jpg'])\n    # term + term\n    yield checkSearch(['brennan', 'twitter'])\n    # term + special\n    yield checkSearch(['brennan', 'from:twitter'])\n    # Not found\n    yield checkSearch(['subject:Moderation', 'kde-isl'], 0)\n    yield checkSearch(['has:crypto'], 4)\n\n    # Test that we do not crash when searching for a non-existant tag.\n    yield checkSearch(['in:doesnotexist'], 0)\n"
  },
  {
    "path": "mailpile/tests/test_ui.py",
    "content": "import json\nimport unittest\nimport mailpile\nfrom mailpile.ui import UserInteraction\n\nfrom mailpile.tests import capture, MailPileUnittest\n\n\nclass TestUI(MailPileUnittest):\n    def _ui_swap(self):\n        o, self.mp._ui = self.mp._ui, UserInteraction(self.mp._session.config)\n        return o\n\n    def test_ui_debug_log_debug_not_set(self):\n        old_ui = self._ui_swap()\n        try:\n            self.mp._ui.log_prefix = 'testprefix'\n            with capture() as out:\n                self.mp._ui._debug_log(\"text\", UserInteraction.LOG_ALL)\n            self.assertNotIn(\"testprefixlog(99): text\", ''.join(out))\n        finally:\n            self.mp._ui = old_ui\n\n    def test_ui_debug_log_debug_set(self):\n        old_ui = self._ui_swap()\n        try:\n            self.mp._ui.log_prefix = 'testprefix'\n            with capture() as out:\n                self.mp.set(\"sys.debug=log\")\n                self.mp._ui._debug_log(\"text\", UserInteraction.LOG_ALL)\n            self.assertIn(\"testprefixlog(99): text\", ''.join(out))\n        finally:\n            self.mp._ui = old_ui\n\n    def test_ui_log_block(self):\n        old_ui = self._ui_swap()\n        try:\n            self.mp._ui.block()\n            with capture() as out:\n                self.mp._ui.log(UserInteraction.LOG_URGENT, \"urgent\")\n                self.mp._ui.log(UserInteraction.LOG_RESULT, \"result\")\n                self.mp._ui.log(UserInteraction.LOG_ERROR, \"error\")\n                self.mp._ui.log(UserInteraction.LOG_NOTIFY, \"notify\")\n                self.mp._ui.log(UserInteraction.LOG_WARNING, \"warning\")\n                self.mp._ui.log(UserInteraction.LOG_PROGRESS, \"progress\")\n                self.mp._ui.log(UserInteraction.LOG_DEBUG, \"debug\")\n                self.mp._ui.log(UserInteraction.LOG_ALL, \"all\")\n            self.assertEquals(out, ['', ''])\n            with capture() as out:\n                self.mp._ui.unblock()\n            self.assertEquals(len(out), 2)\n            self.assertEquals(out[0], '')\n            # Check stripped output\n            output = [x.strip() for x in out[1].split('\\r')]\n            self.assertEquals(output, ['urgent', 'result', 'error',\n                                       'notify', 'warning', 'progress',\n                                       'debug', 'all', ''])\n            # Progress has \\r in the end instead of \\n\n            progress_str = [x for x in out[1].split('\\r\\n')\n                            if 'progress' in x][0].strip()\n            self.assertEquals(progress_str,\n                              ''.join(['progress', ' ' * 71, '\\rdebug']))\n        finally:\n            self.mp._ui = old_ui\n\n    def test_ui_clear_log(self):\n        old_ui = self._ui_swap()\n        try:\n            self.mp._ui.block()\n            with capture() as out:\n                self.mp._ui.log(UserInteraction.LOG_URGENT, \"urgent\")\n                self.mp._ui.log(UserInteraction.LOG_RESULT, \"result\")\n                self.mp._ui.log(UserInteraction.LOG_ERROR, \"error\")\n                self.mp._ui.log(UserInteraction.LOG_NOTIFY, \"notify\")\n                self.mp._ui.log(UserInteraction.LOG_WARNING, \"warning\")\n                self.mp._ui.log(UserInteraction.LOG_PROGRESS, \"progress\")\n                self.mp._ui.log(UserInteraction.LOG_DEBUG, \"debug\")\n                self.mp._ui.log(UserInteraction.LOG_ALL, \"all\")\n                self.mp._ui.clear_log()\n                self.mp._ui.unblock()\n            self.assertEquals(out, ['', ''])\n        finally:\n            self.mp._ui = old_ui\n\n    def test_ui_display_result_text(self):\n        old_ui = self._ui_swap()\n        try:\n            with capture() as out:\n                self.mp._ui.render_mode = 'text'\n                result = self.mp.rescan()\n                self.mp._ui.display_result(result)\n\n            # Parse resulting json for easier assertions\n            json_result = json.loads(out[0])\n            vcard_sources = json_result.get('vcard_sources', [])\n            self.assertEqual(json_result.get('mailboxes'), 0)\n            self.assertEqual(json_result.get('messages'), 0)\n            self.assertEqual(json_result.get('vcards'), 0)\n            self.assertIn('gravatar', vcard_sources)\n            self.assertIn('gpg', vcard_sources)\n            self.assertIn('carddav', vcard_sources)\n            self.assertIn('mork', vcard_sources)\n        finally:\n            self.mp._ui = old_ui\n"
  },
  {
    "path": "mailpile/tests/test_vcard.py",
    "content": "import unittest\nimport mailpile\n\nfrom mailpile.tests import MailPileUnittest\n\nclass TestVCard(MailPileUnittest):\n\n    def test_VCardLine_with_args(self):\n        vcl = mailpile.vcard.VCardLine(name=\"Jason\",value=\"The Dude\",pref=None)\n        self.assertEqual(vcl.as_vcardline(), \"JASON;PREF:The Dude\")\n\n    def test_VCardLine_no_args(self):\n        vcl = mailpile.vcard.VCardLine()\n        vcl.name = \"FN\"\n        vcl.value = \"Lebowski\"\n        self.assertEqual(vcl.as_vcardline(), \"FN:Lebowski\")\n        \n    def test_VCardLine_args_too_long(self): # when input is greater than 75 chars\n        vcl = mailpile.vcard.VCardLine()\n        vcl.name = \"FN\"\n        vcl.value = \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim\"\n        self.assertEqual(vcl.as_vcardline(), \"FN:Lorem ipsum dolor sit amet\\\\, consectetur adipiscing elit\\\\, sed do eiusm\\n od tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim\")\n\n    def test_VCardLine_with_vcard_data(self):\n        vcl = mailpile.vcard.VCardLine(\"FN;type=Nickname:Bjarni\")\n        self.assertEqual(vcl.name, \"fn\")\n        self.assertEqual(vcl.value, \"Bjarni\")\n        self.assertEqual(vcl.get(\"type\"), \"Nickname\")\n\n    def test_VCardLine_set_line_id(self):\n        vcl = mailpile.vcard.VCardLine(\"FN;type=Nickname:Bjarni\")\n        vcl.set_line_id(1)\n        self.assertEqual(vcl.line_id, 1)\n\n    def test_VCardLine_set_attr(self):\n        vcl = mailpile.vcard.VCardLine(\"FN;type=Nickname:Bjarni\")\n        vcl.set_attr(\"TITLE\", \"Shrimp Man\")\n        self.assertEqual(vcl.get(\"TITLE\"), \"Shrimp Man\")\n        vcl.set_attr(\"type\", \"Person\")\n        self.assertEqual(vcl.get(\"type\"), \"Person\")\n\n    #\n    # VCardLine.Quote should quote values for representing them in a VCardLine\n    #\n    def test_VCardLine_Quote(self):\n        quoted = mailpile.vcard.VCardLine.Quote(\"Comma, semicolon; backslash\\\\ newline\\\\n\")\n        self.assertEqual(quoted, \"Comma\\\\, semicolon\\\\; backslash\\\\\\\\ newline\\\\\\\\n\")\n\n    #\n    # VCardLine.ParseLine should parse a single line respecting RFC6350 quoting\n    #\n    # it should return a tuple with name, attrs, value\n    def test_VCardLine_ParseLine_unquoted(self):\n        unquoted_line = \"PHOTO;MEDIATYPE=image/gif:http://testing.mailpile.is/my_foto.gif\"\n        res = mailpile.vcard.VCardLine.ParseLine(unquoted_line)\n        self.assertEqual(len(res), 3)\n        self.assertEqual(res[0], \"photo\")\n        self.assertEqual(res[1][0], (\"mediatype\", \"image/gif\"))\n        self.assertEqual(res[2], \"http://testing.mailpile.is/my_foto.gif\")\n\n    def test_VCardLine_ParseLine_quoted(self):\n        quoted_line = \"PHOTO;THING=comma\\\\, semicolon\\\\; backslash\\\\\\\\:value\"\n        res = mailpile.vcard.VCardLine.ParseLine(quoted_line)\n        self.assertEqual(len(res), 3)\n        self.assertEqual(res[0], \"photo\")\n        self.assertEqual(res[1][0], (\"thing\", 'comma, semicolon; backslash\\\\'))\n        self.assertEqual(res[2], \"value\")\n"
  },
  {
    "path": "mailpile/tests/test_vcard_mork.py",
    "content": "import unittest\r\nimport mailpile\r\n\r\nfrom mailpile.tests import MailPileUnittest\r\n\r\nfrom mailpile.plugins import vcard_mork as vcard_mork\r\n\r\nclass TestVCard(MailPileUnittest):\r\n\r\n    def test_hexcmp(self):\r\n        res = vcard_mork.hexcmp('AB', 'CD')\r\n        self.assertEqual(res, -1)\r\n        res = vcard_mork.hexcmp('b9', '57')\r\n        self.assertEqual(res, 1)\r\n        res = vcard_mork.hexcmp('AA', 'AA')\r\n        self.assertEqual(res, 0)\r\n        res = vcard_mork.hexcmp('1', '2')\r\n        self.assertEqual(res, -1)\r\n        res = vcard_mork.hexcmp('3', '2')\r\n        self.assertEqual(res, 1)\r\n        res = vcard_mork.hexcmp('6', '6')\r\n        self.assertEqual(res, 0)"
  },
  {
    "path": "mailpile/ui.py",
    "content": "#\n# This file contains the UserInteraction and Session classes.\n#\n# The Session encapsulates settings and command results, allowing commands\n# to be chanined in an interactive environment.\n#\n# The UserInteraction classes log the progress and performance of individual\n# operations and assist with rendering the results in various formats (text,\n# HTML, JSON, etc.).\n#\n###############################################################################\nimport datetime\nimport getpass\nimport json\nimport os\nimport random\nimport re\nimport sys\nimport tempfile\nimport time\nimport traceback\nimport urllib\nfrom collections import defaultdict\nfrom json import JSONEncoder\nfrom jinja2 import TemplateError, TemplateSyntaxError, TemplateNotFound\nfrom jinja2 import TemplatesNotFound, TemplateAssertionError, UndefinedError\n\nimport mailpile.commands\nimport mailpile.platforms\nimport mailpile.util\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.util import *\n\n\nclass SuppressHtmlOutput(Exception):\n    pass\n\n\ndef default_dict(*args):\n    d = defaultdict(str)\n    for arg in args:\n        d.update(arg)\n    return d\n\n\nRING_BUFFER_LOCK = UiLock()\nRING_BUFFER_LINES = 50\nRING_BUFFER_COUNT = 0\nRING_BUFFER = [(-1, 0, '')] * RING_BUFFER_LINES\n\n\nclass NoColors:\n    \"\"\"Dummy color constants\"\"\"\n    C_SAVE = ''\n    C_RESTORE = ''\n\n    NORMAL = ''\n    BOLD = ''\n    NONE = ''\n    BLACK = ''\n    RED = ''\n    YELLOW = ''\n    BLUE = ''\n    MAGENTA = ''\n    CYAN = ''\n    FORMAT = \"%s%s\"\n    FORMAT_READLINE = \"%s%s\"\n    RESET = ''\n    LINE_BELOW = ''\n\n    def __init__(self):\n        global RING_BUFFER\n        self.ring_buffer = RING_BUFFER\n        self.lock = UiRLock()\n        self.max_width = 79\n\n    def __enter__(self, *args, **kwargs):\n        return self.lock.__enter__()\n\n    def __exit__(self, *args, **kwargs):\n        return self.lock.__exit__(*args, **kwargs)\n\n    def color(self, text, color='', weight='', readline=False):\n        return '%s%s%s' % ((self.FORMAT_READLINE if readline else self.FORMAT)\n                           % (color, weight), text, self.RESET)\n\n    def replace_line(self, text, chars=None):\n        pad = ' ' * max(0, min(self.max_width,\n                               self.max_width-(chars or len(unicode(text)))))\n        return '%s%s\\r' % (text, pad)\n\n    def clean(self, text):\n        return text\n\n    def write(self, data):\n        with self:\n            sys.stderr.write(data)\n\n    def buffer(self, data):\n        global RING_BUFFER, RING_BUFFER_COUNT, RING_BUFFER_LINES, RING_BUFFER_LOCK\n        buffering = self.clean(data).strip()\n        with RING_BUFFER_LOCK:\n            if (RING_BUFFER_COUNT and\n                    RING_BUFFER[(RING_BUFFER_COUNT-1) % RING_BUFFER_LINES][2]\n                        == buffering):\n                return\n            RING_BUFFER[RING_BUFFER_COUNT % RING_BUFFER_LINES] = (\n                RING_BUFFER_COUNT, time.time(), buffering)\n            if buffering and '\\n' in data:\n                RING_BUFFER_COUNT += 1\n\n    def check_max_width(self):\n        pass\n\n\nclass ANSIColors(NoColors):\n    \"\"\"ANSI color constants\"\"\"\n    NORMAL = ''\n    BOLD = ';1'\n    NONE = '0'\n    BLACK = \"30\"\n    RED = \"31\"\n    YELLOW = \"33\"\n    BLUE = \"34\"\n    MAGENTA = '35'\n    CYAN = '36'\n    RESET = \"\\x1B[0m\"\n    FORMAT = \"\\x1B[%s%sm\"\n    FORMAT_READLINE = \"\\001\\x1B[%s%sm\\002\"\n\n    CURSOR_UP = \"\\x1B[1A\"\n    CURSOR_DN = \"\\x1B[1B\"\n    CURSOR_SAVE = \"\\x1B[s\"\n    CURSOR_RESTORE = \"\\x1B[u\"\n    CLEAR_LINE = \"\\x1B[2K\"\n\n    ANY_CODE = re.compile('\\001|\\x1B\\[(0m|1[AB]|[su]|2K|[\\d;]+?m|002)')\n\n    def __init__(self):\n        NoColors.__init__(self)\n        self.check_max_width()\n\n    def replace_line(self, text, chars=None):\n        return '%s%s%s\\r%s' % (self.CURSOR_SAVE,\n                               self.CLEAR_LINE, text,\n                               self.CURSOR_RESTORE)\n\n    def clean(self, text):\n        return re.sub(self.ANY_CODE, '', text)\n\n    def check_max_width(self):\n        try:\n            import fcntl, termios, struct\n            fcntl_result = fcntl.ioctl(sys.stdin.fileno(),\n                                       termios.TIOCGWINSZ,\n                                       struct.pack('HHHH', 0, 0, 0, 0))\n            h, w, hp, wp = struct.unpack('HHHH', fcntl_result)\n            self.max_width = (w-1)\n        except:\n            self.max_width = 79\n\n\nclass Completer(object):\n    \"\"\"Readline autocompler\"\"\"\n    DELIMS = ' \\t\\n`~!@#$%^&*()-=+[{]}\\\\|;:\\'\",<>?'\n\n    def __init__(self, session):\n        self.session = session\n\n    def _available_opts(self, text):\n        opts = ([s.SYNOPSIS[1] for s in mailpile.commands.COMMANDS] +\n                [s.SYNOPSIS[2] for s in mailpile.commands.COMMANDS] +\n                [t.name.lower() for t in self.session.config.tags.values()])\n        return sorted([o for o in opts if o and o.startswith(text)])\n\n    def _autocomplete(self, text, state):\n        try:\n            return self._available_opts(text)[state] + ' '\n        except IndexError:\n            return None\n\n    def get_completer(self):\n        return lambda t, s: self._autocomplete(t, s)\n\n\nclass UserInteraction:\n    \"\"\"Log the progress and performance of individual operations\"\"\"\n    MAX_BUFFER_LEN = 250\n    JSON_WRAP_TYPES = ('jhtml', 'jjs', 'jtxt', 'jcss', 'jxml', 'jrss')\n\n    LOG_URGENT = 0\n    LOG_RESULT = 5\n    LOG_ERROR = 10\n    LOG_NOTIFY = 20\n    LOG_WARNING = 30\n    LOG_PROGRESS = 40\n    LOG_DEBUG = 50\n    LOG_ALL = 99\n\n    LOG_PREFIX = ''\n\n\n    def __init__(self, config, log_parent=None, log_prefix=None):\n        self.log_buffer = []\n        self.log_buffering = 0\n        self.log_level = self.LOG_ALL\n        self.log_prefix = log_prefix or self.LOG_PREFIX\n        self.interactive = False\n        self.time_tracking = [('Main', [])]\n        self.time_elapsed = 0.0\n        self.render_mode = 'text'\n        self.term = NoColors()\n        self.config = config\n        self.html_variables = {\n            'title': 'Mailpile',\n            'name': 'Chelsea Manning',\n            'csrf': '',\n            'even_odd': 'odd',\n            'mailpile_size': 0\n        }\n        self.valid_csrf_token = lambda t: False\n\n        # Short-circuit and avoid infinite recursion in parent logging.\n        self.log_parent = log_parent\n        recurse = 0\n        while self.log_parent and self.log_parent.log_parent:\n            self.log_parent = self.log_parent.log_parent\n            recurse += 1\n            if recurse > 10:\n                self.log_parent = None\n\n    # Logging\n\n    def _fmt_log(self, text, level=LOG_URGENT):\n        c, w, clip = self.term.NONE, self.term.NORMAL, 1024\n        if level == self.LOG_URGENT:\n            c, w = self.term.RED, self.term.BOLD\n        elif level == self.LOG_ERROR:\n            c = self.term.RED\n        elif level == self.LOG_WARNING:\n            c = self.term.YELLOW\n        elif level == self.LOG_NOTIFY:\n            c = self.term.CYAN\n        elif level == self.LOG_DEBUG:\n            c = self.term.MAGENTA\n        elif level == self.LOG_PROGRESS:\n            c, clip = self.term.BLUE, 78\n\n        try:\n            unicode_text = unicode(text[-clip:]).encode('utf-8', 'replace')\n        except UnicodeDecodeError:\n            unicode_text = 'ENCODING ERROR'\n\n        formatted = self.term.replace_line(self.term.color(\n            unicode_text, color=c, weight=w), chars=len(text[-clip:]))\n        if level != self.LOG_PROGRESS:\n            formatted += '\\n'\n\n        return formatted\n\n    def _display_log(self, text, level=LOG_URGENT, ring_buffer=None):\n        if not text.startswith(self.log_prefix):\n            text = '%slog(%s): %s' % (self.log_prefix, level, text)\n        if ring_buffer is True:\n            self.term.buffer(self._fmt_log(text, level=level))\n        else:\n            if self.log_parent is not None:\n                self.log_parent.log(level, text)\n            else:\n                msg = self._fmt_log(text, level=level)\n                if ring_buffer is not False:\n                    self.term.buffer(msg)\n                self.term.write(msg)\n\n    def _debug_log(self, text, level, ring_buffer=None):\n        if text and 'log' in self.config.sys.debug:\n            if not text.startswith(self.log_prefix):\n                text = '%slog(%s): %s' % (self.log_prefix, level, text)\n            if ring_buffer is True:\n                self.term.buffer(self._fmt_log(text, level=level))\n            else:\n                if self.log_parent is not None:\n                    return self.log_parent.log(level, text)\n                else:\n                    msg = self._fmt_log(text, level=level)\n                    if ring_buffer is not False:\n                        self.term.buffer(msg)\n                    self.term.write(msg)\n\n    def clear_log(self):\n        self.log_buffer = []\n\n    def flush_log(self):\n        try:\n            while len(self.log_buffer) > 0:\n                level, message = self.log_buffer.pop(0)\n                if level <= self.log_level:\n                    self._display_log(message, level, ring_buffer=False)\n        except IndexError:\n            pass\n\n    def block(self):\n        with self.term:\n            self._display_log('')\n            self.log_buffering += 1\n\n    def unblock(self, force=False):\n        with self.term:\n            if self.log_buffering <= 1 or force:\n                self.log_buffering = 0\n                self.flush_log()\n            else:\n                self.log_buffering -= 1\n\n    def log(self, level, message):\n        if self.log_buffering:\n            self._display_log(message, level, ring_buffer=True)\n            self.log_buffer.append((level, message))\n            while len(self.log_buffer) > self.MAX_BUFFER_LEN:\n                self.log_buffer[0:(self.MAX_BUFFER_LEN/10)] = []\n        elif level <= self.log_level:\n            self._display_log(message, level)\n\n    error = lambda self, msg: self.log(self.LOG_ERROR, msg)\n    notify = lambda self, msg: self.log(self.LOG_NOTIFY, msg)\n    warning = lambda self, msg: self.log(self.LOG_WARNING, msg)\n    progress = lambda self, msg: self.log(self.LOG_PROGRESS, msg)\n    debug = lambda self, msg: self.log(self.LOG_DEBUG, msg)\n\n    # Progress indication and performance tracking\n    times = property(lambda self: self.time_tracking[-1][1])\n\n    def mark(self, action=None, percent=None):\n        \"\"\"Note that we are about to perform an action.\"\"\"\n        if not action:\n            try:\n                action = self.times[-1][1]\n            except IndexError:\n                action = 'mark'\n        self.progress(action)\n        self.times.append((time.time(), action))\n\n    def report_marks(self, quiet=False, details=False):\n        t = self.times\n        if t and t[0]:\n            self.time_elapsed = elapsed = t[-1][0] - t[0][0]\n            if not quiet:\n                try:\n                    self.notify(_('Elapsed: %.3fs (%s)') % (elapsed, t[-1][1]))\n                    if details:\n                        for i in range(0, len(self.times)-1):\n                            e = t[i+1][0] - t[i][0]\n                            self.debug(' -> %.3fs (%s)' % (e, t[i][1]))\n                except IndexError:\n                    self.notify(_('Elapsed: %.3fs') % elapsed)\n            return elapsed\n        return 0\n\n    def reset_marks(self, mark=True, quiet=False, details=False):\n        \"\"\"This sequence of actions is complete.\"\"\"\n        if self.times and mark:\n            self.mark()\n        elapsed = self.report_marks(quiet=quiet, details=details)\n        self.times[:] = []\n        return elapsed\n\n    def push_marks(self, subtask):\n        \"\"\"Start tracking a new sub-task.\"\"\"\n        self.time_tracking.append((subtask, []))\n\n    def pop_marks(self, name=None, quiet=True):\n        \"\"\"Sub-task ended!\"\"\"\n        elapsed = self.report_marks(quiet=quiet)\n        if len(self.time_tracking) > 1:\n            if not name or (self.time_tracking[-1][0] == name):\n                self.time_tracking.pop(-1)\n        return elapsed\n\n    # Higher level command-related methods\n    def _display_result(self, ttype, result):\n        with self.term:\n            sys.stdout.write(unicode(result).encode('utf-8').rstrip())\n            sys.stdout.write('\\n')\n\n    def start_command(self, cmd, args, kwargs):\n        self.flush_log()\n        self.push_marks(cmd)\n        self.mark(('%s(%s)'\n                   ) % (cmd, ', '.join((args or tuple()) +\n                                       ('%s' % kwargs, ))))\n\n    def finish_command(self, cmd):\n        self.pop_marks(name=cmd)\n\n    def _parse_render_mode(self):\n        # Split out the template/type and rendering mode\n        if '!' in self.render_mode:\n            ttype, mode = self.render_mode.split('!')\n        else:\n            ttype, mode = self.render_mode, None\n\n        # Figure out whether a template has been requested, or if we\n        # are using the default \"as.foo\" template. Assume :content if\n        # people request a .jfoo, unless otherwise specified.\n        if ttype.split('.')[-1].lower() in self.JSON_WRAP_TYPES:\n            parts = ttype.split('.')\n            parts[-1] = parts[-1][1:]\n            ttype = '.'.join(parts)\n            wrap_in_json = True\n            mode = mode or 'content'\n        else:\n            wrap_in_json = False\n\n        # Figure out which template we're really asking for...\n        if '.' in ttype:\n            template = ttype\n            ttype = ttype.split('.')[1]\n        else:\n            template = 'as.' + ttype\n\n        return ttype.lower(), mode, wrap_in_json, template\n\n    def display_result(self, result):\n        \"\"\"Render command result objects to the user\"\"\"\n        try:\n            if self.render_mode in ('json', 'as.json'):\n                return self._display_result('json', result.as_('json'))\n            if self.render_mode in ('text', 'as.text'):\n                return self._display_result('text', unicode(result))\n            if self.render_mode in ('csv', 'as.csv'):\n                return self._display_result('csv', result.as_csv())\n\n            ttype, mode, wrap_in_json, template = self._parse_render_mode()\n            rendering = result.as_template(ttype,\n                                           mode=mode,\n                                           wrap_in_json=wrap_in_json,\n                                           template=template)\n\n            return self._display_result(ttype, rendering)\n        except (TypeError, ValueError, KeyError, IndexError,\n                UnicodeDecodeError):\n            traceback.print_exc()\n            return '[%s]' % _('Internal Error')\n\n    # Creating output files\n    DEFAULT_DATA_NAME_FMT = '%(msg_mid)s.%(count)s_%(att_name)s.%(att_ext)s'\n    DEFAULT_DATA_ATTRS = {\n        'msg_mid': 'file',\n        'mimetype': 'application/octet-stream',\n        'att_name': 'unnamed',\n        'att_ext': 'dat',\n        'rand': '0000'\n    }\n    DEFAULT_DATA_EXTS = {\n        # FIXME: Add more!\n        'text/plain': 'txt',\n        'text/html': 'html',\n        'image/gif': 'gif',\n        'image/jpeg': 'jpg',\n        'image/png': 'png'\n    }\n\n    def _make_data_filename(self, name_fmt, attributes):\n        return (name_fmt or self.DEFAULT_DATA_NAME_FMT) % attributes\n\n    def _make_data_attributes(self, attributes={}):\n        attrs = self.DEFAULT_DATA_ATTRS.copy()\n        attrs.update(attributes)\n        attrs['rand'] = '%4.4x' % random.randint(0, 0xffff)\n        if attrs['att_ext'] == self.DEFAULT_DATA_ATTRS['att_ext']:\n            if attrs['mimetype'] in self.DEFAULT_DATA_EXTS:\n                attrs['att_ext'] = self.DEFAULT_DATA_EXTS[attrs['mimetype']]\n        return attrs\n\n    def open_for_data(self, name_fmt=None, attributes={}):\n        filename = self._make_data_filename(\n            name_fmt, self._make_data_attributes(attributes))\n        return filename, open(filename, 'w')\n\n    # Rendering helpers for templating and such\n    def render_json(self, data):\n        \"\"\"Render data as JSON\"\"\"\n        class NoFailEncoder(JSONEncoder):\n            def default(self, obj):\n                if isinstance(obj, (list, dict, str, unicode,\n                                    int, float, bool, type(None))):\n                    return JSONEncoder.default(self, obj)\n                else:\n                    return json_helper(obj)\n\n        return json.dumps(data, indent=1, cls=NoFailEncoder,\n                                sort_keys=True, allow_nan=False)\n\n    def _web_template(self, config, tpl_names, elems=None):\n        env = config.jinja_env\n        env.session = Session(config)\n        env.session.ui = HttpUserInteraction(None, config, log_parent=self)\n        for fn in tpl_names:\n            try:\n                # FIXME(Security): Here we need to sanitize the file name\n                #                  very strictly in case it somehow came\n                #                  from user data.\n                return env.get_template(fn)\n            except (IOError, OSError, AttributeError) as e:\n                pass\n        return None\n\n    def _render_error(self, cfg, error_info):\n        emsg = \"<h1>%(error)s</h1>\"\n        if 'http' in cfg.sys.debug:\n            emsg += \"<p>%(details)s</p>\"\n            if 'traceback' in error_info:\n                emsg += \"<h3>TRACEBACK:</h3><pre>%(traceback)s</pre>\"\n            if 'source' in error_info:\n                emsg += \"<h3>SOURCE:</h3><xmp>%(source)s</xmp>\"\n            if 'data' in error_info:\n                emsg += \"<h3>DATA:</h3><pre>%(data)s</pre>\"\n            if 'config' in error_info.get('data'):\n                del error_info['data']['config']\n            if 'platforms' in error_info.get('data'):\n                del error_info['data']['platforms']\n        ei = {}\n        for kw in ('error', 'details', 'traceback', 'source', 'data'):\n            value = error_info.get(kw, '')\n            if isinstance(value, dict):\n                ei[kw] = escape_html('%.8196s' % self.render_json(value))\n            else:\n                ei[kw] = escape_html('%.2048s' % value).replace('\\n', '<br>')\n        return emsg % ei\n\n    def render_web(self, cfg, tpl_names, data):\n        \"\"\"Render data as HTML\"\"\"\n        alldata = default_dict(self.html_variables)\n        alldata['config'] = cfg\n        alldata['platforms'] = mailpile.platforms\n        alldata.update(data)\n        try:\n            template = self._web_template(cfg, tpl_names)\n            if template:\n                return template.render(alldata)\n            else:\n                tpl_esc_names = [escape_html(tn) for tn in tpl_names]\n                return self._render_error(cfg, {\n                    'error': _('Template not found'),\n                    'details': ' or '.join(tpl_esc_names),\n                    'data': alldata\n                })\n        except (UndefinedError, ):\n            tpl_esc_names = [escape_html(tn) for tn in tpl_names]\n            return self._render_error(cfg, {\n                'error': _('Template error'),\n                'details': ' or '.join(tpl_esc_names),\n                'traceback': traceback.format_exc(),\n                'data': alldata\n            })\n        except (TemplateNotFound, TemplatesNotFound) as e:\n            tpl_esc_names = [escape_html(tn) for tn in tpl_names]\n            return self._render_error(cfg, {\n                'error': _('Template not found'),\n                'details': 'In %s:\\n%s' % (e.name, e.message),\n                'data': alldata\n            })\n        except (TemplateError, TemplateSyntaxError,\n                TemplateAssertionError,) as e:\n            return self._render_error(cfg, {\n                'error': _('Template error'),\n                'details': ('In %s (%s), line %s:\\n%s'\n                            % (e.name, e.filename, e.lineno, e.message)),\n                'source': e.source,\n                'traceback': traceback.format_exc(),\n                'data': alldata\n            })\n\n    def edit_messages(self, session, emails):\n        if not self.interactive:\n            return False\n\n        for e in emails:\n            if not e.is_editable():\n                from mailpile.mailutils import NotEditableError\n                raise NotEditableError(_('Message %s is not editable')\n                                       % e.msg_mid())\n\n        sep = '%s(%8.8x%3.3x)-\\n' % ('-' * 68, time.time(),\n                                     random.randint(0, 0xfff))\n        edit_this = ('\\n'+sep).join([e.get_editing_string() for e in emails])\n\n        tf = tempfile.NamedTemporaryFile()\n        tf.write(edit_this.encode('utf-8'))\n        tf.flush()\n        with self.term:\n            try:\n                self.block()\n                os.system('%s %s' % (os.getenv('VISUAL', default='vi'),\n                                     tf.name))\n            finally:\n                self.unblock()\n        tf.seek(0, 0)\n        edited = tf.read().decode('utf-8')\n        tf.close()\n\n        if edited == edit_this:\n            return False\n\n        updates = [t.strip() for t in edited.split(sep)]\n        if len(updates) != len(emails):\n            raise ValueError(_('Number of edited messages does not match!'))\n        for i in range(0, len(updates)):\n            emails[i].update_from_string(session, updates[i])\n        return True\n\n    def get_password(self, prompt):\n        if not (self.interactive or sys.stdout.isatty()):\n            return ''\n        with self.term:\n            try:\n                self.block()\n                return getpass.getpass(prompt.encode('utf-8')).decode('utf-8')\n            finally:\n                self.unblock()\n\n\nclass HttpUserInteraction(UserInteraction):\n    LOG_PREFIX = 'http/'\n\n    def __init__(self, request, *args, **kwargs):\n        UserInteraction.__init__(self, *args, **kwargs)\n        self.request = request\n        self.logged = []\n        self.results = []\n\n    # Just buffer up rendered data\n    def _display_log(self, text, level=UserInteraction.LOG_URGENT, ring_buffer=None):\n        self._debug_log(text, level, ring_buffer=ring_buffer)\n        self.logged.append((level, text))\n\n    def _display_result(self, ttype, result):\n        self.results.append((ttype, result))\n\n    # Stream raw data to the client on open_for_data\n    def open_for_data(self, name_fmt=None, attributes={}):\n        return 'HTTP Client', RawHttpResponder(self.request, attributes)\n\n    def _render_text_responses(self, config):\n        if config.sys.debug:\n            return '%s\\n%s' % (\n                '\\n'.join([l[1] for l in self.logged]),\n                ('\\n%s\\n' % ('=' * 79)).join(r for t, r in self.results)\n            )\n        else:\n            return ('\\n%s\\n' % ('=' * 79)).join(r for t, r in self.results)\n\n    def _ttype_to_mimetype(self, ttype, result):\n        return ({\n            'css': 'text/css',\n            'csv': 'text/csv',\n            'js': 'text/javascript',\n            'json': 'application/json',\n            'html': 'text/html',\n            'rss': 'application/rss+xml',\n            'text': 'text/plain',\n            'txt': 'text/plain',\n            'xml': 'application/xml'\n        }.get(ttype.lower(), 'text/plain'), result)\n\n    def render_response(self, config):\n        ttype, mode, wrap_in_json, template = self._parse_render_mode()\n        if (ttype == 'json' or wrap_in_json):\n            if len(self.results) == 1:\n                data = self.results[0][1]\n            else:\n                data = '[%s]' % ','.join(r for t, r in self.results)\n            return ('application/json', data)\n        else:\n            if len(self.results) == 1:\n                return self._ttype_to_mimetype(*self.results[0])\n            if len(self.results) > 1:\n                raise Exception('FIXME: Multiple results, OMG WTF')\n            return \"\"\n\n    def edit_messages(self, session, emails):\n        return False\n\n\nclass BackgroundInteraction(UserInteraction):\n    LOG_PREFIX = 'bg/'\n\n    def _display_log(self, text, level=UserInteraction.LOG_URGENT, ring_buffer=None):\n        self._debug_log(text, level, ring_buffer=ring_buffer)\n\n    def edit_messages(self, session, emails):\n        return False\n\n\nclass SilentInteraction(UserInteraction):\n    LOG_PREFIX = 'silent/'\n\n    def _display_log(self, text, level=UserInteraction.LOG_URGENT, ring_buffer=None):\n        self._debug_log(text, level, ring_buffer=ring_buffer)\n\n    def _display_result(self, ttype, result):\n        return result\n\n    def edit_messages(self, session, emails):\n        return False\n\n\nclass CapturingUserInteraction(UserInteraction):\n    def __init__(self, config, **kwargs):\n        mailpile.ui.UserInteraction.__init__(self, config, **kwargs)\n        self.captured = ''\n\n    def _display_result(self, ttype, result):\n        self.captured = unicode(result)\n\n\nclass RawHttpResponder:\n\n    def __init__(self, request, attributes={}):\n        self.raised = False\n        self.request = request\n        #\n        # FIXME: Security risks here, untrusted content may find its way into\n        #                our raw HTTP headers etc.\n        #\n        mimetype = attributes.get('mimetype', 'application/octet-stream')\n        filename = attributes.get('filename', 'attachment.dat'\n                                  ).replace('\"', '')\n        disposition = attributes.get('disposition', 'attachment')\n        length = attributes.get('length')\n        request.send_http_response(200, 'OK')\n        headers = []\n        if length is not None:\n            headers.append(('Content-Length', '%s' % length))\n        if disposition and filename:\n            encfilename = urllib.quote(filename.encode(\"utf-8\"))\n            headers.append(('Content-Disposition',\n                            '%s; filename*=UTF-8\\'\\'%s' % (disposition,\n                                                           encfilename)))\n        elif disposition:\n            headers.append(('Content-Disposition', disposition))\n        request.send_standard_headers(header_list=headers,\n                                      mimetype=mimetype)\n\n    def write(self, data):\n        self.request.wfile.write(data)\n\n    def close(self):\n        if not self.raised:\n            self.raised = True\n            raise SuppressHtmlOutput()\n\n\nclass Session(object):\n\n    @classmethod\n    def Snapshot(cls, session, **copy_kwargs):\n        return cls(session.config).copy(session, **copy_kwargs)\n\n    def __init__(self, config):\n        self.config = config\n\n        self.main = False\n        self.ui = UserInteraction(config)\n\n        self.wait_lock = threading.Condition(UiRLock())\n        self.task_results = []\n\n        self.order = None\n        self.results = []\n        self.searched = []\n        self.search_index = None\n        self.last_event_id = None\n        self.displayed = None\n        self.context = None\n\n    def set_interactive(self, val):\n        self.ui.interactive = val\n\n    interactive = property(lambda s: s.ui.interactive,\n                           lambda s, v: s.set_interactive(v))\n\n    def copy(self, session, ui=None, copy_ui=False, search=True):\n        if copy_ui is True:\n            self.main = session.main\n            self.ui = session.ui\n        if ui is not None:\n            self.ui = ui\n        if search:\n            self.order = session.order\n            self.results = session.results[:]\n            self.searched = session.searched[:]\n            self.search_index = session.search_index\n            self.displayed = session.displayed\n            self.context = session.context\n        self.ui.term.max_width = session.ui.term.max_width\n        return self\n\n    def get_context(self, update=False):\n        if update or not self.context:\n            if self.searched and not self.search_index:\n                sid = self.config.search_history.add(self.searched,\n                                                     self.results,\n                                                     self.order)\n                self.context = 'search:%s' % sid\n        return self.context\n\n    def load_context(self, context):\n        if self.context and self.context == context:\n            return context\n        try:\n            if context.startswith('search:'):\n                s, r, o = self.config.search_history.get(self, context[7:])\n                self.searched, self.results, self.order = s, r, o\n                self.search_index = None\n                self.displayed = None\n                self.context = context\n                return context\n            else:\n                return False\n        except (KeyError, ValueError):\n            return False\n\n    def report_task_completed(self, name, result):\n        with self.wait_lock:\n            self.task_results.append((name, result))\n            self.wait_lock.notify_all()\n\n    def report_task_failed(self, name):\n        self.report_task_completed(name, None)\n\n    def wait_for_task(self, wait_for, quiet=False):\n        while not mailpile.util.QUITTING:\n            with self.wait_lock:\n                for i in range(0, len(self.task_results)):\n                    if self.task_results[i][0] == wait_for:\n                        tn, rv = self.task_results.pop(i)\n                        self.ui.reset_marks(quiet=quiet)\n                        return rv\n                self.wait_lock.wait()\n\n    def fatal_error(self, message):\n        self.ui.error(message)\n        if not self.interactive:\n            sys.exit(1)\n"
  },
  {
    "path": "mailpile/urlmap.py",
    "content": "from __future__ import print_function\nimport cgi\nimport time\nfrom urlparse import parse_qs, urlparse\nfrom urllib import quote, urlencode\n\nimport mailpile.auth\nimport mailpile.security as security\nfrom mailpile.commands import Command, COMMANDS\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.plugins import PluginManager\nfrom mailpile.util import *\n\n\nclass BadMethodError(Exception):\n    pass\n\n\nclass BadDataError(Exception):\n    pass\n\n\nclass _FancyString(str):\n    def __init__(self, *args):\n        str.__init__(self, *args)\n        self.filename = None\n\n\nclass UrlMap:\n    \"\"\"\n    This class will map URLs/requests to Mailpile commands and back.\n\n    The URL space is divided into three main classes:\n\n       1. Versioned API endpoints\n       2. Nice looking shortcuts to common data\n       3. Shorthand paths to API endpoints (current version only)\n\n    Depending on the endpoint, it is often possible to request alternate\n    rendering templates or generate output in a variety of machine readable\n    formats, such as JSON, XML or VCard. This is done by appending a\n    psuedo-filename to the path. If ending in `.html`, the full filename is\n    used to choose an alternate rendering template, for other extensions the\n    name is ignored but the extension used to choose an output format.\n\n    The default rendering for API endpoints is JSON, for other endpoints\n    it is HTML. It is strongly recommended that only the versioned API\n    endpoints be used for automation.\n    \"\"\"\n    API_VERSIONS = (0, )\n\n    def __init__(self, session=None, config=None):\n        self.config = config or session.config\n        self.session = session\n\n    def _prefix_to_query(self, path, query_data, post_data):\n        \"\"\"\n        Turns the /var/value prefix into a query-string argument.\n        Returns a new path with the prefix stripped.\n\n        >>> query_data = {}\n        >>> path = urlmap._prefix_to_query('/var/val/stuff', query_data, {})\n        >>> path, query_data\n        ('/stuff', {'var': ['val']})\n        \"\"\"\n        which, value, path = path[1:].split('/', 2)\n        query_data[which] = [value]\n        return '/' + path\n\n    def _api_commands(self, method, strict=False):\n        return [c for c in COMMANDS\n                if ((not method)\n                    or (c.SYNOPSIS[2] and (method in c.HTTP_CALLABLE\n                                           or not strict)))]\n\n    def _command(self, name,\n                 args=None, query_data=None, post_data=None,\n                 method='GET', async=False):\n        \"\"\"\n        Return an instantiated mailpile.command object or raise a UsageError.\n\n        >>> urlmap._command('output', args=['html'], method=False)\n        <mailpile.plugins.core.Output...>\n        >>> urlmap._command('bogus')\n        Traceback (most recent call last):\n            ...\n        UsageError: Unknown command: bogus\n        >>> urlmap._command('message/update', method='GET')\n        Traceback (most recent call last):\n            ...\n        BadMethodError: Invalid method (GET): message/update\n        >>> urlmap._command('message/update', method='POST',\n        ...                                   query_data={'evil': '1'})\n        Traceback (most recent call last):\n            ...\n        BadDataError: Bad variable (evil): message/update\n        >>> urlmap._command('search', args=['html'],\n        ...                 query_data={'ui_': '1', 'q[]': 'foobar'})\n        <mailpile.plugins.search.Search...>\n        \"\"\"\n        try:\n            match = [c for c in self._api_commands(method, strict=False)\n                     if ((method and name == c.SYNOPSIS[2]) or\n                         (not method and name == c.SYNOPSIS[1]))]\n            if len(match) != 1:\n                raise UsageError('Unknown command: %s' % name)\n        except ValueError as e:\n            raise UsageError(str(e))\n        command = match[0]\n\n        if method and (method not in command.HTTP_CALLABLE):\n            raise BadMethodError('Invalid method (%s): %s' % (method, name))\n\n        # FIXME: Move this somewhere smarter\n        SPECIAL_VARS = ('csrf', 'arg', 'context')\n\n        if command.HTTP_STRICT_VARS:\n            prefixes = ['ui_'] + [vn[:-1] for vn in\n                                  (command.HTTP_QUERY_VARS.keys() +\n                                   command.HTTP_POST_VARS.keys())\n                                  if vn[-1:] == '*']\n\n            copy_q = []\n            for var in (query_data or []):\n                var = var.replace('[]', '')\n                if (var not in command.HTTP_QUERY_VARS and\n                        (var not in SPECIAL_VARS) and\n                        (not [v for v in prefixes if var.startswith(v)])):\n                    raise BadDataError('Bad variable (%s): %s' % (var, name))\n                else:\n                    copy_q.append(var)\n\n            copy_p = []\n            for var in (post_data or []):\n                var = var.replace('[]', '')\n                if ((var not in command.HTTP_QUERY_VARS) and\n                        (var not in command.HTTP_POST_VARS) and\n                        (var not in SPECIAL_VARS) and\n                        (not [v for v in prefixes if var.startswith(v)])):\n                    raise BadDataError('Bad variable (%s): %s' % (var, name))\n                else:\n                    copy_p.append(var)\n\n            copy_vars = [(copy_q, query_data),\n                         (copy_p, post_data),\n                         (['arg'], query_data)]\n        else:\n            for var in command.HTTP_BANNED_VARS:\n                var = var.replace('[]', '')\n                if ((query_data and var in query_data) or\n                        (post_data and var in post_data)):\n                    raise BadDataError('Bad variable (%s): %s' % (var, name))\n\n            copy_vars = (((query_data or {}).keys(), query_data),\n                         ((post_data or {}).keys(), post_data),\n                         (['arg'], query_data))\n\n        data = {\n            '_method': method\n        }\n        for vlist, src in copy_vars:\n            for var in vlist:\n                varBB = var + '[]'\n                if src and (var in src or varBB in src):\n                    sdata = src[var] if (var in src) else src[varBB]\n                    if isinstance(sdata, cgi.FieldStorage):\n                        if hasattr(sdata, 'filename'):\n                            data[var] = [_FancyString(sdata.value)]\n                            data[var][0].filename = sdata.filename\n                        else:\n                            data[var] = [sdata.value.decode('utf-8')]\n                    else:\n                        data[var] = [d.decode('utf-8') for d in sdata]\n\n        return command(self.session, name, args, data=data, async=async)\n\n    OUTPUT_SUFFIXES = ['.css', '.html', '.js',  '.json', '.rss', '.txt',\n                       '.text', '.vcf', '.xml', '.csv',\n                       # These are the template-based ones which can\n                       # be embedded in JSON.\n                       '.jcss', '.jhtml', '.jjs', '.jrss', '.jtxt',\n                       '.jxml']\n\n    def _choose_output(self, path_parts, fmt='html'):\n        \"\"\"\n        Return an output command based on the URL filename component.\n\n        As a side-effect, the filename component will be removed from the\n        path_parts list.\n        >>> path_parts = '/a/b/as.json'.split('/')\n        >>> command = urlmap._choose_output(path_parts)\n        >>> (path_parts, command)\n        (['', 'a', 'b'], <mailpile.plugins.core.Output...>)\n\n        If there is no filename part, the path_parts list is unchanged\n        aside from stripping off the trailing empty string if present.\n        >>> path_parts = '/a/b/'.split('/')\n        >>> command = urlmap._choose_output(path_parts)\n        >>> (path_parts, command)\n        (['', 'a', 'b'], <mailpile.plugins.core.Output...>)\n\n        >>> path_parts = '/a/b'.split('/')\n        >>> command = urlmap._choose_output(path_parts)\n        Traceback (most recent call last):\n          ...\n        UsageError: Invalid output format: b\n\n        >>> path_parts = '/a/b/%%%%%bogon.json'.split('/')\n        >>> command = urlmap._choose_output(path_parts)\n        Traceback (most recent call last):\n          ...\n        UsageError: Invalid output format: %%%%%bogon.json\n        \"\"\"\n        if len(path_parts) > 1 and not path_parts[-1]:\n            path_parts.pop(-1)\n        else:\n            om = path_parts.pop(-1)\n            if re.match(r'^[a-zA-Z0-9\\.!_-]+$', om):\n                fn = om.split('!')[0]  # Strip off !mode suffixes\n                for suffix in self.OUTPUT_SUFFIXES:\n                    if fn.endswith(suffix) or suffix == ('.' + fn):\n                        return self._command('output', [om], method=False)\n            raise UsageError('Invalid output format: %s' % om)\n        return self._command('output', [fmt], method=False)\n\n    def _map_root(self, request, path_parts, query_data, post_data):\n        \"\"\"Redirects to /profiles/ for now.  (FIXME)\"\"\"\n        destination = '%s/profiles/' % self.config.sys.http_path\n        return [UrlRedirect(self.session, 'redirect', arg=[destination])]\n\n    def _map_tag(self, request, path_parts, query_data, post_data):\n        \"\"\"\n        Map /in/TAG_NAME/[@<pos>]/ to tag searches.\n\n        >>> path = '/in/inbox/@20/as.json'\n        >>> commands = urlmap._map_tag(request, path[1:].split('/'), {}, {})\n        >>> commands\n        [<mailpile.plugins.core.Output...>, <mailpile.plugins.search.Search...>]\n        >>> commands[0].args\n        ('as.json',)\n        >>> commands[1].args\n        ('@20', 'in:inbox')\n        \"\"\"\n        output = self._choose_output(path_parts)\n\n        pos = None\n        while path_parts and (path_parts[-1][0] in ('@', )):\n            pos = path_parts[-1].startswith('@') and path_parts.pop(-1)\n\n        tag_slug = '/'.join([p for p in path_parts[1:] if p])\n        tag = self.config.get_tag(tag_slug)\n        tag_search = [term for term in (tag.search_terms % tag).split()\n                      if term] if tag is not None else [\"in:%s\" % tag_slug]\n        if tag is not None and tag.search_order and 'order' not in query_data:\n            query_data['order'] = [tag.search_order]\n\n        if pos:\n            tag_search[:0] = [pos]\n\n        return [\n            output,\n            self._command('search',\n                          args=tag_search,\n                          query_data=query_data,\n                          post_data=post_data)\n        ]\n\n    def _map_thread(self, request, path_parts, query_data, post_data):\n        \"\"\"\n        Map /thread/METADATA_ID/... to view or extract commands.\n\n        >>> path = '/thread/=123/'\n        >>> commands = urlmap._map_thread(request, path[1:].split('/'), {}, {})\n        >>> commands\n        [<mailpile.plugins.core.Output...>, <mailpile.plugins.search.View...>]\n        >>> commands[1].args\n        ('=123',)\n        \"\"\"\n        message_mids, i = [], 1\n        while path_parts[i].startswith('='):\n            message_mids.append(path_parts[i])\n            i += 1\n        return [\n            self._choose_output(path_parts),\n            self._command('message',\n                          args=message_mids,\n                          query_data=query_data,\n                          post_data=post_data)\n        ]\n\n    def _map_RESERVED(self, *args):\n        \"\"\"RESERVED FOR LATER.\"\"\"\n\n    def _map_api_command(self, method, path_parts,\n                         query_data, post_data, fmt='html', async=False):\n        \"\"\"Map a path to a command list, prefering the longest match.\n\n        >>> urlmap._map_api_command('GET', ['message', 'draft', ''], {}, {})\n        [<mailpile.plugins.core.Output...>, <...Draft...>]\n        >>> urlmap._map_api_command('POST', ['message', 'update', ''], {}, {})\n        [<mailpile.plugins.core.Output...>, <...Update...>]\n        >>> urlmap._map_api_command('GET', ['message', 'update', ''], {}, {})\n        Traceback (most recent call last):\n            ...\n        UsageError: Not available for GET: message/update\n        \"\"\"\n        output = self._choose_output(path_parts, fmt=fmt)\n        for bp in reversed(range(1, len(path_parts) + 1)):\n            try:\n                return [\n                    output,\n                    self._command('/'.join(path_parts[:bp]),\n                                  args=path_parts[bp:],\n                                  query_data=query_data,\n                                  post_data=post_data,\n                                  method=method,\n                                  async=async)\n                ]\n            except UsageError:\n                pass\n            except BadMethodError:\n                break\n        raise UsageError('Not available for %s: %s' % (method,\n                                                       '/'.join(path_parts)))\n\n    MAP_API = 'api'\n    MAP_ASYNC_API = 'async'\n    MAP_PATHS = {\n        '': _map_root,\n        'in': _map_tag,\n        'thread': _map_thread,\n        'static': _map_RESERVED,\n    }\n\n    def map(self, request, method, path, query_data, post_data,\n            authenticate=False):\n        \"\"\"\n        Convert an HTTP request to a list of mailpile.command objects.\n\n        >>> urlmap.map(request, 'GET', '/in/inbox/', {}, {})\n        [<mailpile.plugins.core.Output...>, <mailpile.plugins.search.Search...>]\n\n        The /api/ URL space is versioned and provides access to all the\n        built-in commands. Requesting the wrong version or a bogus command\n        throws exceptions.\n        >>> urlmap.map(request, 'GET', '/api/999/bogus/', {}, {})\n        Traceback (most recent call last):\n            ...\n        UsageError: Unknown API level: 999\n        >>> urlmap.map(request, 'GET', '/api/0/bogus/', {}, {})\n        Traceback (most recent call last):\n            ...\n        UsageError: Not available for GET: bogus\n\n        This is the async version of the API.\n        >>> urlmap.map(request, 'GET', '/async/0/search/', {}, {})\n        [<mailpile.plugins.core.Output...>, <mailpile.plugins.search.Search...>]\n\n        The root currently just redirects to /profiles/:\n        >>> r = urlmap.map(request, 'GET', '/', {}, {})[0]\n        >>> r, r.args\n        (<...UrlRedirect...>, ('/profiles/',))\n\n        Tag searches have an /in/TAGNAME shorthand:\n        >>> urlmap.map(request, 'GET', '/in/inbox/', {}, {})\n        [<mailpile.plugins.core.Output...>, <mailpile.plugins.search.Search...>]\n\n        Thread shortcuts are /thread/METADATAID/:\n        >>> urlmap.map(request, 'GET', '/thread/123/', {}, {})\n        [<mailpile.plugins.core.Output...>, <mailpile.plugins.search.View...>]\n\n        Other commands use the command name as the first path component:\n        >>> urlmap.map(request, 'GET', '/search/bjarni/', {}, {})\n        [<mailpile.plugins.core.Output...>, <mailpile.plugins.search.Search...>]\n        >>> urlmap.map(request, 'GET', '/message/draft/=123/', {}, {})\n        [<mailpile.plugins.core.Output...>, <mailpile.plugins.compose.Draft...>]\n        \"\"\"\n\n        if self.session:\n            sid = self.session.ui.html_variables.get('http_session')\n            user_session = (mailpile.auth.SESSION_CACHE.get(sid)\n                            if sid else None)\n        else:\n            sid = user_session = None\n\n        is_async = path.startswith('/%s/' % self.MAP_ASYNC_API)\n        is_api = path.startswith('/%s/' % self.MAP_API)\n\n        if authenticate:\n            def auth(commands, user_session):\n                if user_session:\n                    if user_session.is_expired():\n                        mailpile.auth.SESSION_CACHE.delete_expired()\n                        user_session = None\n                    else:\n                        user_session.update_ts()\n                    if user_session and method == 'POST':\n                        if isinstance(post_data, cgi.FieldStorage):\n                            try:\n                                csrf = post_data['csrf'].value\n                            except KeyError:\n                                csrf = ''\n                        else:\n                            csrf = post_data.get('csrf', [''])[0]\n                        if not security.valid_csrf_token(\n                                request.server.secret, sid, csrf):\n                            user_session = None\n                if not user_session or not user_session.auth:\n                    for c in commands:\n                        if (c.HTTP_AUTH_REQUIRED is True or\n                               (c.HTTP_AUTH_REQUIRED == 'Maybe' and\n                                self.session.config.prefs.gpg_recipient)):\n                            if is_async or is_api:\n                                raise AccessError()\n                            self.redirect_to_auth_or_setup(method, path,\n                                                           query_data)\n                return commands\n        else:\n            def auth(commands, user_session):\n                return commands\n\n        # Check the API first.\n        if is_api or is_async:\n            path_parts = path.split('/')\n            if int(path_parts[2]) not in self.API_VERSIONS:\n                raise UsageError('Unknown API level: %s' % path_parts[2])\n            return auth(self._map_api_command(method, path_parts[3:],\n                                              query_data, post_data,\n                                              fmt='json', async=is_async),\n                        user_session)\n\n        path_parts = path[1:].split('/')\n        try:\n            return auth(self._map_api_command(method, path_parts[:],\n                                              query_data, post_data),\n                        user_session)\n        except UsageError:\n            # Finally check for the registered shortcuts\n            if path_parts[0] in self.MAP_PATHS:\n                # Just redirect potentially mapped paths straightaway\n                if authenticate and (not user_session or\n                                     not user_session.auth):\n                    self.redirect_to_auth_or_setup(method, path, query_data)\n\n                mapper = self.MAP_PATHS[path_parts[0]]\n                return auth(mapper(self, request, path_parts,\n                                   query_data, post_data),\n                            user_session)\n            raise\n\n    def _url(self, url, output='', qs=''):\n        if output and '.' not in output:\n            output = 'as.%s' % output\n        return ''.join([self.config.sys.http_path,\n                        url, output, qs and '?' or '', qs])\n\n    def url_thread(self, message_id, output=''):\n        \"\"\"Map a message to it's short-hand thread URL.\"\"\"\n        return self._url('/thread/=%s/' % message_id, output)\n\n    def url_source(self, message_id, output=''):\n        \"\"\"Map a message to it's raw message source URL.\"\"\"\n        return self._url('/message/raw/=%s/as.text' % message_id, output)\n\n    def url_edit(self, message_id, output=''):\n        \"\"\"Map a message to it's short-hand editing URL.\"\"\"\n        return self._url('/message/draft/=%s/' % message_id, output)\n\n    def redirect_to_auth_or_setup(self, method, path, query_data, setup=True):\n        \"\"\"Redirect to the /auth/ or a /setup/* endpoint\"\"\"\n        from mailpile.plugins.setup_magic import Setup\n\n        if method.lower() == 'get':\n            qd = [(k, v) for k, vl in query_data.iteritems() for v in vl]\n            if '_path' not in query_data:\n                qd.append(('_path', path))\n        else:\n            qd = []\n\n        if setup:\n            nxt = Setup.Next(self.session.config, mailpile.auth.Authenticate)\n            if nxt.HTTP_AUTH_REQUIRED is True:\n                nxt = mailpile.auth.Authenticate\n            path = '/%s/' % nxt.SYNOPSIS[2]\n        else:\n            path = '/%s/' % mailpile.auth.Authenticate.SYNOPSIS[2]\n\n        raise UrlRedirectException(self._url(path, qs=urlencode(qd)))\n\n    def redirect_to_auth(self, method, path, query_data):\n        return self.redirect_to_auth_or_setup(method, path, query_data,\n                                              setup=False)\n\n    def url_tag(self, tag_id, output=''):\n        \"\"\"\n        Map a tag to it's short-hand URL.\n\n        >>> urlmap.url_tag('Inbox')\n        '/in/inbox/'\n        >>> urlmap.url_tag('inbox', output='json')\n        '/in/inbox/as.json'\n        >>> urlmap.url_tag('1')\n        '/in/inbox/'\n\n        Unknown tags raise an exception.\n        >>> urlmap.url_tag('99')\n        Traceback (most recent call last):\n            ...\n        ValueError: Unknown tag: 99\n        \"\"\"\n        try:\n            tag = self.config.tags[tag_id]\n            if tag is None:\n                raise KeyError('oops')\n        except (KeyError, IndexError):\n            tag = [t for t in self.config.tags.values()\n                   if t.slug == tag_id.lower()]\n            tag = tag and tag[0]\n        if tag:\n            return self._url('/in/%s/' % tag.slug, output)\n        raise ValueError('Unknown tag: %s' % tag_id)\n\n    def url_sent(self, output=''):\n        \"\"\"Return the URL of the Sent tag\"\"\"\n        return self.url_tag('Sent', output=output)\n\n    def url_search(self, search_terms, tag=None, output=''):\n        \"\"\"\n        Map a search query to it's short-hand URL, using Tag prefixes if\n        there is exactly one tag in the search terms or we have tag context.\n\n        >>> urlmap.url_search(['foo', 'bar', 'baz'])\n        '/search/?q=foo%20bar%20baz'\n        >>> urlmap.url_search(['foo', 'tag:Inbox', 'wtf'], output='json')\n        '/in/inbox/as.json?q=foo%20wtf'\n        >>> urlmap.url_search(['foo', 'in:Inbox', 'wtf'], output='json')\n        '/in/inbox/as.json?q=foo%20wtf'\n        >>> urlmap.url_search(['foo', 'in:Inbox', 'tag:New'], output='xml')\n        '/search/as.xml?q=foo%20in%3AInbox%20tag%3ANew'\n        >>> urlmap.url_search(['foo', 'tag:Inbox', 'in:New'], tag='Inbox')\n        '/in/inbox/?q=foo%20in%3ANew'\n        \"\"\"\n        tags = tag and [tag] or [t for t in search_terms\n                                 if (t.startswith('tag:') or\n                                     t.startswith('in:'))]\n        if len(tags) == 1:\n            prefix = self.url_tag(\n                tags[0].replace('tag:', '').replace('in:', ''))\n            search_terms = [t for t in search_terms\n                            if (t not in tags and\n                                t.replace('tag:', '').replace('in:', '')\n                                not in tags)]\n        else:\n            prefix = '/search/'\n        return self._url(prefix, output, 'q=' + quote(' '.join(search_terms)))\n\n    def canonical_url(self, cls):\n        \"\"\"Return the full versioned URL for a command\"\"\"\n        return '/api/%s/%s/' % (cls.API_VERSION or self.API_VERSIONS[-1],\n                                cls.SYNOPSIS[2])\n\n    def ui_url(self, cls):\n        \"\"\"Return the full user-facing URL for a command\"\"\"\n        return '/%s/' % cls.SYNOPSIS[2]\n\n    def context_url(self, cls):\n        \"\"\"Return the UI context URL for a command\"\"\"\n        return '/%s/' % (cls.UI_CONTEXT or cls.SYNOPSIS[2])\n\n    def map_as_markdown(self, prefix=None):\n        \"\"\"Describe the current URL map as markdown\"\"\"\n\n        api_version = self.API_VERSIONS[-1]\n        text = []\n\n        def cmds(method):\n            return sorted([(c.SYNOPSIS[2], c)\n                           for c in self._api_commands(method, strict=True)])\n\n        text.extend([\n            '# Mailpile URL map (autogenerated by %s)' % __file__,\n            '',\n            '\\n'.join([line.strip() for line\n                       in UrlMap.__doc__.strip().splitlines()[2:]]),\n            '',\n            '## The API paths (version=%s, JSON output)' % api_version,\n            '',\n        ])\n        api = '/api/%s' % api_version\n        for method in ('GET', 'POST', 'UPDATE', 'DELETE'):\n            commands = [c for c in cmds(method)\n                        if not prefix or (c[0] and c[0].startswith(prefix))]\n            if commands:\n                text.extend([\n                    '### %s%s' % (method, method == 'GET' and\n                                  ' (also accept POST)' or ''),\n                    '',\n                ])\n            commands.sort()\n            for command in commands:\n                cls = command[1]\n                url = self.canonical_url(cls)\n                query_vars = cls.HTTP_QUERY_VARS\n                pos_args = (cls.SYNOPSIS[3] and\n                            unicode(cls.SYNOPSIS[3]).replace(' ', '/') or '')\n                padding = ' ' * (18 - len(command[0]))\n                newline = '\\n' + ' ' * (len(api) + len(command[0]) + 6)\n                if query_vars:\n                    qs = '?' + '&'.join(['%s=[%s]' % (v, query_vars[v])\n                                         for v in query_vars])\n                else:\n                    qs = ''\n                if qs:\n                    qs = '%s%s' % (padding, qs)\n                if pos_args:\n                    pos_args = '%s%s/' % (padding, pos_args)\n                    if qs:\n                        qs = newline + qs\n                text.append('    %s%s%s' % (url, pos_args, qs))\n                if cls.HTTP_POST_VARS:\n                    ps = '&'.join(['%s=[%s]' % (v, cls.HTTP_POST_VARS[v])\n                                   for v in cls.HTTP_POST_VARS])\n                    text.append('    ... POST only: %s' % ps)\n            text.append('')\n\n        if not prefix:\n            text.extend([\n                '',\n                '## Pretty shortcuts (HTML output)',\n                '',\n            ])\n            for path in sorted(self.MAP_PATHS.keys()):\n                doc = self.MAP_PATHS[path].__doc__.strip().split('\\n')[0]\n                path = ('/%s/' % path).replace('//', '/')\n                text.append('    %s %s %s' % (path, ' ' * (10 - len(path)), doc))\n\n        text.extend([\n            '',\n            '## Default command URLs (HTML output)',\n            '',\n            '*These accept the same arguments as the API calls above.*',\n            '',\n        ])\n        for command in sorted(list(set(cmds('GET') + cmds('POST')))):\n            if prefix and not command[0].startswith(prefix):\n                continue\n            text.append('    /%s/' % (command[0], ))\n        text.append('')\n        return '\\n'.join(text)\n\n    def print_map_markdown(self):\n        \"\"\"Prints the current URL map to stdout in markdown\"\"\"\n        print(self.map_as_markdown())\n\n\nclass UrlRedirect(Command):\n    \"\"\"A stub command which just throws UrlRedirectException.\"\"\"\n    SYNOPSIS = (None, None, 'http/redirect', '<url>')\n    HTTP_CALLABLE = ()\n\n    def command(self):\n        raise UrlRedirectException(self.args[0])\n\n\nclass UrlRedirectEdit(Command):\n    \"\"\"A stub command which just throws UrlRedirectException.\"\"\"\n    SYNOPSIS = (None, None, 'http/redirect/url_edit', '<mid>')\n    HTTP_CALLABLE = ()\n\n    def command(self):\n        mid = self.args[0]\n        raise UrlRedirectException(UrlMap(self.session).url_edit(mid))\n\n\nclass UrlRedirectThread(Command):\n    \"\"\"A stub command which just throws UrlRedirectException.\"\"\"\n    SYNOPSIS = (None, None, 'http/redirect/url_thread', '<mid>')\n    HTTP_CALLABLE = ()\n\n    def command(self):\n        mid = self.args[0]\n        raise UrlRedirectException(UrlMap(self.session).url_thread(mid))\n\n\nclass HelpUrlMap(Command):\n    \"\"\"Describe the current API and URL mapping\"\"\"\n    SYNOPSIS = (None, 'help/urlmap', 'help/urlmap', '[<prefix>]')\n\n    class CommandResult(Command.CommandResult):\n        def as_text(self):\n            return self.result.get('urlmap', 'Missing')\n\n        def as_html(self, *args, **kwargs):\n            try:\n                from markdown import markdown\n                html = markdown(str(self.result['urlmap']))\n            except:\n                import traceback\n                print(traceback.format_exc())\n                html = '<pre>%s</pre>' % escape_html(self.result['urlmap'])\n            self.result['markdown'] = html\n            return Command.CommandResult.as_html(self, *args, **kwargs)\n\n    def command(self):\n        prefix = self.args[0] if self.args else None\n        return {'urlmap': UrlMap(self.session).map_as_markdown(prefix=prefix)}\n\n\nplugin_manager = PluginManager(builtin=True)\nif __name__ != \"__main__\":\n    plugin_manager.register_commands(HelpUrlMap, UrlRedirect,\n                                     UrlRedirectEdit, UrlRedirectThread)\n\nelse:\n    # If run as a python script, print map and run doctests.\n    import doctest\n    import sys\n    import mailpile.app\n    import mailpile.config.defaults as defaults\n    import mailpile.config.manager as cfg_manager\n    import mailpile.plugins\n    import mailpile.ui\n\n    # Import all the default plugins\n    from mailpile.plugins import *\n\n    rules = defaults.CONFIG_RULES\n    config = cfg_manager.ConfigManager(rules=rules)\n    config.tags.extend([\n        {'name': 'New',   'slug': 'New'},\n        {'name': 'Inbox', 'slug': 'Inbox'},\n    ])\n    session = mailpile.ui.Session(config)\n    urlmap = UrlMap(session)\n    if '-nomap' in sys.argv:\n        # For the UrlMap._map_api_command test\n        plugin_manager.register_commands(UrlRedirect)\n\n        results = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                                  extraglobs={'urlmap': urlmap,\n                                              'request': None})\n        print('%s' % (results, ))\n        if results.failed:\n            sys.exit(1)\n    else:\n        urlmap.print_map_markdown()\n"
  },
  {
    "path": "mailpile/util.py",
    "content": "# coding: utf-8\n#\n# Misc. utility functions for Mailpile.\n#\nfrom __future__ import print_function\nimport cgi\nimport copy\nimport ctypes\nimport datetime\nimport hashlib\nimport inspect\nimport locale\nimport os\nimport platform\nimport random\nimport re\nimport string\nimport subprocess\nimport sys\nimport tempfile\nimport threading\nimport time\nimport StringIO\nimport cStringIO\nfrom distutils import spawn\n\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.safe_popen import Popen, PIPE\n\n\ntry:\n    import imgsize\nexcept:\n    imgsize = None\n\ntry:\n    from PIL import Image, ExifTags\nexcept:\n    Image = None\n\n\nglobal WORD_REGEXP, STOPLIST, BORING_HEADERS, DEFAULT_PORT, QUITTING\n\n\nTESTING = False\nQUITTING = False\nLAST_USER_ACTIVITY = 0\nLIVE_USER_ACTIVITIES = 0\n\nTHREAD_LOCAL = threading.local()\n\nRID_COUNTER = 0\nRID_COUNTER_LOCK = threading.Lock()\n\nMAIN_PID = os.getpid()\nDEFAULT_PORT = 33411\n\n# Warning: this is duplicated in the javascript, grep for WORD_REGEXP\n#          to keep any changes in sync.\nWORD_REGEXP = re.compile('[^\\s!@#$%^&*\\(\\)_+=\\{\\}\\[\\]'\n                         ':\\\"|;`\\'\\\\\\<\\>\\?,\\.\\/\\-]{2,}')\n\n# These next two variables are important for reducing hot-spots in the\n# search index and polluting it with spammy results. But adding too many\n# terms here makes searches fail, so we need to be careful. Also, the\n# spam classifier won't see these things. So again, careful...\nSTOPLIST = set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9',\n                'a', 'an', 'and', 'any', 'are', 'as', 'at',\n                'but', 'by', 'can', 'div', 'do', 'for', 'from',\n                'has', 'hello', 'hi', 'i', 'in', 'if', 'is', 'it',\n                'mailto', 'me', 'my',\n                'og', 'of', 'on', 'or', 'p', 're', 'span', 'so',\n                'that', 'the', 'this', 'td', 'to', 'tr',\n                'was', 'we', 'were', 'you'])\n\nBORING_HEADERS = ('received', 'received-spf', 'date', 'autocrypt',\n                  'content-type', 'content-disposition', 'mime-version',\n                  'list-archive', 'list-help', 'list-unsubscribe',\n                  'dkim-signature', 'domainkey-signature',\n                  'arc-message-signature', 'arc-seal',\n                  'arc-authentication-results', 'authentication-results')\n\n# For the spam classifier, if these headers are missing a special\n# note is made of that in the message keywords.\nEXPECTED_HEADERS = ('from', 'to', 'subject', 'date', 'message-id')\n\n# Different attachment types we create keywords for during indexing\nATT_EXTS = {\n    'audio': ['aiff', 'aac', 'mid', 'midi', 'mp3', 'mp2', '3gp', 'wav'],\n    'code': ['c', 'cpp', 'c++', 'css', 'cxx',\n             'h', 'hpp', 'h++', 'html', 'hxx', 'py', 'php', 'pl', 'rb',\n             'java', 'js', 'xml'],\n    'crypto': ['asc', 'pgp', 'key'],\n    'data': ['cfg', 'csv', 'gz', 'json', 'log', 'sql', 'rss', 'tar',\n             'tgz', 'vcf', 'xls', 'xlsx'],\n    'document': ['csv', 'doc', 'docx', 'htm', 'html', 'md',\n                 'odt', 'ods', 'odp', 'ps', 'pdf', 'ppt', 'pptx', 'psd',\n                 'txt', 'xls', 'xlsx', 'xml'],\n    'font': ['eot', 'otf', 'pfa', 'pfb', 'gsf', 'pcf', 'ttf', 'woff'],\n    'image': ['bmp', 'eps', 'gif', 'ico', 'jpeg', 'jpg',\n              'png', 'ps', 'psd', 'svg', 'svgz', 'tiff', 'xpm'],\n    'video': ['avi', 'divx'],\n}\nATT_EXTS['media'] = (ATT_EXTS['audio'] + ATT_EXTS['font'] +\n                     ATT_EXTS['image'] + ATT_EXTS['video'])\n\nB64C_STRIP = '\\r\\n='\n\nB64C_TRANSLATE = string.maketrans('/', '_')\n\nB64W_TRANSLATE = string.maketrans('/+', '_-')\n\nSTRHASH_RE = re.compile('[^0-9a-z]+')\n\nALPHA_RE  = re.compile(\"\\A[a-zA-Z]+\\Z\")\nEMAIL_RE = re.compile(\"\\A.+@.+\\Z\")\nDNSNAME_RE = re.compile(\"\\A([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,32}\\Z\")\n\nB36_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'\n\nRE_LONG_LINE_SPLITTER = re.compile('([^\\n]{,72}) ')\n\n# see: http://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml\n# currently we just use common ones\nPERMANENT_URI_SCHEMES = set([\n  \"data\", \"file\", \"ftp\", \"gopher\", \"http\", \"https\", \"imap\",\n  \"jabber\", \"mailto\", \"news\", \"telnet\", \"tftp\", \"ws\", \"wss\"\n])\nPROVISIONAL_URI_SCHEMES = set([\n  \"bitcoin\", \"chrome\", \"cvs\", \"feed\", \"git\", \"irc\", \"magnet\",\n  \"sftp\", \"smtp\", \"ssh\", \"steam\", \"svn\"\n])\nURI_SCHEMES = PERMANENT_URI_SCHEMES.union(PROVISIONAL_URI_SCHEMES)\n\nUNI_BOX_FLIPS = [\n    (u'\\u250c', u'\\u2514'), (u'\\u250d', u'\\u2515'), (u'\\u250e', u'\\u2516'),\n    (u'\\u250f', u'\\u2517'), (u'\\u2510', u'\\u2518'), (u'\\u2511', u'\\u2519'),\n    (u'\\u2512', u'\\u251a'), (u'\\u2513', u'\\u251b'), (u'\\u251d', u'\\u251f'),\n    (u'\\u2521', u'\\u2522'), (u'\\u2526', u'\\u2527'), (u'\\u2529', u'\\u252a'),\n    (u'\\u252c', u'\\u2534'), (u'\\u252d', u'\\u2535'), (u'\\u252e', u'\\u2536'),\n    (u'\\u252f', u'\\u2537'), (u'\\u2530', u'\\u2538'), (u'\\u2531', u'\\u2539'),\n    (u'\\u2532', u'\\u253a'), (u'\\u2533', u'\\u253b'), (u'\\u2543', u'\\u2545'),\n    (u'\\u2544', u'\\u2546'), (u'\\u2547', u'\\u2548'), (u'\\u2552', u'\\u2558'),\n    (u'\\u2553', u'\\u2559'), (u'\\u2554', u'\\u255a'), (u'\\u2555', u'\\u255b'),\n    (u'\\u2556', u'\\u255c'), (u'\\u2557', u'\\u255d'), (u'\\u2564', u'\\u2567'),\n    (u'\\u2565', u'\\u2568'), (u'\\u2566', u'\\u2569'), (u'\\u256d', u'\\u2570'),\n    (u'\\u256e', u'\\u256f'), (u'\\u2571', u'\\u2572'), (u'\\u2575', u'\\u2577'),\n    (u'\\u2579', u'\\u257a'), (u'\\u257d', u'\\u257f')\n]\nUNI_BOX_FLIP = dict(UNI_BOX_FLIPS + [(b, a) for a, b in UNI_BOX_FLIPS])\n\n\n##[ Lock debugging tools ]##################################################\n\ndef WhereAmI(start=1):\n    stack = inspect.stack()\n    return '%s' % '->'.join(\n        ['%s:%s' % ('/'.join(stack[i][1].split('/')[-2:]), stack[i][2])\n         for i in reversed(range(start, len(stack)-1))])\n\n\ndef _TracedLock(what, *a, **kw):\n    lock = what(*a, **kw)\n\n    class Wrapper:\n        def acquire(self, *args, **kwargs):\n            if self.locked():\n                print('==!== Waiting for %s at %s' % (str(lock), WhereAmI(2)))\n            return lock.acquire(*args, **kwargs)\n        def release(self, *args, **kwargs):\n            return lock.release(*args, **kwargs)\n        def __enter__(self, *args, **kwargs):\n            if self.locked():\n                print('==!== Waiting for %s at %s' % (str(lock), WhereAmI(2)))\n            return lock.__enter__(*args, **kwargs)\n        def __exit__(self, *args, **kwargs):\n            return lock.__exit__(*args, **kwargs)\n        def _is_owned(self, *args, **kwargs):\n            return lock._is_owned(*args, **kwargs)\n        def locked(self, *args, **kwargs):\n            acquired = False\n            try:\n                acquired = lock.acquire(False)\n                return (not acquired)\n            finally:\n                if acquired:\n                    lock.release()\n\n    return Wrapper()\n\n\ndef TracedLock(*args, **kwargs):\n    return _TracedLock(threading.Lock, *args, **kwargs)\n\n\ndef TracedRLock(*args, **kwargs):\n    return _TracedLock(threading.RLock, *args, **kwargs)\n\n\nTracedLocks = (TracedLock, TracedRLock)\nUnTracedLocks = (threading.Lock, threading.RLock)\n\n# Replace with as necessary TracedLocks to track down deadlocks.\nEventLock, EventRLock = UnTracedLocks\nConfigLock, ConfigRLock = UnTracedLocks\nCryptoLock, CryptoRLock = UnTracedLocks\nUiLock, UiRLock = UnTracedLocks\nWorkerLock, WorkerRLock = UnTracedLocks\nMboxLock, MboxRLock = UnTracedLocks\nSearchLock, SearchRLock = UnTracedLocks\nPListLock, PListRLock = UnTracedLocks\nVCardLock, VCardRLock = UnTracedLocks\nMSrcLock, MSrcRLock = UnTracedLocks\n\n##############################################################################\n\n\nclass WorkerError(Exception):\n    pass\n\n\nclass UsageError(Exception):\n    pass\n\n\nclass AccessError(Exception):\n    pass\n\n\nclass InternalError(AssertionError):\n    pass\n\n\nclass UrlRedirectException(Exception):\n    \"\"\"An exception indicating we need to redirecting to another URL.\"\"\"\n    def __init__(self, url):\n        Exception.__init__(self, 'Should redirect to: %s' % url)\n        self.url = url\n\n\nclass JobPostponingException(Exception):\n    seconds = 300\n\n\nclass MultiContext:\n    def __init__(self, contexts):\n        self.contexts = contexts or []\n\n    def __enter__(self, *args, **kwargs):\n        for ctx in self.contexts:\n            ctx.__enter__(*args, **kwargs)\n        return self\n\n    def __exit__(self, *args, **kwargs):\n        raised = []\n        for ctx in reversed(self.contexts):\n            try:\n                ctx.__exit__(*args, **kwargs)\n            except Exception as e:\n                raised.append(e)\n        if raised:\n            raise raised[0]\n\n\ndef safe_assert(check, *args):\n    \"\"\"A safe-to-use assert() replacement that never gets compiled out.\"\"\"\n    if not check:\n        raise InternalError(*args)\n\n\ndef thread_context_push(**kwargs):\n    if not hasattr(THREAD_LOCAL, 'context'):\n        THREAD_LOCAL.context = []\n    THREAD_LOCAL.context.append(kwargs)\n\n\ndef thread_context():\n    return THREAD_LOCAL.context if hasattr(THREAD_LOCAL, 'context') else []\n\n\ndef thread_context_pop():\n    if hasattr(THREAD_LOCAL, 'context'):\n        THREAD_LOCAL.context.pop(-1)\n\n\ndef FixupForWith(obj):\n    if not hasattr(obj, '__enter__'):\n        obj.__enter__ = lambda: obj\n    if not hasattr(obj, '__exit__'):\n        obj.__exit__ = lambda a, b, c: True\n    return obj\n\n\ndef b64c(b):\n    \"\"\"\n    Rewrite a base64 string:\n        - Remove LF and = characters\n        - Replace slashes by underscores\n\n    >>> b64c(\"abc123456def\")\n    'abc123456def'\n    >>> b64c(\"\\\\na/=b=c/\")\n    'a_bc_'\n    >>> b64c(\"a+b+c+123+\")\n    'a+b+c+123+'\n    \"\"\"\n    return string.translate(b, B64C_TRANSLATE, B64C_STRIP)\n\n\ndef b64w(b):\n    \"\"\"\n    Rewrite a base64 string by replacing\n    \"+\" by \"-\" (e.g. for URLs).\n\n    >>> b64w(\"abc123456def\")\n    'abc123456def'\n    >>> b64w(\"a+b+c+123+\")\n    'a-b-c-123-'\n    \"\"\"\n    return string.translate(b, B64W_TRANSLATE, B64C_STRIP)\n\n\ndef escape_html(t):\n    \"\"\"\n    Replace characters that have a special meaning in HTML\n    by their entity equivalents. Return the replaced\n    string.\n\n    >>> escape_html(\"Hello, Goodbye.\")\n    'Hello, Goodbye.'\n    >>> escape_html(\"Hello<>World\")\n    'Hello&lt;&gt;World'\n    >>> escape_html(\"<&>\")\n    '&lt;&amp;&gt;'\n\n    Keyword arguments:\n    t -- The string to escape\n    \"\"\"\n    return cgi.escape(t)\n\n\ndef flip_unicode_boxes(text):\n    return ''.join(UNI_BOX_FLIP.get(c, c) for c in text)\n\n\ndef _hash(cls, data):\n    h = cls()\n    for s in data:\n        if isinstance(s, unicode):\n            h.update(s.encode('utf-8'))\n        else:\n            h.update(s)\n    return h\n\n\ndef sha1b64(*data):\n    \"\"\"\n    Apply the SHA1 hash algorithm to a string\n    and return the base64-encoded hash value\n\n    >>> sha1b64(\"Hello\")\n    '9/+ei3uy4Jtwk1pdeF4MxdnQq/A=\\\\n'\n\n    >>> sha1b64(u\"Hello\")\n    '9/+ei3uy4Jtwk1pdeF4MxdnQq/A=\\\\n'\n\n    Keyword arguments:\n    s -- The string to hash\n    \"\"\"\n    return _hash(hashlib.sha1, data).digest().encode('base64')\n\n\ndef sha512b64(*data):\n    \"\"\"\n    Apply the SHA512 hash algorithm to a string\n    and return the base64-encoded hash value\n\n    >>> sha512b64(\"Hello\")[:64]\n    'NhX4DJ0pPtdAJof5SyLVjlKbjMeRb4+sf933+9WvTPd309eVp6AKFr9+fz+5Vh7p'\n    >>> sha512b64(u\"Hello\")[:64]\n    'NhX4DJ0pPtdAJof5SyLVjlKbjMeRb4+sf933+9WvTPd309eVp6AKFr9+fz+5Vh7p'\n\n    Keyword arguments:\n    s -- The string to hash\n    \"\"\"\n    return _hash(hashlib.sha512, data).digest().encode('base64')\n\n\ndef md5_hex(*data):\n    return _hash(hashlib.md5, data).hexdigest()\n\n\ndef strhash(s, length, obfuscate=None):\n    \"\"\"\n    Create a hash of\n\n    >>> strhash(\"Hello\", 10)\n    'hello9_+ei'\n    >>> strhash(\"Goodbye\", 5, obfuscate=\"mysalt\")\n    'voxpj'\n\n    Keyword arguments:\n    s -- The string to be hashed\n    length -- The length of the hash to create.\n                        Might be limited by the hash method\n    obfuscate -- None to disable SHA512 obfuscation,\n                             or a salt to append to the string\n                             before hashing\n    \"\"\"\n    if obfuscate:\n        hashedStr = b64c(sha512b64(s, obfuscate).lower())\n    else:  # Don't obfuscate\n        hashedStr = re.sub(STRHASH_RE, '', s.lower())[:(length - 4)]\n        while len(hashedStr) < length:\n            hashedStr += b64c(sha1b64(s)).lower()\n    return hashedStr[:length]\n\n\ndef b36(number):\n    \"\"\"\n    Convert a number to base36\n\n    >>> b36(2701)\n    '231'\n    >>> b36(12345)\n    '9IX'\n    >>> b36(None)\n    '0'\n\n    Keyword arguments:\n    number -- An integer to convert to base36\n    \"\"\"\n    if not number or number < 0:\n        return B36_ALPHABET[0]\n    base36 = []\n    while number:\n        number, i = divmod(number, 36)\n        base36.append(B36_ALPHABET[i])\n    return ''.join(reversed(base36))\n\n\ndef string_to_rank(text, maxint=sys.maxsize):\n    \"\"\"\n    Approximate lexographical order with an int. It's accurate near\n    the front of the string, but gets fuzzy towards letter 10.\n    \"\"\"\n    rs = CleanText(text, banned=CleanText.NONALNUM).clean.lower()\n    rank = 0.0\n    frac = 1.0\n    for pos in range(0, min(15, len(rs))):\n        rank += frac * (int(rs[pos], 36) / (36.0 + 0.09 * pos))\n        frac *= 1.0 / (36-pos)\n    return long(rank * (maxint - 100)) + min(100, len(text))\n\n\ndef string_to_intlist(text):\n    \"\"\"Converts a string into an array of integers\"\"\"\n    try:\n        return [ord(c) for c in text.encode('utf-8')]\n    except (UnicodeEncodeError, UnicodeDecodeError):\n        return [ord(c) for c in text]\n\n\ndef intlist_to_string(intlist):\n    chars = ''.join([chr(c) for c in intlist])\n    try:\n        return chars.decode('utf-8')\n    except (UnicodeEncodeError, UnicodeDecodeError):\n        return chars\n\n\ndef intlist_to_bitmask(intlist):\n    if not intlist:\n        return str('\\0')\n    bitmask = [0] * (max(intlist) // 8 + 1)\n    for r in intlist:\n        bitmask[r//8] |= 1 << (r % 8)\n    return ''.join(chr(b) for b in bitmask)\n\n\ndef bitmask_to_intlist(bitmask):\n    results = []\n    for i in range(0, len(bitmask)):\n        v = ord(bitmask[i])\n        if v:\n            results += [(i * 8 + b) for b in range(0, 8) if v & (1 << b)]\n    return results\n\n\ndef truthy(txt, default=False, special=None):\n    try:\n        # Floats are fun! :-P\n        return (abs(float(txt)) >= 0.00001)\n    except (ValueError, TypeError):\n        pass\n\n    txt = unicode(txt).lower()\n    if special is not None and txt in special:\n        return special[txt]\n    elif txt in ('n', 'no', 'false', 'off'):\n        return False\n    elif txt in ('y', 'yes', 'true', 'on'):\n        return True\n    elif txt in (_('false'), _('no'), _('off')):\n        return False\n    elif txt in (_('true'), _('yes'), _('on')):\n        return True\n    else:\n        return default\n\n\ndef try_decode(text, charset, replace=''):\n    # FIXME: We need better heuristics for choosing charsets, as pretty\n    #        much any 8-bit legacy charset will decode pretty much any\n    #        blob of data. At least utf-8 will raise on some things\n    #        (which is why we make it the 1st guess), but still not all.\n    for cs in (charset, 'utf-8', 'iso-8859-1'):\n        if cs:\n            try:\n                return text.decode(cs)\n            except (UnicodeEncodeError, UnicodeDecodeError, LookupError):\n                pass\n    return \"\".join((i if (ord(i) < 128) else replace) for i in text)\n\n\ndef randomish_uid():\n    \"\"\"\n    Generate a weakly random unique ID. Might not actually be unique.\n    Leaks the time; uniqueness depends on time moving forward and not\n    being invoked too rapidly.\n    \"\"\"\n    with RID_COUNTER_LOCK:\n        global RID_COUNTER\n        RID_COUNTER += 1\n        RID_COUNTER %= 0x1000\n        return '%3.3x%7.7x%x' % (random.randint(0, 0xfff),\n                                 time.time() // 16,\n                                 RID_COUNTER)\n\n\ndef okay_random(length, *seeds):\n    \"\"\"\n    Generate a psuedo-random string, mixing some seed data with os.urandom().\n    The mixing is \"just in case\" os.urandom() is lame for some unfathomable\n    reason. This is hopefully all overkill.\n    \"\"\"\n    secret = ''\n    while len(secret) < length:\n        # Generate unpredictable bytes from the base64 alphabet\n        secret += sha512b64(os.urandom(128 + length * 2),\n                            '%s' % time.time(),\n                            '%x' % random.randint(0, 0xffffffff),\n                            *seeds)\n        # Strip confusing characters and truncate\n        secret = CleanText(secret, banned=CleanText.NONALNUM + 'O01l\\n \\t'\n                           ).clean[:length]\n    return secret\n\n\ndef split_secret(secret, recipients, pad_to=24):\n    while len(secret) < pad_to:\n        secret += '\\x00'\n    as_bytes = string_to_intlist(secret)\n    parts = []\n    while len(parts) < recipients-1:\n        parts.append(string_to_intlist(os.urandom(len(as_bytes))))\n    last = []\n    parts.append(last)\n    for i in range(0, len(as_bytes)):\n        c = as_bytes[i]\n        for j in range(0, recipients-1):\n            c ^= parts[j][i]\n        last.append(c & 0xff)\n    return [':'.join(['%2.2x' % x for x in p]) for p in parts]\n\n\ndef merge_secret(parts):\n    parts = [[int(c, 16) for c in p.split(':')] for p in parts]\n    secret = []\n    for i in range(0, len(parts[0])):\n        c = parts[0][i]\n        for j in range(1, len(parts)):\n            c ^= parts[j][i]\n        secret.append(c & 0xff)\n    while secret[-1] == 0:\n        secret[-1:] = []\n    return intlist_to_string(secret)\n\n\nREFLOW_PROSE_START = re.compile(r'\\S*\\w+')\nREFLOW_NONBLANK = re.compile(r'\\S')\n\ndef reflow_text(text, quoting=False, target_width=65):\n    \"\"\"\n    Reflow text so lines are roughly of a uniform length suitable for\n    reading or replying. Tries to detect whether the text has already\n    been manually formatted and preserve unmodified in such cases.\n\n    >>> test_string = (('abcd efgh ijkl mnop ' + ('q' * 72) + ' ') * 2)[:-1]\n    >>> print(reflow_text(test_string))\n    abcd efgh ijkl mnop\n    qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\n    abcd efgh ijkl mnop\n    qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\n\n    >>> print(reflow_text('> ' + ('q' * 72)))\n    > qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\n\n    The function should be stable:\n\n    >>> reflow_text(test_string) == reflow_text(reflow_text(test_string))\n    True\n    \"\"\"\n    if quoting:\n        target_width -= 2\n    inlines = text.splitlines()\n    outlines = []\n    def line_length(l, word):\n        return sum(len(w) for w in l) + len(l) + len(word)\n    while inlines:\n        thisline = inlines.pop(0)\n        if (re.match(REFLOW_PROSE_START, thisline)\n                and not thisline.endswith('  ')\n                and len(thisline) > target_width-10):\n            # This line looks like the beginning of a paragraph, go get\n            # the rest of the paragraph for reflowing...\n            para = thisline.strip().split()\n            while (inlines\n                    and not inlines[0].endswith('  ')\n                    and re.match(REFLOW_PROSE_START, inlines[0])):\n                para += inlines.pop(0).strip().split()\n\n            # Once we have the full paragraph, reflow using target width\n            paralines = [[]]\n            for word in para:\n               if line_length(paralines[-1], word) <= target_width:\n                   paralines[-1].append(word)\n               elif 0 == len(paralines[-1]):\n                   paralines[-1].append(word)\n               else:\n                   paralines.append([word])\n            outlines.extend([' '.join(l) for l in paralines])\n\n        else:\n            # Not a paragraph, just preserve this line unchanged\n            outlines.append(thisline)\n\n    return '\\n'.join(outlines)\n\n\ndef elapsed_datetime(timestamp):\n    \"\"\"\n    Return \"X days ago\" style relative dates for recent dates.\n    \"\"\"\n    ts = datetime.datetime.fromtimestamp(timestamp)\n    elapsed = datetime.datetime.today() - ts\n    days_ago = elapsed.days\n    hours_ago, remainder = divmod(elapsed.seconds, 3600)\n    minutes_ago, seconds_ago = divmod(remainder, 60)\n\n    if days_ago < 1:\n        if hours_ago < 1:\n            if minutes_ago < 3:\n                return _('now')\n            else:\n                return _('%d mins') % minutes_ago\n        elif hours_ago < 2:\n            return _('%d hour') % hours_ago\n        else:\n            return _('%d hours') % hours_ago\n    elif days_ago < 2:\n        return _(ts.strftime('%A'))  #return _('%d day') % days_ago\n    elif days_ago < 7:\n        return _(ts.strftime('%A'))  #return _('%d days') % days_ago\n    elif days_ago < 366:\n        return _(ts.strftime(\"%b\")) + ts.strftime(\" %d\")\n    else:\n        return _(ts.strftime(\"%b\")) + ts.strftime(\" %d %Y\")\n\n_translate_these = [_('Monday'), _('Mon'), _('Tuesday'), _('Tue'),\n                    _('Wednesday'), _('Wed'), _('Thursday'), _('Thu'),\n                    _('Friday'), _('Fri'), _('Saturday'), _('Sat'),\n                    _('Sunday'), _('Sun'),\n                    _('January'), _('Jan'), _('February'), _('Feb'),\n                    _('March'), _('Mar'), _('April'), _('Apr'),\n                    _('May'), _('June'), _('Jun'),\n                    _('July'), _('Jul'), _('August'), _('Aug'),\n                    _('September'), _('Sep'), _('October'), _('Oct'),\n                    _('November'), _('Nov'), _('December'), _('Dec')]\n\n\ndef friendly_datetime(timestamp):\n    date = datetime.date.fromtimestamp(timestamp)\n    return date.strftime(\"%b %d, %Y\")\n\n\ndef friendly_time(timestamp):\n    date = datetime.datetime.fromtimestamp(timestamp)\n    return date.strftime(\"%H:%M\")\n\n\ndef friendly_number(number, base=1000, decimals=0, suffix='',\n                    powers=['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']):\n    \"\"\"\n    Format a number as friendly text, using common suffixes.\n\n    >>> friendly_number(102)\n    '102'\n    >>> friendly_number(10230)\n    '10k'\n    >>> friendly_number(12341234, decimals=1)\n    '12.3M'\n    >>> friendly_number(1024000000, base=1024, suffix='iB')\n    '977MiB'\n    \"\"\"\n    count = 0\n    number = float(number)\n    while number > base and count < len(powers):\n        number /= base\n        count += 1\n    if decimals:\n        fmt = '%%.%df%%s%%s' % decimals\n    else:\n        number = round(number)\n        fmt = '%d%s%s'\n    return fmt % (number, powers[count], suffix)\n\n\ndef decrypt_and_parse_lines(fd, parser, config,\n                            newlines=False, decode='utf-8',\n                            passphrase=None, gpgi=None,\n                            _raise=IOError, error_cb=None):\n    import mailpile.crypto.streamer as cstrm\n    symmetric_key = config and config.get_master_key() or 'missing'\n    passphrase_reader = (passphrase.get_reader()\n                         if (passphrase is not None) else\n                         (config.passphrases['DEFAULT'].get_reader()\n                          if (config is not None) else None))\n\n    if not newlines:\n        if decode:\n            _parser = lambda ll: parser((l.rstrip('\\r\\n').decode(decode)\n                                         for l in ll))\n        else:\n            _parser = lambda ll: parser((l.rstrip('\\r\\n') for l in ll))\n    elif decode:\n        _parser = lambda ll: parser((l.decode(decode) for l in ll))\n    else:\n        _parser = parser\n\n    for line in fd:\n        if cstrm.PartialDecryptingStreamer.StartEncrypted(line):\n            with cstrm.PartialDecryptingStreamer(\n                    [line], fd,\n                    name='decrypt_and_parse',\n                    mep_key=symmetric_key,\n                    gpg_pass=passphrase_reader,\n                    gpgi=gpgi) as pdsfd:\n                _parser(pdsfd)\n                if not pdsfd.verify(_raise=_raise) and error_cb:\n                    error_cb(fd.tell())\n        else:\n            _parser([line])\n\n\n# This is a hack to deal with the fact that Windows sometimes won't\n# let us delete files right away because it thinks they are still open.\n# Any failed removal just gets queued up for later.\n#\nPENDING_REMOVAL = []\nPENDING_REMOVAL_LOCK = threading.Lock()\n\ndef safe_remove(filename=None):\n    with PENDING_REMOVAL_LOCK:\n        if filename:\n            PENDING_REMOVAL.append(filename)\n        for fn in PENDING_REMOVAL[:]:\n            try:\n                os.remove(fn)\n                PENDING_REMOVAL.remove(fn)\n            except (OSError, IOError):\n                pass\n        return (filename and filename not in PENDING_REMOVAL)\n\n\ndef backup_file(filename, backups=5, min_age_delta=0):\n    if os.path.exists(filename):\n        if os.stat(filename).st_mtime >= time.time() - min_age_delta:\n            return\n\n        for ver in reversed(range(1, backups)):\n            bf = '%s.%d' % (filename, ver)\n            if os.path.exists(bf):\n                nbf = '%s.%d' % (filename, ver+1)\n                if os.path.exists(nbf):\n                    os.remove(nbf)\n                os.rename(bf, nbf)\n        os.rename(filename, '%s.1' % filename)\n\n\n# Thanks to:\n# https://stackoverflow.com/questions/51658/cross-platform-space-remaining-on-volume-using-python\ndef get_free_disk_bytes(dirname):\n    \"\"\"Return folder/drive free space in bytes\"\"\"\n    if platform.system().lower().startswith('win'):\n        free_bytes = ctypes.c_ulonglong(0)\n        ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(dirname), None, None, ctypes.pointer(free_bytes))\n        return free_bytes.value\n    else:\n        st = os.statvfs(dirname)\n        return st.f_bavail * st.f_frsize\n\n\ndef json_helper(obj):\n    try:\n        return unicode(obj)\n    except:\n        return \"COMPLEXBLOB\"\n\nclass GpgWriter(object):\n    def __init__(self, gpg):\n        self.fd = gpg.stdin\n        self.gpg = gpg\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.close()\n        return False\n\n    def write(self, data):\n        self.fd.write(data)\n\n    def close(self):\n        self.fd.close()\n        self.gpg.wait()\n\n\ndef dict_merge(*dicts):\n    \"\"\"\n    Merge one or more dicts into one.\n\n    >>> d = dict_merge({'a': 'A'}, {'b': 'B'}, {'c': 'C'})\n    >>> sorted(d.keys()), sorted(d.values())\n    (['a', 'b', 'c'], ['A', 'B', 'C'])\n    \"\"\"\n    final = {}\n    for d in dicts:\n        final.update(d)\n    return final\n\n\ndef user_probably_asleep():\n    \"\"\"\n    Returns true if we think it is night time and/or the user has been\n    idle for a good amount of time.\n    \"\"\"\n    hour = datetime.datetime.now().hour\n    idle = time.time() - LAST_USER_ACTIVITY\n    return ((hour > 22) or (hour < 7) or (idle > 7200)) and (idle > 1800)\n\n\ndef play_nice(niceness):\n    if hasattr(os, 'nice'):\n        try:\n            # Note: This fails on WSL (the \"native\" Ubuntu on Windows)\n            return os.nice(niceness)\n        except OSError:\n            pass\n    # FIXME: Try alternate strategies on other platforms?\n\n\ndef play_nice_with_threads(sleep=True, weak=False, deadline=None):\n    \"\"\"\n    Long-running batch jobs should call this now and then to pause\n    their activities in case there are other threads that would like to\n    run. Recent user activity increases the delay significantly, to\n    hopefully make the app more responsive when it is in use.\n    \"\"\"\n    if weak or threading.activeCount() < 4:\n        time.sleep(0)\n        return 0\n\n    deadline = (time.time() + 5) if (deadline is None) else deadline\n    while True:\n        activity_threshold = (180 - time.time() + LAST_USER_ACTIVITY) / 120\n        delay = max(0.001, min(0.1, 0.1 * activity_threshold))\n        if not sleep:\n            break\n\n        # This isn't just about sleeping, this is also basically a hack\n        # to release the GIL and let other threads run.\n        if LIVE_USER_ACTIVITIES < 1:\n            time.sleep(delay)\n        else:\n            time.sleep(max(delay, 0.250))\n\n        if QUITTING or LIVE_USER_ACTIVITIES < 1:\n            break\n        if time.time() > deadline:\n            break\n\n    return delay\n\n\nclass PeekableStringIO(StringIO.StringIO):\n    def peek(self, n):\n        StringIO._complain_ifclosed(self.closed)\n        if self.buflist:\n            self.buf += ''.join(self.buflist)\n            self.buflist = []\n        newpos = min(self.pos+n, self.len)\n        r = self.buf[self.pos:newpos]\n        return r\n\n\nSQUISH_MIME_RULES = (\n    # IMPORTANT: Order matters a great deal here! Full mime-types should come\n    #            first, with the shortest codes preceding the longer ones.\n    ('text/plain', 'tp/'),\n    ('text/html', 'h/'),\n    ('application/zip', 'z/'),\n    ('application/json', 'j/'),\n    ('application/pdf', 'p/'),\n    ('application/rtf', 'r/'),\n    ('application/octet-stream', 'o/'),\n    ('application/msword', 'ms/d'),\n    ('application/vnd.ms-excel', 'ms/x'),\n    ('application/vnd.ms-access', 'ms/m'),\n    ('application/vnd.ms-powerpoint', 'ms/p'),\n    ('application/pgp-keys', 'pgp/k'),\n    ('application/pgp-signature', 'pgp/s'),\n    ('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'ms/xx'),\n    ('application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'ms/dx'),\n    ('application/vnd.openxmlformats-officedocument.presentationml.presentation', 'ms/px'),\n    # These are prefixes that apply to many document types\n    ('application/vnd.openxmlformats-officedocument.', 'msx/'),\n    ('application/vnd.', 'vnd/'),\n    ('application/x-', 'x/'),\n    ('application/', '/'),\n    ('video/', 'v/'),\n    ('audio/', 'a/'),\n    ('image/', 'i/'),\n    ('text/', 't/'))\n\n\ndef squish_mimetype(mimetype):\n    for prefix, rep in SQUISH_MIME_RULES:\n        if mimetype.startswith(prefix):\n            return rep + mimetype[len(prefix):]\n    return mimetype\n\n\ndef unsquish_mimetype(mimetype):\n    for prefix, rep in reversed(SQUISH_MIME_RULES):\n        if mimetype.startswith(rep):\n            return prefix + mimetype[len(rep):]\n    return mimetype\n\n\ndef image_size(img_data, pure_python=False):\n    try:\n        if imgsize is not None:\n            return imgsize.get_size(PeekableStringIO(img_data))\n        if Image is not None and not pure_python:\n            return Image.open(cStringIO.StringIO(img_data)).size\n    except (ValueError, imgsize.UnknownSize):\n        pass\n    return None\n\n\ndef thumbnail(fileobj, output_fd, height=None, width=None):\n    \"\"\"\n    Generates a thumbnail image , which should be a file,\n    StringIO, or string, containing a PIL-supported image.\n    FIXME: Failure modes unmanaged.\n\n    Keyword arguments:\n    fileobj -- Either a StringIO instance, a file object or\n                         a string (containing the image) to\n                         read the source image from\n    output_fd -- A file object or filename, or StringIO to\n    \"\"\"\n    if not Image:\n        # If we don't have PIL, we just return the supplied filename in\n        # the hopes that somebody had the good sense to extract the\n        # right attachment to that filename...\n        return None\n\n    # Ensure the source image is either a file-like object or a StringIO\n    if (not isinstance(fileobj, (file, StringIO.StringIO))):\n        fileobj = cStringIO.StringIO(fileobj)\n\n    image = Image.open(fileobj)\n    fmt = image.format\n\n    # If we have Exif rotation data, make use of it\n    try:\n        for orientation in ExifTags.TAGS.keys():\n            if ExifTags.TAGS[orientation]=='Orientation':\n                break\n        exif=dict(image._getexif().items())\n        if exif[orientation] == 3:\n            image = image.rotate(180, expand=True)\n        elif exif[orientation] == 6:\n            image = image.rotate(270, expand=True)\n        elif exif[orientation] == 8:\n            image = image.rotate(90, expand=True)\n    except (AttributeError, KeyError, IndexError):\n        pass\n\n    # defining the size\n    if height is None and width is None:\n        raise Exception(\"Must supply width or height!\")\n    # If only one coordinate is given, calculate the\n    # missing one in order to make the thumbnail\n    # have the same proportions as the source img\n    if height and not width:\n        x = height\n        y = int((float(height) / image.size[0]) * image.size[1])\n    elif width and not height:\n        y = width\n        x = int((float(width) / image.size[1]) * image.size[0])\n    else:  # We have both sizes\n        y = width\n        x = height\n    try:\n        image.thumbnail([x, y], Image.ANTIALIAS)\n    except IOError:\n        return None\n\n    # If saving an optimized image fails, save it unoptimized\n    # Keep the format (png, jpg) of the source image\n    try:\n        image.save(output_fd, format=fmt, quality=90, optimize=1)\n    except:\n        image.save(output_fd, format=fmt, quality=90)\n\n    return image\n\n\nclass CleanText:\n    \"\"\"\n    This is a helper class for aggressively cleaning text, dumbing it\n    down to just ASCII and optionally forbidding some characters.\n\n    >>> CleanText(u'clean up\\\\xfe', banned='up ').clean\n    'clean'\n    >>> CleanText(u'clean\\\\xfe', replace='_').clean\n    'clean_'\n    >>> CleanText(u'clean\\\\t').clean\n    'clean\\\\t'\n    >>> str(CleanText(u'c:\\\\\\\\l/e.an', banned=CleanText.FS))\n    'clean'\n    >>> CleanText(u'c_(l e$ a) n!', banned=CleanText.NONALNUM).clean\n    'clean'\n    \"\"\"\n    FS = ':/.\\'\\\"\\\\'\n    CRLF = '\\r\\n'\n    HTML = '<>&\"\\''\n    WHITESPACE = '\\r\\n\\t '\n    NONALNUM = ''.join([chr(c) for c in (set(range(32, 127)) -\n                                         set(range(ord('0'), ord('9') + 1)) -\n                                         set(range(ord('a'), ord('z') + 1)) -\n                                         set(range(ord('A'), ord('Z') + 1)))])\n    NONDNS = ''.join([chr(c) for c in (set(range(32, 127)) -\n                                       set(range(ord('0'), ord('9') + 1)) -\n                                       set(range(ord('a'), ord('z') + 1)) -\n                                       set(range(ord('A'), ord('Z') + 1)) -\n                                       set([ord('-'), ord('_'), ord('.')]))])\n    NONVARS = ''.join([chr(c) for c in (set(range(32, 127)) -\n                                        set(range(ord('0'), ord('9') + 1)) -\n                                        set(range(ord('a'), ord('z') + 1)) -\n                                        set([ord('_')]))])\n    NONPATH = ''.join([chr(c) for c in (set(range(32, 127)) -\n                                        set(range(ord('0'), ord('9') + 1)) -\n                                        set(range(ord('a'), ord('z') + 1)) -\n                                        set(range(ord('A'), ord('Z') + 1)) -\n                                        set([ord('-'), ord('_'), ord('/')]))])\n\n    def __init__(self, text, banned='', replace=''):\n        self.clean = str(\"\".join([i if (((ord(i) > 31 and ord(i) < 127) or\n                                         (i in self.WHITESPACE)) and\n                                        i not in banned) else replace\n                                  for i in (text or '')]))\n\n    def __str__(self):\n        return str(self.clean)\n\n    def __unicode__(self):\n        return unicode(self.clean)\n\n\ndef HideBinary(text):\n    try:\n        text.decode('utf-8')\n        return text\n    except UnicodeDecodeError:\n        return '[BINARY DATA, %d BYTES]' % len(text)\n\n\nclass TimedOut(IOError):\n    \"\"\"We treat timeouts as a particular type of IO error.\"\"\"\n    pass\n\n\nTIMED_THREAD_LOCK = threading.Lock()\nTIMED_THREADS = {}\n\nclass RunTimedThread(threading.Thread):\n    def __init__(self, name, func, unique=None):\n        threading.Thread.__init__(self, target=func)\n        self.daemon = True\n        self.name = name\n        self.unique = unique\n\n    def run_timed(self, timeout):\n        if self.unique:\n            with TIMED_THREAD_LOCK:\n                old_thread = TIMED_THREADS.get(self.unique)\n                if (old_thread is not None) and old_thread.isAlive():\n                    raise TimedOut('Old thread still alive: %s' % self.name)\n                TIMED_THREADS[self.unique] = self\n\n        self.start()\n        self.join(timeout=timeout)\n\n        if self.isAlive() or QUITTING:\n            raise TimedOut('Timed out: %s' % self.name)\n        else:\n            if self.unique:\n                with TIMED_THREAD_LOCK:\n                    TIMED_THREADS[self.unique] = None\n\n\ndef RunTimed(timeout, func, *args, **kwargs):\n    result, exception = [], []\n    unique = kwargs.get('unique_thread')\n    if unique:\n        kwargs = copy.copy(kwargs)\n        del kwargs['unique_thread']\n    def work():\n        try:\n            result.append(func(*args, **kwargs))\n        except:\n            et, ev, etb = sys.exc_info()\n            exception.append((et, ev, etb))\n    RunTimedThread(func.__name__, work, unique=unique).run_timed(timeout)\n    if exception:\n        t, v, tb = exception[0]\n        raise t, v, tb\n    return result[0]\n\n\nclass DebugFileWrapper(object):\n    def __init__(self, dbg, fd):\n        self.fd = fd\n        self.dbg = dbg\n\n    def __getattribute__(self, name):\n        if name in ('fd', 'dbg', 'write', 'flush', 'close'):\n            return object.__getattribute__(self, name)\n        else:\n            self.dbg.write('==(%d.%s)\\n' % (self.fd.fileno(), name))\n            return object.__getattribute__(self.fd, name)\n\n    def write(self, data, *args, **kwargs):\n        self.dbg.write('<=(%d.write)= %s\\n' % (self.fd.fileno(),\n                                               HideBinary(data).rstrip()))\n        return self.fd.write(data, *args, **kwargs)\n\n    def flush(self, *args, **kwargs):\n        self.dbg.write('==(%d.flush)\\n' % self.fd.fileno())\n        return self.fd.flush(*args, **kwargs)\n\n    def close(self, *args, **kwargs):\n        self.dbg.write('==(%d.close)\\n' % self.fd.fileno())\n        return self.fd.close(*args, **kwargs)\n\n\ndef monkey_patch(org_func, wrapper):\n    \"\"\"\n    A utility to help with monkey patching, returns a new function where\n    org_func has been wrapped by the given wrapper.\n\n    >>> foo = monkey_patch(lambda a: a + 1, lambda o, a: o(a + 100))\n    >>> foo(1)\n    102\n    \"\"\"\n    def wrap(*args, **kwargs):\n        return wrapper(org_func, *args, **kwargs)\n    return wrap\n\n\n# If 'python util.py' is executed, start the doctest unittest\nif __name__ == \"__main__\":\n    import doctest\n    import sys\n    result = doctest.testmod()\n    print('%s' % (result, ))\n    if result.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/vcard.py",
    "content": "from __future__ import print_function\nimport random\nimport threading\nimport time\n\nfrom markupsafe import escape\n\nimport mailpile.util\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.util import *\n\nGLOBAL_VCARD_LOCK = VCardRLock()\n\n\nclass VCardLine(dict):\n    \"\"\"\n    This class represents a single line in a VCard file. It knows how\n    to parse the most common \"structured\" lines into attributes and\n    values and also convert a name, attributes and value into a properly\n    encoded/escaped VCard line.\n\n    For specific values, the object can be initialized directly.\n    >>> vcl = VCardLine(name='name', value='The Dude', pref=None)\n    >>> vcl.as_vcardline()\n    'NAME;PREF:The Dude'\n\n    Alternately, the name and value attributes can be set after the fact.\n    >>> vcl.name = 'FN'\n    >>> vcl.value = 'Lebowski'\n    >>> vcl.attrs = []\n    >>> vcl.as_vcardline()\n    'FN:Lebowski'\n\n    The object mostly behaves like a read-only dict.\n    >>> print(vcl)\n    {u'fn': u'Lebowski'}\n    >>> print(vcl.value)\n    Lebowski\n\n    VCardLine objects can also be initialized by passing in a line of VCard\n    data, which will then be parsed:\n    >>> vcl = VCardLine('FN;TYPE=Nickname:Bjarni')\n    >>> vcl.name\n    u'fn'\n    >>> vcl.value\n    u'Bjarni'\n    >>> vcl.get('type')\n    u'Nickname'\n\n    Note that the as_vcardline() method may return more than one actual line\n    of text, as RFC6350 mandates that lines over 75 characters be wrapped:\n    >>> print(VCardLine(name='bogus', value=('B' * 100)+'C').as_vcardline())\n    BOGUS:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\n     BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBC\n    \"\"\"\n    QUOTE_MAP = {\n        \"\\\\\": \"\\\\\\\\\",\n        \",\": \"\\\\,\",\n        \";\": \"\\\\;\",\n        \"\\n\": \"\\\\n\",\n        \"\\r\": \"\\\\r\",\n    }\n    QUOTE_RMAP = dict([(v, k) for k, v in QUOTE_MAP.iteritems()])\n\n    def __init__(self, line=None, name=None, value=None, **attrs):\n        self._name = name and unicode(name).lower() or None\n        self._value = unicode(value)\n        self._attrs = []\n        self._line_id = 0\n        for k in attrs:\n            self._attrs.append((k.lower(), attrs[k]))\n        if line is not None:\n            self.parse(line)\n        else:\n            self._update_dict()\n\n    def set_line_id(self, value):\n        self._line_id = value\n        self._update_dict()\n\n    line_id = property(\n        lambda self: self._line_id,\n        set_line_id)\n\n    def set_name(self, value):\n        self._name = unicode(value).lower()\n        self._update_dict()\n\n    def set_value(self, value):\n        self._value = unicode(value)\n        self._update_dict()\n\n    def set_attrs(self, value):\n        self._attrs = value\n        self._update_dict()\n\n    def set_attr(self, attr, value):\n        try:\n            for i, av in enumerate(self._attrs):\n                if av[0] == attr:\n                    nav = (av[0], value)\n                    self.attrs[i] = nav\n                    return\n            self._attrs.append((attr, value))\n        finally:\n            self._update_dict()\n\n    name = property(\n        lambda self: self._name,\n        lambda self, v: self.set_name(v))\n\n    value = property(\n        lambda self: self._value,\n        lambda self, v: self.set_value(v))\n\n    attrs = property(\n        lambda self: self._attrs,\n        lambda self, v: self.set_attrs(v))\n\n    def parse(self, line):\n        self._name, self._attrs, self._value = self.ParseLine(line)\n        self._update_dict()\n\n    def _update_dict(self):\n        for key in self.keys():\n            dict.__delitem__(self, key)\n        dict.update(self, dict(reversed(self._attrs)))\n        if self.name:\n            dict.__setitem__(self, self._name, self._value)\n        if self._line_id:\n            dict.__setitem__(self, 'line_id', self._line_id)\n\n    def __delitem__(self, *args, **kwargs):\n        raise ValueError('This dict is read-only')\n\n    def __setitem__(self, *args, **kwargs):\n        raise ValueError('This dict is read-only')\n\n    def update(self, *args, **kwargs):\n        raise ValueError('This dict is read-only')\n\n    def as_vcardline(self):\n        key = self.Quote(self._name.upper())\n        for k, v in self._attrs:\n            k = k.upper()\n            if v is None:\n                key += ';%s' % (self.Quote(k))\n            else:\n                key += ';%s=%s' % (self.Quote(k), self.Quote(unicode(v)))\n\n        wrapped, line = '', '%s:%s' % (key, self.Quote(self._value))\n        llen = 0\n        for char in line:\n            char = char.encode('utf-8')\n            clen = len(char)\n            if llen + clen >= 75:\n                wrapped += '\\n '\n                llen = 0\n            wrapped += char\n            llen += clen\n\n        return wrapped\n\n    @classmethod\n    def Quote(self, text):\n        \"\"\"\n        Quote values so they can be safely represented in a VCard.\n\n        >>> print(VCardLine.Quote('Comma, semicolon; backslash\\\\ newline\\\\n'))\n        Comma\\\\, semicolon\\\\; backslash\\\\\\\\ newline\\\\n\n        \"\"\"\n        return unicode(''.join([self.QUOTE_MAP.get(c, c) for c in text]))\n\n    @classmethod\n    def ParseLine(self, text):\n        \"\"\"\n        Parse a single line, respecting to the VCard (RFC6350) quoting.\n\n        >>> VCardLine.ParseLine('foo:val;ue')\n        (u'foo', [], u'val;ue')\n        >>> VCardLine.ParseLine('foo;BAR;\\\\\\\\baz:value')\n        (u'foo', [(u'bar', None), (u'\\\\\\\\baz', None)], u'value')\n        >>> VCardLine.ParseLine('FOO;bar=comma\\\\,semicolon\\\\;'\n        ...                     'backslash\\\\\\\\\\\\\\\\:value')\n        (u'foo', [(u'bar', u'comma,semicolon;backslash\\\\\\\\')], u'value')\n        \"\"\"\n        # The parser is a state machine with two main states: quoted or\n        # unquoted data. The unquoted data has three sub-states, to track\n        # which part of the line is being parsed.\n\n        def parse_quoted(char, state, parsed, name, attrs):\n            pair = \"\\\\\" + char\n            parsed = parsed[:-1] + self.QUOTE_RMAP.get(pair, pair)\n            return parse_char, state, parsed, name, attrs\n\n        def parse_char(char, state, parsed, name, attrs):\n            if char == \"\\\\\":\n                parsed += char\n                return parse_quoted, state, parsed, name, attrs\n            else:\n                if state == 0 and char in (';', ':'):\n                    name = parsed.lower()\n                    parsed = ''\n                    state += (char == ';') and 1 or 2\n                elif state == 1 and char in (';', ':'):\n                    if '=' in parsed:\n                        k, v = parsed.split('=', 1)\n                    else:\n                        k = parsed\n                        v = None\n                    attrs.append((k.lower(), v))\n                    parsed = ''\n                    if char == ':':\n                        state += 1\n                else:\n                    parsed += char\n                return parse_char, state, parsed, name, attrs\n\n        parser, state, parsed, name, attrs = parse_char, 0, '', None, []\n        for char in unicode(text):\n            parser, state, parsed, name, attrs = parser(\n                char, state, parsed, name, attrs)\n\n        return name, attrs, parsed\n\n\nclass SimpleVCard(object):\n    \"\"\"\n    This is a very simplistic implementation of VCard 4.0.\n\n    The card can be initialized with a series of VCardLine objects.\n    >>> vcard = SimpleVCard(VCardLine(name='fn', value='Bjarni'),\n    ...                     VCardLine(name='email', value='bre@example.com'),\n    ...                     VCardLine(name='email', value='bre2@example.com'),\n    ...                     VCardLine('EMAIL;TYPE=PREF:bre@evil.com'),\n    ...                     client='default')\n\n    The preferred (or Nth) line of any type can be retrieved using\n    the get method. Lines are sorted by (preference, card order).\n    >>> vcard.get('email').value\n    u'bre@evil.com'\n    >>> vcard.get('email', n=2).value\n    u'bre@example.com'\n    >>> vcard.get('email', n=4).value\n    Traceback (most recent call last):\n        ...\n    IndexError: ...\n\n    If the client attribute is specified on creation, all lines will\n    have a PID attribute.\n    >>> vcard.get('clientpidmap').value\n    u'990;default'\n    >>> vcard.get('email')['pid']\n    '990.1'\n\n    There are shorthand methods for accessing or setting the values of\n    the full name and e-mail lines:\n    >>> vcard.email\n    u'bre@evil.com'\n    >>> vcard.fn = 'Bjarni R. E.'\n    >>> vcard.get('fn').value\n    u'Bjarni R. E.'\n\n    To fetch all lines, use the get_all method. In this case no\n    sorting is performed and lines are simply returned in card order.\n    >>> [vcl.value for vcl in vcard.get_all('email')]\n    [u'bre@example.com', u'bre2@example.com', u'bre@evil.com']\n\n    \"\"\"\n    VCARD_OTHER_KEYS = {\n        'AGENT': '',\n        'CLASS': '',\n        'EXPERTISE': '',\n        'HOBBY': '',\n        'INTEREST': '',\n        'LABEL': '',\n        'MAILER': '',\n        'NAME': '',\n        'ORG-DIRECTORY': '',\n        'PROFILE': '',\n        'SORT-STRING': '',\n    }\n    VCARD4_KEYS = {\n        # General properties\n        # .. BEGIN\n        # .. END\n        'SOURCE': ['*', None, None],\n        'KIND': ['*1', None, 'individual'],\n        'XML': ['*', None, None],\n        # Identification properties\n        'FN': ['1*', None, 'Anonymous'],\n        'N': ['*1', None, None],\n        'NICKNAME': ['*', None, None],\n        'PHOTO': ['*', None, None],\n        'BDAY': ['*1', None, None],\n        'ANNIVERSARY': ['*1', None, None],\n        'GENDER': ['*1', None, None],\n        # Delivery Addressing Properties\n        'ADR': ['*', None, None],\n        # Communications Properties\n        'TEL': ['*', None, None],\n        'EMAIL': ['*', None, None],\n        'IMPP': ['*', None, None],\n        'LANG': ['*', None, None],\n        # Geographical Properties\n        'TZ': ['*', None, None],\n        'GEO': ['*', None, None],\n        # Organizational Properties\n        'TITLE': ['*', None, None],\n        'ROLE': ['*', None, None],\n        'LOGO': ['*', None, None],\n        'ORG': ['*', None, None],\n        'MEMBER': ['*', None, None],\n        'RELATED': ['*', None, None],\n        # Explanitory Properties\n        'VERSION': ['1', None, '4.0'],\n        'CATEGORIES': ['*', None, None],\n        'NOTE': ['*', None, None],\n        'PRODID': ['*', None, None],\n        'REV': ['*1', None, None],\n        'SOUND': ['*', None, None],\n        'UID': ['*1', None, None],\n        'CLIENTPIDMAP': ['*', None, None],\n        'URL': ['*', None, None],\n        # Security Properties\n        'KEY': ['*', None, None],\n        # Calendar Properties\n        'FBURL': ['*', None, None],\n        'CALADRURI': ['*', None, None],\n        'CALURI': ['*', None, None],\n    }\n    VCARD4_REQUIRED = ('VERSION', 'FN')\n    MAX_SRC_PID = 990\n\n    def __init__(self, *lines, **kwargs):\n        self.filename = None\n        self._lines = []\n        self._lock = VCardRLock()\n        self._default_src_pid = None\n        if 'data' in kwargs and kwargs['data'] is not None:\n            self.load(data=kwargs['data'])\n        if 'client' in kwargs and kwargs['client']:\n            self.add(VCardLine(name='CLIENTPIDMAP',\n                               value='%s;%s' % (self.MAX_SRC_PID,\n                                                kwargs['client'])))\n            self._default_src_pid = self.MAX_SRC_PID\n        self.add(*lines)\n\n    def _cardinality(self, vcl):\n        if vcl.name.startswith('x-'):\n            return '*'\n        else:\n            return self.VCARD4_KEYS.get(vcl.name.upper(), [''])[0]\n\n    UNREMOVABLE = ('x-mailpile-rid', 'x-mailpile-kind-hint',\n                   'clientpidmap', 'version')\n\n    def __enter__(self, *args, **kwargs):\n        return GLOBAL_VCARD_LOCK.__enter__(*args, **kwargs)\n\n    def __exit__(self, *args, **kwargs):\n        return GLOBAL_VCARD_LOCK.__exit__(*args, **kwargs)\n\n    def remove(self, *line_ids):\n        \"\"\"\n        Remove one or more lines from the VCard.\n\n        >>> vc = SimpleVCard(VCardLine(name='fn', value='Houdini'))\n        >>> vc.remove(vc.get('fn').line_id)\n        1\n        >>> vc.get('fn')\n        Traceback (most recent call last):\n            ...\n        IndexError: ...\n        \"\"\"\n        removed = 0\n        with self._lock:\n            for index in range(0, len(self._lines)):\n                vcl = self._lines[index]\n                if vcl and vcl.line_id in line_ids:\n                    if self._lines[index].name in self.UNREMOVABLE:\n                        raise ValueError('Cannot remove %s from VCard'\n                                         % self._lines[index].name)\n                    self._lines[index] = None\n                    removed += 1\n        return removed\n\n    def remove_all(self, name):\n        \"\"\"\n        Remove one or more lines from the VCard.\n\n        >>> vc = SimpleVCard(VCardLine(name='fn', value='Houdini'))\n        >>> vc.remove_all('fn')\n        >>> vc.get('fn')\n        Traceback (most recent call last):\n            ...\n        IndexError: ...\n        \"\"\"\n        self.remove(*[line.line_id for line in self.get_all(name)])\n\n    def _handle_pidmap_args(self, **kwargs):\n        src_id = kwargs.get('client', self._default_src_pid)\n        if src_id:\n            create = kwargs.get('client_create', False)\n            pid, pidmap, ver, is_new = self.get_pidmap(src_id, create=create)\n            if kwargs.get('client_increment_version', True):\n                ver += 1\n            return pid, pidmap, ver, is_new\n        else:\n            return None, None, None, None\n\n    def add(self, *vcls, **kwargs):\n        \"\"\"\n        Add one or more lines to a VCard.\n\n        >>> vc = SimpleVCard()\n        >>> vc.add(VCardLine(name='fn', value='Bjarni'))\n        >>> vc.get('fn').value\n        u'Bjarni'\n\n        Line types are checked against VCard 4.0 for validity.\n        >>> vc.add(VCardLine(name='evil', value='Bjarni'))\n        Traceback (most recent call last):\n            ...\n        ValueError: Not allowed on card: evil\n        \"\"\"\n        src_pid, pidmap, version, is_new = self._handle_pidmap_args(**kwargs)\n        for vcl in vcls:\n            with self._lock:\n                if not vcl.name:\n                    continue\n                cardinality = self._cardinality(vcl)\n                count = len([l for l in self._lines\n                             if l and l.name == vcl.name])\n                if not cardinality:\n                    raise ValueError('Not allowed on card: %s' % vcl.name)\n                if cardinality in ('1', '*1'):\n                    if count:\n                        raise ValueError('Already on card: %s' % vcl.name)\n\n                # Special case to avoid duplicate CLIENTPIDMAP lines\n                if vcl.name == 'clientpidmap':\n                    cpm = self.get_clientpidmap()\n                    pid, src_id = vcl.value.split(';', 1)\n                    if src_id in cpm:\n                        if int(pid) != cpm[src_id]['pid']:\n                            raise ValueError('CLIENTPIDMAP pid mismatch!')\n                        continue\n\n                if (src_pid is not None and\n                        vcl.name not in self.UNREMOVABLE and\n                        'pid' not in vcl):\n                    vcl.set_attr('pid', '%s.%s' % (src_pid, version))\n                self._lines.append(vcl)\n                vcl.line_id = len(self._lines)\n\n    def set_line(self, ln, vcl, **kwargs):\n        \"\"\"\n        Modify one line of a VCard.\n\n        >>> vc = SimpleVCard(VCardLine(name='fn', value='Bjarni'))\n        >>> vc.get('fn').value\n        u'Bjarni'\n\n        >>> vc.set_line(vc.get('fn').line_id,\n        ...             VCardLine(name='fn', value='Dude'))\n        >>> vc.get('fn').value\n        u'Dude'\n        \"\"\"\n        if not (ln > 0 and ln <= len(self._lines)):\n            raise ValueError(_('Line number %s is out of range') % ln)\n\n        src_pid, pidmap, version, is_new = self._handle_pidmap_args(**kwargs)\n        if (src_pid is not None and\n                vcl.name not in self.UNREMOVABLE and\n                'pid' not in vcl):\n            vcl.set_attr('pid', '%s.%s' % (src_pid, version))\n\n        with self._lock:\n            vcl.line_id = ln\n            self._lines[ln-1] = vcl\n\n    def get_clientpidmap(self):\n        \"\"\"\n        Return a dictionary representing the CLIENTPIDMAP, grouping VCard\n        lines by data sources.\n\n        >>> vc = SimpleVCard(VCardLine(name='fn', value='Bjarni', pid='1.2'),\n        ...                  VCardLine(name='clientpidmap',\n        ...                            value='1;thisisauid'))\n        >>> vc.get_clientpidmap()['thisisauid']['pid']\n        1\n        >>> vc.get_clientpidmap()[1]['lines'][0][0]\n        2\n        >>> vc.get_clientpidmap()[1]['lines'][0][1].value\n        u'Bjarni'\n        \"\"\"\n        cpm = {}\n        for pm in self.get_all('clientpidmap'):\n            pid, guid = pm.value.split(';')\n            pid = int(pid)\n            cpm[guid] = cpm[pid] = {\n                'pid': pid,\n                'lines': []\n            }\n        for vcl in self.as_lines():\n            if 'pid' in vcl:\n                pv = [v.strip().split('.', 1) for v in vcl['pid'].split(',')]\n                for pid, version in pv:\n                    pid = int(pid)\n                    if pid in cpm:\n                        cpm[pid]['lines'].append((int(version), vcl))\n                    else:\n                        pass  # FIXME: Something is weird, but we have no\n                              #        repair strategy, so nothing to say.\n        return cpm\n\n    def get_pidmap(self, src_id, create=False):\n        \"\"\"\n        Fetch the pidmap, src_pid and version for a given src_id, optionally\n        creating new CLIENTPIDMAP entries on demand.\n        \"\"\"\n        with self._lock:\n            cpm = self.get_clientpidmap()\n            pidmap = cpm.get(src_id, False)\n            if pidmap:\n                src_pid = pidmap['pid']\n                is_new = False\n            elif create:\n                # A tad inefficient, but avoids artificial ID inflation\n                # and puts a bound on how insanely huge a VCard can get.\n                pids = [p['pid'] for p in cpm.values()]\n                src_pid = None\n                for pid in range(1, self.MAX_SRC_PID):  # Skip MAX, is default\n                    if pid not in pids:\n                        src_pid = pid\n                        break\n                if src_pid is None:\n                    raise ValueError(\"Client PID map is too big\")\n\n                self.add(VCardLine(name='clientpidmap',\n                                   value='%s;%s' % (src_pid, src_id)))\n                pidmap = cpm[src_pid] = cpm[src_id] = {'lines': []}\n                is_new = True\n            else:\n                raise KeyError('No such CLIENTPIDMAP: %s' % src_id)\n\n            version = 0\n            for ver, ol in pidmap['lines'][:]:\n                version = max(ver, version)\n\n            return src_pid, pidmap, version, is_new\n\n    def merge(self, src_id, lines):\n        \"\"\"\n        Merge a set of VCard lines from a given source into this card.\n\n        >>> vc = SimpleVCard(VCardLine(name='fn', value='Bjarni', pid='1.2'),\n        ...                  VCardLine(name='email', value='bre@foo', t='1'),\n        ...                  VCardLine(name='email', value='bre@bar', t='2'),\n        ...                  VCardLine(name='clientpidmap',\n        ...                            value='1;thisisauid'))\n        >>> vc.merge('thisisauid', [VCardLine(name='fn', value='Bjarni'),\n        ...                         VCardLine(name='x-a', value='b')])\n        1\n        >>> vc.get('x-a')['pid']\n        '1.3'\n        >>> vc.get('fn')['pid']\n        '1.2'\n        >>> vc.get('email', prefer={'t': '1'}).value\n        u'bre@foo'\n        >>> vc.get('email', prefer={'t': '2'}).value\n        u'bre@bar'\n        >>> vc.get('email', prefer={'t': 'unfindable'}).value\n        u'bre@foo'\n\n        >>> vc.merge('otheruid', [VCardLine(name='x-b', value='c')])\n        2\n        >>> vc.get('x-b')['pid']\n        '2.1'\n\n        >>> vc.merge('thisisauid', [VCardLine(name='fn', value='Inrajb')])\n        3\n        >>> vc.get('fn')['pid']\n        '1.4'\n        >>> vc.fn\n        u'Inrajb'\n        >>> vc.get('x-a')\n        Traceback (most recent call last):\n           ...\n        IndexError: ...\n\n        >>> print(vc.as_vCard())\n        BEGIN:VCARD\n        VERSION:4.0\n        CLIENTPIDMAP:1\\\\;thisisauid\n        CLIENTPIDMAP:2\\\\;otheruid\n        EMAIL;T=1:bre@foo\n        EMAIL;T=2:bre@bar\n        FN;PID=1.4:Inrajb\n        X-B;PID=2.1:c\n        END:VCARD\n        \"\"\"\n        if not lines:\n            return\n\n        lines = [l for l in lines if not l.name in self.UNREMOVABLE]\n        changes = 0\n        with self._lock:\n            # First, we figure out which CLIENTPIDMAP applies, if any\n            src_pid, pidmap, version, is_new = self.get_pidmap(src_id,\n                                                               create=True)\n            if is_new:\n                changes += 1\n\n            # Deduplicate the lines, but give them a rank if they are repeated\n            lines.sort(key=lambda k: (k.name, k.value))\n            dedup = [lines[0]]\n            rank = 0\n\n            def rankit(rank):\n                if rank:\n                    rank += int(dedup[-1].get('x-rank', 0))\n                    dedup[-1].set_attr('x-rank', rank)\n\n            for line in lines[1:]:\n                if (dedup[-1].name == line.name and\n                        dedup[-1].value == line.value):\n                    rank += 1\n                else:\n                    rankit(rank)\n                    rank = 0\n                    dedup.append(line)\n            rankit(rank)\n            lines = dedup\n\n            # 1st, iterate through existing lines for this source, removing\n            # all that differ from our input. Remove any input lines which\n            # are identical to those already on this card.\n            to_remove = []\n            for ver, ol in pidmap['lines'][:]:\n                match = [l for l in lines if (l\n                                              and l.name == ol.name\n                                              and l.value == ol.value)]\n                for l in match:\n                    lines.remove(l)\n                if not match:\n                    pids = [pid for pid in ol.get('pid', '').split(',')\n                            if pid and int(pid.split('.')[0]) != src_pid]\n                    if pids:\n                        ol.set_attr('pid', ','.join(pids))\n                        changes += 1\n                    elif not ol.get('pref') and ol.name not in ('email', ):\n                        # Note: We never remove e-mail addresses or a user's\n                        # preferred settings. That just causes trouble.\n                        self.remove(ol.line_id)\n                        changes += 1\n\n            # 2nd, iterate through any lines that are left and copy them.\n            version += 1\n            for vcl in lines:\n                old = [ol for ol in self.get_all(vcl.name)\n                       if ol.value == vcl.value]\n                for ol in old:\n                    pids = [pid for pid in ol.get('pid', '').split(',')\n                            if pid and int(pid.split('.')[0]) != src_pid]\n                    pids.append('%s.%s' % (src_pid, version))\n                    ol.set_attr('pid', ','.join(sorted(pids)))\n                if not old:\n                    vcl.set_attr('pid', '%s.%s' % (src_pid, version))\n                    self.add(vcl)\n                    changes += 1\n\n        return changes\n\n    def get_all(self, key, sort=False):\n        with self._lock:\n            lines = [l for l in self._lines if l and l.name == key.lower()]\n        if sort:\n            self._sort_lines(lines)\n        return lines\n\n    def get(self, key, default=None, n=0, prefer=None):\n        lines = self.get_all(key)\n        if prefer:\n            for k, v in prefer.iteritems():\n                llines = [l for l in lines if l.get(k) == v]\n                if llines:\n                    lines = llines\n        if lines:\n            return self._sort_lines(lines)[n]\n        elif default is not None:\n            return default\n        else:\n            raise IndexError(n)\n\n    def _sort_lines(self, lines=None):\n        lines = self._lines if (lines is None) else lines\n\n        def sortkey(l):\n            if not l:\n                return (3, 0, 0, 0, 0)\n\n            preferred = ('pref' in l or 'pref' in l.get('type', '').lower())\n            versions = [[int(v) for v in pid.split('.')]\n                        for pid in l.get('pid', '').split(',') if pid]\n\n            return ((l.name == 'version') and 1 or 2,\n                    (l.name),\n                    # This is the important line - here we give elements a\n                    # boost depending on various factors. The pid factor is\n                    # what boosts users preferences above others, as the\n                    # MailpileVCard assigns a very high PID to that source.\n                    (1 - ((10000 if preferred else 0) +\n                          (int(l.get('x-rank', 0)) * 10) +\n                          sum((pid if (pid != self._default_src_pid) else 1)\n                              for pid, version in versions))),\n                    (-len(l.value)),\n                    (l.line_id))\n\n        with self._lock:\n            lines.sort(key=sortkey)\n        return lines\n\n    def as_jCard(self):\n        with self._lock:\n            card = [[key.lower(), {}, \"text\", self[key][0][0]]\n                    for key in self.order]\n        stream = [\"vcardstream\", [\"vcard\", card]]\n        return stream\n\n    def as_vCard(self):\n        \"\"\"\n        This method returns the VCard data in its native format.\n        Note: the output is a string of bytes, not unicode characters.\n\n        >>> print(SimpleVCard().as_vCard())\n        BEGIN:VCARD\n        VERSION:4.0\n        FN:Anonymous\n        END:VCARD\n        \"\"\"\n        # Add any missing required keys...\n        for key in self.VCARD4_REQUIRED:\n            with self._lock:\n                if not self.get_all(key):\n                    default = self.VCARD4_KEYS[key][2]\n                    self._lines[:0] = [VCardLine(name=key, value=default)]\n\n        # Make sure VERSION is first, order is stable.\n        with self._lock:\n            self._sort_lines()\n            return '\\n'.join(['BEGIN:VCARD'] +\n                             [l.as_vcardline() for l in self._lines if l] +\n                             ['END:VCARD'])\n\n    def as_lines(self):\n        with self._lock:\n            self._sort_lines()\n            return [vcl for vcl in self._lines if vcl]\n\n    def _vcard_get(self, key, default=None):\n        try:\n            return self.get(key).value\n        except IndexError:\n            if default is None:\n                default = self.VCARD4_KEYS.get(key.upper(), ['', '', None])[2]\n            return default\n\n    def _vcard_set(self, key, value):\n        try:\n            self.get(key).value = value\n        except IndexError:\n            self.add(VCardLine(name=key, value=value, pref=None))\n\n    nickname = property(\n        lambda self: unicode(self._vcard_get('nickname')),\n        lambda self, e: self._vcard_set('nickname', e))\n\n    email = property(\n        lambda self: unicode(self._vcard_get('email')),\n        lambda self, e: self._vcard_set('email', e))\n\n    kind = property(\n        lambda self: unicode(self._vcard_get('kind')),\n        lambda self, e: self._vcard_set('kind', e))\n\n    fn = property(\n        lambda self: unicode(self._vcard_get('fn')),\n        lambda self, e: self._vcard_set('fn', e))\n\n    note = property(\n        lambda self: unicode(self._vcard_get('note')),\n        lambda self, e: self._vcard_set('note', e.replace('\\n', ' ')))\n\n\nclass MailpileVCard(SimpleVCard):\n    \"\"\"\n    This is adds some mailpile-specific extensions to the SimpleVCard.\n    \"\"\"\n    HISTORY_MAX_AGE = 31 * 24 * 3600\n\n    DEFAULT_CLIENT  = 'default'\n    PRIORITY_CLIENT = 'priority'\n    USER_CLIENT     = 'priority'  # An alias to make code more readable\n\n    def __init__(self, *lines, **kwargs):\n        if 'client' not in kwargs:\n            kwargs['client'] = self.DEFAULT_CLIENT\n        SimpleVCard.__init__(self, *lines, **kwargs)\n\n        # Add the priority CLIENTPIDMAP line, for user settings\n        self.add(VCardLine(name='CLIENTPIDMAP',\n                           value='%s;%s' % (self.MAX_SRC_PID + 1,\n                                            self.PRIORITY_CLIENT)))\n        self._priority_client = self.MAX_SRC_PID + 1\n\n        self.configure_encryption(kwargs.get('config'))\n\n    def configure_encryption(self, config):\n        if config:\n            dec = lambda: config.get_master_key()\n            enc = lambda: (config.prefs.encrypt_vcards and\n                           config.get_master_key())\n            self.config = config\n        else:\n            enc = dec = lambda: None\n            self.config = None\n        self.encryption_key_func = enc\n        self.decryption_key_func = dec\n\n    def _mpcdict(self, vcl):\n        d = {}\n        for k in vcl.keys():\n            if k not in ('line_id', ):\n                if k.startswith('x-mailpile-'):\n                    d[k[len('x-mailpile-'):]] = vcl[k]\n                else:\n                    d[k] = vcl[k]\n        return d\n\n    MPCARD_SINGLETONS = ('fn', 'kind', 'note',\n                         'x-mailpile-html-policy',\n                         'x-mailpile-crypto-policy',\n                         'x-mailpile-crypto-format',\n                         'x-mailpile-profile-tag',\n                         'x-mailpile-profile-signature',\n                         'x-mailpile-profile-route',\n                         'x-mailpile-last-pgp-key-share')\n    MPCARD_SUPPRESSED = ('version', 'x-mailpile-rid')\n\n    def as_mpCard(self):\n        mpCard, added = {}, set()\n        with self._lock:\n            self._sort_lines()\n            for vcl in self._lines:\n                if not vcl or vcl.name in self.MPCARD_SUPPRESSED:\n                    continue\n                name = vcl.name\n                if (name, vcl.value) in added:\n                    continue\n                if name not in mpCard:\n                    if name in self.MPCARD_SINGLETONS:\n                        mpCard[name] = vcl.value\n                    else:\n                        mpCard[name] = [self._mpcdict(vcl)]\n                elif name not in self.MPCARD_SINGLETONS:\n                    mpCard[name].append(self._mpcdict(vcl))\n                added.add((name, vcl.value))\n            return mpCard\n\n    def load(self, filename=None, data=None, config=None):\n        \"\"\"\n        Load VCard lines from a file on disk or data in memory.\n        \"\"\"\n        if data:\n            pass\n        elif filename:\n            from mailpile.crypto.streamer import DecryptingStreamer\n            self.filename = filename or self.filename\n            with open(self.filename, 'rb') as fd:\n                with DecryptingStreamer(fd,\n                                        mep_key=self.decryption_key_func(),\n                                        name='VCard/load(%s)' % self.filename\n                                        ) as streamer:\n                    data = streamer.read().decode('utf-8')\n                    streamer.verify(_raise=IOError)\n        else:\n            raise ValueError('Need data or a filename!')\n\n        def unwrap(text):\n            # This undoes the VCard standard line wrapping\n            return text.replace('\\n ', '').replace('\\n\\t', '')\n\n        lines = [l.strip() for l in unwrap(data.strip()).splitlines()]\n        if (not len(lines) >= 2 or\n                not lines.pop(0).upper() == 'BEGIN:VCARD' or\n                not lines.pop(-1).upper() == 'END:VCARD'):\n            raise ValueError('Not a valid VCard: %s' % '\\n'.join(lines))\n\n        with self._lock:\n            for line in lines:\n                self.add(VCardLine(line))\n\n        return self\n\n    def save(self, filename=None):\n        filename = filename or self.filename\n        if filename:\n            encryption_key = self.encryption_key_func()\n            if encryption_key:\n                from mailpile.crypto.streamer import EncryptingStreamer\n                subj = self.config.mailpile_path(filename)\n                with EncryptingStreamer(encryption_key,\n                                        delimited=False,\n                                        dir=self.config.tempfile_dir(),\n                                        header_data={'subject': subj},\n                                        name='VCard/save') as es:\n                    es.write(self.as_vCard())\n                    es.save(filename)\n            else:\n                with open(filename, 'wb') as fd:\n                    fd.write(self.as_vCard())\n            return self\n        else:\n            raise ValueError('Save to what file?')\n\n    ## Attributes ##################################################\n\n    def _history_parse_expire(self, history_vcl, now):\n        history = {\n            'sent': [],\n            'received': []\n        }\n        entries = []\n        for entry in [e for e in history_vcl.value.split(',') if e]:\n            try:\n                what, when, mid = entry.split('-')\n                when = int(when, 36)\n                if when > now - self.HISTORY_MAX_AGE:\n                    history['sent' if (what == 's') else 'received'].append(\n                        (when, mid))\n                    entries.append(entry)\n            except (ValueError, IndexError, TypeError):\n                pass\n        history_vcl.value = ','.join(entries)\n        return entries, history\n\n    def recent_history(self, now=None):\n        try:\n            now = now if (now is not None) else time.time()\n            history_vcl = self.get('x-mailpile-history')\n            return self._history_parse_expire(history_vcl, now)[1]\n        except IndexError:\n            return {}\n\n    def record_history(self, what, when, mid, now=None):\n        safe_assert(what[0] in ('s', 'r'))\n        with self._lock:\n            try:\n                history_vcl = self.get('x-mailpile-history')\n            except IndexError:\n                history_vcl = VCardLine(name='x-mailpile-history', value='')\n                self.add(history_vcl)\n            now = now if (now is not None) else time.time()\n            entries, history = self._history_parse_expire(history_vcl, now)\n            entries.append('%s-%s-%s' % (what[0], b36(int(when)), mid))\n            history_vcl.value = ','.join(entries)\n\n    def same_domain(self, address):\n        domain = address.rsplit('#')[0].rsplit('@')[-1].lower()\n        for vcl in self.get_all('email'):\n            if domain == vcl.value.rsplit('@', 1)[-1].lower():\n                return vcl.value\n        return False\n\n    def _random_uid(self):\n        with self._lock:\n            try:\n                rid = self.get('x-mailpile-rid').value\n            except IndexError:\n                rid = randomish_uid()\n                self.add(VCardLine(name='x-mailpile-rid', value=rid))\n        return rid\n\n    random_uid = property(_random_uid)\n\n    ## Attributes provided by contacts ##############################\n\n    def prefer_sender(self, address, sender):\n        address = address.lower()\n        for vcl in self.get_all('x-mailpile-prefer-profile'):\n            addr = vcl.get('address')\n            if addr and addr == address:\n                vcl.value = sender.random_uid\n                return\n        self.add(VCardLine(name='x-mailpile-prefer-profile',\n                           value=sender.random_uid,\n                           address=address))\n\n    def sending_profile(self, address):\n        default = None\n        which_email = None\n        for vcl in self.get_all('x-mailpile-prefer-profile'):\n            addr = vcl.get('address')\n            value = vcl.value\n            if addr:\n                if addr == address.lower():\n                    if ',' in value:\n                        value, which_email = value.split(',')\n                    return (value, which_email)\n            else:\n                if ',' in value:\n                    default, which_email = value.split(',')\n                else:\n                    default, which_email = value, None\n        return (default, which_email)\n\n    pgp_key = property(\n        lambda self: self._vcard_get('key', '').split(',')[-1],\n        lambda self, v: self._vcard_set('key',\n            'data:application/x-pgp-fingerprint,' + v))\n\n    pgp_key_pinned = property(\n        lambda self: (\n            self._vcard_get('x-mailpile-pgpkey-pinned', '')[:1].lower() in ('t', 'y')),\n        lambda self, v: self._vcard_set('x-mailpile-pgpkey-pinned', v))\n\n    pgp_key_shared = property(\n        lambda self: self._vcard_get('x-mailpile-last-pgp-key-share'),\n        lambda self, v: self._vcard_set('x-mailpile-last-pgp-key-share', v))\n\n    html_policy = property(\n        lambda self: self._vcard_get('x-mailpile-html-policy'),\n        lambda self, v: self._vcard_set('x-mailpile-html-policy', v))\n\n    crypto_policy = property(\n        lambda self: self._vcard_get('x-mailpile-crypto-policy'),\n        lambda self, v: self._vcard_set('x-mailpile-crypto-policy', v))\n\n    crypto_format = property(\n        lambda self: self._vcard_get('x-mailpile-crypto-format'),\n        lambda self, v: self._vcard_set('x-mailpile-crypto-format', v))\n\n    ## Attributes provided by profiles ##############################\n\n    def add_scope(self, scope):\n        scope = scope.split('#')[0].lower()\n        for vcl in self.get_all('x-mailpile-profile-scope'):\n            if vcl.value == scope:\n                return\n        self.add(VCardLine(name='x-mailpile-profile-scope', value=scope))\n\n    def sends_to(self, address):\n        domain = address.rsplit('#')[0].rsplit('@', 1)[-1].lower()\n        address = address.lower()\n        my_email = self.email\n        for vcl in self.get_all('x-mailpile-profile-scope'):\n            if vcl.value in (domain, address):\n                return vcl.get('address') or my_email\n        return False\n\n    def add_source(self, source_id):\n        for vcl in self.get_all('x-mailpile-profile-source'):\n            if vcl.value == source_id:\n                return\n        self.add(VCardLine(name='x-mailpile-profile-source', value=source_id))\n\n    def get_source_by_proto(self, protocol, create=False, name=None):\n        my_rid = self.random_uid\n        source = None\n        for src_id, src in self.config.sources.iteritems():\n            if src.profile == my_rid and src.protocol == protocol:\n                if not name or src.name == name:\n                    source = src\n                    break\n\n        if source is None:\n            if not create:\n                return None\n            new_src_id = create if (create is not True) else randomish_uid()\n            if new_src_id not in self.config.sources:\n                self.config.sources[new_src_id] = {}\n            source = self.config.sources[new_src_id]\n            source.name = name or ''\n            source.protocol = protocol\n            source.profile = my_rid\n            if self.tag:\n                source.discovery.apply_tags = [self.tag]\n\n            # This starts the source thread as as side-effect\n            self.config.save()\n\n        return source\n\n    def sources(self):\n        sources = []\n        for vcl in self.get_all('x-mailpile-profile-source'):\n            sources.append(vcl.value)\n        return sources\n\n    signature = property(\n        lambda self: self._vcard_get('x-mailpile-profile-signature'),\n        lambda self, v: self._vcard_set('x-mailpile-profile-signature', v))\n\n    route = property(\n        lambda self: self._vcard_get('x-mailpile-profile-route'),\n        lambda self, v: self._vcard_set('x-mailpile-profile-route', v))\n\n    tag = property(\n        lambda self: self._vcard_get('x-mailpile-profile-tag'),\n        lambda self, v: self._vcard_set('x-mailpile-profile-tag', v))\n\n\nclass AddressInfo(dict):\n\n    fn = property(\n        lambda self: unicode(self['fn']),\n        lambda self, v: self.__setitem__('fn', v))\n\n    address = property(\n        lambda self: unicode(self['address']),\n        lambda self, v: self.__setitem__('address', v))\n\n    rank = property(\n        lambda self: self['rank'],\n        lambda self, v: self.__setitem__('rank', v))\n\n    protocol = property(\n        lambda self: self['protocol'],\n        lambda self, v: self.__setitem__('protocol', v))\n\n    flags = property(\n        lambda self: self['flags'],\n        lambda self, v: self.__setitem__('flags', v))\n\n    keys = property(\n        lambda self: self.get('keys'),\n        lambda self, v: self.__setitem__('keys', v))\n\n    html_policy = property(\n        lambda self: self.get('html-policy'),\n        lambda self, v: self.__setitem__('html-policy', v))\n\n    crypto_policy = property(\n        lambda self: self.get('crypto-policy'),\n        lambda self, v: self.__setitem__('crypto-policy', v))\n\n    def __init__(self, addr, fn, vcard=None, rank=0, proto='smtp', keys=None):\n        info = {\n            'fn': fn,\n            'address': addr,\n            'rank': rank,\n            'protocol': proto,\n            'flags': {}\n        }\n        if keys:\n            info['keys'] = keys\n            info['flags']['secure'] = True\n        self.update(info)\n        if vcard:\n            self.merge_vcard(vcard)\n\n    def merge_vcard(self, vcard):\n        if vcard.kind == 'profile':\n            base_rank = 5.0\n            self['flags']['profile'] = True\n        else:\n            base_rank = 10.0\n            self['flags']['contact'] = True\n\n        keys = []\n        for k in vcard.get_all('KEY'):\n            val = k.value.split(\"data:\")[1]\n            mime, fp = val.split(\",\")\n            keys.append({'fingerprint': fp, 'type': 'openpgp', 'mime': mime})\n        if keys:\n            self['keys'] = self.get('keys', []) + [k for k in keys[:1]]\n            self['flags']['secure'] = True\n\n        photos = vcard.get_all('photo')\n        if photos:\n            self['photo'] = escape(photos[0].value)\n\n        crypto_policy = vcard.crypto_policy\n        if crypto_policy:\n            self['crypto-policy'] = crypto_policy\n\n        html_policy = vcard.html_policy\n        if html_policy:\n            self['html-policy'] = html_policy\n\n        self['x-mailpile-rid'] = vcard.random_uid\n        self['rank'] += base_rank + 25 * len(keys) + 5 * len(photos)\n        if vcard.email == self.address:\n            self['rank'] *= 2\n\n\nclass VCardStore(dict):\n    \"\"\"\n    This is a disk-backed in-memory collection of VCards.\n\n    >>> vcs = VCardStore(cfg, '/tmp')\n\n    # VCards are added to the collection using add_vcard. This will\n    # create a file for the card on disk, using a random name.\n    >>> vcs.add_vcards(MailpileVCard(VCardLine('FN:Dude'),\n    ...                              VCardLine('EMAIL:d@evil.com')),\n    ...                MailpileVCard(VCardLine('FN:Guy')))\n\n    VCards can be looked up directly by e-mail.\n    >>> vcs.get_vcard('d@evil.com').fn\n    u'Dude'\n    >>> vcs.get_vcard('nosuch@email.address') is None\n    True\n\n    Or they can be found using searches...\n    >>> vcs.find_vcards(['guy'])[0].fn\n    u'Guy'\n\n    Cards can be removed using del_vcards\n    >>> vcs.del_vcards(vcs.get_vcard('d@evil.com'))\n    >>> vcs.get_vcard('d@evil.com') is None\n    True\n    >>> vcs.del_vcards(*vcs.find_vcards(['guy']))\n    >>> vcs.find_vcards(['guy'])\n    []\n    \"\"\"\n    KINDS_ALL = ('individual', 'group', 'profile', 'internal')\n    KINDS_PEOPLE = ('individual', 'profile', 'internal')\n\n    def __init__(self, config, vcard_dir):\n        dict.__init__(self)\n        self.config = config\n        self.vcard_dir = vcard_dir\n        self.loading = False\n        self.loaded = False\n        self._lock = VCardRLock()\n\n    def __enter__(self, *args, **kwargs):\n        return GLOBAL_VCARD_LOCK.__enter__(*args, **kwargs)\n\n    def __exit__(self, *args, **kwargs):\n        return GLOBAL_VCARD_LOCK.__exit__(*args, **kwargs)\n\n    def index_vcard(self, card, collision_callback=None):\n        attrs = (['email'] if (card.kind in self.KINDS_PEOPLE)\n                 else ['nickname'])\n        with self._lock:\n            for attr in attrs:\n                for n, vcl in enumerate(card.get_all(attr, sort=True)):\n                    key = vcl.value.lower()\n                    if n == 0 or (key not in self):\n                        if key in self:\n                            if collision_callback is not None:\n                                existing = self[key].get(attr, 0)\n                                if existing is not 0 and existing.value == key:\n                                    collision_callback(key, card)\n                                self[key] = card\n                            else:\n                                pass  # Do not override existing cards\n                        else:\n                            self[key] = card\n            self[card.random_uid] = card\n\n    def deindex_vcard(self, card):\n        attrs = (['email'] if (card.kind in self.KINDS_PEOPLE)\n                 else ['nickname'])\n        with self._lock:\n            for attr in attrs:\n                for vcl in card.get_all(attr):\n                    key = vcl.value.lower()\n                    indexed = self.get(key)\n                    if indexed and indexed.random_uid == card.random_uid:\n                        del self[key]\n            if card.random_uid in self:\n                del self[card.random_uid]\n\n    def load_vcards(self, session=None):\n        with self._lock:\n            if self.loaded or self.loading:\n                return\n            self.loaded = False\n            self.loading = True\n\n        try:\n            prfs = self.config.prefs\n            key_func = lambda: self.config.get_master_key()\n            paths = [(fn, os.path.join(self.vcard_dir, fn))\n                     for fn in os.listdir(self.vcard_dir)\n                     if fn.endswith('.vcf')]\n\n            # Due to the way the eclipsing cleaner works, we want to\n            # load the most interesting VCards first - so we sort by\n            # size as a rough approximation of that.\n            paths.sort(key=lambda k: -os.path.getsize(k[1]))\n            for fn, path in paths:\n                if mailpile.util.QUITTING:\n                    return\n                try:\n                    c = MailpileVCard(config=self.config)\n                    c.load(path, config=self.config)\n                    try:\n                        def ccb(key, card):\n                            if card.kind == 'profile':\n                                return  # Deleting user input is never OK!\n                            if session:\n                                session.ui.error('DISABLING %s, eclipses %s'\n                                                 % (path, key))\n                            os.rename(path, path + '.bak')\n                            raise ValueError('Eclipsing')\n                        self.index_vcard(c, collision_callback=ccb)\n                        if session:\n                            session.ui.mark('Loaded %s from %s'\n                                            % (c.email, fn))\n                    except ValueError:\n                        pass\n                except KeyboardInterrupt:\n                    raise\n                except ValueError:\n                    if fn.startswith('tmp'):\n                        safe_remove(os.path.join(self.vcard_dir, fn))\n                except:\n                    if session:\n                        if 'vcard' in self.config.sys.debug:\n                            import traceback\n                            traceback.print_exc()\n                        session.ui.warning('Failed to load vcard %s' % fn)\n            self.loaded = True\n        except (OSError, IOError):\n            pass\n        finally:\n            self.loading = False\n\n    def get_vcard(self, email):\n        return self.get(email.lower(), None)\n\n    def find_vcards_with_line(vcards, name, value):\n        # FIXME: This is pretty slow. Can we do better?\n        vcards = [vc for vc in set(vcards.values())\n                  if [vcl for vcl in vc.get_all(name) if vcl.value == value]]\n        vcards.sort(key=lambda vc: (vc.fn, vc.email))\n        return vcards\n\n    def find_vcards(vcards, terms, kinds=None):\n        kinds = kinds or vcards.KINDS_ALL\n        results = []\n        with vcards._lock:\n            if not terms:\n                results = [set([vcards[k].random_uid for k in vcards\n                                if (vcards[k].kind in kinds) or not kinds])]\n            for term in terms:\n                term = term.lower()\n                results.append(set([vcards[k].random_uid for k in vcards\n                                    if (term in k or\n                                        term in vcards[k].fn.lower())\n                                    and ((vcards[k].kind in kinds) or\n                                         not kinds)]))\n            while len(results) > 1:\n                results[0] &= results.pop(-1)\n            results = [vcards[rid] for rid in results[0]]\n            results.sort(key=lambda card: card.fn)\n            return results\n\n    def add_vcards(self, *cards):\n        prefs = self.config.prefs\n        for card in cards:\n            card.filename = os.path.join(self.vcard_dir,\n                                         card.random_uid) + '.vcf'\n            card.configure_encryption(self.config)\n            card.save()\n            self.index_vcard(card)\n\n    def del_vcards(self, *cards):\n        for card in cards:\n            self.deindex_vcard(card)\n            safe_remove(card.filename)\n\n    def choose_from_address(vcards, *args, **kwargs):\n        \"\"\"\n        This method will choose a from address from the available\n        profiles, using the given config and lists of addresses as\n        a guideline. An address is chosen by assigning each potential\n        from address a cumulative score, where scores express roughly\n        the following preferences.\n\n        1. If one of the profiles' e-mail addresses is present in the\n           headers, prefer that so replies come from the address they\n           were sent to.\n        2. Else, if we have a preferred profile for communicating\n           with a given contact, use that.\n        3. Else, if any of the profiles lists one of the addresses\n           or their domains as being \"in scope\", use that.\n        4. Else, try and match on domain names.\n        5. Finally, use the global default or pick a profile at random.\n\n        >>> vcs = VCardStore(cfg, '/tmp')\n        >>> vcs.add_vcards(MailpileVCard(VCardLine('FN:Evil Dude'),\n        ...                              VCardLine('EMAIL:d@evil.com'),\n        ...                              VCardLine('KIND:profile')),\n        ...                MailpileVCard(VCardLine('FN:Guy'),\n        ...                              VCardLine('EMAIL;TYPE=PREF:g@f.com'),\n        ...                              VCardLine('EMAIL:ok@foo.com'),\n        ...                              VCardLine('X-MAILPILE-PROFILE-SCOPE;zzz'),\n        ...                              VCardLine('X-MAILPILE-PROFILE-SCOPE;'\n        ...                                        'address=ok@foo.com:x.y'),\n        ...                              VCardLine('KIND:profile')),\n        ...                MailpileVCard(VCardLine('FN:Icelander'),\n        ...                              VCardLine('EMAIL:x@bla.is'),\n        ...                              VCardLine('KIND:individual')))\n        >>> c41 = AddressInfo(u'dude@evil.com', 'Dude')\n        >>> c42 = AddressInfo(u'dude@f.com', 'Other dude')\n        >>> c31 = AddressInfo(u'd@x.y', 'D at X dot Y')\n        >>> c32 = AddressInfo(u'd@zzz', 'D at ZZZ')\n        >>> c21 = AddressInfo(u'x@bla.is', 'Icelander')\n        >>> c11 = AddressInfo(u'd@evil.com', 'Evil dude')\n\n        # Case 5\n        >>> '@' in vcs.choose_from_address(None, [c21]).address\n        True\n\n        # Case 4\n        >>> vcs.choose_from_address(None, [c41]).address\n        u'd@evil.com'\n        >>> vcs.choose_from_address(None, [c42]).address\n        u'g@f.com'\n\n        # Case 3\n        >>> vcs.choose_from_address(None, [c31], [c42, c41]).address\n        u'ok@foo.com'\n        >>> vcs.choose_from_address(None, [c32], [c42, c41]).address\n        u'g@f.com'\n\n        # Case 2\n        >>> vcs.get_vcard(c21.address).add(\n        ...    VCardLine(name='X-MAILPILE-PREFER-PROFILE',\n        ...              value=vcs.get_vcard('g@f.com').random_uid))\n        >>> vcs.choose_from_address(None, [c42, c41], [c31, c32],\n        ...                               [c21]).address\n        u'g@f.com'\n\n        # Case 1\n        >>> vcs.choose_from_address(None, [c42, c41], [c31, c32],\n        ...                               [c21, c11]).address\n        u'd@evil.com'\n\n        \"\"\"\n        fa_list = vcards.choose_from_addresses(*args, **kwargs)\n        return fa_list and fa_list[0] or None\n\n    def choose_from_addresses(vcards, config, *address_lists):\n        # Generate all the possible e-mail address / vcard pairs\n        profile_cards = vcards.find_vcards([], kinds=['profile'])\n        matches = []\n        for pc in profile_cards:\n            for vcl in pc.get_all('email'):\n                ai = AddressInfo(vcl.value, pc.fn, vcard=pc)\n                if config and config.prefs.default_email == vcl.value:\n                    ai.rank *= 1.75\n                matches.append((ai, pc))\n\n        # Iterate through all the provided addresses, and update the match\n        # scores based on how suitable each is for that address.  We assume\n        # the most important addresses are first.\n        order = 1.0\n        for addrinfo in (ai for src in address_lists for ai in src):\n            vcs = vcards.get_vcard(addrinfo.address)\n            if vcs:\n                sp_rid, sp_e = vcs.sending_profile(addrinfo.address)\n            else:\n                sp_rid = sp_e = None\n\n            for pc_ai, pc in matches:\n                pc_e = pc.sends_to(addrinfo.address)\n                pc_d = pc.same_domain(addrinfo.address)\n\n                # Is this address already in the headers?\n                if pc_ai.address == addrinfo.address:\n                    pc_ai.rank += (100000 * order)\n\n                # Does the user's card have a preference for this profile?\n                if sp_rid and sp_rid == pc.random_uid:\n                    if sp_e and sp_e == pc_ai.address:\n                        pc_ai.rank += (15000 * order)\n                    else:\n                        pc_ai.rank += (10000 * order)\n\n                # Does the profile card have a prefernce for this user?\n                if pc_e == pc_ai.address:\n                    pc_ai.rank += (1000 * order)\n\n                # Does the domain at least match??\n                if pc_d == pc_ai.address:\n                    pc_ai.rank += (100 * order)\n\n            order *= 0.95\n\n        if not matches:\n            return None\n\n        matches.sort(key=lambda m: -m[0].rank)\n        return [m[0] for m in matches]\n\n\nGUID_COUNTER = 0\n\n\nclass VCardPluginClass:\n    REQUIRED_PARAMETERS = []\n    OPTIONAL_PARAMETERS = []\n    FORMAT_NAME = None\n    FORMAT_DESCRIPTION = 'VCard Import/Export plugin'\n    SHORT_NAME = None\n    CONFIG_RULES = None\n\n    def __init__(self, session, config, guid=None):\n        self.session = session\n        self.config = config\n        if not self.config.guid:\n            if not guid:\n                global GUID_COUNTER\n                guid = 'urn:uuid:mp-%s-%x-%x' % (self.SHORT_NAME, time.time(),\n                                                 GUID_COUNTER)\n                GUID_COUNTER += 1\n            self.config.guid = guid\n            self.session.config.save()\n\n\nclass VCardImporter(VCardPluginClass):\n    MERGE_BY = ['email']\n    UPDATE_INDEX = False\n\n    def get_guid(self, vcard):\n        return self.config.guid\n\n    def import_vcards(self, session, vcard_store, **kwargs):\n        update_profiles = kwargs.get('profiles', False)\n        if 'profiles' in kwargs:\n            del kwargs['profiles']\n\n        session.ui.mark(_('Generating new vCards'))\n        all_vcards = self.get_vcards(**kwargs)\n        all_vcards.sort(key=lambda k: (k.email, k.random_uid))\n        counter = len(all_vcards)\n\n        updated = {}\n        for vcard in all_vcards:\n            session.ui.mark(_('Merging %s') % vcard.email)\n            counter += 1\n\n            # Some importers want to update the index's idea of what names go\n            # with what e-mail addresses. Not all do, but some...\n            if (self.UPDATE_INDEX and vcard.fn and\n                    session.config and session.config.index):\n                for email in vcard.get_all('email'):\n                    session.config.index.update_email(email.value,\n                                                      name=vcard.fn)\n\n            # Update existing vcards if possible...\n            existing = []\n            for merge_by in self.MERGE_BY:\n                for vcl in vcard.get_all(merge_by):\n                    existing.extend(\n                        vcard_store.find_vcards_with_line(merge_by, vcl.value))\n            last = ''\n            existing.sort(key=lambda k: (k.email, k.random_uid))\n            if not update_profiles:\n                existing = [e for e in existing if e.kind != 'profile']\n            for card in existing:\n                if card.random_uid == last:\n                    continue\n                last = card.random_uid\n                try:\n                    counter += 1\n                    vcard_store.deindex_vcard(card)\n                    if card.merge(self.get_guid(vcard), vcard.as_lines()):\n                        updated[card.random_uid] = card\n                    vcard_store.index_vcard(card)\n                except ValueError:\n                    session.ui.error(_('Failed to merge vCard %s into %s'\n                                       ) % (vcard.email, card.random_uid))\n\n            # Otherwise, create new ones.\n            kindhint = vcard.get('x-mailpile-kind-hint', 0)\n            if not existing and (update_profiles or\n                                 kindhint is 0 or\n                                 kindhint.value != 'profile'):\n                try:\n                    new_vcard = MailpileVCard(config=self.config)\n                    new_vcard.merge(self.get_guid(vcard), vcard.as_lines())\n                    if kindhint is not 0:\n                        new_vcard.add(VCardLine(name='kind',\n                                                value=kindhint.value))\n                    vcard_store.add_vcards(new_vcard)\n                    updated[new_vcard.random_uid] = new_vcard\n                    counter += 1\n                except ValueError:\n                    session.ui.error(_('Failed to create new vCard for %s'\n                                       ) % (vcard.email, card.random_uid))\n\n            if counter > 100:\n                if not kwargs.get('fast'):\n                    play_nice_with_threads()\n                    counter = 0\n\n        session.ui.mark(_('Saving %d updated vCards') % len(updated))\n        for vcard in updated.values():\n            vcard.save()\n            if not kwargs.get('fast'):\n                counter += 1\n                if counter % 10 == 0:\n                    play_nice_with_threads()\n\n        return len(updated)\n\n    def get_vcards(self):\n        raise Exception('Please override this function')\n\n\nclass VCardExporter(VCardPluginClass):\n\n    def __init__(self):\n        self.exporting = []\n\n    def add_contact(self, contact):\n        self.exporting.append(contact)\n\n    def remove_contact(self, contact):\n        self.exporting.remove(contact)\n\n    def save(self):\n        pass\n\n\nclass VCardContextProvider(VCardPluginClass):\n\n    def __init__(self, contact):\n        self.contact = contact\n\n    def get_recent_context(self, max=10):\n        pass\n\n    def get_related_context(self, query, max=10):\n        pass\n\n\nif __name__ == \"__main__\":\n    import doctest\n    import mailpile.config.defaults\n    import mailpile.config.manager\n    cfg = mailpile.config.manager.ConfigManager(\n        rules=mailpile.config.defaults.CONFIG_RULES)\n    results = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                              extraglobs={'cfg': cfg})\n    print('%s' % (results, ))\n    if results.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/vfs.py",
    "content": "# This is a very simple virtual file system abstraction for Mailpile.\n#\n# The purpose of this code is not to be a general-purpose VFS layer, but\n# to solve these specific problems within Mailpile:\n#\n#   - Provide a uniform, pluggable interface for browsing both traditional\n#     filesystems, remote IMAP servers and pretty much anything else within\n#     the app that fits well into the filesystem metaphor.\n#\n#   - Provide a uniform, pluggable interface within Mailpile for working\n#     with traditional filesystems which allows Mailpile's data to be\n#     stored either locally or remotely.\n#\n#   - Get rid of absolute paths in Mailpile's configuration, so Mailpile's\n#     settings and data can be moved around.\n#\n#   - Localize the code which deals with character sets and visual\n#     representation of file names and paths to one place.\n#\nimport copy\nimport glob\nimport os\nimport posixpath\n\nfrom mailpile.i18n import gettext as _\nfrom mailpile.util import safe_assert\n\n\nVFS_HANDLERS = []\nVFS_ALIASES = {}\n\n\ndef register_handler(prio, obj):\n    global VFS_HANDLERS\n    VFS_HANDLERS.append((prio, obj))\n    VFS_HANDLERS.sort()\n\ndef register_alias(name, prefix):\n    global VFS_ALIASES\n    safe_assert(name[:1] == '/')\n    VFS_ALIASES[name] = prefix\n\n\nclass FilePath(object):\n    \"\"\"\n    Wrapper for file-names, to manage the insanity of paths being binary\n    data that people insist on treating as strings. This is also where we\n    add and remove the /PREFIX$ stuff from paths.\n\n    The Mailpile VFS knows to use the raw_fp attribute instead of the\n    unicode() or str() representation, those methods expose data which\n    is suitable for writing to a config file or JSON stream.\n    \"\"\"\n    def __init__(self, cooked_fp=None, binary_fp=None, flags=None):\n        safe_assert((cooked_fp or binary_fp) and not (cooked_fp and binary_fp))\n        if cooked_fp:\n            if isinstance(cooked_fp, FilePath):\n                self.raw_fp = cooked_fp.raw_fp\n                flags = cooked_fp.flags if (flags is None) else flags\n            elif (isinstance(cooked_fp, (str, unicode)) and\n                    cooked_fp[-2:] == '=!'):\n                self.raw_fp = self.unalias(cooked_fp[:-2].decode('base64'))\n            elif isinstance(cooked_fp, unicode):\n                self.raw_fp = self.unalias(cooked_fp.encode('utf-8'))\n            else:\n                self.raw_fp = self.unalias(str(cooked_fp))\n        else:\n            self.raw_fp = binary_fp\n        self.flags = flags\n\n    @classmethod\n    def unalias(self, fp):\n        if '$' in fp:\n            alias, path = fp.split('$', 1)\n            if alias in VFS_ALIASES:\n                return VFS_ALIASES[alias] + path\n        return fp\n\n    @classmethod\n    def alias(self, fp):\n        while fp[:2] == './':\n            fp = fp[2:]\n        alias, prefix = None, ''\n        for a, p in VFS_ALIASES.iteritems():\n            if len(p) > len(prefix) and fp.startswith(p):\n                alias, prefix = a, p\n        if alias:\n            return '$'.join([alias, fp[len(prefix):]])\n        return fp\n\n    def __unicode__(self, errors='strict'):\n        \"\"\"Render file path as a cooked unicode string\"\"\"\n        raw_fp = self.alias(self.raw_fp)\n        try:\n            return raw_fp.decode('utf-8', errors)\n        except (UnicodeDecodeError, UnicodeEncodeError):\n            return raw_fp.encode('base64').strip() + '=!'\n\n    def __str__(self):\n        \"\"\"Render file path as a cooked string\"\"\"\n        return unicode(self).encode('utf-8')\n\n    def __eq__(self, other):\n        return unicode(self) == unicode(other)\n\n    def encoded(self):\n        return self.alias(self.raw_fp).encode('base64').strip() + '=!'\n\n    def display(self):\n        \"\"\"Lossy, user-friendly representation of this path.\"\"\"\n        return self.__unicode__('replace')\n\n    def display_basename(self):\n        \"\"\"Lossy, user-friendly representation of path's base name.\"\"\"\n        return posixpath.basename(self.__unicode__('replace'))\n\n    def startswith(self, stuff): return self.raw_fp.startswith(stuff)\n    def endswith(self, stuff): return self.raw_fp.endswith(stuff)\n    def lower(self): return self.__unicode__('replace').lower()\n    def upper(self): return self.__unicode__('replace').upper()\n\n    def join(self, *fpaths):\n        joined = posixpath.join(self.raw_fp,\n                                *[FilePath(fp).raw_fp for fp in fpaths])\n        return FilePath(binary_fp=joined, flags=FilePath(fpaths[-1]).flags)\n\n\nclass MailpileVfsBase(object):\n    \"\"\"\n    Base class for VFS Handler objects.\n    \"\"\"\n    FS_ROOT = None\n\n    def __init__(self):\n        pass\n\n    @classmethod\n    def Handles(cls, path):\n        if cls.FS_ROOT:\n            return path.startswith(cls.FS_ROOT)\n        return False\n\n    def path_join(cls, fp, *fps):\n        return FilePath(fp).join(*fps)\n\n    def open(cls, fp, *args, **kwargs):\n        return cls.open_(FilePath(fp).raw_fp, *args, **kwargs)\n\n    # FIXME: Give us an open mailbox object, or raise IOError\n    def open_mailbox(cls, fp, *args, **kwargs):\n        return cls.open_(FilePath(fp).raw_fp, *args, **kwargs)\n\n    def glob(cls, fp, *args, **kwargs):\n        return [FilePath(binary_fp=f) for f in\n                cls.glob_(FilePath(fp).raw_fp, *args, **kwargs)]\n\n    def listdir(cls, fp, *args, **kwargs):\n        return [(f if isinstance(f, FilePath) else FilePath(binary_fp=f))\n                for f in cls.listdir_(FilePath(fp).raw_fp, *args, **kwargs)]\n\n    def getinfo(cls, fp, config):\n        return cls.getinfo_(FilePath(fp).raw_fp, config)\n\n    def getinfo_(cls, fp, config):\n        fp = FilePath(fp)\n        ap = cls.abspath(fp)\n        info = {'path': ap}\n        for f, m, args in (('flags', cls.getflags, (fp, config)),\n                           ('bytes', cls.getsize, (fp,)),\n                           ('display_name', cls.display_name, (ap, config)),\n                           ('display_path', unicode, (ap,)),\n                           ('encoded', ap.encoded, [])):\n            try:\n                info[f] = m(*args)\n            except (IOError, OSError, UnicodeDecodeError):\n                info[f] = info.get(f)\n        return info\n\n    def display_name(cls, fp, config):\n        return cls.display_name_(FilePath(fp).raw_fp, config)\n\n    def display_name_(cls, fp, config):\n        return FilePath(fp).display_basename()\n\n    def abspath(cls, fp):\n        return FilePath(binary_fp=cls.abspath_(FilePath(fp).raw_fp))\n\n    def getflags(cls, fp, config):\n        return cls.getflags_(FilePath(fp).raw_fp, config)\n\n    def getflags_(cls, fp, config):\n        # By default, this method just checks isdir_ and mailbox_type_,\n        # but subclasses can override this (and even do things the other\n        # way around, e.g. for IMAP).\n        flags, fp = [], FilePath(fp)\n        mailbox_type = cls.mailbox_type_(fp.raw_fp, config)\n        flags.extend(['Mailbox', mailbox_type[1]] if mailbox_type else\n                     ['NoSelect'])\n        flags.extend(['Directory'] if cls.isdir_(fp.raw_fp) else\n                     ['HasNoChildren', 'NoInferiors'])\n        if cls.ismailsource_(fp.raw_fp):\n            flags.append('MailSource')\n        return flags\n\n    def isdir(cls, fp):\n        return cls.isdir_(FilePath(fp).raw_fp)\n\n    def ismailsource(cls, fp):\n        return cls.ismailsource_(FilePath(fp).raw_fp)\n\n    def mailbox_type(cls, fp, config):\n        return cls.mailbox_type_(FilePath(fp).raw_fp, config)\n\n    def getsize(cls, fp):\n        return cls.getsize_(FilePath(fp).raw_fp)\n\n    def exists(cls, fp):\n        return cls.exists_(FilePath(fp).raw_fp)\n\n    def _fixme(self):\n        raise NotImplementedError('FIXME')\n\n    glob_ = _fixme\n    open_ = _fixme\n    listdir_ = _fixme\n    abspath_ = _fixme\n    isdir_ = _fixme\n    ismailsource_ = _fixme\n    mailbox_type_ = _fixme\n    getsize_ = _fixme\n    exists_ = _fixme\n\n\nclass MailpileVFS(MailpileVfsBase):\n    \"\"\"\n    This is a router object that implements the VFS interface but,\n    delegating calls to individual implementations.\n    \"\"\"\n    def _delegate(cls, path):\n        for prio, handler in VFS_HANDLERS:\n            if handler.Handles(path):\n                return handler\n        raise IOError('Invalid path: %s' % path)\n\n    def glob_(self, path, *args, **kwargs):\n        return self._delegate(path).glob_(path, *args, **kwargs)\n\n    def open_(self, path, *args, **kwargs):\n        return self._delegate(path).open_(path, *args, **kwargs)\n\n    def listdir_(self, path, *args, **kwargs):\n        return self._delegate(path).listdir_(path, *args, **kwargs)\n\n    def abspath_(self, path, *args, **kwargs):\n        return self._delegate(path).abspath_(path, *args, **kwargs)\n\n    def isdir_(self, path, *args, **kwargs):\n        return self._delegate(path).isdir_(path, *args, **kwargs)\n\n    def getflags_(self, path, *args, **kwargs):\n        return self._delegate(path).getflags_(path, *args, **kwargs)\n\n    def ismailsource_(self, path, *args, **kwargs):\n        return self._delegate(path).ismailsource_(path, *args, **kwargs)\n\n    def mailbox_type_(self, path, config):\n        return self._delegate(path).mailbox_type_(path, config)\n\n    def getsize_(self, path, *args, **kwargs):\n        return self._delegate(path).getsize_(path, *args, **kwargs)\n\n    def display_name_(self, path, *args, **kwargs):\n        return self._delegate(path).display_name_(path, *args, **kwargs)\n\n    def exists_(self, path, *args, **kwargs):\n        return self._delegate(path).exists_(path, *args, **kwargs)\n\n\nclass MailpileVfsLocal(MailpileVfsBase):\n    \"\"\"\n    Local filesystem VFS handler; pipes through to built-in Python API.\n\n    FIXME: Our VFS uses Unix separators for everything, which implies that\n           all the functions below need path mapping wrappers for other\n           operating systems (Windows, in particular).\n    \"\"\"\n    @classmethod\n    def Handles(cls, path):\n        return True\n\n    def glob_(self, *args, **kwargs): return glob.iglob(*args, **kwargs)\n    def open_(self, *args, **kwargs): return open(*args, **kwargs)\n    def listdir_(self, *args, **kwargs): return os.listdir(*args, **kwargs)\n    def abspath_(self, path): return os.path.abspath(path)\n    def isdir_(self, path): return os.path.isdir(path)\n    def ismailsource_(self, fp): return False\n    def mailbox_type_(self, path, config):\n        from mailpile.mailboxes import IsMailbox\n        return IsMailbox(path, config)\n    def getsize_(self, path): return os.path.getsize(path)\n    def exists_(self, path): return os.path.exists(path)\n\n\nclass MailpileVfsRoot(MailpileVfsBase):\n    \"\"\"\n    This VFS implements a fancy root listing, including things like:\n\n       - Discovery of Thunderbird, Mail.app and Unix mail spools\n       - Listing configured sources\n       - Shortcut to the user's Home$\n       - Shortcuts to any root level registered VFSes\n\n    \"\"\"\n    def __init__(self, config):\n        self.config = config\n        self.rescan()\n\n    def rescan(self):\n        self.entries = {\n            'home': (FilePath('/Home$'), _('My Files')),\n#           'config': (FilePath('/Config$'), _('Settings')),\n        }\n        self._discover_mail_spool()\n# FIXME: enable post beta III\n#       self._discover_thunderbird()\n        self._discover_local_mailboxes()\n\n    def _discover_mail_spool(self):\n        user = os.getenv('USER')\n        for search in ('/var/mail', '/var/spool/mail'):\n            if user and os.path.isdir(search):\n                spool_path = os.path.join(search, user)\n                if os.path.exists(spool_path):\n                    spool_path = os.path.normpath(spool_path)\n                    self.entries['spool'] = (FilePath(spool_path),\n                                             _('Unix mail spool'))\n                    return\n\n    def _discover_local_mailboxes(self):\n        \"\"\"\n        This exposes at the root local mailboxes which would not be listed\n        otherwise, because their path falls outside of the user's home\n        directory.\n        \"\"\"\n        user_home = os.path.expanduser('~')\n        for mbx_id, path, ms in self.config.get_mailboxes():\n            path = FilePath(path)\n            if (path.raw_fp[:4] != 'src:' and\n                    not vfs.abspath(path).startswith(user_home)):\n                path = FilePath(os.path.normpath(path.raw_fp))\n                if not [e for e in self.entries if self.entries[e][0] == path]:\n                    self.entries[mbx_id] = (path, path.display_basename())\n\n    def _discover_thunderbird(self):\n        for search in ('~/.thunderbird', ):\n            tbird_home = os.path.expanduser(search)\n            if os.path.exists(tbird_home):\n                for profile in os.listdir(tbird_home):\n                    profpath = os.path.join(tbird_home, profile)\n                    if os.path.exists(os.path.join(profpath, 'Mail')):\n                        eid = 'tbird-%s' % profile\n                        name = 'Thunderbird %s' % profile.split('.', 1)[-1]\n                        self.entries[eid] = (FilePath(profpath), name)\n\n    def _entries(self):\n        e = copy.copy(self.entries)\n        for msid, msobj in self.config.mail_sources.iteritems():\n            if msobj and msobj.my_config and msobj.my_config.enabled:\n                e['msrc.%s' % msid] = (\n                    FilePath('/src:%s' % msid), msobj.name, 'MailSource')\n        return e\n\n    def Handles(self, path):\n        path = FilePath(path).raw_fp\n        return (path == '/') or (path[1:] in self._entries())\n\n    def glob_(self, *args, **kwargs):\n        return self.listdir_()\n\n    def listdir_(self, fp, *args, **kwargs):\n        return self._entries().keys() if (fp == '/') else []\n\n    def display_name_(self, fp, config):\n        try:\n            return unicode(self._entries()[fp[1:]][1])\n        except KeyError:\n            return _('Mailpile VFS')\n\n    def open_(self, fp, *args, **kwargs):\n        raise IOError('Cannot open entries in /')\n\n    def abspath_(self, fp):\n        return ('/' if (fp == '/') else\n                vfs.abspath(self._entries()[fp[1:]][0]).raw_fp)\n\n    def isdir_(self, fp):\n        return True if (fp == '/') else vfs.isdir(self._entries()[fp[1:]][0])\n\n    def ismailsource_(self, fp):\n        return (False if (fp == '/') else\n                'MailSource' in self._entries()[fp[1:]][2:])\n\n    def mailbox_type_(self, fp, config):\n        if fp == '/':\n            return False\n        return vfs.mailbox_type(self._entries()[fp[1:]][0], config)\n\n    def getsize_(self, fp):\n        return True if (fp == '/') else vfs.getsize(self._entries()[fp[1:]][0])\n\n    def exists_(self, fp):\n        return True if (fp == '/') else vfs.exists(self._entries()[fp[1:]][0])\n\n\nvfs = MailpileVFS()\nregister_handler(9999, MailpileVfsLocal())\nregister_alias('/Home', os.path.expanduser('~'))\n"
  },
  {
    "path": "mailpile/workers.py",
    "content": "from __future__ import print_function\nimport datetime\nimport random\nimport threading\nimport traceback\nimport time\n\nimport mailpile.util\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.util import *\n\n\n##[ Specialized threads ]######################################################\n\nclass Cron(threading.Thread):\n    \"\"\"\n    An instance of this class represents a cron-like worker thread\n    that manages and executes tasks in regular intervals\n    \"\"\"\n\n    def __init__(self, schedule, name=None, session=None):\n        \"\"\"\n        Initializes a new Cron instance.\n        Note that the thread will not be started automatically, so\n        you need to call start() manually.\n\n        Keyword arguments:\n        name -- The name of the Cron instance\n        session -- Currently unused\n        \"\"\"\n        threading.Thread.__init__(self)\n        self.ALIVE = False\n        self.daemon = mailpile.util.TESTING\n        self.name = name\n        self.session = session\n        self.last_run = time.time()\n        self.running = 'Idle'\n        self.schedule = schedule\n        self.sleep = 10\n        # This lock is used to synchronize\n        self.lock = WorkerLock()\n\n    def __str__(self):\n        return '%s: %s (%ds)' % (threading.Thread.__str__(self),\n                                 self.running, time.time() - self.last_run)\n\n    def add_task(self, name, interval, task):\n        \"\"\"\n        Add a task to the cron worker queue\n\n        Keyword arguments:\n        name -- The name of the task to add\n        interval -- The interval (in seconds) of the task\n        task    -- A task function\n        \"\"\"\n        with self.lock:\n            if name in self.schedule:\n                last = self.schedule[name][3]\n                status = self.schedule[name][4]\n            elif interval == (24*3600):\n                # Special case for exactly once-a-day jobs: schedule\n                # them to run at night by default.\n                hr = (3600 * datetime.datetime.now().hour)\n                last = time.time() - hr + random.randint(3600, 7 * 3600)\n                status = 'new'\n            else:\n                last = time.time() - random.randint(0, interval)\n                status = 'new'\n\n            self.schedule[name] = [name, interval, task, last, status]\n            self.sleep = 1\n            self.__recalculateSleep()\n\n    def __recalculateSleep(self):\n        \"\"\"\n        Recalculate the maximum sleep delay.\n        This shall be called from a lock zone only\n        \"\"\"\n        # (Re)alculate how long we can sleep between tasks\n        #    (sleep min. 1 sec, max. 61 sec)\n        # --> Calculate the GCD of the task intervals\n        for i in range(2, 61):  # i = second\n            # Check if any scheduled task intervals are != 0 mod i\n            filteredTasks = [True for task in self.schedule.values()\n                             if int(task[1]) % i != 0]\n            # We can sleep for i seconds if i divides all intervals\n            if (len(filteredTasks) == 0):\n                self.sleep = i\n\n    def cancel_task(self, name):\n        \"\"\"\n        Cancel a task in the current Cron instance.\n        If a task with the given name does not exist,\n        ignore the request.\n\n        Keyword arguments:\n        name -- The name of the task to cancel\n        \"\"\"\n        if name in self.schedule:\n            with self.lock:\n                del self.schedule[name]\n                self.__recalculateSleep()\n\n    def run(self):\n        \"\"\"\n        Thread main function for a Cron instance.\n\n        \"\"\"\n        play_nice(19)  # Reduce our priority as much as possible\n\n        # Main thread loop\n        self.ALIVE = True\n        while self.ALIVE and not mailpile.util.QUITTING:\n            tasksToBeExecuted = []  # Contains tuples (name, func)\n            now = time.time()\n            # Check if any of the task is (over)due\n            with self.lock:\n                for task_spec in self.schedule.values():\n                    name, interval, task, last, status = task_spec\n                    if (last + interval) <= now:\n                        tasksToBeExecuted.append((name, task))\n                        self.schedule[name][4] = 'scheduled'\n\n            # Execute the tasks\n            for name, task in tasksToBeExecuted:\n                # Set last_executed\n                self.schedule[name][3] = time.time()\n                self.schedule[name][4] = 'running'\n                try:\n                    self.last_run = time.time()\n                    self.running = name\n                    task()\n                except Exception as e:\n                    self.schedule[name][4] = 'FAILED'\n                    self.session.ui.error(('%s failed in %s: %s'\n                                           ) % (name, self.name, e))\n                finally:\n                    self.schedule[name][4] = 'ok'\n                    self.last_run = time.time()\n                    self.running = 'Finished %s' % self.running\n\n            # Some tasks take longer than others, so use the time before\n            # executing tasks as reference for the delay\n            sleepTime = self.sleep\n            delay = time.time() - now + sleepTime\n\n            # Sleep for max. 1 sec to react to the quit signal in time\n            while delay > 0 and self.ALIVE:\n                # self.sleep might change during loop (if tasks are modified)\n                # In that case, just wake up and check if any tasks need\n                # to be executed\n                if self.sleep != sleepTime:\n                    delay = 0\n                else:\n                    # Sleep for max 1 second to check self.ALIVE\n                    time.sleep(max(0, min(1, delay)))\n                    delay -= 1\n\n    def quit(self, session=None, join=True):\n        \"\"\"\n        Send a signal to the current Cron instance\n        to stop operation.\n\n        Keyword arguments:\n        join -- If this is True, this method will wait until\n                        the Cron thread exits.\n        \"\"\"\n        self.ALIVE = False\n        if join and (not self.daemon) and self.isAlive():\n            self.join()\n\n\nclass Worker(threading.Thread):\n\n    PAUSE_DEADLINE = 2\n    NICE_PRIORITY = 15\n\n    def __init__(self, name, session, daemon=False):\n        threading.Thread.__init__(self)\n        self.daemon = mailpile.util.TESTING or daemon\n        self.name = name or 'Worker'\n        self.ALIVE = False\n        self.JOBS = []\n        self.JOBS_LATER = []\n        self.LOCK = threading.Condition(WorkerRLock())\n        self.last_run = time.time()\n        self.running = 'Idle'\n        self.pauses = 0\n        self.session = session\n        self.important = False\n        self.wait_until = None\n\n    def __str__(self):\n        return ('%s: %s (%ds, jobs=%s, jobs_after=%s)'\n                % (threading.Thread.__str__(self),\n                   self.running,\n                   time.time() - self.last_run,\n                   len(self.JOBS), len(self.JOBS_LATER)))\n\n    def add_task(self, session, name, task,\n                 after=None, unique=False, first=False):\n        with self.LOCK:\n            if unique:\n                for s, n, t in self.JOBS:\n                    if n == name:\n                        return\n            if unique and after:\n                for ts, (s, n, t) in self.JOBS_LATER:\n                    if n == name:\n                        return\n\n            snt = (session, name, task)\n            if first:\n                self.JOBS[:0] = [snt]\n            elif after:\n                self.JOBS_LATER.append((after, snt))\n            else:\n                self.JOBS.append(snt)\n\n            self.LOCK.notify()\n\n    def add_unique_task(self, session, name, task, **kwargs):\n        return self.add_task(session, name, task, unique=True, **kwargs)\n\n    def do(self, session, name, task, unique=False, first=False):\n        if session and session.main:\n            # We run this in the foreground on the main interactive session,\n            # so CTRL-C has a chance to work.\n            try:\n                self.pause(session, first=first)\n                rv = task()\n            finally:\n                self.unpause(session)\n        else:\n            self.add_task(session, name, task, unique=unique)\n            if session:\n                rv = session.wait_for_task(name)\n            else:\n                rv = True\n        return rv\n\n    def _pause_for_user_activities(self):\n        if self.wait_until is not None:\n            while not self.wait_until():\n                time.sleep(self.PAUSE_DEADLINE)\n        play_nice_with_threads(deadline=time.time() + self.PAUSE_DEADLINE)\n\n    def _keep_running(self, **ignored_kwargs):\n        return (self.ALIVE and not mailpile.util.QUITTING)\n\n    def _failed(self, session, name, task, e):\n        self.session.ui.debug(traceback.format_exc())\n        self.session.ui.error(('%s failed in %s: %s'\n                               ) % (name, self.name, e))\n        if session:\n            session.report_task_failed(name)\n\n    def is_idle(self):\n        return (len(self.JOBS) + len(self.JOBS_LATER) < 1 and\n                self.running.startswith('Finished') or\n                self.running.startswith('Idle'))\n\n    def run(self):\n        play_nice(self.NICE_PRIORITY)  # Reduce priority\n        self.ALIVE = True\n        while self._keep_running():\n            with self.LOCK:\n                while len(self.JOBS) < 1:\n                    if not self._keep_running(locked=True):\n                        return\n                    self.LOCK.wait()\n\n            self._pause_for_user_activities()\n\n            with self.LOCK:\n                session, name, task = self.JOBS.pop(0)\n                if not self.JOBS:\n                    now = time.time()\n                    self.JOBS.extend(snt for ts, snt\n                                     in self.JOBS_LATER if ts <= now)\n                    self.JOBS_LATER = [(ts, snt) for ts, snt\n                                       in self.JOBS_LATER if ts > now]\n\n            try:\n                self.last_run = time.time()\n                self.running = name\n                if session:\n                    session.ui.mark('Starting: %s' % name)\n                    session.report_task_completed(name, task())\n                else:\n                    task()\n            except (JobPostponingException) as e:\n                session.ui.debug('Postponing: %s' % name)\n                self.add_task(session, name, task,\n                              after=time.time() + e.seconds)\n            except (IOError, OSError) as e:\n                self._failed(session, name, task, e)\n                time.sleep(1)\n            except Exception as e:\n                self._failed(session, name, task, e)\n            finally:\n                self.last_run = time.time()\n                self.running = 'Finished %s' % self.running\n\n    def pause(self, session, first=False):\n        with self.LOCK:\n            self.pauses += 1\n            first = (self.pauses == 1)\n\n        if first:\n            def pause_task():\n                session.report_task_completed('Pause', True)\n                session.wait_for_task('Unpause', quiet=True)\n\n            self.add_task(None, 'Pause', pause_task, first=first)\n            session.wait_for_task('Pause', quiet=True)\n\n    def unpause(self, session):\n        with self.LOCK:\n            self.pauses -= 1\n            if self.pauses == 0:\n                session.report_task_completed('Unpause', True)\n\n    def die_soon(self, session=None):\n        def die():\n            self.ALIVE = False\n        self.add_task(session, '%s shutdown' % self.name, die)\n\n    def quit(self, session=None, join=True):\n        self.die_soon(session=session)\n        if join and (not self.daemon) and self.isAlive():\n            self.join()\n\n\nclass ImportantWorker(Worker):\n\n    PAUSE_DEADLINE = 0.5\n    NICE_PRIORITY = 5\n\n    def _pause_for_user_activities(self):\n        # Our jobs are important, if we have too many we stop playing nice\n        if len(self.JOBS) < 10:\n            Worker._pause_for_user_activities(self)\n\n    def _keep_running(self, _pass=1, locked=False):\n        # This is a much more careful shutdown test, that refuses to\n        # stop with jobs queued up and tries to compensate for potential\n        # race conditions in our quitting code by waiting a bit and\n        # then re-checking if it looks like it is time to die.\n        if len(self.JOBS) > 0:\n            return True\n        else:\n             if _pass == 2:\n                 return Worker._keep_running(self)\n             if self.ALIVE and not mailpile.util.QUITTING:\n                 return True\n             else:\n                 if locked:\n                     try:\n                         self.LOCK.release()\n                         time.sleep(1)\n                     finally:\n                         self.LOCK.acquire()\n                 else:\n                     time.sleep(1)\n                 return self._keep_running(_pass=2, locked=locked)\n\n    def _failed(self, session, name, task, e):\n        # Important jobs!  Re-queue if they fail, it might be transient\n        Worker._failed(self, session, name, task, e)\n        self.add_unique_task(session, name, task)\n\n\nclass DumbWorker(Worker):\n    def add_task(self, session, name, task, unique=False):\n        with self.LOCK:\n            return task()\n\n    def add_unique_task(self, session, name, task, **kwargs):\n        return self.add_task(session, name, task)\n\n    def do(self, session, name, task, unique=False):\n        return self.add_task(session, name, task)\n\n    def run(self):\n        pass\n\n    def die_soon(self, *args, **kwargs):\n        pass\n\n    def quit(self, *args, **kwargs):\n        pass\n\n\nif __name__ == \"__main__\":\n    import doctest\n    import sys\n    result = doctest.testmod(optionflags=doctest.ELLIPSIS,\n                             extraglobs={'junk': {}})\n    print('%s' % (result, ))\n    if result.failed:\n        sys.exit(1)\n"
  },
  {
    "path": "mailpile/www/__init__.py",
    "content": ""
  },
  {
    "path": "mailpile/www/jinjaextensions.py",
    "content": "import copy\nimport datetime\nimport hashlib\nimport random\nimport re\nimport urllib\nimport json\nimport shlex\nimport time\nfrom jinja2 import nodes, UndefinedError, Markup\nfrom jinja2.ext import Extension\nfrom jinja2.utils import contextfunction, import_string, escape\n\n#from markdown import markdown\n\nfrom mailpile.commands import Action\nfrom mailpile.config.defaults import APPVER\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.util import *\nfrom mailpile.ui import HttpUserInteraction\nfrom mailpile.urlmap import UrlMap\nfrom mailpile.plugins import PluginManager\nfrom mailpile.vcard import AddressInfo\n\n\nVERSION_IDENTIFIER = None\n\n# This looks for a .git folder and uses the current state to augment\n# our version... cachebusting during development.\ndirname, tail = os.path.split(__file__)\nwhile dirname and tail and os.path.exists(dirname):\n    fetch_head = os.path.join(dirname, '.git', 'FETCH_HEAD')\n    if os.path.exists(fetch_head):\n        try:\n            md5 = md5_hex(open(fetch_head, 'r').read())\n            VERSION_IDENTIFIER = '%s-%s' % (APPVER, md5[:8])\n            break\n        except (OSError, IOError):\n            break\n    dirname, tail = os.path.split(dirname)\n\n\nclass MailpileCommand(Extension):\n    \"\"\"Run Mailpile Commands, \"\"\"\n    tags = set(['mpcmd'])\n\n    def __init__(self, environment):\n        Extension.__init__(self, environment)\n        e = self.env = environment\n        s = self\n        e.globals['mailpile'] = s._command\n        e.globals['mailpile_render'] = s._command_render\n        e.globals['U'] = s._url_path_fix\n        e.globals['make_rid'] = randomish_uid\n        e.globals['is_dev_version'] = s._is_dev_version\n        e.globals['is_configured'] = s._is_configured\n        e.globals['version_identifier'] = s._version_identifier\n        e.filters['min'] = s._min\n        e.globals['min'] = s._min\n        e.filters['max'] = s._max\n        e.globals['max'] = s._max\n        e.filters['random'] = s._random\n        e.globals['random'] = s._random\n        e.filters['truthy'] = s._truthy\n        e.globals['truthy'] = s._truthy\n        e.filters['with_context'] = s._with_context\n        e.globals['with_context'] = s._with_context\n        e.filters['url_path_fix'] = s._url_path_fix\n        e.globals['use_data_view'] = s._use_data_view\n        e.globals['regex_replace'] = s._regex_replace\n        e.filters['regex_replace'] = s._regex_replace\n        e.globals['friendly_bytes'] = s._friendly_bytes\n        e.filters['friendly_bytes'] = s._friendly_bytes\n        e.globals['friendly_number'] = s._friendly_number\n        e.filters['friendly_number'] = s._friendly_number\n        e.globals['friendly_received'] = s._friendly_received\n        e.filters['friendly_received'] = s._friendly_received\n        e.globals['body_part_metadata'] = s._body_part_metadata\n        e.filters['body_part_metadata'] = s._body_part_metadata\n        e.globals['show_avatar'] = s._show_avatar\n        e.filters['show_avatar'] = s._show_avatar\n        e.globals['navigation_on'] = s._navigation_on\n        e.filters['navigation_on'] = s._navigation_on\n        e.globals['has_label_tags'] = s._has_label_tags\n        e.filters['has_label_tags'] = s._has_label_tags\n        e.globals['show_message_signature'] = s._show_message_signature\n        e.filters['show_message_signature'] = s._show_message_signature\n        e.globals['show_message_encryption'] = s._show_message_encryption\n        e.filters['show_message_encryption'] = s._show_message_encryption\n        e.globals['show_text_part_signature'] = s._show_text_part_signature\n        e.filters['show_text_part_signature'] = s._show_text_part_signature\n        e.globals['show_text_part_encryption'] = s._show_text_part_encryption\n        e.filters['show_text_part_encryption'] = s._show_text_part_encryption\n        e.globals['show_crypto_policy'] = s._show_crypto_policy\n        e.filters['show_crypto_policy'] = s._show_crypto_policy\n        e.globals['contact_url'] = s._contact_url\n        e.filters['contact_url'] = s._contact_url\n        e.globals['contact_name'] = s._contact_name\n        e.filters['contact_name'] = s._contact_name\n        e.globals['thread_upside_down'] = s._thread_upside_down\n        e.filters['thread_upside_down'] = s._thread_upside_down\n        e.globals['fix_urls'] = s._fix_urls\n        e.filters['fix_urls'] = s._fix_urls\n        e.globals['stoplist'] = STOPLIST\n\n        # See utils.py for these functions:\n        e.globals['elapsed_datetime'] = elapsed_datetime\n        e.filters['elapsed_datetime'] = elapsed_datetime\n        e.globals['friendly_datetime'] = friendly_datetime\n        e.filters['friendly_datetime'] = friendly_datetime\n        e.globals['friendly_time'] = friendly_time\n        e.filters['friendly_time'] = friendly_time\n\n        # These are helpers for injecting plugin elements\n        e.globals['get_ui_elements'] = s._get_ui_elements\n        e.globals['ui_elements_setup'] = s._ui_elements_setup\n        e.globals['add_state_query_string'] = s._add_state_query_string\n        e.filters['add_state_query_string'] = s._add_state_query_string\n\n        # This is a worse versin of urlencode, but without it we require\n        # Jinja 2.7, which isn't apt-get installable.\n        e.globals['urlencode'] = s._urlencode\n        e.filters['urlencode'] = s._urlencode\n        # Same thing for selectattr\n        e.globals['selectattr'] = s._selectattr\n        e.filters['selectattr'] = s._selectattr\n\n        # Make a function-version of the safe command\n        e.globals['safe'] = s._safe\n        e.filters['json'] = s._json\n        e.filters['escapejs'] = s._escapejs\n\n        # Strip trailing blank lines from email\n        e.globals['nice_text'] = s._nice_text\n        e.filters['nice_text'] = s._nice_text\n\n        # Transforms \\n into HTML <br />\n        e.globals['to_br'] = s._to_br\n        e.filters['to_br'] = s._to_br\n\n        # Strip Re: Fwd: from subject lines\n        e.globals['nice_subject'] = s._nice_subject\n        e.filters['nice_subject'] = s._nice_subject\n        # And [list] headings as well\n        e.globals['bare_subject'] = s._bare_subject\n        e.filters['bare_subject'] = s._bare_subject\n\n        # Filter the raw header list\n        e.globals['get_all'] = s._get_all\n        e.filters['get_all'] = s._get_all\n        e.globals['get_addresses'] = s._get_addresses\n        e.filters['get_addresses'] = s._get_addresses\n\n        # Make unruly names a lil bit nicer\n        e.globals['nice_name'] = s._nice_name\n        e.filters['nice_name'] = s._nice_name\n\n        # Makes a UI usable classification of attachment from mimetype\n        e.globals['attachment_type'] = s._attachment_type\n        e.filters['attachment_type'] = s._attachment_type\n\n        # Loads theme settings JSON manifest\n        e.globals['theme_settings'] = s._theme_settings\n        e.filters['theme_settings'] = s._theme_settings\n\n        # Separates Fingerprint in 4 char groups\n        e.globals['nice_fingerprint'] = s._nice_fingerprint\n        e.filters['nice_fingerprint'] = s._nice_fingerprint\n        e.globals['group_fingerprint'] = s._group_fingerprint\n        e.filters['group_fingerprint'] = s._group_fingerprint\n\n        # Converts Filter +/- tags into arrays\n        e.globals['make_filter_groups'] = s._make_filter_groups\n        e.filters['make_filter_groups'] = s._make_filter_groups\n\n        # Make Nice Summary of Recipients\n        e.globals['recipient_summary'] = s._recipient_summary\n        e.filters['recipient_summary'] = s._recipient_summary\n\n        # Nagifications\n        e.globals['show_nagification'] = s._show_nagification\n        e.filters['show_nagification'] = s._show_nagification\n\n    def _debug(self, msg):\n        if 'jinja' in self.env.session.config.sys.debug:\n            sys.stderr.write('jinja: ')\n            sys.stderr.write(msg)\n            sys.stderr.write('\\n')\n            sys.stderr.flush()\n\n    def _command(self, command, *args, **kwargs):\n        rv = Action(self.env.session, command, args, data=kwargs).as_dict()\n        self._debug('mailpile(%s, %s, %s) -> %s'\n                    % (command, args, kwargs, rv))\n        return rv\n\n    def _command_render(self, how, command, *args, **kwargs):\n        self._debug('mailpile_render(%s, %s, ...)' % (how, command))\n        old_ui, config = self.env.session.ui, self.env.session.config\n        try:\n            ui = self.env.session.ui = HttpUserInteraction(None, config,\n                                                           log_parent=old_ui,\n                                                           log_prefix='jinja/')\n            ui.html_variables = copy.deepcopy(old_ui.html_variables)\n            ui.render_mode = how\n            ui.display_result(Action(self.env.session, command, args,\n                                     data=kwargs))\n            rv = ui.render_response(config)\n            return (rv[0], rv[1].strip())\n        finally:\n            self.env.session.ui = old_ui\n\n    def _use_data_view(self, view_name, result):\n        self._debug('use_data_view(%s, ...)' % (view_name))\n        dv = UrlMap(self.env.session).map(None, 'GET', view_name, {}, {})[-1]\n        return dv.view(result)\n\n    def _get_ui_elements(self, ui_type, state, context=None):\n        self._debug('get_ui_element(%s, %s, ...)' % (ui_type, state))\n        ctx = context or state.get('context_url', '')\n        return copy.deepcopy(PluginManager().get_ui_elements(ui_type, ctx))\n\n    @classmethod\n    def _add_state_query_string(cls, url, state, elem=None):\n        if not url:\n            url = state.get('command_url', '')\n        if '#' in url:\n            url, frag = url.split('#', 1)\n            frag = '#' + frag\n        else:\n            frag = ''\n        if url:\n            args = []\n            query_args = state.get('query_args', {})\n            for key in sorted(query_args.keys()):\n                if key.startswith('_'):\n                    continue\n                values = query_args[key]\n                if elem:\n                    for rk, rv in elem.get('url_args_remove', []):\n                        if rk == key:\n                            values = [v for v in values if rv and (v != rv)]\n                if elem:\n                    for ak, av in elem.get('url_args_add', []):\n                        if ak == key and av not in values:\n                            values.append(av)\n                args.extend([(key, unicode(v).encode(\"utf-8\")) for v in values])\n            return url + '?' + urllib.urlencode(args) + frag\n        else:\n            return url + frag\n\n    def _ui_elements_setup(self, classfmt, elements):\n        self._debug('ui_elements_setup(%s, %s)' % (classfmt, elements))\n        setups = []\n        for elem in elements:\n            if elem.get('javascript_setup'):\n                setups.append('$(\"%s\").each(function(){%s(this);});'\n                              % (classfmt % elem, elem['javascript_setup']))\n            if elem.get('javascript_events'):\n                for event, call in elem.get('javascript_events').iteritems():\n                    setups.append('$(\"%s\").bind(\"%s\", %s);' %\n                        (classfmt % elem, event, call))\n        return Markup(\"function(){%s}\" % ''.join(setups))\n\n    def _regex_replace(self, s, find, replace):\n        \"\"\"A non-optimal implementation of a regex filter\"\"\"\n        return re.sub(find, replace, s)\n\n    def _friendly_received(self, data):\n        if data in ('', None, False):\n            return ''\n        match = re.search(\n            '^.*?by (\\S+?) [^;]*;\\s+([A-Za-z]{3,3}, +\\d+ +\\S+ \\d\\d\\d\\d \\d+:\\d+:\\d+(?: +[+-]\\d\\d\\d\\d)?)',\n            data)\n        if match:\n            return re.sub('\\s+', ' ', ('%s (%s)' % (match.group(2), match.group(1))))\n        match = re.search('^.*([A-Za-z]{3,3}, +\\d+ +\\S+ +20\\d\\d \\d+:\\d+:\\d+(?: +[+-]\\d\\d\\d\\d)?)', data)\n        if match:\n            return re.sub('\\s+', ' ', (match.group(1)))\n        else:\n            return data[:50] + ('...' if (len(data) > 50) else '')\n\n    def _friendly_number(self, number, decimals=0):\n        # See mailpile/util.py:friendly_number if this needs fixing\n        if number in ('', None, False):\n            return ''\n        return friendly_number(number, decimals=decimals, base=1000)\n\n    def _friendly_bytes(self, number, decimals=0):\n        # See mailpile/util.py:friendly_number if this needs fixing\n        if number in ('', None, False):\n            return ''\n        return friendly_number(number,\n                               decimals=decimals, base=1024, suffix='B')\n\n    def _body_part_metadata(self, bp):\n        bp = bp.split(':', 2)\n        pixels = 0\n        size = (0, 0)\n        if len(bp) < 2:\n            mimetype = 'application/octet-stream'\n        elif not bp[1]:\n            mimetype = 'text/html' if (bp[2] == 'H') else 'text/plain'\n        elif '/' in bp[1]:\n            mimetype = unsquish_mimetype(bp[1])\n        else:\n            xy = bp[1].split('x')\n            size = (int(xy[0]), int(xy[1]))\n            pixels = size[0] * size[1]\n            mimetype = 'image/jpeg'  # A lie!\n        return {\n            'filename': ((bp and bp[-1] or '').strip() or\n                         ('(%s)' % _('unnamed'))),\n            'bytes': self._friendly_bytes(int((bp and bp[0] or '0'), 16)),\n            'size': size,\n            'pixels': pixels,\n            'mimetype': mimetype}\n\n    def _friendly_hex_bytes(self, number, decimals=0):\n        # See mailpile/util.py:friendly_number if this needs fixing\n        if number in ('', None, False):\n            return ''\n        return friendly_number(int(number, 16),\n                               decimals=decimals, base=1024, suffix='B')\n\n    def _show_avatar(self, contact):\n        if \"photo\" in contact:\n            photo = contact['photo']\n        else:\n            photo = ('%s/static/img/avatar-default.png'\n                     % self.env.session.config.sys.http_path)\n        return photo\n\n    def _navigation_on(self, search_tag_ids, on_tid):\n        if search_tag_ids:\n            for tid in search_tag_ids:\n                if tid == on_tid:\n                    return \"navigation-on\"\n                else:\n                    return \"\"\n\n    def _has_label_tags(self, tags, tag_tids):\n        self._debug('has_label_tags(..., %s, ...)' % (tag_tids,))\n        count = 0\n        for tid in tag_tids:\n            if tags[tid][\"label\"] and not tags[tid][\"searched\"]:\n                count += 1\n        return count\n\n    _DEFAULT_SIGNATURE = [\n            \"crypto-color-gray\",\n            \"icon-signature-none\",\n            _(\"Unknown\"),\n            _(\"There is something unknown or wrong with this signature\")]\n    _STATUS_SIGNATURE = {\n        \"none\": [\n            \"crypto-color-gray\",\n            \"icon-signature-none\",\n            _(\"Not Signed\"),\n            _(\"This data has no digital signature, which means it could have\"\n              \" come from anyone, not necessarily the apparent sender\")],\n        \"error\": [\n            \"crypto-color-red\",\n            \"icon-signature-invalid\",\n            _(\"Error\"),\n            _(\"There was a weird error with this digital signature\")],\n        \"mixed-error\": [\n            \"crypto-color-red\",\n            \"icon-signature-invalid\",\n            _(\"Mixed Error\"),\n            _(\"Parts of this message have a signature with a weird error\")],\n        \"unsigned\": [\n            \"crypto-color-red\",\n            \"icon-signature-unknown\",\n            _(\"Unsigned\"),\n            _(\"This data has no digital signature, which means it could \"\n              \"easily have been forged. This sender usually signs their \"\n              \"messages, so be careful!\")],\n        \"mixed-unsigned\": [\n            \"crypto-color-red\",\n            \"icon-signature-unknown\",\n            _(\"Mixed Unsigned\"),\n            _(\"This message has no digital signature, which means it could \"\n              \"easily have been forged. This sender usually signs their \"\n              \"messages, so be careful!\")],\n        \"invalid\": [\n            \"crypto-color-red\",\n            \"icon-signature-invalid\",\n            _(\"Invalid\"),\n            _(\"The digital signature was invalid or bad\")],\n        \"mixed-invalid\": [\n            \"crypto-color-red\",\n            \"icon-signature-invalid\",\n            _(\"Mixed Invalid\"),\n            _(\"Parts of this message have a digital signature that is invalid\"\n              \" or bad\")],\n        \"revoked\": [\n            \"crypto-color-red\",\n            \"icon-signature-revoked\",\n            _(\"Revoked\"),\n            _(\"Watch out, the digital signature was made with a key that has\"\n              \" been revoked - this is not a good thing\")],\n        \"mixed-revoked\": [\n            \"crypto-color-red\",\n            \"icon-signature-revoked\",\n            _(\"Mixed Revoked\"),\n            _(\"Watch out, parts of this message were digitally signed with a\"\n              \" key that has been revoked\")],\n        \"expired\": [\n            \"crypto-color-orange\",\n            \"icon-signature-expired\",\n            _(\"Expired\"),\n            _(\"The digital signature was made with an expired key\")],\n        \"mixed-expired\": [\n            \"crypto-color-orange\",\n            \"icon-signature-expired\",\n            _(\"Mixed Expired\"),\n            _(\"Parts of this message have a digital signature made with an \"\n              \"expired key\")],\n        \"unknown\": [\n            \"crypto-color-gray\",\n            \"icon-signature-unknown\",\n            _(\"Unknown\"),\n            _(\"The digital signature was made with an unknown key, so we can\"\n              \" not verify it\")],\n        \"mixed-unknown\": [\n            \"crypto-color-gray\",\n            \"icon-signature-unknown\",\n            _(\"Mixed Unknown\"),\n            _(\"Parts of this message have a signature made with an unknown\"\n              \" key which we can not verify\")],\n        \"changed\": [\n            \"crypto-color-orange\",\n            \"icon-signature-unknown\",\n            _(\"Changed\"),\n            _(\"The digital signature was made with an unexpected key.\"\n              \" Be careful!\")],\n        \"mixed-changed\": [\n            \"crypto-color-orange\",\n            \"icon-signature-unknown\",\n            _(\"Mixed Changed\"),\n            _(\"Parts of this message have a digital signature that was made\"\n              \" with an unexpected key. Be careful!\")],\n        \"unverified\": [\n            \"crypto-color-blue\",\n            \"icon-signature-unverified\",\n            _(\"Unverified\"),\n            _(\"The signature was good but it came from a key that is not\"\n              \" verified yet\")],\n        \"mixed-unverified\": [\n            \"crypto-color-blue\",\n            \"icon-signature-unverified\",\n            _(\"Mixed Unverified\"),\n            _(\"Parts of this message have an unverified signature\")],\n        \"signed\": [\n            \"crypto-color-green\",\n            \"icon-signature-verified\",\n            _(\"Signed\"),\n            _(\"The digital signature is valid and was made with a key we have\"\n              \" seen before. Looks good!\")],\n        \"mixed-signed\": [\n            \"crypto-color-blue\",\n            \"icon-signature-verified\",\n            _(\"Mixed Signed\"),\n            _(\"Parts of the message have a good digital signature, made with\"\n              \" a key we have seen before.\")],\n        \"verified\": [\n            \"crypto-color-green\",\n            \"icon-signature-verified\",\n            _(\"Verified\"),\n            _(\"The signature was good and came from a verified key, w00t!\")],\n        \"mixed-verified\": [\n            \"crypto-color-blue\",\n            \"icon-signature-verified\",\n            _(\"Mixed Verified\"),\n            _(\"Parts of the message have a verified signature, but other \"\n              \"parts do not\")]\n    }\n\n    @classmethod\n    def _show_text_part_signature(self, status):\n        # Within a text part, mixed state is equivalent to no encryption, and\n        # no signature - the signed/encrypted parts are explictly marked.\n        try:\n            if status and status.startswith('mixed-'):\n                status = 'none'\n        except UndefinedError:\n            status = 'none'\n        return self._show_message_signature(status)\n\n    @classmethod\n    def _show_message_signature(self, status):\n        # This avoids crashes when attributes are missing.\n        try:\n            if status.startswith('hack the planet'):\n                pass\n        except UndefinedError:\n            status = ''\n\n        color, icon, text, message = self._STATUS_SIGNATURE.get(status, self._DEFAULT_SIGNATURE)\n\n        return {\n            'color': color,\n            'icon': icon,\n            'text': text,\n            'message': message\n        }\n\n    _DEFAULT_ENCRYPTION = [\n        \"crypto-color-gray\",\n        \"icon-lock-open\",\n        _(\"Unknown\"),\n        _(\"There is some unknown thing wrong with this encryption\")]\n    _STATUS_ENCRYPTION = {\n        \"none\": [\n            \"crypto-color-gray\",\n            \"icon-lock-open\",\n            _(\"Not Encrypted\"),\n            _(\"This content was not encrypted. It could have been intercepted \"\n              \"and read by an unauthorized party\")],\n        \"decrypted\": [\n            \"crypto-color-green\",\n            \"icon-lock-closed\",\n            _(\"Encrypted\"),\n            _(\"This content was encrypted, great job being secure\")],\n        \"mixed-decrypted\": [\n            \"crypto-color-blue\",\n            \"icon-lock-closed\",\n            _(\"Mixed Encrypted\"),\n            _(\"Part of this message were encrypted, but other parts were not \"\n              \"encrypted\")],\n        \"lockedkey\": [\n            \"crypto-color-green\",\n            \"icon-lock-closed\",\n            _(\"Locked Key\"),\n            _(\"You have the encryption key to decrypt this, \"\n              \"but the key itself is locked.\")],\n        \"mixed-lockedkey\": [\n            \"crypto-color-green\",\n            \"icon-lock-closed\",\n            _(\"Mixed Locked Key\"),\n            _(\"Parts of the message could not be decrypted because your \"\n              \"encryption key is locked.\")],\n        \"missingkey\": [\n            \"crypto-color-red\",\n            \"icon-lock-closed\",\n            _(\"Missing Key\"),\n            _(\"You don't have the encryption key to decrypt this, \"\n              \"perhaps it was encrypted to an old key you don't have anymore?\")],\n        \"mixed-missingkey\": [\n            \"crypto-color-red\",\n            \"icon-lock-closed\",\n            _(\"Mixed Missing Key\"),\n            _(\"Parts of the message could not be decrypted because you \"\n              \"are missing the private key. Perhaps it was encrypted to an \"\n              \"old key you don't have anymore?\")],\n        \"error\": [\n            \"crypto-color-red\",\n            \"icon-lock-error\",\n            _(\"Error\"),\n            _(\"We failed to decrypt and are unsure why.\")],\n        \"mixed-error\": [\n            \"crypto-color-red\",\n            \"icon-lock-error\",\n            _(\"Mixed Error\"),\n            _(\"We failed to decrypt parts of this message and are unsure why\")]\n    }\n\n    @classmethod\n    def _show_text_part_encryption(self, status):\n        # Within a text part, mixed state is equivalent to no encryption, and\n        # no signature - the signed/encrypted parts are explictly marked.\n        try:\n            if status and status.startswith('mixed-'):\n                status = 'none'\n        except UndefinedError:\n            status = 'none'\n        return self._show_message_encryption(status)\n\n    @classmethod\n    def _show_message_encryption(self, status):\n        # This avoids crashes when attributes are missing.\n        try:\n            if status.startswith('hack the planet'):\n                pass\n        except UndefinedError:\n            status = ''\n\n        color, icon, text, message = self._STATUS_ENCRYPTION.get(status, self._DEFAULT_ENCRYPTION)\n\n        return {\n            'color': color,\n            'icon': icon,\n            'text': text,\n            'message': message\n        }\n\n    _DEFAULT_CRYPTO_POLICY = [\n        _(\"Automatic\"),\n        _(\"Mailpile will intelligently try to guess and suggest the best \"\n          \"security with the given contact\")]\n    _CRYPTO_POLICY = {\n        \"default\": [\n            _(\"Automatic\"),\n            _(\"Mailpile will intelligently try to guess and suggest the best \"\n              \"security with the given contact\")],\n        \"none\": [\n            _(\"Don't Sign or Encrypt\"),\n            _(\"Messages will not be encrypted nor signed by your encryption key\")],\n        \"sign\": [\n            _(\"Only Sign\"),\n            _(\"Messages will only be signed by your encryption key\")],\n        \"encrypt\": [\n            _(\"Only Encrypt\"),\n            _(\"Messages will only be encrypted but not signed by your encryption key\")],\n        \"sign-encrypt\": [\n            _(\"Always Encrypt & Sign\"),\n            _(\"Messages will be both encrypted and signed by your encryption key\")]\n    }\n\n    @classmethod\n    def _show_crypto_policy(self, policy):\n        # This avoids crashes when attributes are missing.\n        try:\n            if policy.startswith('hack the planet'):\n                pass\n        except UndefinedError:\n            policy = ''\n\n        text, message = self._CRYPTO_POLICY.get(policy, self._DEFAULT_CRYPTO_POLICY)\n\n        return {\n            'text': text,\n            'message': message\n        }\n\n    def _contact_url(self, person):\n        if not self._is_dev_version():\n            return ('%s/search/?q=email:%s'\n                    ) % (self.env.session.config.sys.http_path,\n                         person['address'])\n\n        if 'contact' in person['flags']:\n            url = (\"%s/contacts/view/%s/\"\n                   % (self.env.session.config.sys.http_path,\n                      person['address']))\n        else:\n            url = \"%s/#add-contact\" % self.env.session.config.sys.http_path\n        return url\n\n    def _contact_name(self, person):\n        self._debug('contact_name(%s)' % (person,))\n        name = person['fn']\n        if (not name or '@' in name) and person.get('email'):\n            vcard = self.env.session.config.vcards.get_vcard(person['email'])\n            if vcard:\n                return vcard.fn\n        return name\n\n    @classmethod\n    def _thread_upside_down(self, thread):\n        return [(i, flip_unicode_boxes(a), c) for i, a, c in reversed(thread)]\n\n    URL_RE_HTTP = re.compile('(<a [^>]*?)'            # 1: <a\n                             '(href=[\"\\'])'           # 2:    href=\"\n                             '(https?:[^>]+)'         # 3:  URL!\n                             '([\"\\'][^>]*>)'          # 4:          \">\n                             '(.*?)'                  # 5:  Description!\n                             '(</a>)')                # 6: </a>\n\n    # We deliberately leave the https:// prefix on, because it is both\n    # rare and worth drawing attention to.\n    URL_RE_HTTP_PROTO = re.compile('(?i)^https?://((w+\\d*|[a-z]+\\d+)\\.)?')\n\n    URL_RE_MAILTO = re.compile('(<a [^>]*?)'          # 1: <a\n                               '(href=[\"\\']mailto:)'  # 2:    href=\"mailto:\n                               '([^\"]+)'              # 3:  Email address!\n                               '([\"\\'][^>]*>)'        # 4:          \">\n                               '(.*?)'                # 5:  Description!\n                               '(</a>)')              # 6: </a>\n\n    URL_DANGER_ALERT = ('onclick=\\'return confirm(\"' +\n                        _(\"Mailpile security tip: \\\\n\\\\n\"\n                          \"  Uh oh! This web site may be dangerous!\\\\n\"\n                          \"  Are you sure you want to continue?\\\\n\") +\n                        '\");\\'')\n\n    def _fix_urls(self, text, truncate=45, danger=False):\n        def http_fixer(m):\n            url = m.group(3)\n            odesc = desc = m.group(5)\n            url_danger = danger\n\n            if len(desc) > truncate:\n                desc = desc[:truncate-3] + '...'\n                noproto = re.sub(self.URL_RE_HTTP_PROTO, '', desc)\n                if ('/' not in noproto) and ('?' not in noproto):\n                    # Phishers sometimes create subdomains that look like\n                    # something legit: yourbank.evil.com.\n                    # So, if the domain was getting truncated reveal the TLD\n                    # even if that means overflowing our truncation request.\n                    noproto = re.sub(self.URL_RE_HTTP_PROTO, '', odesc)\n                    if '/' in noproto:\n                        desc = '.'.join(noproto.split('/')[0]\n                                        .rsplit('.', 3)[-2:]) + '/...'\n                    else:\n                        desc = '.'.join(noproto.split('?')[0]\n                                        .rsplit('.', 3)[-2:]) + '/...'\n                    url_danger = True\n\n            return ''.join([m.group(1),\n                            url_danger and self.URL_DANGER_ALERT or '',\n                            ' target=_blank ',\n                            m.group(2), url, m.group(4), desc, m.group(6)])\n\n        def mailto_fixer(m):\n            return ''.join([m.group(1), 'href=\"mailto:', m.group(3),\n                            '\" class=\"compose-to-email\">',\n                            m.group(5), m.group(6)])\n\n        return Markup(re.sub(self.URL_RE_HTTP, http_fixer,\n                             re.sub(self.URL_RE_MAILTO, mailto_fixer,\n                                    text)))\n\n    @classmethod\n    def _min(self, sequence):\n        return min(sequence)\n\n    @classmethod\n    def _max(self, sequence):\n        return max(sequence)\n\n    @classmethod\n    def _random(self, sequence):\n        return sequence[random.randint(0, len(sequence)-1)]\n\n    @classmethod\n    def _truthy(cls, txt, default=False):\n        return truthy(txt, default=default)\n\n    def _is_dev_version(self):\n        return (self.env.session.config.web.developer_mode)\n\n    def _is_configured(self):\n        return (self.env.session.config.prefs.web_content != \"unknown\")\n\n    @classmethod\n    def _version_identifier(cls):\n        return VERSION_IDENTIFIER or APPVER\n\n    def _with_context(self, sequence, context=1):\n        return [[(sequence[j] if (0 <= j < len(sequence)) else None)\n                 for j in range(i - context, i + context + 1)]\n                for i in range(0, len(sequence))]\n\n    def _url_path_fix(self, *urlparts):\n        url = ''.join([unicode(p) for p in urlparts])\n        if url[:1] in ('/', ):\n            http_path = self.env.session.config.sys.http_path or ''\n            if not url.startswith(http_path+'/'):\n                url = http_path + url\n        return self._safe(url)\n\n    def _urlencode(self, s):\n        if type(s) == 'Markup':\n            s = s.unescape()\n        return Markup(urllib.quote_plus(unicode(s).encode('utf-8')))\n\n    def _selectattr(self, seq, attr, value=None):\n        if value is None:\n            return [s for s in seq if s.get(attr)]\n        else:\n            return [s for s in seq if s.get(attr) == value]\n\n    def _safe(self, s):\n        if type(s) == 'Markup':\n            return s.unescape()\n        else:\n            return Markup(s).unescape()\n\n    def _json(self, d):\n        json = self.env.session.ui.render_json(d)\n        # These are necessary so the browser doesn't get confused by things\n        # when JSON is included directly into the HTML as a <script>.\n        json = json.replace('<', '\\\\x3c')\n        json = json.replace('&', '\\\\x26')\n        return json\n\n    _JS_ESCAPES = (\n            ('\\\\', '\\\\x5c'),\n            ('\\'', '\\\\x27'),\n            ('\"', '\\\\x22'),\n            ('>', '\\\\x3e'),\n            ('<', '\\\\x3c'),\n            ('&', '\\\\x26'),\n            ('=', '\\\\x3d'),\n            ('-', '\\\\x2d'),\n            (';', '\\\\x3b'),\n    )\n\n    def _escapejs(self, value):\n        \"\"\" Hex encodes some characters for use in JavaScript strings.\n\n        Lightly inspired from https://github.com/django/django/blame/ebc773ada3e4f40cf5084268387b873d7fe22e8b/django/utils/html.py#L63\n        \"\"\"\n        for bad, good in self._JS_ESCAPES:\n            value = value.replace(bad, good)\n        return self._safe(value)\n\n    @classmethod\n    def _nice_text(self, text):\n        trimmed = ''\n        previous = 'not'\n        for line in text.splitlines():\n            if line or previous == 'not':\n                trimmed += line + '\\n'\n                if line:\n                    previous = 'not'\n                else:\n                    previous = 'blank'\n        return trimmed.strip()\n\n    _TEXT_LINEBREAK_RE = re.compile(r'(?:\\r\\n|\\r|\\n)')\n\n    @classmethod\n    def _to_br(self, text):\n        \"\"\" Replaces \\n by <br />\n\n        Inspired from http://jinja.pocoo.org/docs/dev/api/#custom-filters\n        \"\"\"\n        result = '<br />'.join(p for p in self._TEXT_LINEBREAK_RE.split(escape(text)))\n        return Markup(result)\n\n    @classmethod\n    def _nice_subject(self, subject):\n        if subject:\n            output = re.sub('(?i)^((re|fw|fwd|aw|wg):\\s+)+', '', subject)\n        else:\n            output = '(' + _(\"No Subject\") + ')'\n        return output\n\n    @classmethod\n    def _bare_subject(self, subject):\n        if subject:\n            output = re.sub('(?i)^((re|fw|fwd|aw|wg):\\s+|\\[\\S+\\]\\s+)+', '', subject)\n        else:\n            output = '(' + _(\"No Subject\") + ')'\n        return output\n\n    @classmethod\n    def _get_all(self, pairs, name):\n        return [v for n, v in pairs if n.lower() == name.lower()]\n\n    def _get_addresses(self, pairs, name):\n        from mailpile.mailutils.addresses import AddressHeaderParser\n        config = self.env.session.config\n\n        addresses = []\n        for hdr in self._get_all(pairs, name):\n            addresses.extend(AddressHeaderParser(unicode_data=hdr))\n\n        for ai in addresses:\n            vcard = config.vcards.get_vcard(ai.address)\n            if vcard:\n                ai.merge_vcard(vcard)\n\n        return addresses\n\n    @classmethod\n    def _nice_name(self, name, truncate=100):\n        if len(name) > truncate:\n            name = name[:truncate-3] + '...'\n        return name\n\n    @classmethod\n    def _recipient_summary(self, editing_strings, addresses, truncate):\n        summary_list = []\n        recipients = (editing_strings.get('to_aids', []) +\n                      editing_strings.get('cc_aids', []) +\n                      editing_strings.get('bcc_aids', []))\n        for aid in recipients:\n            summary_list.append(addresses[aid].fn)\n        summary = ', '.join(summary_list)\n        if len(summary) > truncate:\n            others = ''\n            if len(recipients) > 1:\n                others = _(\"and %d others\") % (len(recipients) - 1)\n            summary = summary[:truncate] + '... ' + others\n        return summary\n\n    @classmethod\n    def _attachment_type(self, mime):\n        if mime in [\n            \"application/octet-stream\",\n            \"application/mac-binhex40\",\n            \"application/x-shockwave-flash\",\n            \"application/x-director\",\n            \"application/x-x509-ca-cert\",\n            \"application/x-director\",\n            \"application/x-msdownload\",\n            \"application/x-director\"\n            ]:\n            attachment = \"application\"\n        elif mime in [\n            \"application/x-compress\",\n            \"application/x-compressed\",\n            \"application/x-tar\",\n            \"application/zip\",\n            \"application/x-stuffit\",\n            \"application/x-gzip\",\n            \"application/x-gzip-compressed\",\n            \"application/x-tar\",\n            \"application/x-winzip\",\n            \"application/x-zip\",\n            \"application/x-zip-compressed\"\n            ]:\n            attachment = \"archive\"\n        elif mime in [\n            \"audio/midi\",\n            \"audio/mid\",\n            \"audio/mpeg\",\n            \"audio/basic\",\n            \"audio/x-aiff\",\n            \"audio/x-pn-realaudio\",\n            \"audio/x-pn-realaudio\",\n            \"audio/mid\",\n            \"audio/basic\",\n            \"audio/x-wav\",\n            \"audio/x-mpegurl\",\n            \"audio/wave\",\n            \"audio/wav\"\n            ]:\n            attachment = \"audio\"\n        elif mime in [\n            \"text/x-vcard\"\n            ]:\n            attachment = \"contact\"\n        elif mime in [\n            \"image/bmp\",\n            \"image/gif\",\n            \"image/jpeg\",\n            \"image/pjpeg\",\n            \"image/svg+xml\",\n            \"image/x-png\",\n            \"image/png\"\n            ]:\n            attachment = \"image-visible\"\n        elif mime in [\n            \"image/cis-cod\",\n            \"image/ief\",\n            \"image/pipeg\",\n            \"image/tiff\",\n            \"image/x-cmx\",\n            \"image/x-cmu-raster\",\n            \"image/x-rgb\",\n            \"image/x-icon\",\n            \"image/x-xbitmap\",\n            \"image/x-xpixmap\",\n            \"image/x-xwindowdump\",\n            \"image/x-portable-anymap\",\n            \"image/x-portable-graymap\",\n            \"image/x-portable-pixmap\",\n            \"image/x-portable-bitmap\",\n            \"application/x-photoshop\",\n            \"application/postscript\"\n            ]:\n            attachment = \"image\"\n        elif mime in [\n            \"application/pgp-signature\"\n            ]:\n            attachment = \"signature\"\n        elif mime in [\n            \"application/pgp-keys\"\n            ]:\n            attachment = \"keys\"\n        elif mime in [\n            \"application/rtf\",\n            \"application/vnd.ms-works\",\n            \"application/msword\",\n            \"application/pdf\",\n            \"application/x-download\",\n            \"message/rfc822\",\n            \"text/scriptlet\",\n            \"text/plain\",\n            \"text/iuls\",\n            \"text/plain\",\n            \"text/richtext\",\n            \"text/x-setext\",\n            \"text/x-component\",\n            \"text/webviewhtml\",\n            \"text/h323\"\n            ]:\n            attachment = \"document\"\n        elif mime in [\n            \"application/x-javascript\",\n            \"text/html\",\n            \"text/css\",\n            \"text/xml\",\n            \"text/json\"\n            ]:\n            attachment = \"code\"\n        elif mime in [\n            \"application/excel\",\n            \"application/msexcel\",\n            \"application/vnd.ms-excel\",\n            \"application/vnd.msexcel\",\n            \"application/csv\",\n            \"application/x-csv\",\n            \"text/tab-separated-values\",\n            \"text/x-comma-separated-values\",\n            \"text/comma-separated-values\",\n            \"text/csv\",\n            \"text/x-csv\"\n            ]:\n            attachment = \"spreadsheet\"\n        elif mime in [\n            \"application/powerpoint\",\n            \"application/vnd.ms-powerpoint\"\n            ]:\n            attachment = \"slideshow\"\n        elif mime in [\n            \"video/quicktime\",\n            \"video/x-sgi-movie\",\n            \"video/mpeg\",\n            \"video/x-la-asf\",\n            \"video/x-ms-asf\",\n            \"video/x-msvideo\"\n            ]:\n            attachment = \"video\"\n        else:\n            attachment = \"unknown\"\n        return attachment\n\n    def _theme_settings(self):\n        self._debug('theme_settings()')\n        path, handle, mime = self.env.session.config.open_file('html_theme', 'theme.json')\n        return json.load(handle)\n\n    def _nice_fingerprint(self, fingerprint):\n        if fingerprint:\n            slices = [fingerprint[i:i + 4] for i in range(0, len(fingerprint), 4)]\n            output = \"\"\n            for group in slices:\n                output += group + \" \"\n            return output\n        else:\n            return _(\"No Fingerprint\")\n\n    def _group_fingerprint(self, fingerprint, size=4):\n        if fingerprint:\n            return [fingerprint[i:i + size] for i in range(0, len(fingerprint), size)]\n        else:\n            return []\n\n    def _make_filter_groups(self, tags):\n        split = shlex.split(tags)\n        output = dict();\n        add = []\n        remove = []\n        for item in split:\n            out = item.strip('+-')\n            if item[0] == \"+\":\n                add.append(out)\n            elif item[0] == \"-\":\n                remove.append(out)\n        output['add'] = add\n        output['remove'] = remove\n        return output\n\n    def _show_nagification(self, nag):\n        now = long((time.time() + 0.5) * 1000)\n        if now > nag and nag != -1:\n            return True\n        return False\n"
  },
  {
    "path": "mailpile/www/jinjaloader.py",
    "content": "import os\n\nfrom jinja2 import BaseLoader, TemplateNotFound\n\n\nclass MailpileJinjaLoader(BaseLoader):\n    \"\"\"\n    A Jinja2 template loader which uses the Mailpile configuration\n    and plugin system to find template files.\n    \"\"\"\n    def __init__(self, config):\n        self.config = config\n\n    def get_template_path(self, tpl):\n        return self.config.data_file_and_mimetype('html_theme', tpl)[0]\n\n    def get_source(self, environment, template):\n        tpl = os.path.join('html', template)\n\n        path = self.get_template_path(tpl)\n        if not path:\n            raise TemplateNotFound(tpl)\n\n        mtime = os.path.getmtime(path)\n        unchanged = lambda: (\n            path == self.get_template_path(tpl)\n            and mtime == os.path.getmtime(path))\n\n        with file(path) as f:\n            source = f.read().decode('utf-8')\n\n        return source, path, unchanged\n"
  },
  {
    "path": "mp.cmd",
    "content": "@echo off\nset PATH=%PATH%;GnuPG\\;OpenSSL\\;\nset PYTHONPATH=%~dp0;%~dp0\\GnuPG\\;%~dp0\\OpenSSL\\;\nif exist python27\\python.exe (\n  set PYTHONBIN=python27\\python.exe\n) else if exist c:\\python27\\python.exe (\n  set PYTHONBIN=c:\\python27\\python.exe\n) else (\n  set PYTHONBIN=python\n)\nREM i18n support doesn't work on Windows, default to English.\nset LANG=en\nSTART /B %PYTHONBIN% scripts\\mailpile %*\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"mailpile\",\n  \"version\": \"0.4.1\",\n  \"devDependencies\": {\n    \"grunt\": \"latest\",\n    \"grunt-contrib-concat\": \"latest\",\n    \"grunt-contrib-uglify\": \"latest\",\n    \"grunt-contrib-less\": \"latest\",\n    \"grunt-contrib-watch\": \"latest\"\n  }\n}\n"
  },
  {
    "path": "packages/Dockerfile_debian",
    "content": "# ----------------------------------------------------------------------------#\n# This Dockerfile creates a Debian package for mailpile.                      #\n#                                                                             #\n# Usage: cd .. && make dpkg                                                   #\n# ----------------------------------------------------------------------------#\nFROM debian:stretch\nMAINTAINER Alexandre Viau <aviau@debian.org>\n\n# The big list below is a subset of what we get from mk-build-deps; manually\n# copied so the Docker intermediate images will change a little bit less when\n# the Debian package rules themselves get updated. Less wasted bandwidth,\n# quicker development cycles...\nRUN apt-get update && \\\n    apt-get install -y -qq software-properties-common \\\n                           build-essential \\\n                           debhelper \\\n                           devscripts \\\n                           equivs \\\n  python-all python-bs4 python-dns python-funcsigs \\\n  python-jinja2 python-lxml python-markupsafe \\\n  python-mock python-nose python-pbr python-pgpdump \\\n  python-pil python-selenium python-setuptools python-webencodings \\\n  python-socksipychain xdg-utils\n\nRUN mkdir /root/mailpile /mnt/dist\nCOPY packages/debian /root/mailpile/debian\nCOPY dist/version.txt /root/mailpile-version.txt\nVOLUME /mnt/dist\n\nRUN ln -s /mnt/dist/mailpile.tar.gz /root/mailpile_$(cat /root/mailpile-version.txt).orig.tar.gz\nRUN sed -i \"s|<-- version -->|$(cat /root/mailpile-version.txt)-1|\" /root/mailpile/debian/changelog\n\nRUN mk-build-deps --install /root/mailpile/debian/control --tool \"apt-get --force-yes -y\"\n\nWORKDIR /root/mailpile\nENV DESTINATION_DPKG_DIR /mnt/dist\nCMD tar xvf ../mailpile_$(cat /root/mailpile-version.txt).orig.tar.gz -C ./ \\\n    && dpkg-buildpackage -us -uc -b \\\n    && cp ../*.deb /mnt/dist\n"
  },
  {
    "path": "packages/debian/changelog",
    "content": "mailpile (<-- version -->) unstable; urgency=medium\n\n  * Automated build.\n\n -- Mailpile Team <team@mailpile.is>  Mon, 28 Dec 2015 02:16:41 -0500\n"
  },
  {
    "path": "packages/debian/compat",
    "content": "9\n"
  },
  {
    "path": "packages/debian/control",
    "content": "Source: mailpile\nMaintainer: Alexandre Viau <aviau@debian.org>\nSection: mail\nPriority: optional\nBuild-Depends: debhelper (>= 9),\n               dh-python,\n               python-all,\n               python-setuptools,\n# requirements.txt\n               python-lxml,\n               python-jinja2,\n               python-markupsafe,\n               python-dns,\n               python-pgpdump,\n               python-fasteners,\n               python-pil,\n# requirements-dev.txt\n               python-mock,\n               python-selenium,\n               python-nose,\nStandards-Version: 3.9.6\nVcs-Git: git://anonscm.debian.org/collab-maint/mailpile.git\nVcs-Browser: http://anonscm.debian.org/cgit/collab-maint/mailpile.git\nHomepage: https://www.mailpile.is/\n\nPackage: mailpile\nArchitecture: all\nDepends: ${misc:Depends},\n         wordlist,\n         gnupg,\n         dirmngr,\n         spambayes,\n         python (<< 2.8),\n         python (>= 2.7.5),\n         python-lxml,\n         python-jinja2,\n         python-appdirs,\n         python-cryptography,\n         python-dns,\n         python-fasteners,\n         python-imgsize,\n         python-jinja2,\n         python-lxml,\n         python-markupsafe,\n         python-pgpdump,\n         python-pil,\n         python-icalendar,\n         python-socksipychain\nRecommends: tor, pagekite, python-stem, wamerican-small, gnupg-curl\nDescription: library and command-line interface for mailpile\n Mailpile is a modern, fast web-mail client with user-friendly encryption and\n privacy features. Mailpile places great emphasis on providing a clean, elegant\n user interface and pleasant user experience. In particular, Mailpile aims to\n make it easy and convenient to receive and send PGP encrypted or signed e-mail.\n .\n This package contains the core library and command-line interface\n\nPackage: mailpile-apache2\nArchitecture: all\nDepends: ${misc:Depends},\n         mailpile,\n         apache2,\n         python,\n         screen,\n         net-tools,\n         sudo\nDescription: multi-user mailpile web server\n Mailpile is a modern, fast web-mail client with user-friendly encryption and\n privacy features. Mailpile places great emphasis on providing a clean, elegant\n user interface and pleasant user experience. In particular, Mailpile aims to\n make it easy and convenient to receive and send PGP encrypted or signed e-mail.\n .\n This package configures Apache with \"Multipile\", a thin wrapper that allows\n system users to launch their Mailpile by logging in on the web interface.\n .\n Details: https://github.com/mailpile/Mailpile/tree/master/shared-data/multipile\n\nPackage: mailpile-desktop\nArchitecture: all\nDepends: ${misc:Depends},\n         mailpile,\n         screen,\n         xterm,\n         python,\n         python-gui-o-matic,\nDescription: GTK-based desktop integration for Mailpile\n Mailpile is a modern, fast web-mail client with user-friendly encryption and\n privacy features. Mailpile places great emphasis on providing a clean, elegant\n user interface and pleasant user experience. In particular, Mailpile aims to\n make it easy and convenient to receive and send PGP encrypted or signed e-mail.\n .\n This package provides a minimal native GUI for starting and stopping Mailpile,\n as well as receiving desktop notifications.\n"
  },
  {
    "path": "packages/debian/copyright",
    "content": "Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\nUpstream-Name: mailpile\nSource: https://github.com/mailpile/Mailpile\nFiles-Excluded: packages/debian\n                packages/macosx\n                packages/redhat\n                packages/windows\n                shared-data/default-theme/css/default.css\n                shared-data/default-theme/js/libraries.min.js\n                shared-data/contrib/forcegrapher\n                bower_components/animate.css\n                bower_components/autosize\n                bower_components/bootstrap\n                bower_components/d3\n                bower_components/dompurify\n                bower_components/eventEmitter\n                bower_components/favico.js\n                bower_components/html5shiv\n                bower_components/imagesloaded\n                bower_components/jquery\n                bower_components/jquery-confirm\n                bower_components/jquery-slugify\n                bower_components/jquery-timer\n                bower_components/jquery-ui\n                bower_components/jquery.ui\n                bower_components/jqueryui-touch-punch\n                bower_components/less-elements\n                bower_components/listjs\n                bower_components/mousetrap\n                bower_components/moxie\n                bower_components/plupload\n                bower_components/purl\n                bower_components/qtip2\n                bower_components/rebar\n                bower_components/select2\n                bower_components/typeahead.js\n                bower_components/underscore\n\n\nFiles: *\nCopyright: 2011-2015 Bjarni R. Einarsson, Mailpile ehf and friends.\nLicense: AGPL-3+\nComment: Upstream embeds the JavaScript code that was used to generate\n libraries.min.js into the tarball. Everything that is packaged in Debian\n is excluded and we generate our own version of libraries.min.js\n\nFiles: shared-data/default-theme/webfonts/*\n       shared-data/default-theme/less/app/webfonts.less\nCopyright: 2015 Mailpile Ehf. <team@mailpile.is>\nLicense: SIL-1.1\n\nFiles: mailpile/mail_generator.py\nCopyright: 2001-2010 Python Software Foundation <email-sig@python.org>\n           2014 Bjarni R. Einarsson <bre@mailpile.is>\nLicense: PSF\n\nFiles: debian/*\nCopyright: 2015 Alexandre Viau <aviau@debian.org>\nLicense: AGPL-3+\nComment: Debian packaging is licensed under the same terms as upstream\n\nLicense: AGPL-3+\n This program is free software: you can redistribute it and/or modify\n it under the terms of the GNU Affero General Public License as published by\n the Free Software Foundation, either version 3 of the License, or\n (at your option) any later version.\n .\n This program is distributed in the hope that it will be useful,\n but WITHOUT ANY WARRANTY; without even the implied warranty of\n MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n GNU Affero General Public License for more details.\n .\n You should have received a copy of the GNU Affero General Public License\n along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nLicense: SIL-1.1\n This Font Software is licensed under the SIL Open Font License, Version 1.1.\n This license is copied below, and is also available with a FAQ at:\n http://scripts.sil.org/OFL\n .\n -----------------------------------------------------------\n SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n -----------------------------------------------------------\n .\n PREAMBLE\n The goals of the Open Font License (OFL) are to stimulate worldwide\n development of collaborative font projects, to support the font creation\n efforts of academic and linguistic communities, and to provide a free and\n open framework in which fonts may be shared and improved in partnership\n with others.\n .\n The OFL allows the licensed fonts to be used, studied, modified and\n redistributed freely as long as they are not sold by themselves. The\n fonts, including any derivative works, can be bundled, embedded,\n redistributed and/or sold with any software provided that any reserved\n names are not used by derivative works. The fonts and derivatives,\n however, cannot be released under any other type of license. The\n requirement for fonts to remain under this license does not apply\n to any document created using the fonts or their derivatives.\n .\n DEFINITIONS\n \"Font Software\" refers to the set of files released by the Copyright\n Holder(s) under this license and clearly marked as such. This may\n include source files, build scripts and documentation.\n .\n \"Reserved Font Name\" refers to any names specified as such after the\n copyright statement(s).\n .\n \"Original Version\" refers to the collection of Font Software components as\n distributed by the Copyright Holder(s).\n .\n \"Modified Version\" refers to any derivative made by adding to, deleting,\n or substituting -- in part or in whole -- any of the components of the\n Original Version, by changing formats or by porting the Font Software to a\n new environment.\n .\n \"Author\" refers to any designer, engineer, programmer, technical\n writer or other person who contributed to the Font Software.\n .\n PERMISSION & CONDITIONS\n Permission is hereby granted, free of charge, to any person obtaining\n a copy of the Font Software, to use, study, copy, merge, embed, modify,\n redistribute, and sell modified and unmodified copies of the Font\n Software, subject to the following conditions:\n .\n 1) Neither the Font Software nor any of its individual components,\n in Original or Modified Versions, may be sold by itself.\n .\n 2) Original or Modified Versions of the Font Software may be bundled,\n redistributed and/or sold with any software, provided that each copy\n contains the above copyright notice and this license. These can be\n included either as stand-alone text files, human-readable headers or\n in the appropriate machine-readable metadata fields within text or\n binary files as long as those fields can be easily viewed by the user.\n .\n 3) No Modified Version of the Font Software may use the Reserved Font\n Name(s) unless explicit written permission is granted by the corresponding\n Copyright Holder. This restriction only applies to the primary font name as\n presented to the users.\n .\n 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\n Software shall not be used to promote, endorse or advertise any\n Modified Version, except to acknowledge the contribution(s) of the\n Copyright Holder(s) and the Author(s) or with their explicit written\n permission.\n .\n 5) The Font Software, modified or unmodified, in part or in whole,\n must be distributed entirely under this license, and must not be\n distributed under any other license. The requirement for fonts to\n remain under this license does not apply to any document created\n using the Font Software.\n .\n TERMINATION\n This license becomes null and void if any of the above conditions are\n not met.\n .\n DISCLAIMER\n THE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\n MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\n OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\n COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\n DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\n OTHER DEALINGS IN THE FONT SOFTWARE.\n\nLicense: PSF\n PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2\n --------------------------------------------\n .\n 1. This LICENSE AGREEMENT is between the Python Software Foundation\n (\"PSF\"), and the Individual or Organization (\"Licensee\") accessing and\n otherwise using this software (\"Python\") in source or binary form and\n its associated documentation.\n .\n 2. Subject to the terms and conditions of this License Agreement, PSF\n hereby grants Licensee a nonexclusive, royalty-free, world-wide\n license to reproduce, analyze, test, perform and/or display publicly,\n prepare derivative works, distribute, and otherwise use Python\n alone or in any derivative version, provided, however, that PSF's\n License Agreement and PSF's notice of copyright, i.e., \"Copyright (c)\n 2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation; All Rights\n Reserved\" are retained in Python alone or in any derivative version\n prepared by Licensee.\n .\n 3. In the event Licensee prepares a derivative work that is based on\n or incorporates Python or any part thereof, and wants to make\n the derivative work available to others as provided herein, then\n Licensee hereby agrees to include in any such work a brief summary of\n the changes made to Python.\n .\n 4. PSF is making Python available to Licensee on an \"AS IS\"\n basis.  PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR\n IMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND\n DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS\n FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT\n INFRINGE ANY THIRD PARTY RIGHTS.\n .\n 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON\n FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS\n A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,\n OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.\n .\n 6. This License Agreement will automatically terminate upon a material\n breach of its terms and conditions.\n .\n 7. Nothing in this License Agreement shall be deemed to create any\n relationship of agency, partnership, or joint venture between PSF and\n Licensee.  This License Agreement does not grant permission to use PSF\n trademarks or trade name in a trademark sense to endorse or promote\n products or services of Licensee, or any third party.\n .\n 8. By copying, installing or otherwise using Python, Licensee\n agrees to be bound by the terms and conditions of this License\n Agreement.\n"
  },
  {
    "path": "packages/debian/gbp.conf",
    "content": "[DEFAULT]\npristine-tar = True\n"
  },
  {
    "path": "packages/debian/mailpile-apache2.dirs",
    "content": "var/lib/mailpile/pids\n"
  },
  {
    "path": "packages/debian/mailpile-apache2.install",
    "content": "mailpile-apache etc/sudoers.d\nmailpile-apache.conf etc/mailpile\nshared-data/multipile/multipile.rc.sample etc/mailpile\nshared-data/multipile usr/share/mailpile\n"
  },
  {
    "path": "packages/debian/mailpile-apache2.links",
    "content": "usr/share/mailpile/multipile/mailpile-admin.py usr/share/mailpile/multipile/www/admin.cgi\n"
  },
  {
    "path": "packages/debian/mailpile-apache2.lintian-overrides",
    "content": "# we depend on python\npython-script-but-no-python-dep\n"
  },
  {
    "path": "packages/debian/mailpile-apache2.postinst",
    "content": "#!/bin/sh\n# postinst script for mailpile-apache2\n\nset -e\n\napache_install() {\n    mkdir -p /etc/apache2/conf-available\n\n    mkdir -p /var/lib/mailpile/apache\n    /usr/share/mailpile/multipile/mailpile-admin.py --configure-apache-usermap\n    chown -R root:www-data /var/lib/mailpile/apache\n    chmod -R 770 /var/lib/mailpile/apache\n\n    ln -sf /etc/mailpile/mailpile-apache.conf /etc/apache2/conf-available/mailpile.conf\n\n    a2enmod headers rewrite proxy proxy_http cgi\n\n    if [ -e /usr/share/apache2/apache2-maintscript-helper ] ; then\n        . /usr/share/apache2/apache2-maintscript-helper\n        apache2_invoke enconf mailpile\n    fi\n}\n\n\nif [ \"$1\" = \"configure\" ]; then\n    chmod go+rwxt /var/lib/mailpile/pids\n    apache_install $@\nfi\n\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "packages/debian/mailpile-apache2.postrm",
    "content": "#!/bin/sh\n# postrm script for mailpile-apache2\n\nset -e\n\napache_remove() {\n    if [ -e /usr/share/apache2/apache2-maintscript-helper ] ; then\n        . /usr/share/apache2/apache2-maintscript-helper\n        apache2_invoke disconf mailpile\n    fi\n    rm -f /etc/apache2/conf-available/mailpile.conf\n}\n\nif [ \"$1\" = \"purge\" ]; then\n    rm -rf /var/lib/mailpile\nfi\n\nif [ \"$1\" = \"remove\" ] || [ \"$1\" = \"purge\" ]; then\n    apache_remove $@\nfi\n\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "packages/debian/mailpile-desktop.install",
    "content": "shared-data/mailpile-gui usr/share/mailpile\n"
  },
  {
    "path": "packages/debian/mailpile-desktop.links",
    "content": "usr/share/mailpile/mailpile-gui/mailpile-gui.py usr/bin/mailpile-gui.py\nusr/share/mailpile/mailpile-gui/mailpile.desktop usr/share/applications/mailpile.desktop\nusr/share/mailpile/mailpile-gui/media/logo-color-wb.png usr/share/icons/hicolor/256x256/apps/mailpile.png\n"
  },
  {
    "path": "packages/debian/mailpile.install",
    "content": "usr/lib usr\nusr/bin usr\nshared-data/contrib usr/share/mailpile\nshared-data/locale usr/share/mailpile\nshared-data/default-theme usr/share/mailpile\n"
  },
  {
    "path": "packages/debian/mailpile.lintian-overrides",
    "content": "# Package depends on python, there is no python2 package to depend on.\npython-script-but-no-python-dep\n"
  },
  {
    "path": "packages/debian/mailpile.manpages",
    "content": "packages/mailpile.1\n"
  },
  {
    "path": "packages/debian/rules",
    "content": "#!/usr/bin/make -f\n\n# pybuild config\n#export PYBUILD_NAME=mailpile\nexport PYBUILD_DESTDIR_python2=debian/tmp\nexport PYBUILD_TEST_NOSE=1\nexport PYBUILD_DISABLE=test\n#export PYBUILD_TEST_ARGS={dir} --verbose\n\n# pbr version\nPKD  = $(abspath $(dir $(MAKEFILE_LIST)))\nexport PBR_VERSION = $(shell dpkg-parsechangelog -l$(PKD)/changelog --show-field Version | sed 's/-[^-]*$$//' | sed 's/[+~].*$$//')\n\n%:\n\tdh $@ --with python2 --buildsystem=pybuild\n\noverride_dh_auto_build:\n\t# Remove unused files\n\trm -f shared-data/default-theme/index.html\n\trm -f shared-data/default-theme/webfonts/LICENSE\n\trm -rf mailpile/tests\n\trm -rf shared-data/default-theme/less\n\n\t# Remove plugins that aren't fit for use\n\trm -f shared-data/contrib/{experiments,forcegrapher,maildeck,i18nhelper}\n\n\t########################\n\t# Generate apache conf #\n\t########################\n\tpython shared-data/multipile/mailpile-admin.py --generate-apache-config \\\n\t\t--mailpile-share /usr/share/mailpile \\\n\t\t--mailpile-theme /usr/share/mailpile/default-theme \\\n\t\t--multipile-www /usr/share/mailpile/multipile/www \\\n\t\t> mailpile-apache.conf\n\tpython shared-data/multipile/mailpile-admin.py --generate-apache-sudoers \\\n\t\t--mailpile-share /usr/share/mailpile \\\n\t\t> mailpile-apache\n\n\tdh_auto_build\n\noverride_dh_install:\n\t# FIXME: This is a build step, but the .mo files are ignored. :(\n\tscripts/compile-messages.sh\n\tdh_install\n"
  },
  {
    "path": "packages/debian/source/format",
    "content": "3.0 (quilt)\n"
  },
  {
    "path": "packages/debian/watch",
    "content": "version=2\nopts=filenamemangle=s/.+\\/v?(\\d\\S*)\\.tar\\.gz/mailpile-$1\\.tar\\.gz/ \\\n  https://github.com/mailpile/Mailpile/tags .*/v?(\\d\\S*)\\.tar\\.gz\n"
  },
  {
    "path": "packages/docker/entrypoint.sh",
    "content": "#!/bin/sh\n\nchown mailpile: /mailpile-data/ -R\n\nsu-exec mailpile \"$@\"\n"
  },
  {
    "path": "packages/macos/README.md",
    "content": "# README\n\nIn this README, we explain how to build and package Mailpile for macOS.\nWe also provide an overview of the files involved in the packaging process. \n\n## Packaging Scripts for macOS\nThe directory containing this README, contains packaging scripts and their required resource files.\n\n### Directory Contents\nThe following lists the files contained within this directory. The packaging scripts, which build and package Mailpile, are marked in bold.\n\n| File | Description |\n| ---- | ----------- |\n| appdmg.json.template\t\t\t| An [appdmg](https://www.npmjs.com/package/appdmg) specification. (Used by package.sh) |\n| background/background.png \t| A [.dmg](https://en.wikipedia.org/wiki/Apple_Disk_Image) background image for non-retina displays. (Used by package.sh.)|\n| background/background@2x.png| A .dmg background image for retina displays. (Used by package.sh.)|\n| **build.sh** \t\t\t\t\t\t| A script which builds Mailpile.app. |\n| configurator.sh \t\t\t\t| A script which is used by the built Mailpile.app, at runtime. It configures Mailpile.app's GUI. (Used by build.sh.)|\n| mailpile \t\t\t\t\t\t| A script which is used by the built Mailpile.app, at runtime. It sets environment variables and launches Mailpile. |\n| README.md \t\t\t\t\t\t| This file. |\n\n## Usage\nIn this section, we state requirements on the build machine, then we demonstrate how to use the packaging scripts.\n\n### Prerequisites\nThe following software must be installed prior to running the packaging scripts.\n\n- macOS 10.13 (or later) - Available in the App Store.\n- Xcode 9.3 (or later) - Available in the App Store.\n- Command Line Tools for Xcode - Install them by executing `xcode-select --install` in Terminal.app.\n- JDK 10 (or later) - Available on [Oracle's website](http://www.oracle.com/technetwork/java/javase/downloads/index.html).\n\nAnd either:\n\n- Node.js - Available on [nodejs.org](https://nodejs.org/en/). (Provides the following dependency, namely appdmg.)\n- appdmg - Install it by executing `npm install -g appdmg` in Terminal.app. (Make sure to add it's install target to *PATH*.)\n\nOr:\n\n- dmgbuild - Available from PyPI (pip install dmgbuild)\n\n### Requirements\nAn internet connection is required as the packaging scripts use [Homebrew](https://brew.sh) and git to fetch dependencies.\n\nYou must have installed your [Developer ID certificates](https://help.apple.com/xcode/mac/current/#/dev520c0324f) (both a *Developer ID Application* certificate and a *Developer ID Installer* certificate) into *Keychain Access.app*. See [developer.apple.com](https://developer.apple.com/support/certificates/) to learn how to obtain and install such certificates.\n\n### Environment\nBefore executing the package scripts, ensure that the following statements are true:\n\n- The directory in which appdmg was installed, is on *PATH*\n- You have set the `DMG_SIGNING_IDENTITY` environment variable to be the *ID* of your Developer Certificate. (The ID is the parenthesised part of the certificate's Common Name). This is needed because appdmg does not automatically select a signing certificate. Example: For a certificate which has the Common Name *Mac Developer: Petur Ingi Egilsson (4P78A94863)*, execute `export DMG_SIGNING_IDENTITY=4P78A94863` before launching the build scripts.\n- The directory ~/build is empty or non-existing.\n\n\n### Packaging Mailpile\nPackaging Mailpile is a three step process.\n\n1. Execute `export DMG_SIGNING_IDENTITY=4P78A94863` after replacing 4P78A94863 with your Developer Certificate's ID.\n2. Execute `./build.sh` in the directory which contains build.sh. This outputs ~/build/Mailpile.app and ~/build/Mailpile.dmg.\n\nYou might want to run ~/build/Mailpile.app to test the build before shipping ~/build/Mailpile.dmg.\n\n\n## Taxonomy\n| Term | Definition |\n| ---- | ---------- |\n| Mailpile | [Mailpile](https://github.com/mailpile/Mailpile) is a free & open modern, fast email client with user-friendly encryption and privacy features |\n| Mailpile.app | A macOS App ([Application Bundle](https://developer.apple.com/library/content/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html)) which contains Mailpile and it's dependencies. The app also contains a macOS desktop GUI for Mailpile - the GUI which is displayed at launch.\n"
  },
  {
    "path": "packages/macos/appdmg.json.template",
    "content": "{\n  \"title\": \"Mailpile\",\n  \"background\": \"BACKGROUND\",\n  \"icon-size\": \"112pt\",\n  \"window\": {\n    \"position\": {\"x\": 250, \"y\": 250},\n    \"size\": {\"width\": 440, \"height\": 320}\n  },\n  \"contents\": [\n    { \"x\": 300, \"y\": 160, \"type\": \"link\", \"path\": \"/Applications\" },\n    { \"x\": 80, \"y\": 160, \"type\": \"file\", \"path\": \"APP\", \"name\": \"Mailpile.app\" }\n  ],\n  \"format\": \"ULFO\",\n  \"code-sign\": { \n    \"signing-identity\": \"DMG_SIGNING_IDENTITY\"\n  }\n}\n\n"
  },
  {
    "path": "packages/macos/brew/symlinks.rb",
    "content": "class Symlinks < Formula\n  desc \"scan/change symbolic links\"\n  homepage \"http://www.ibiblio.org/pub/Linux/utils/file/symlinks.lsm\"\n  url \"http://www.ibiblio.org/pub/Linux/utils/file/symlinks-1.4.tar.gz\"\n  sha256 \"b0bb689dd0a2c46d9a7dd111b053707aba7b9cf29c4f0bad32984b14bdbe0399\"\n\n  def install\n    inreplace \"Makefile\", \"/usr/local/bin\", \"#{bin}/\"\n    inreplace \"Makefile\", \"/usr/local/man/man8\", \"#{man8}/\"\n    inreplace \"Makefile\", \"-o root -g root\", \"\"\n\n    mkdir_p \"#{bin}\"\n    mkdir_p \"#{man8}\"\n\n    ENV[\"CFLAGS\"]=\"-I/usr/include/malloc\"\n    system \"make\", \"CFLAGS=#{ENV.cflags}\"\n    system \"make\", \"install\"\n  end\n\n  test do\n    system \"#{bin}/symlinks\", \".\"\n  end\nend\n"
  },
  {
    "path": "packages/macos/build-script/00-check-dependencies",
    "content": "#!/bin/bash\nset -e\nset -x\n\n# Some of the homebrew build stuff depends on a java compiler.\nif ! javac -version&>/dev/null ; then\n  echo \"This script depends on javac\"\n  echo \"Please install version 10, or later, of the Java Developer Kit.\"\n  exit 1\nfi\n\n# The GUI-o-MacTic build fails without this\nif [ \"$DMG_SIGNING_IDENTITY\" = \"\" ]; then\n  echo \"Please set the DMG_SIGNING_IDENTITY environment variable.\"\n  exit 1\nfi\n\n\n"
  },
  {
    "path": "packages/macos/build-script/10-create-app-icons",
    "content": "#!/bin/bash\nset -e\nset -x\n\n[ \"$KEEP_BUNDLE\" = \"\" ] || exit 0\n[ \"$SKIP_GUI_O_MAC_TIC\" = \"\" ] || exit 0\n\nmkdir -p $ICONSET_DIR\nif [ ! -e \"$ICONSET_DIR/Icon-1024.png\" ]; then\n  export Icon1024=\"../icons/1024x1024.png\"\n  sips -z 16 16     $Icon1024 --out $ICONSET_DIR/Icon-16.png\n  sips -z 32 32     $Icon1024 --out $ICONSET_DIR/Icon-32.png\n  sips -z 32 32     $Icon1024 --out $ICONSET_DIR/Icon-33.png\n  sips -z 64 64     $Icon1024 --out $ICONSET_DIR/Icon-64.png\n  sips -z 128 128   $Icon1024 --out $ICONSET_DIR/Icon-128.png\n  sips -z 256 256   $Icon1024 --out $ICONSET_DIR/Icon-256.png\n  sips -z 256 256   $Icon1024 --out $ICONSET_DIR/Icon-257.png\n  sips -z 512 512   $Icon1024 --out $ICONSET_DIR/Icon-512.png\n  sips -z 512 512   $Icon1024 --out $ICONSET_DIR/Icon-513.png\n  cp $Icon1024 $ICONSET_DIR/Icon-1024.png\nfi\n"
  },
  {
    "path": "packages/macos/build-script/11-build-gui-o-mac-tic",
    "content": "#!/bin/bash\nset -e\nset -x\n\n[ \"$KEEP_BUNDLE\" = \"\" ] || exit 0\n[ \"$SKIP_GUI_O_MAC_TIC\" = \"\" ] || exit 0\n\n# Clean up...\nrm -rf \"$BUILD_DIR/gui-o-mac-tic\"\nif [ \"$GUI_O_MAC_TIC_REPO\" = \"\" ]; then\n    cp -va $SOURCE_DIR/submodules/gui-o-mac-tic $BUILD_DIR/gui-o-mac-tic\nelse\n    git clone --recursive -b \"$GUI_O_MAC_TIC_BRANCH\" \\\n        \"$GUI_O_MAC_TIC_REPO\" \"$BUILD_DIR/gui-o-mac-tic\"\nfi\n\n# Build GUI-o-Mac-tic\ncd $BUILD_DIR/gui-o-mac-tic\nrm -f src/Assets.xcassets/AppIcon.appiconset/Icon-*.png\nmv $ICONSET_DIR/* src/Assets.xcassets/AppIcon.appiconset/\nrmdir $ICONSET_DIR\n\ncp \"$SOURCE_DIR/packages/macos/configurator.sh\" \\\n  \"$BUILD_DIR/gui-o-mac-tic/share/configurator.sh\"\n\nperl -pi -e \"s/32325SM62E/$DMG_SIGNING_IDENTITY/g\" \\\n\t\"$BUILD_DIR/gui-o-mac-tic/GUI-o-Mac-tic.xcodeproj/project.pbxproj\"\nxcodebuild -project \"$BUILD_DIR/gui-o-mac-tic/GUI-o-Mac-tic.xcodeproj\" \\\n\tEXECUTABLE_NAME=Mailpile \\\n\tPRODUCT_BUNDLE_IDENTIFIER=is.mailpile.gui-o-mac-tic.mailpile \\\n\tPRODUCT_MODULE_NAME=Mailpile \\\n\tPRODUCT_NAME=Mailpile \\\n\tPROJECT_NAME=Mailpile \\\n\tWRAPPER_NAME=Mailpile.app\n\n# This dance gives us a fresh new Mailpile.app, but keeps the old homebrew\n# setup, if one exists.\nif [ -e \"$MAILPILE_BREW_ROOT\" ]; then\n    rm -rf $BUILD_DIR/last_brew\n    mv \"$MAILPILE_BREW_ROOT\" \"$BUILD_DIR/last_brew\"\nfi\nmkdir -p \"$BUILD_DIR/last_brew\"\nrm -rf \"$BUILD_DIR/Mailpile.app\"\nmv \"$BUILD_DIR/gui-o-mac-tic/build/Release/Mailpile.app\" \"$BUILD_DIR/\"\nmv \"$BUILD_DIR/last_brew\" \"$MAILPILE_BREW_ROOT\"\n\n# Cleanup\nrm -rf \"$BUILD_DIR/gui-o-mac-tic\"\n\n#############################################\n# xcodebuild argument that might be useful. #\n#############################################\n#\n#Asset Catalog App Icon Set Name (ASSETCATALOG_COMPILER_APPICON_NAME) - Name of an asset catalog app icon set whose contents will be merged into the Info.plist.\n#Development Team (DEVELOPMENT_TEAM) - The team ID of a development team to use for signing certificates and provisioning profiles.\n#macOS Deployment Target (MACOSX_DEPLOYMENT_TARGET) - Code will load on this and later versions of macOS. Framework APIs that are unavailable in earlier versions will be weak-linked; your code should check for null function pointers or specific system versions before calling newer APIs.\n#Product Module Name (PRODUCT_MODULE_NAME) - The name to use for the source code module constructed for this target, and which will be used to import the module in implementation source files. Must be a valid identifier.\n#Product Name (PRODUCT_NAME) - This is the basename of the product generated by the target.\n#Project Name (PROJECT_NAME) - The name of the current project.\n#WRAPPER_NAME - Specifies the filename, including the appropriate extension, of the product bundle.\n#\n\n"
  },
  {
    "path": "packages/macos/build-script/20-build-homebrew",
    "content": "#!/bin/bash\nset -e\nset -x\n\nmkdir -p $MAILPILE_BREW_ROOT\ncd \"$MAILPILE_BREW_ROOT\"\n\nif [ \"$KEEP_BUNDLE\" = \"\" -a \"$SKIP_HOMEBREW\" = \"\" ]; then\n\n    # Install Mailpile Dependencies from homebrew.\n    #\n    [ -e \"$BUILD_DIR/Homebrew.git\"  ] && mv \"$BUILD_DIR/Homebrew.git\" .git || true\n    [ -e \"$BUILD_DIR/Homebrew.lib\"  ] && mv \"$BUILD_DIR/Homebrew.lib\" Library/Homebrew || true\n    [ -e \"$BUILD_DIR/Homebrew.taps\" ] && mv \"$BUILD_DIR/Homebrew.taps\" Library/Taps || true\n\n    [ -e bin/brew ] || \\\n        curl -kL https://github.com/Homebrew/brew/tarball/master \\\n        | tar xz --strip 1\n    echo\n    brew update\n    brew install openssl@$OPENSSL_VERSION \\\n        || brew upgrade openssl@$OPENSSL_VERSION\n    brew link --force openssl@$OPENSSL_VERSION\n    brew install \"$SYMLINKS_SRC\"\n    for p in \\\n        libjpeg \\\n        gnupg@$GNUPG_VERSION \\\n        python@$PYTHON_MAJOR_VERSION \\\n        tor \\\n    ; do\n        brew install \"$p\" || brew upgrade \"$p\"\n    done\nfi\n\n[ -e .git ]             && mv .git             \"$BUILD_DIR/Homebrew.git\"  || true\n[ -e Library/Homebrew ] && mv Library/Homebrew \"$BUILD_DIR/Homebrew.lib\"  || true\n[ -e Library/Taps ]     && mv Library/Taps     \"$BUILD_DIR/Homebrew.taps\" || true\n"
  },
  {
    "path": "packages/macos/build-script/21-install-mailpile-deps",
    "content": "#!/bin/bash\nset -e\nset -x\n\n[ \"$SKIP_PYTHON_DEPS\" = \"\" ] || exit 0\n\n# Install Mailpile Python Dependencies with PIP. \n#\ncd \"$SOURCE_DIR\"\nif [ \"$KEEP_BUNDLE\" = \"\" ]; then\n    pip install -r requirements.txt --ignore-installed\nelse\n    # This is the KEEP_BUNDLE mode; just add new things\n    pip install -r requirements.txt\nfi\n"
  },
  {
    "path": "packages/macos/build-script/30-slim-down",
    "content": "#!/bin/bash\n#\n# Note: We do this before installing Mailpile, to make sure we never\n#       accidentally delete part of the app itself. And we do this before\n#       the library fixup/cleanup scripts, to save cycles during build.\n#\nset -e\nset -x\n\ncd \"$MAILPILE_BREW_ROOT\"\n\nrm -f bin/*.py bin/*.pyc bin/*.pyo\n#find . -name \\*.pyc      -type f | xargs rm -f\n#find . -name \\*.pyo      -type f | xargs rm -f\n\nfind . -name man          -type d | xargs rm -rf\nfind . -name doc          -type d | xargs rm -rf\nfind . -name gtk-doc      -type d | xargs rm -rf\nfind . -name info         -type d | xargs rm -rf\n\n# By default, this rest are skipped because they break the build tools.\n[ \"$DO_CLEANUP\" = \"\" ] && exit 0\n\nfind . -name .brew        -type d | xargs rm -rf\nfind . -name test         -type d | xargs rm -rf\nfind . -name tests        -type d | xargs rm -rf\nfind . -name examples     -type d | xargs rm -rf\nfind . -name aclocal      -type d | xargs rm -rf\nfind . -name pkgconfig    -type d | xargs rm -rf\nfind . -name emacs        -type d | xargs rm -rf\nfind . -name include      -type d | xargs rm -rf\nfind . -name lib-tk       -type d | xargs rm -rf\nfind . -name tkinter      -type d | xargs rm -rf\nfind . -name lib2to3      -type d | xargs rm -rf\nfind . -name Extras       -type d | xargs rm -rf\nfind . -name sphinx-doc   -type d | xargs rm -rf\n\nfind . -name \\*.c         -type f | xargs rm -f\nfind . -name \\*.h         -type f | xargs rm -f\nfind . -name \\*.a         -type f | xargs rm -f\nfind . -name .github      -type f | xargs rm -f\nfind . -name .travis.yml  -type f | xargs rm -f\nfind . -name TODO         -type f | xargs rm -f\nfind . -name NEWS         -type f | xargs rm -f\nfind . -name README       -type f | xargs rm -f\n\nrm -rf bin/sb_*py bin/tor-prompt bin/pbr docs etc/tor etc/pkcs11\nrm -rf Cellar/adns/*/bin/*\nrm -rf Cellar/gdbm/*/bin\nrm -rf Cellar/gettext/*/bin\nrm -rf Cellar/jpeg/9c/*/bin\nrm -rf Cellar/libtasn1/*/bin\nrm -rf Cellar/nettle/*/bin\n\n# Remove any dangling symlinks (we may have just removed their targets)\nfind -L . -type l -exec rm -f \\{\\} \\;\n"
  },
  {
    "path": "packages/macos/build-script/55-install-mailpile",
    "content": "#!/bin/bash\nset -e\nset -x\n\n[ \"$SKIP_MAILPILE\" = \"\" ] || exit 0\n\nTARGET=\"$BUILD_DIR/Mailpile.app/Contents/Resources/app/opt/mailpile\"\n\n# Install Mailpile\ncd \"$SOURCE_DIR\"\nrm -rf \"$TARGET\"\nmkdir \"$TARGET\"\ncp -va \\\n    mailpile \\\n    scripts \\\n    shared-data \\\n    \"$TARGET\"\n\n# These will have been compiled by the wrong python, if present,\n# so get rid of them.\nfind \"$TARGET\" -name \\*.pyc -type f | xargs rm -f\nfind \"$TARGET\" -name \\*.pyo -type f | xargs rm -f\n\n# Install wrapper so mailpile can launch with correct path and python path\ncp -a packages/macos/mailpile \"$BUILD_DIR/Mailpile.app/Contents/Resources/app/bin/\"\n"
  },
  {
    "path": "packages/macos/build-script/90-fix-libraries",
    "content": "#!/bin/bash\nset -e\nset -x\n\n# Make library paths relative\n#\n_readlink() {\n    FN=\"$1\"\n    LN=\"$(readlink $1)\"\n    while [ \"$LN\" != \"\" -a \"$LN\" != \"$FN\" ]; do\n        FN=\"$LN\"\n        LN=\"$(readlink $1)\"\n    done\n    echo \"$FN\"\n}\n_relpath() {\n    TARGET=\"$1\"\n    SOURCE=\"$2\"\n    cat <<tac |python\nimport os, sys\nsource, target = os.path.abspath('$SOURCE').split('/'), '$TARGET'.split('/')\nwhile target and source and target[0] == source[0]:\n    target.pop(0)\n    source.pop(0)\nprint '/'.join(['..' for i in source] + target)\ntac\n}\n_fixup_lib_names() {\n    bin=\"$1\"\n    typ=\"$2\"\n    otool -L \"$bin\" |grep \"$MAILPILE_BREW_ROOT\" |while read lib JUNK; do\n        bin_path=\"$(_readlink \"$bin\")\"\n        new=$(_relpath \"$lib\" $(dirname \"$bin_path\"))\n        chmod u+w \"$bin\" \"$lib\"\n        echo install_name_tool -change \"$lib\" \"$typ/$new\" \"$bin_path\"\n        install_name_tool -change \"$lib\" \"$typ/$new\" \"$bin_path\"\n        install_name_tool -id $(basename \"$lib\") \"$lib\"\n        chmod u-w \"$bin\" \"$lib\"\n    done\n}\n_remove_arch() {\n    lipo -output \"$2\".tmp -remove \"$1\" \"$2\" \\\n        && mv -f \"$2\".tmp \"$2\" \\\n        || rm -f \"$2\".tmp\n}\ncd \"$MAILPILE_BREW_ROOT/bin\"\nfor bin in *; do\n#   _remove_arch i386 \"$bin\"\n    _fixup_lib_names \"$bin\" \"@executable_path\"\ndone\n\ncd \"$MAILPILE_BREW_ROOT\"\n(\n    find . -type f -name 'Python'\n    find Cellar opt -type f -name '*.dylib'\n    find Cellar opt -type f -name '*.so'\n) | while read bin; do\n#   _remove_arch i386 \"$bin\"\n    _fixup_lib_names \"$bin\" \"@loader_path\"\ndone\n\n"
  },
  {
    "path": "packages/macos/build-script/91-fix-paths",
    "content": "#!/bin/bash\nset -e\nset -x\n\n# Make symbolic links relative. This needs to run twice... just because.\ncd \"$MAILPILE_BREW_ROOT\"\n./bin/symlinks -s -c -r \"$MAILPILE_BREW_ROOT\"\n./bin/symlinks -s -c -r \"$MAILPILE_BREW_ROOT\"\n./bin/symlinks -d \"$MAILPILE_BREW_ROOT\"\n\n#\n# Fix brew's Python to not hardcode the full path\n#\ncd \"$MAILPILE_BREW_ROOT\"\nfor target in /lib/python$PYTHON_VERSION/site-packages/sitecustomize.py \\\n              /Cellar/python@$PYTHON_MAJOR_VERSION/$PYTHON_VERSION.*/Frameworks/Python.framework/Versions/$PYTHON_VERSION/lib/python$PYTHON_VERSION/_sysconfigdata.py \\\n; do\n    perl -pi.bak -e \\\n        \"s|'$MAILPILE_BREW_ROOT|__file__.replace('$target', '') + '|g\" \\\n        .$target\ndone\n\n"
  },
  {
    "path": "packages/macos/build-script/92-fix-python-launcher",
    "content": "#!/bin/bash\nset -e\nset -x\n\n# Fix Python's launcher to avoid the rocket ship icon shows when\n# launching python apps.\n#\nLSUIELEM=\"<key>LSUIElement</key><string>1</string>\"\nperl -pi.bak -e \\\n    \"s|(\\\\s+)(<key>CFBundleDocumentTypes)|\\\\1$LSUIELEM\\\\2|\" \\\n    ./Cellar/python@$PYTHON_MAJOR_VERSION/*/Frame*/Python*/V*/C*/Res*/Python.app/Cont*/Info.plist\n\n\n"
  },
  {
    "path": "packages/macos/build-script/98-codesign",
    "content": "#!/bin/bash\nset -e\nset -x\n\nCODESIGN_ID=$(security find-identity \\\n    |grep $DMG_SIGNING_IDENTITY \\\n    |grep 'Developer ID Application' \\\n    |awk '{print $2}' \\\n    |head -1)\n\n# Codesign all the homebrew'ed binaries.\ncd \"$MAILPILE_BREW_ROOT\"\n(\n    find . -type f -name 'Python'\n    find bin -type f |grep -v -e \\.py\n    find Cellar opt -type f -name '*.dylib'\n    find Cellar opt -type f -name '*.so'\n) | while read bin; do\n    codesign -dvvvv  --force --sign \"$CODESIGN_ID\" \"$bin\"\ndone\n\ncodesign -dvvvv  --force --sign \"$CODESIGN_ID\" \"$BUILD_DIR/Mailpile.app\"\n"
  },
  {
    "path": "packages/macos/build-script/99-make-dmg",
    "content": "#!/bin/bash\nset -e\nset -x\n\ncd \"$(dirname \"$0\")/..\"\nexport APPDMG_TEMPLATE=appdmg.json.template\nexport TARGET=$BUILD_DIR/Mailpile.dmg\nexport ASSETS_DIR=\"$(pwd)\"\nexport BACKGROUND=$ASSETS_DIR/background/background.png\nexport APP=$BUILD_DIR/Mailpile.app\n\n# Ensure dependencies are met.\ncommand -v appdmg >/dev/null 2>&1 || \\\n  command -v dmgbuild >/dev/null 2>&1 || {\n    cat <<tac >&2\n\nThis script depends on 'appdmg' or 'dmgbuild'.\nNeither were found on PATH; please ensure appdmg or dmgbuild are installed.\nFor more information, see:\n   - https://github.com/LinusU/node-appdmg.\n   - https://dmgbuild.readthedocs.io\n\nAborting.\ntac\n    exit 1\n}\n\n# Check if the ID of the key, to be used for signing, is set.\nif [ -z ${DMG_SIGNING_IDENTITY+x} ]\nthen\n    cat <<tac\n\nThe environment variable DMG_SIGNING_IDENTITY must be set to the ID of a\ncertificate, which is located within Keychain Access.app, which is to be\nused to sign the .dmg.\n\nExample:\n\n   To use a cert with the common name 'Mac Developer: John Doe (4P78A94863)',\n   set DMG_SIGNING_IDENTITY to 4P78A94863.\n\nAborting.\ntac\n    exit 1\nfi\nCODESIGN_ID=$(security find-identity \\\n    |grep $DMG_SIGNING_IDENTITY \\\n    |grep 'Developer ID Application' \\\n    |awk '{print $2}' \\\n    |head -1)\n\nexport APPDMG_CONFIG=\"$(/usr/bin/mktemp -d)/appdmg.json\"\nsed -e \"s|DMG_SIGNING_IDENTITY|$DMG_SIGNING_IDENTITY|g;\" \\\n    -e \"s|BACKGROUND|$BACKGROUND|g;\" \\\n    -e \"s|APP|$APP|g\" \\\n    appdmg.json.template >\"$APPDMG_CONFIG\"\n\n[ -e \"$TARGET\" ] && mv -f \"$TARGET\" \"$TARGET.old\"\ncommand -v appdmg >/dev/null \\\n && appdmg \"$APPDMG_CONFIG\" \"$TARGET\" \\\n || (command -v dmgbuild >/dev/null \\\n &&  dmgbuild -s dmgbuild-settings.py Mailpile \"$TARGET\" \\\n &&  codesign --force --sign \"$CODESIGN_ID\" \"$TARGET\")\n\nrm -rf $(dirname \"$APPDMG_CONFIG\")\n"
  },
  {
    "path": "packages/macos/build.sh",
    "content": "#!/bin/bash\n#\n# This script will use Homebrew to build a complete environment for packacing\n# mailpile. This script is tested on macOS 10.13.4, with XCode 9.3.\n#\n# Note: In a couple of places, the $(cd FOO; pwd) construct is used to\n#       normalize paths. Without this we'd be chasing trailing slashes\n#       or other inconsistencies all over the place.\n#\nset -e\nexport SOURCE_DIR=$(cd $(dirname \"$0\")/../..; pwd)\nexport HOME=$(cd ~; pwd)\n\n# Target directories\nexport BUILD_DIR=$(cd ${BUILD_DIR:-~/build}; pwd)\nexport ICONSET_DIR=$BUILD_DIR/AppIcon.appiconset\nexport MAILPILE_BREW_ROOT=\"$BUILD_DIR/Mailpile.app/Contents/Resources/app\"\n\n# This has far-reaching and magical side-effects, including the use of\n# brew's Python and pip, and those installing in turn to the homebrew tree\n# instead of globally\nexport PATH=\"$MAILPILE_BREW_ROOT\"/bin:$PATH\n\n# Tools, versions\nexport GIT_SSL_NO_VERIFY=1\nexport HOMEBREW_CC=gcc-4.2\nexport MACOSX_DEPLOYMENT_TARGET=10.13\nexport PYTHON_MAJOR_VERSION=2\nexport PYTHON_VERSION=2.7\nexport GNUPG_VERSION=2.2\nexport OPENSSL_VERSION=1.0\nexport SYMLINKS_SRC=\"$SOURCE_DIR/packages/macos/brew/symlinks.rb\"\nexport KEYCHAIN=~/Library/Keychains/login.keychain\n\n# See this mailing list post: http://curl.haxx.se/mail/archive-2013-10/0036.html\nexport OSX_MAJOR_VERSION=\"$(sw_vers -productVersion | cut -d . -f 2)\"\nif [ $(echo \"$OSX_MAJOR_VERSION  < 9\" | bc) == 1 ]; then\n   export CURL_CA_BUNDLE=/usr/share/curl/curl-ca-bundle.crt\nfi\n\n# Load user settings/overrides/credentials:\n#\n#    1. DMG_SIGNING_IDENTITY=... # Needed by GUI-o-Mac-Tic code signing\n#    2. KEYCHAIN_PASSWORD=...    #  - ditto -\n#    3. HOME=/Users/botuser      # Needed by Homebrew under launchd\n#\n[ -e ~/mailpile-build-settings ] && . ~/mailpile-build-settings\n\n# Unlock the MacOS keychain\nif [ \"$KEYCHAIN_PASSWORD\" = \"\" ]; then\n  security unlock-keychain $KEYCHAIN\nelse\n  security unlock-keychain -p \"$KEYCHAIN_PASSWORD\" $KEYCHAIN\nfi\n\n# Just run all the scripts in alphanumerical order.\ncp /dev/null build.log\nfor script in $(ls -1 build-script/ |sort); do\n  echo -n \"$script \"\n  echo \"===[ $script ]=====[ $(date +%Y-%m-%d/%H:%M) ]=\" >>build.log\n  ./build-script/$script 2>&1 |tee -a build.log |while read LINE; do\n    echo -n .\n  done\n  RESULT=\"${PIPESTATUS[0]}\"\n  echo |tee -a build.log\n  if [ \"$RESULT\" != 0 ]; then\n    echo \"FAILED[$RESULT]\" |tee -a build.log\n    exit 1\n  fi\ndone\n"
  },
  {
    "path": "packages/macos/configurator.sh",
    "content": "#!/bin/sh\n# This script shall returns a stage 1 configuration and one or more stage 2 commands.\n# Returns a stage 1 sample configuration.\ncd \"$(dirname \"$0\")\"\nexport PYTHONPATH=\"$(pwd)/app/opt/mailpile\"\nexport PATH=\"$(pwd)/app/bin:$PATH\"\nexec ./app/bin/python \\\n  ./app/opt/mailpile/shared-data/mailpile-gui/mailpile-gui.py \\\n  --script --trust-os-path\n"
  },
  {
    "path": "packages/macos/dmgbuild-settings.py",
    "content": "import biplist\nimport json\nimport os\n\n# Load our settings from the appdmg json config\nwith open(os.getenv('APPDMG_CONFIG'), 'r') as fd:\n    _json_data = json.load(fd)\n\n\n# A helpful helper (copied from dmgbuild settings.py example)\ndef icon_from_app(app_path):\n    plist_path = os.path.join(app_path, 'Contents', 'Info.plist')\n    plist = biplist.readPlist(plist_path)\n    icon_name = plist['CFBundleIconFile']\n    icon_root, icon_ext = os.path.splitext(icon_name)\n    if not icon_ext:\n        icon_ext = '.icns'\n    icon_name = icon_root + icon_ext\n    return os.path.join(app_path, 'Contents', 'Resources', icon_name)\n\n\n# Basics\nappname      = _json_data.get('title', 'Mailpile')\nvolume_name  = _json_data.get('title', 'Mailpile')\nformat       = _json_data.get('format', 'ULFO')\nbackground   = _json_data.get('background', '#fff')\ndefault_view = 'icon-view'\nicon_size    = float(_json_data.get('icon-size', '64pt').replace('pt', ''))\nwindow_rect  = ((\n        _json_data.get('window', {}).get('position', {}).get('x', 100),\n        _json_data.get('window', {}).get('position', {}).get('y', 100)\n    ), (\n        _json_data.get('window', {}).get('size', {}).get('width', 400),\n        _json_data.get('window', {}).get('size', {}).get('height', 400)))\n\n# Files to include, derived badge icon, etc.\nfiles = []\nsymlinks = {}\nicon_locations = {}\nfor _elem in _json_data.get('contents', []):\n    _name = os.path.basename(_elem['path'])\n    if _elem.get('type') == 'link':\n        symlinks[_name] = _elem['path']\n    else:\n        files.append(_elem['path'])\n        # Will be used to badge the system's Removable Disk icon\n        if _elem['path'].endswith('.app'):\n            badge_icon = icon_from_app(_elem['path'])\n\n    icon_locations[_name] = (_elem['x'], _elem['y'])\n\n# EOF #\n"
  },
  {
    "path": "packages/macos/mailpile",
    "content": "#!/bin/sh\ncd \"${0%/*}\" # Sets current directory to Mailpile.app/Contents/Resources/app/bin/\nexport PATH=`pwd`/:$PATH\nexport SSL_CERT_FILE=`pwd`/../etc/openssl/cert.pem\nexec python ../opt/mailpile/scripts/mailpile \"$@\"\n"
  },
  {
    "path": "packages/mailpile.1",
    "content": ".\\\" Manpage for mailpile.\n.TH man 8 \"05 January 2016\" \"1.0\" \"mailpile man page\"\n.SH NAME\nmailpile \\- Mailpile Server\n.SH SYNOPSIS\nmailpile [options]\n.SH DESCRIPTION\nSome longer description here\n.SH OPTIONS\n.B \\-\\-start\nWhat does this do exactly?\n.TP\n.B \\-\\-wait\nWhat does this do already?\n.TP\n.B \\-\\-host=<host>\nThe host Mailpile should bind to.\n.TP\n.B \\-\\-port=<port>\nThe port Mailpile should listen on.\n.SH AUTHOR\nAlexandre Viau (aviau@debian.org)\n"
  },
  {
    "path": "packages/redhat/mailpile.spec.in",
    "content": "%define name mailpile\n%define version 0.4.0.dev20141126\n%define unmangled_version 0.4.0.dev20141126\n%define release 1\n\nSummary: An e-mail search engine and webmail client\nName: %{name}\nVersion: %{version}\nRelease: %{release}%{?dist}\nSource0: %{name}-%{unmangled_version}.tar.gz\nLicense: AGPL-3.0+\nGroup: Applications/Internet\nBuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot\nBuildRequires: python-devel \nBuildRequires: python-setuptools\nBuildRequires: gettext\nRequires: python-lxml\nRequires: python-jinja2\nRequires: python-markupsafe\nPrefix: %{_prefix}\nBuildArch: noarch\nVendor: Mailpile ehf. <team@mailpile.is>\nUrl: https://www.mailpile.is/\n\n%description\n# Welcome to Mailpile! #\n\n[![Build Status](https://img.shields.io/travis/mailpile/Mailpile/master.svg)](https://travis-ci.org/mailpile/Mailpile)\n\n\n## Introduction ##\n\nMailpile (<https://www.mailpile.is/>) is a modern, fast web-mail client with\nuser-friendly encryption and privacy features. The development of Mailpile\nis funded by [a large community of backers](https://www.mailpile.is/#community)\nand all code related to the project is and will be released under an OSI\napproved Free Software license.\n\nMailpile places great emphasis on providing a clean, elegant user interface\nand pleasant user experience. In particular, Mailpile aims to make it easy\nand convenient to receive and send PGP encrypted or signed e-mail.\n\nMailpile's primary user interface is web-based, but it also has a basic\ncommand-line interface and an API for developers. Using web technology for\nthe interface allows Mailpile to function both as a local desktop\napplication (accessed by visiting `localhost` in the browser) or a remote\nweb-mail on a personal server or VPS.\n\nThe core of Mailpile is a fast search engine, custom written to deal\nwith large volumes of e-mail on consumer hardware. The search engine\nallows e-mail to be organized using tags (similar to GMail's labels) and\nthe application can be configured to automatically tag incoming mail\neither based on static rules or bayesian classifiers.\n\n**Note:** We are currently \"in beta\", which means the app's basic features\nare (mostly) in place and packages are available for popular operating\nsystems, for people who would like to help test and debug. For more details\n[follow @MailpileTeam on Twitter](https://twitter.com/MailpileTeam)\nor [read our blog](https://www.mailpile.is/blog/).\n\n\n### Trying Mailpile\n\nWe have live demos up and running [on our\nwebsite](https://www.mailpile.is/demos/). If you are curious about what\nMailpile looks like, please feel free to check it out.\n\n\n### Installing Mailpile\n\nNote that Mailpile is still in development and is not suitable for\nproduction or end-user use. However, developers and early adopters are\nencouraged to give it a try and even help us find bugs, fix them\nand develop new features.\n\nPlease see our [download page](https://www.mailpile.is/download/) or read\n[the Getting Started guide on our wiki](https://github.com/pagekite/Mailpile/wiki/Getting-started).\n\n\n## Credits and License ##\n\nBjarni R. Einarsson (<http://bre.klaki.net/>) created this!  If you think\nit's neat, you should also check out PageKite: <https://pagekite.net/>. At some point Bjarni, that crafty fellow convinced [Smári](<http://www.smarimccarthy.is/>) and [Brennan](https://brennannovak.com) to start working on the project as well.\n\nThe GMail guys get mad props for creating the best webmail service out\nthere.  Wishing the Free Software world had something like it is what\ninspired Bjarni to start working on Mailpile. Edward Snowden also gets mad props for inspiring us to try and make PGP usable for journalists and everday folks!\n\nContributors:\n\n- Bjarni R. Einarsson (<http://bre.klaki.net/>)\n- Brennan Novak (<https://brennannovak.com/>)\n- Smari McCarthy (<http://www.smarimccarthy.is/>)\n- Lots more, run `git shortlog -s` for a list! (Or see the list [on Github](https://github.com/pagekite/Mailpile/graphs/contributors).)\n\nAnd of course, we couldn't do this without [our community of\nbackers](https://www.mailpile.is/#community).\n\nThis program is free software: you can redistribute it and/or modify it under\nthe terms of either the GNU Affero General Public License as published by the\nFree Software Foundation. See the file `COPYING.md` for details.\n\n\n%prep\n%setup -n %{name}-%{unmangled_version}\n\n%build\npython setup.py build\n\n%install\npython setup.py install --single-version-externally-managed -O1 --prefix=%{_prefix} --root=%{buildroot} --record=INSTALLED_FILES\n\n# FIXME: This does not work...\n#%find_lang %{name}\n#%files -f %{name}.lang\n\n%files\n%defattr(-,root,root)\n%doc README.md LICENSE-2.0.txt\n%{python_sitelib}/*\n%{_bindir}/*\n#%{_mandir}/*\n\n%changelog\n* Tue Nov 25 2014 Bjarni R. Einarsson <bre@mailpile.is> - 0.4.0.dev20141126-1\n- Merging/adapting setup.py-generated .spec file\n\n* Fri Oct 17 2014 Smári McCarthy <smari@mailpile.is> - 0.4.1-1\n- First RPM build\n"
  },
  {
    "path": "packages/scripts/build-controller.py",
    "content": "#!/usr/bin/python\n\"\"\"\nFIXME...\n\n\"\"\"\nfrom __future__ import print_function\nimport getopt\nimport os\nimport time\nimport traceback\ntry:\n    from xmlrpclib import ServerProxy  # Python 2.7\nexcept ImportError:\n    from xmlrpc.client import ServerProxy  # Python 3.x\n\n\nclass ArgumentError(Exception):\n    pass\n\n\ndef config_assert(condition):\n    if not condition:\n        raise ArgumentError()\n\n\nclass BuildbotController(object):\n    COMMON_OPT_FLAGS = 'c:'\n    COMMON_OPT_ARGS = [\n       'config=', 'buildmac_url=', 'buildwin_url=']\n\n    DEFAULT_CONFIG_FILE = '~/.mailpile-build-controller.cfg'\n    DEFAULT_PORT = 33011\n\n    def __init__(self):\n        self.config_file = os.path.expanduser(self.DEFAULT_CONFIG_FILE)\n        self.buildmac_url = None\n        self.buildwin_url = None\n        self.running = None\n        self.api = None\n        if os.path.exists(self.config_file):\n            self.load_config()\n\n    def load_config(self, filename=None):\n        with open(filename or self.config_file, 'r') as fd:\n            lines = [l.split('#')[0].strip()\n                     for l in fd.read().replace(\"\\\\\\n\", '').splitlines()]\n        args = []\n        for line in lines:\n            if line:\n                args.append('--%s' % line.replace(' = ', '='))\n        return self.parse_args(args)\n\n    def parse_with_common_args(self, args, opt_flags='', opt_args=[]):\n        opts, args = getopt.getopt(\n           args,\n           '%s%s' % (opt_flags, self.COMMON_OPT_FLAGS),\n           list(opt_args) + self.COMMON_OPT_ARGS)\n\n        try:\n            for opt, arg in opts:\n                if opt in ('-c', '--config'):\n                    self.config_file = os.path.expanduser(arg)\n                    if not os.path.exists(self.config_file):\n                        raise ValueError('No such file: %s' % self.config_file)\n                    self.load_config()\n\n                elif opt in ('--buildmac_url',):\n                    self.buildmac_url = arg\n\n                elif opt in ('--buildwin_url',):\n                    self.buildwin_url = arg\n\n        except ValueError as e:\n            raise ArgumentError(e)\n\n        return opts, args\n\n    def cmd_hello(self, args):\n        print('Hello: %s' % ' '.join(args))\n        args[:] = []\n\n    def cmd_win(self, args):\n        config_assert(self.buildwin_url is not None)\n        self.api = ServerProxy(self.buildwin_url)\n        self.running = None\n\n    def cmd_mac(self, args):\n        config_assert(self.buildmac_url is not None)\n        self.api = ServerProxy(self.buildmac_url)\n        self.running = None\n\n    def cmd_run(self, args):\n        config_assert(self.api is not None)\n        self.running = args.pop(0)\n        print('%s' % self.api.run(self.running, 1))\n\n    def cmd_status(self, args):\n        config_assert(self.api is not None)\n        self.running = args.pop(0)\n        print('%s' % self.api.status(self.running))\n\n    def cmd_wait(self, args):\n        config_assert(self.running is not None)\n        while True:\n            time.sleep(5)\n            status = self.api.status(self.running)\n            print('%s' % status)\n            if status[-1]:\n                break\n\n    def parse_args(self, args):\n        opts, args = self.parse_with_common_args(args)\n        while args:\n            command = args.pop(0)\n            try:\n                self.__getattribute__('cmd_%s' % command)(args)\n            except AttributeError:\n                raise ArgumentError('No such command: %s' % command)\n        return self\n\n    def run(self):\n        pass\n\n    def cleanup(self):\n        pass\n\n    @classmethod\n    def Main(cls, args):\n        bc = None\n        try:\n            bc = cls()\n            bc.parse_args(args).run()\n        except (getopt.GetoptError, ArgumentError):\n            print(__doc__)\n            traceback.print_exc()\n            sys.exit(1)\n        except (RuntimeError, KeyboardInterrupt):\n            print('Quitting...')\n        except Exception:\n            traceback.print_exc()\n            sys.exit(2)\n        finally:\n            if bc:\n                bc.cleanup()\n\n\nif __name__ == \"__main__\":\n    import sys\n    BuildbotController.Main(sys.argv[1:])\n"
  },
  {
    "path": "packages/scripts/build-deb.sh",
    "content": "#!/bin/bash\n#\n# Script to build Mailpile packages for a specific branch/repo.\n#\n\nREPO=${1:-nightly}\nBRANCH=${2:-master}\nFORCE=$3\n\n(\n  set -e\n  set -x\n\n  cd\n  mkdir -p incoming/$REPO\n\n  cd mailpile\n  git checkout -f $BRANCH\n  if [ \"$FORCE$(git pull origin $BRANCH 2>&1 |grep -c up-to-date)\" != 1 ]; then\n    rm -rf dist/*\n    make mrproper dpkg\n    rm -f dist/mailpile.tar.gz\n    cp dist/*.deb dist/*.tar.gz ~/incoming/$REPO\n    dpkg-sig --sign builder ~/incoming/$REPO/*deb\n  fi\n) \\\n  >~/$REPO-last.log 2>&1\n"
  },
  {
    "path": "packages/scripts/build-desktop.py",
    "content": "#!/usr/bin/python\n\"\"\"\nbuild-desktop.py - Checkout and build Mailpile for desktop platforms (win/mac)\n\nUsage: build-desktop.py [clean] <nightly|release>\n\n\"\"\"\nfrom __future__ import print_function\nimport os\nimport subprocess\nimport sys\nimport traceback\n\n\nDEBUG = True\nGIT_BINARY = 'git'\n\n\n##[ Boilerplate to make scripts more readable... ]############################\n\nclass Sub(object):\n    def __init__(self, command, env=None):\n        self.command = command\n        self.stdout = self.stderr = self.rcode = ''\n        self.env = env\n\n    def communicate(self):\n        process = subprocess.Popen(\n            self.command,\n            env=self.env,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE)\n        stdout, stderr = process.communicate()\n        self.stdout = stdout.decode('utf-8')\n        self.stderr = stderr.decode('utf-8')\n        self.rcode = process.poll()\n\n    def __str__(self):\n        return (\n            '###[ %s ]###\\n==[STDOUT]==\\n%s\\n==[STDERR]==\\n%s\\n==[RCODE: %s]\\n'\n            % (' '.join(self.command), self.stdout, self.stderr, self.rcode))\n\n\ndef run(*args, **kwargs):\n    result = Sub(args, env=kwargs.get('env'))\n    try:\n        result.communicate()\n        if DEBUG:\n            print('%s' % result)\n        if kwargs.get('_raise') and result.rcode != 0:\n            raise kwargs.get('_raise')('Returned: %s' % result.rcode)\n    except Exception as e:\n        traceback.print_exc()\n    return result\n\n\ndef git(*args, **kwargs):\n    return run(*([GIT_BINARY] + list(args)), **kwargs)\n\n\n##[ Actual build rules... ]###################################################\n\ndef macOS_build(mailpile_tree, repo, branch, clean_build):\n    os.chdir(os.path.join(mailpile_tree, 'packages', 'macos'))\n    build_dir = os.path.expanduser('~/build-%s' % repo)\n\n    if clean_build and os.path.exists(build_dir) and os.path.isdir(build_dir):\n        sub('rm', '-rf', build_dir)\n\n    run('./build.sh', env={'BUILD_DIR': build_dir}, _raise=ValueError)\n\n\ndef windows_build(mailpile_tree, repo, branch, clean_build):\n    os.chdir(os.path.join(mailpile_tree, 'packages', 'windows-wix'))\n    build_dir = os.path.expanduser('~/build-%s' % repo)\n\n    if clean_build and os.path.exists(build_dir) and os.path.isdir(build_dir):\n        sub('bash', '-c', 'rm -rf \"%s\"' % build_dir)\n\n    run('python', 'provide', '-i', 'provide.json', _raise=ValueError)\n\n\nif __name__ == '__main__':\n    os.chdir(os.path.dirname(__file__) or '.')\n    if len(sys.argv) < 2:\n        print(__doc__)\n        print('\\nERROR: Missing arguments! What to do?')\n        sys.exit(1)\n\n    force_build = True\n    clean_build = False\n    repo = 'nightly'\n    branch = 'master'\n    for arg in sys.argv[1:]:\n        if arg == 'clean':\n            clean_build = True\n            force_build = True\n            continue\n        elif arg == 'force':\n            force_build = True\n            continue\n        elif arg == 'nightly':\n            repo, branch = 'nightly', 'master'\n        elif arg == 'release':\n            repo, branch = 'release', 'release/1.0'\n        else:\n            print(__doc__)\n            print('\\nERROR: Unrecognized argument: %s' % arg)\n            sys.exit(2)\n\n        # Ensure ~/Mailpile exists and has a Mailpile tree\n        mailpile_tree = os.path.expanduser('~/Mailpile')\n        try:\n            os.chdir(mailpile_tree)\n            if not os.path.exists('.git'):\n                raise OSError('Boo')\n        except OSError:\n            if not os.path.exists(mailpile_tree):\n                os.mkdir(mailpile_tree)\n            os.chdir(mailpile_tree)\n            git('clone', '--recurse-submodules',\n                'https://github.com/Mailpile/mailpile/', '.',\n                _raise=ValueError)\n\n        # Check out and update the requested branch, triggering a build if we\n        # are either forcing or if something has changed.\n        git('checkout', '-f', branch, _raise=True)\n        if (force_build or clean_build or\n                'up to date' not in git('pull', 'origin', branch).stdout):\n\n            git('pull', '--recurse-submodules', 'origin', branch,\n                _raise=ValueError)\n\n            if sys.platform == 'darwin':\n                macOS_build(mailpile_tree, repo, branch, clean_build)\n\n            elif sys.platform.startswith('win'):\n                windows_build(mailpile_tree, repo, branch, clean_build)\n\n            else:\n                print(__doc__)\n                print('\\nERROR: Unknown platform: %s' % sys.platform)\n                sys.exit(3)\n\n# EOF #\n"
  },
  {
    "path": "packages/scripts/crontab",
    "content": "20 * * * * /home/buildbot/bin/build.sh release release/1.0\n30 * * * * /home/buildbot/bin/update-repos.sh\n50 3 * * * /home/buildbot/bin/build.sh nightly master\n00 4 * * * /home/buildbot/bin/update-repos.sh\n"
  },
  {
    "path": "packages/scripts/deb-package-py-deps.sh",
    "content": "#!/bin/bash\n#\n# This is a quick'n'dirty shell script to build packages for some of\n# the Python packages we depend on, but aren't yet in Debian.\n#\nALL_PACKAGES=\"imgsize gui-o-matic\"\n\n\n# Bail on errors\nset -e\n\n# Start fresh!\nrm -rf src dist\n\n\nmkdir -p src\ncd src\n\n    # Pull things from PyPI\n    pypi-download --release=2.0 imgsize\n\n    # Pull things from Github\n    git clone https://github.com/mailpile/gui-o-matic  # FIXME: version?\n\ncd ..\n\n\nmkdir -p dist\nfor package in $ALL_PACKAGES; do\n\n    if [ -d src/$package ]; then\n        cd src/$package\n        python setup.py --command-packages=stdeb.command bdist_deb\n        cd ../..\n        mv -v src/$package/deb_dist/*.deb dist/\n    else\n        py2dsc-deb \\\n            -m \"Mailpile Team <packages@mailpile.is>\" \\\n            src/$package*.tar.gz\n        mv -v deb_dist/*.deb dist/\n        rm -rf deb_dist\n    fi\n\ndone\n\ndpkg-sig --sign builder dist/*deb\ncat <<tac\nPackages built!\n\nNow, please run one or more of these to publish the results:\n\n    cp dist/*.deb ~/incoming/nightly\n    cp dist/*.deb ~/incoming/stable\n\ntac\n"
  },
  {
    "path": "packages/scripts/kite-runner-mac.cfg",
    "content": "# Install (as root), like so:\n#\n# $ echo 'xmlrpc_path = /SECRETSECRET' >/Users/buildbot/kite-runner-secrets.cfg\n# $ cp kiterunner.service.plist /Library/LaunchDaemons\n# $ shutdown -r now\n#\n# Note that /Users/buildbot/.pagekite.rc also needs to useful settings in it.\n#\n\n# This is the port our XML-RPC server listens on\nport = 9999\nrunas = buildbot\n\n# This is our PageKite invocation; use whatever is in ~/.pagekite.rc\npk_binary = /usr/local/bin/pagekite.py\npagekite = --defaults\n\n# These are the build scripts that can be launched\nscript = nightly: \\\n    /Users/buildbot/Mailpile/packages/scripts/build-desktop.py nightly\nscript = nightly-clean: \\\n    /Users/buildbot/Mailpile/packages/scripts/build-desktop.py clean nightly\n\nscript = release: \\\n    /Users/buildbot/Mailpile/packages/scripts/build-desktop.py release\nscript = release-clean: \\\n    /Users/buildbot/Mailpile/packages/scripts/build-desktop.py clean release\n"
  },
  {
    "path": "packages/scripts/kite-runner-sample.cfg",
    "content": "# Example config for kite-runner.py\n#\n# ./kite-runner.py --pk_secret=XYZ --pk_kite=you.pagekite.me -c kite-runner.cfg\n#\n\n# This is the port our XML-RPC server listens on\nport = 9999\n\n# TOP SEKRIT\nxmlrpc_path = /SEKRIT\n\n# This is our PageKite invocation.\n# We expose the XML-RPC server on port 8080, package on 443/80 and SSH.\npagekite = --clean --defaults \\\n    --logfile=stdio \\\n    --httpd=127.0.0.1:9998 \\\n    --webpath=%KITE%/80:/:default:/home/bre/tmp/ \\\n    --service_cfg=%KITE%/80:indexes:True \\\n    --service_on=raw-22:%KITE%:localhost:22:%SECRET% \\\n    --service_on=http-8080:%KITE%:localhost:%PORT%:%SECRET% \\\n    --service_on=http-443:%KITE%:localhost:builtin:%SECRET%\n\n# These are the build scripts that can be launched\nscript = nightly: \\\n    grep -c Bjarni /etc/passwd\n\nscript = release: \\\n    grep -c -v Bjarni /etc/passwd && sleep 600 && false\n"
  },
  {
    "path": "packages/scripts/kite-runner-win.cfg",
    "content": "# This gets invoked like so:\n#\n#  C:\\Python27\\python.exe\n#    C:\\Users\\MAILPI~1\\Mailpile\\packages\\scripts\\build-desktop.py\n#    --config=C:\\Users\\MAILPI~1\\Mailpile\\packages\\scripts\\kite-runner-win.cfg\n#    --config=C:\\Users\\MAILPI~1\\kite-runner-secrets.cfg\n#\n# Where the kite-runner-secrets.cfg file contains an xmlrpc_path setting to\n# prevent unauthorized access to the API server.\n#\n# Also of note, is that C:\\Users\\MAILPI~1\\pagekite.cfg contains the actual\n# lines which expose out server (and other things) to the public Internet.\n#\n# For any of this to work, git-bash and python both need to be on the user's\n# PATH, and the Windows security notifications have to be disabled.\n#\n\n# This is the port our XML-RPC server listens on\nport = 9999\n\n# This is our PageKite invocation; use whatever is in ~/.pagekite.rc\npk_binary = C:\\Python27\\python.exe\npagekite = C:\\Users\\MAILPI~1\\pagekite.py --nullui\n\n# These are the build scripts that can be launched\nscript = nightly: C:\\Python27\\python.exe \\\n    C:\\Users\\MAILPI~1\\Mailpile\\packages\\scripts\\build-desktop.py nightly\nscript = nightly-clean: C:\\Python27\\python.exe \\\n    C:\\Users\\MAILPI~1\\Mailpile\\packages\\scripts\\build-desktop.py clean nightly\n\nscript = release: C:\\Python27\\python.exe \\\n    C:\\Users\\MAILPI~1\\Mailpile\\packages\\scripts\\build-desktop.py release\nscript = release-clean: C:\\Python27\\python.exe \\\n    C:\\Users\\MAILPI~1\\Mailpile\\packages\\scripts\\build-desktop.py clean release\n\n"
  },
  {
    "path": "packages/scripts/kite-runner.py",
    "content": "#!/usr/bin/python\n\"\"\"\n\nkite-runner.py  - XMLRPC-based script launcher with PageKite integration.\n\n\"\"\"\nfrom __future__ import print_function\nimport getopt\nimport json\nimport os\nimport sys\nimport subprocess\nimport threading\nimport time\nimport traceback\ntry:\n    import SimpleXMLRPCServer  # Python 2.7\nexcept ImportError:\n    import xmlrpc.server as SimpleXMLRPCServer  # Python 3.x\n\n\nclass ArgumentError(Exception):\n    pass\n\n\nclass Killed(Exception):\n    pass\n\n\nclass OutputEater(threading.Thread):\n    def __init__(self, fd, callback):\n        threading.Thread.__init__(self)\n        self.daemon = True\n        self.fd = fd\n        self.cb = callback\n\n    def run(self):\n        while True:\n            line = self.fd.readline()\n            if not line:\n                break\n            self.cb(line.decode('utf-8').strip())\n        self.cb(None)\n\n\nclass PagekiteThread(threading.Thread):\n    def __init__(self, server):\n        threading.Thread.__init__(self)\n        self.kid = None\n        self.server = server\n        self.lock = threading.Lock()\n\n    def _handle_logline(self, line):\n        print(line)\n\n    def run(self):\n        try:\n            self._run()\n        finally:\n            self.reap()\n\n    def _run(self):\n        skip_loops = 0\n        crash_count = 0\n        while not self.server.quitting:\n            command = ' '.join([\n                self.server.pagekite_binary,\n                self.server.pagekite_args])\n            command = command.replace('%PORT%', str(self.server.port))\n            command = command.replace('%KITE%', self.server.pagekite_kite)\n            command = command.replace('%SECRET%', self.server.pagekite_secret)\n            with self.lock:\n                if skip_loops:\n                    skip_loops -= 1\n                else:\n                    print('Launching: %s' % command)\n                    self.kid = subprocess.Popen(\n                        [c for c in command.split() if c],\n                        stdout=subprocess.PIPE,\n                        stderr=subprocess.PIPE,\n                        env=self.server.subprocess_env(),\n                        bufsize=0)\n                    OutputEater(self.kid.stdout, self._handle_logline).start()\n                    OutputEater(self.kid.stderr, self._handle_logline).start()\n            rv = None\n            time.sleep(0.5)\n            while (self.kid is not None\n                    and (rv is None)\n                    and not self.server.quitting):\n                with self.lock:\n                    if self.kid is not None:\n                        rv = self.kid.poll()\n                        if rv is not None:\n                            self.kid = None\n                if rv not in (None, 0):\n                    crash_count += 1\n                    skip_loops = min(crash_count, 24) * 10\n                    print('PageKite exited with status: %d' % rv)\n                time.sleep(0.5)\n        self.reap()\n\n    def reap(self):\n        with self.lock:\n            if self.kid is not None:\n                self.kid.kill()\n                self.kid.wait()\n                self.kid = None\n\n    def quit(self):\n        self.reap()\n        self.join()\n\n\nclass BuildbotXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer):\n    def __init__(self, config, *args, **kwargs):\n        SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, *args, **kwargs)\n        self.config = config\n\n\nclass BuildbotRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):\n    def __init__(self, request, client_address, server):\n        self.rpc_paths = server.config.rpc_paths\n        SimpleXMLRPCServer.SimpleXMLRPCRequestHandler.__init__(\n            self, request, client_address, server)\n\n\nclass BuildbotAPI(object):\n    def __init__(self, server):\n        self.server = server\n        self.scripts = {}\n        self.script_lock = threading.Lock()\n        self.script_running = None\n        self.script_last = None\n\n    def _collect_stdout(self, sdata, logline):\n        if logline is None:\n            with self.script_lock:\n                self.script_running = None\n                self.script_last = sdata\n            sdata[5] = sdata[0].wait()\n            sdata[6] = time.time()\n        else:\n            sdata[3].append(logline)\n\n    def _collect_stderr(self, sdata, logline):\n        if logline is not None:\n            sdata[4].append(logline)\n\n    def status(self, name, error='No matching script logs found'):\n        with self.script_lock:\n            if self.script_running and self.script_running[1] == name:\n                return self.script_running[1:]\n            elif self.script_last and self.script_last[1] == name:\n                return self.script_last[1:]\n            else:\n                raise ValueError(error)\n\n    def run(self, name, delay):\n        with self.script_lock:\n            if name not in self.scripts:\n                raise ValueError('No such script')\n            elif self.script_running is None:\n                kid = subprocess.Popen(\n                    [self.server.shell_binary, '-c',\n                     self.scripts[name].replace('\\\\', '\\\\\\\\\\\\\\\\')],\n                    stdout=subprocess.PIPE,\n                    stderr=subprocess.PIPE,\n                    env=self.server.subprocess_env(),\n                    bufsize=0)\n                sdata = [kid, name, time.time(), [], [], 0, 0]\n                self.script_running = sdata\n                OutputEater(\n                    kid.stdout,\n                    lambda ll: self._collect_stdout(sdata, ll)).start()\n                OutputEater(\n                    kid.stderr,\n                    lambda ll: self._collect_stderr(sdata, ll)).start()\n            elif self.script_running[1] != name:\n                raise ValueError('Busy running other script(s)')\n\n        time.sleep(max(0.1, min(5.0, float(delay))))\n        return self.status(name)\n\n    def test(self, arg):\n        return 'You said: %s' % arg\n\n    def quit(self):\n        with self.script_lock:\n            if self.script_running:\n                self.script_running[0].kill()\n\n\nclass BuildbotServer(object):\n    COMMON_OPT_FLAGS = 'p:k:c:'\n    COMMON_OPT_ARGS = [\n       'port=', 'runas=', 'config=',\n       'xmlrpc_path=', 'sh_binary=', 'script=', 'bin_path=',\n       'pagekite=', 'pk_binary=', 'pk_kite=', 'pk_secret=']\n\n    DEFAULT_CONFIG_FILE = '~/kite-runner.cfg'\n    DEFAULT_PORT = 33011\n\n    def __init__(self):\n        self.quitting = False\n        self.port = self.DEFAULT_PORT\n        self.api = BuildbotAPI(self)\n        self.shell_binary = 'bash'\n        self.rpc_paths = ('/KiteRunner',)\n        self.bin_path = []\n        self.pagekite_binary = 'pagekite'\n        self.pagekite_kite = ''\n        self.pagekite_secret = ''\n        self.pagekite_args = None\n        self.pagekite_thread = None\n        self.config_file = os.path.expanduser(self.DEFAULT_CONFIG_FILE)\n        if os.path.exists(self.config_file):\n            self.load_config()\n\n    def load_config(self, filename=None):\n        with open(filename or self.config_file, 'r') as fd:\n            lines = [l.split('#')[0].strip()\n                     for l in fd.read().replace(\"\\\\\\n\", '').splitlines()]\n        args = []\n        for line in lines:\n            if line:\n                args.append('--%s' % line.replace(' = ', '='))\n        return self.parse_args(args)\n\n    def _set_uid_and_gid(self, identity):\n        import pwd\n        import grp\n        parts = identity.split(':')\n        pwnam = pwd.getpwnam(parts[0])\n        if len(parts) > 1:\n            uid, gid = pwnam.pw_uid, grp.getgrnam(parts[1]).gr_gid\n        else:\n            uid, gid = pwnam.pw_uid, pwnam.pw_gid\n        os.setgid(gid)\n        os.setuid(uid)\n\n    def subprocess_env(self):\n        env = os.environ.copy()\n        env['HOME'] = os.getenv('HOME', os.path.expanduser('~'))\n        env['PATH'] = os.getenv('PATH')\n        for path in self.bin_path:\n            env['PATH'] = '%s%s%s' % (path, os.pathsep, env['PATH'])\n        return env\n\n    def parse_with_common_args(self, args, opt_flags='', opt_args=[]):\n        opts, args = getopt.getopt(\n           args,\n           '%s%s' % (opt_flags, self.COMMON_OPT_FLAGS),\n           list(opt_args) + self.COMMON_OPT_ARGS)\n\n        try:\n            for opt, arg in opts:\n                if opt in ('-p', '--port'):\n                    self.port = int(arg.strip())\n\n                if opt in ('--bin_path',):\n                    self.bin_path.append(arg)\n\n                if opt in ('--sh_binary',):\n                    self.shell_binary = arg.strip()\n                    if not os.path.exists(self.shell_binary):\n                        raise ArgumentError('Not found: ' + self.shell_binary)\n\n                if opt in ('--xmlrpc_path',):\n                    self.rpc_paths = (arg,)\n\n                if opt in ('--pk_binary',):\n                    self.pagekite_binary = arg.strip()\n                    if not os.path.exists(self.pagekite_binary):\n                        raise ArgumentError(\n                            'Not found: ' + self.pagekite_binary)\n\n                if opt in ('--pk_kite',):\n                    self.pagekite_kite = arg.strip()\n\n                if opt in ('--pk_secret',):\n                    self.pagekite_secret = arg.strip()\n\n                if opt in ('--runas',):\n                    self._set_uid_and_gid(arg)\n\n                if opt in ('-k', '--pagekite'):\n                    self.pagekite_args = arg.strip()\n\n                if opt in ('--script',):\n                    name, script = arg.split(':', 1)\n                    self.api.scripts[name.strip()] = script.strip()\n\n                if opt in ('-c', '--config'):\n                    self.config_file = os.path.expanduser(arg)\n                    if not os.path.exists(self.config_file):\n                        raise ValueError('No such file: %s' % self.config_file)\n                    self.load_config()\n\n        except ValueError as e:\n            raise ArgumentError(e)\n\n        return opts, args\n\n    def parse_args(self, args):\n        self.parse_with_common_args(args)\n        return self\n\n    def launch_xmlrpc_server(self):\n        rpc_server = BuildbotXMLRPCServer(\n            self,\n            ('localhost', self.port),\n            requestHandler=BuildbotRequestHandler)\n        rpc_server.register_introspection_functions()\n        rpc_server.register_instance(self.api)\n        rpc_server.serve_forever()\n\n    def run(self):\n        try:\n            if self.pagekite_args:\n                self.pagekite_thread = PagekiteThread(self)\n                self.pagekite_thread.start()\n            self.launch_xmlrpc_server()\n        finally:\n            self.quitting = True\n\n    def cleanup(self):\n        if self.pagekite_thread is not None:\n            self.pagekite_thread.quit()\n        self.api.quit()\n\n\ndef kite_runner_main(args):\n    try:\n        bb = BuildbotServer()\n        bb.parse_args(args).run()\n    except (getopt.GetoptError, ArgumentError):\n        print(__doc__)\n        traceback.print_exc()\n        sys.exit(1)\n    except (RuntimeError, KeyboardInterrupt):\n        print('Quitting...')\n    except Exception:\n        traceback.print_exc()\n        sys.exit(2)\n    finally:\n        bb.cleanup()\n\n\nif __name__ == \"__main__\":\n    kite_runner_main(sys.argv[1:])\n"
  },
  {
    "path": "packages/scripts/kiterunner.service.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n<key>Label</key>\n<string>kiterunner.service</string>\n\n<key>ProgramArguments</key>\n<array>\n  <string>/Users/buildbot/Mailpile/packages/scripts/kite-runner.py</string>\n  <string>--config=/Users/buildbot/Mailpile/packages/scripts/kite-runner-mac.cfg</string>\n  <string>--config=/Users/buildbot/kite-runner-secrets.cfg</string>\n</array>\n\n<key>EnvironmentVariables</key>\n<dict>\n  <key>HOME</key>\n  <string>/Users/buildbot</string>\n</dict>\n\n<key>SessionCreate</key>\n<true/>\n\n<key>KeepAlive</key>\n<true/>\n\n<key>RunAtLoad</key>\n<true/>\n\n</dict>\n</plist> \n"
  },
  {
    "path": "packages/scripts/update-repos.sh",
    "content": "#!/bin/bash\n#\n# This script updates our repositories and syncs to the public site.\n#\n(\n  export LANG=C\n  export LC_ALL=C\n  set -x\n  set -e\n\n  cd ~/incoming\n  for repo in *; do\n    if [ \"$(find $repo -name '*.deb'|wc -l)\" != 0 ]; then\n      reprepro -b ../deb includedeb $repo $repo/*deb\n      rm -f $repo/*deb\n    fi\n  done\n\n  cd ~/deb\n  sha256sum $(find pool -name '*.deb') >.sha256sums.txt.new\n  diff .sha256sums.txt.new sha256sums.txt >/dev/null || {\n    cp .sha256sums.txt.new sha256sums.txt\n    echo '{' > .packages.json.new\n    cat sha256sums.txt |while read SUM FN; do\n      if [ \"$(basename $FN |grep -c dev)\" = 1 ]; then\n        R=nightly\n      else\n        R=release\n      fi\n      echo \"  \\\"$(basename $FN)\\\": {\"               >> .packages.json.new\n      echo \"    \\\"repo\\\": \\\"$R\\\",\"                  >> .packages.json.new\n      echo \"    \\\"package\\\": \\\"$(basename $FN |cut -f1 -d_)\\\",\" \\\n\t                                            >> .packages.json.new\n      echo \"    \\\"path\\\": \\\"$FN\\\",\"                 >> .packages.json.new\n      echo \"    \\\"sha256\\\": \\\"$SUM\\\",\"              >> .packages.json.new\n      echo \"    \\\"mtime\\\": \\\"$(stat -c %Y $FN)\\\",\"  >> .packages.json.new\n      echo \"    \\\"size\\\": \\\"$(ls -hs $FN |cut -f1 -d\\ )\\\"},\" \\\n                                                    >> .packages.json.new\n    done\n    echo '\"EOF\": 1}' >> .packages.json.new\n    mv -f .packages.json.new packages.json\n  }\n  rm -f .sha256sums.txt.new\n\n  cd\n  rsync -prvac --delete deb packages@mailpile.is:www/\n)\\\n  > ~/update-repos.log 2>&1\n"
  },
  {
    "path": "packages/windows-wix/README.QUICK_START.md",
    "content": "# Windows Mailpile Package Build #\n\nMailpile uses the [WIX toolset](http://wixtoolset.org/) to generate an MSI package for windows. To do so, it has to gather and prepare Mailpile's dependencies for packaging. A small python framework aptly named `provide` addresses those needs and serves as a scripting glue to automate generating a package. This page describes how to invoke, configure, and maintain the `provide` framework.\n\nThere are READMEs in the code base that describe how various components work, and detailed help screens for the tools in use. This page aims to stitch them all together at a high level.\n\n## Quick-Start ##\n\nIf you're looking to create a regular or signed package, look no further! These two sections describe how to go from zero to building packages.\n\n### Generating a Plain Package ###\n\nGenerating a package requires python (either 2.7 or 3.x), git, and a checkout of mailpile(~1.0.0rc3 or later). The build script is very configurable in effort to be portable/reproducible. Assuming `git` is on your PATH, building is as simple as changing directory to where you'd like the output, and invoking the build script:\n\n``` sh\npython.exe path\\to\\mailpile\\packages\\windows-wix\\provide -i path\\to\\mailpile\\packages\\windows-wix\\provide-example-fork.json\n```\n\nIf git isn't on your path, you can specify it's location:\n\n``` sh\npython.exe path\\to\\mailpile\\packages\\windows-wix\\provide ^\n -i path\\to\\mailpile\\packages\\windows-wix\\provide-example-fork.json ^\n --configure-git=path\\to\\git.exe\n```\n\nIt will download, process, and package dependencies, preparing a \"package\" directory in your current directory. *You will need to manually grant privileges to unpack win4gpg when prompted.* The directory will contain several files:\n\n* `mailpile-<version>-<lang>.msi` the created mailpile package.\n* `mailpile-<version>-<lang>.wixpdb` debugging symbols for the package.\n* `mailpile.package.json` the inflated template for `mailpile\\packages\\windows-wix\\package.py`.\n* `mailpile.wxs` the wix configuration script generated by `provide`.\n* `mailpile.wixobj` the wixobj file made from the `mailpile.wxs` configuration.\n\nThe first should be sufficient to install mailpile. If making a release, it is recommended to keep the remainder for troubleshooting/archival purposes.\n\n### Generating a signed package ###\n\nThe `provide` script supports signing *all* PDE package content if `signtool.exe` and a code signing certificate is available. To secure `signtool.exe`, install the windows SDK(if only installing the code-signing checkbox, the install size is only a few MB). 'provide` will attempt to find the most recent version of `signtool.exe` installed (the SDK version bumps very regularly)--you can check if it's found it by invoking `python.exe mailpile\\packages\\windows-wix\\provide -h` and looking for the default value of `--config_signtool`: If it is not simply `signtool.exe`, `provide` was able to find a version of the windows SDK. If need you can manually specify `signtool.exe` on the command line:\n\n``` sh\npython.exe path\\to\\mailpile\\packages\\windows-wix\\provide ^\n -i path\\to\\mailpile\\packages\\windows-wix\\provide-example-fork.json ^\n --config_signtool=\"C:\\Program Files(x86)\\Windows Kits\\10\\bin\\10.0.17134.0\\x86\\signtool.exe\"\n```\nTo actually sign the build, you must provide the path to the code signing certificate and password for the certificate:\n\n``` sh\npython.exe path\\to\\mailpile\\packages\\windows-wix\\provide ^\n -i path\\to\\mailpile\\packages\\windows-wix\\provide-example-fork.json ^\n --config_signing_key=path\\to\\code_signing_key.p12 ^\n --config_signing_passwd=a_very_secure_passwd\n```\nThe build proceeds exactly like a regular build, except that all PDE (exe, dll, msi) content is signed.\n\n## Maintaining Dependencies ##\n\nProvide works by fetching and caching dependencies, processing them into a portable install state, and packaging them. From time to time it will be necessary to update or modify external dependencies for the build script. Dependency sources are kept in a simple JSON file: `mailpile\\packages\\windows-wix\\resources.json`. They can be updated as needed: Either directly edit the JSON(providing URLs and SHA1s), or use the helper program `mailpile\\packages\\windows-wix\\provide\\cache.py` to calculate the SHA1s for you:\n\n```sh\npython.exe mailpile\\packages\\windows-wix\\provide\\cache.py ^\n -c mailpile\\packages\\windows-wix\\download_cache ^\n -r mailpile\\packages\\windows-wix\\resource.json ^\n -i \"[\\\"resource_hacker\\\", \\\"https://www.mailpile.is/files/windows-deps/20180901/resource_hacker.zip\\\", \\\"optionally replace the previous comment text with this text\\\"]\"\n```\n\nResources are identified by a common name throughout the build scripts to decouple build script uses from remote sources. Currently mailpile uses the following sources as part of the build:\n\n* CPython 2.7: `provide` downloads the known-good version, configures it with required packages, strips unnecessary additions, then either partially(fork build) or entirely(bootstrap build) uses it for the rest of the build.\n* Tor: Bundled as a dependency.\n* Win4gpg: provides relatively recent GPG infrastructure. Portable version used--see their documentation about \"mkportable\".\n* OpenSSL: Bundled as a dependency.\n* Resource Hacker: Used to make mailpile-branded python and wpython executable, not shipped in package.\n* Mailpile and submodules: Checked-out or copied and reset to a specific commit(default most recent on current branch).\n\n## Customizing the Build ###\n\nThe `provide` script is highly configurable. In part, this is to facilitate portable/reproducible builds. It also facilitates maintainability and customization points by allowing various configuration points to be re-targeted at a very high level. It is also designed to safely decouple working state from package state.\n\n### Configuration file ###\n\nAt it's core, provide evaluates a declarative configuration JSON, building a one-off context in the process. The JSON key-value pairs are as described by `python.exe provide -h`. Configuration is evaluated lazily from one of two entry points:\n\n* `export`: Export accepts a dictionary of build targets to export(copy) locations. It constructs each specified target and copies it to the specified location. The standard configuration for export is `{\"package\": \".\\\\package\\\"}`: construct the msi package product directory and copy it to the directory named `package`. *Multiple targets can be specified to help debug packaging issues.*\n* `bootstrap`: Bootstrap accepts a configuration to evaluate *within the build context*. It allows you to ensure the build script version exactly matches the content being built. Bootstrap sets up python and a mailpile checkout/clean copy, then invokes the version of `provide` within that checkout with the configuration specified. A few items are carried over: log level, cache location, git path, etc.; however it is necessary to nest an export configuration within bootstrap. (Yes, multiple bootstrap recursion is possible, though _not at all recommended_.\n\nOf the two, `bootstrap` should be preferred for reproducible builds, and `export` should be preferred for debugging.\n\n### Building the installer for multiple languages ###\n\nThe build script primarily relies on wixtooset's language support for localized installers. Mailpile localization itself is a separate topic handled at run time. To build installers in a specific language, specify a list of wix 'cultures' to build. On the command line (if directly using `export`):\n\n```\n--config_package_cultures=\"[\\\"fr-fr\\\",\\\"en-us\\\",\\\"en-gb\\\"]\"\n```\n\nIn the root of the configuration file(or `bootstrap` configuration):\n\n```\n   \"package_cultures\": [\"fr-fr\",\"en-us\"]\n```\n\nOne installer will be created for each localization in the output directory.\n\n### Specifying an alternate branch or repository ###\n\nBy default, `provide` copies the current repository checkout, cleanly resets it, then performs a build. This nicely avoids re-checking out submodules etc. that might otherwise slow down a build, and is generally what people want most of the time. You can ask `provide` to build a specific branch/checkout from a specific repository at configuration time. In a configuration file, add the following to the root configuration object:\n\n```\n  \"mailpile\": {\n    \"commit\": \"<branch name or commit hash>\",\n    \"repo\": \"<url to a mailpile git repository>\"\n  }\n```\n\nOr on the command line, add the following option:\n\n```\n--config_mailpile=\"{\\\"commit\\\": \\\"<branch name or commit hash>\\\", \\\"repo\\\": \\\"<url to a mailpile git repository>\\\"}\"\n```\n\nSee the example provide configuration JSONs in `mailpile/packages/windows-wix`.\n\n### Build Scripts ###\n\nTargets are provided by as a modular/extensible scripting framework. See `mailpile/packages/windows-wix/README.md`.\n"
  },
  {
    "path": "packages/windows-wix/README.md",
    "content": "# WIX Mailpile packaging framework #\n\nThe WIX mailpile packaging framework automates constructing a windows package\nfor mailpile and it's dependencies. It attempts to pull in dependencies in as\nlocalized/portable resources, and appropriately construct a run-time\nenvironment on the installed machine.\n\nThe intent is for the packaging framework to *just work* assuming no interface\npoints between dependencies change. To do so, the framework primarily functions\naround sourcing, discovery, and versioning of files. While there's a fair\namount of automation in the process, ultimately customizations are baked in to\nvarious points in the framework.\n\n## Producing a package ##\n\nProducing a package has minimal requirements:\n\n  - A windows machine (tested: win 7, win 10)\n  - Python installed (tested: python 2.7.15, 3.4.3)\n  - gpg4win **NOT** installed\n  - git installed\n  - mailpile checked out\n  - administrator privileges\n  - Optional software signing requirements:\n      - a pkcs12 certificate suitable for software signing\n      - the software signing portion of the windows sdk (~50MB)\n\nTo produce a package, go to the 'mailpile/packages/windows-wix/' directory and\nrun `python provide -i provide.json`. It will output a directory called\n'package' in the current working directory. The msi is located in\n'package/mailpile-<version>-<culture>.msi'.\n\nTo customize the build, view options by running `python provide -h`. Most can\nalso be passed via a configuration json--see 'provide.json' as an example. It\nis also possible to output partial products by adding multiple targets to the\n'export' configuration.\n\nNote: work in progress on customization and improving automation. See TODO.md.\n\n### Software Signing ###\n\nSoftware signing is an entirely optional build step. If not configured, the\nbuild will succeed, but produce several warnings that it was not able to sign\nthe contents. While functional, an unsigned build does not provide authenticity\nassurances.\n\nSoftware signing uses `signtool.exe` to sign recognized file types prior to\npackaging, as well as the output package itself. Since Microsoft doesn't provide\na portable/stand-alone download for signtool, anyone wanting to sign a mailpile\npackage must install the relevant part of the windows sdk. The install feature\n\"Windows SDK Signing Tools for Desktop Apps\" should be enough on it's own.\n\nThe packaging script will attempt to detect `signtool.exe` in it's default\ninstall location. This can be checked by inspecting the help output from\n`python provide -h`: If the default for `--config_signtool` is `signtool.exe`,\nprovide was not able to locate signtool on the local machine, and expects it\nto exist on the system path. The location of `signtool.exe` can also be defined\nby manually setting this parameter.\n\nActually signing software components requires a pkcs12 PKI certificate capable\nof software signing. A self-signed cert can be made following these intsructions\nhttps://stackoverflow.com/questions/9428335/make-your-own-certificate-for-signing-files\n\nAlternately, a software signing certifacte can be sourced. Note that Microsoft\nonly includes select few roots of trust by default--any other CA, no matter how\nvalid, will not be validated a vanila windows install. Check the list carefully!\n\nWith a certificate in hand, the `--config_signing_key` and\n`--config_signing_passwd` options can be set to enable software signing. Both\nare required as it is assumed the key is stored in an encrypted state.\nAdditionally, the signing timestamp server can be configured--see\n`python provide -h` for full details.\n\n## Integration points ##\n\nThe mailpile package integrates various mailpile dependencies in various ways.\nPhysicially, each top-level dependency is placed in it's own sub-directory\ninside the install directory. Components are logically joined via a helper\nscript 'with-mailpile-env' that probes and constructs an environment\nprioritizing mailpile's packaged dependencies. The script contains a hard-coded\nset of dependencies to resolve, starting from the script's physical location.\nPython libraries are injected into the PYTHONPATH environment variable, and\nbinaries are pre-pended to the system path.\n\nUsing the above strategy, tor, gpg, python, and gui-o-matic are made available.\n\nA launch-mailpile script is also provided to invoke the above script to start\nmailpile-gui.py. The intent is to provide a simple/intuitive high-level entry\npoint.\n\nAll mailpile dependencies run in the above environment unless otherwise specified.\n\n## Modifying the packaging process ##\n\nModifying packaging convers a very wide range of subjects:\n\n  - Specifying dependency sources (a.k.a. resources)\n  - Modifying the build system to include new dependencies\n  - Including new dependencies in packages\n  - ...probably other things I'm forgetting right now\n\nThis reflects the internal flow in the build system: build scripts interact\nwith resources to prepare an install enviornment(somewhat akin to a fakeroot),\nwhich is then rolled into a package. Resources and package layout are largely\ndescribed via configuration jsons, where as build steps are largely scripted.\n\n### Specifying dependencies sources (a.k.a. resources) ###\n\nResources cover anything that needs to be downloaded and manipulated to produce\nthe package. Resources are specified as a json dictionary where each entry is\na dependency name with a url and sha1 sub-entry:\n\n```json\n\n{\n  \"<dependency name>\": {\"url\": \"<url string>\", \"sha1\": \"<sha1 digest of file>\"}\n}\n\n```\n\nThe `provide/cache.py` utility is helpful for constructing and/or manipulating\nresources files. See help for the utility for more info. Note that resources\ncan only be single files.\n\nInternally the build system uses dependency names to interact with resources.\nAs long as the rest of the build steps are uneffected, it's possible to change\nresource sources (i.e. version of python, tor, etc.) opaquely at this level.\n\nSee 'Producing a package' above for how to use a custom resource json in the\nbuild system.\n\n### Modifying the build system to include new dependencies ###\n\nThe build system is based off of lazily evaluating dependent scripts to produce\nbuild artifacts. The artifacts need not be a package--the framework is flexible\nenough to produce any intermediary. The build system is primarily organized\naround decorating build scripts, which are later invoked around a build\ncontext to help incrementally construct build artifacts. As part of the\nbuild process, build scripts can also publish helper functions which can be\ninvoked by other scripts. In that sense, the build system simultaneously \nbootstraps itself and the build artifacts.\n\nThe auto-bootstrapping nature is increadibly useful for having a polite build\nsystem: rather than requring the machine to be statically configured for\nbuilding, the build system incrementally assembles it's own dependencies on an\nas-needed basis. In that sense, the build system does not distinguish between\nit's own dependencies and those of the output artifact--both are assembled as\nneeded.\n\nThe build system has three customization points:\n\n  - Build scripts to provide new dependencies\n  - configuration points to easily change run time\n  - default configuration functions\n\n#### Build Scripts ####\n\nEach dependency is associated with a build script, which is responsible for\ncompletely configuring the dependency and returning a 'built' path for that\ndependency:\n\n```python\n\n# Register this build script as a provider of multiple dependencies\n#\ndef bind( framework ):\n    '''\n    bind the script to a build framework--maybe we'll need multiple build\n    recipies at some point.\n    '''\n    \n    @framework.provide('dependency1', 'dependency2')\n    def provide_example(build, keyword):\n        '''\n        Build script example\n\n        :build: build context\n        :keyword: dependency name we're being asked to build\n        :dep_path: suggested output path in the build directory\n        '''\n\n        # Depend on another build script's output\n        # (and optionally get the path to that output)\n        #\n        external_dep = build.depend('external_dep')\n\n        # 'root'--the build root, is a _very_ common depenendency. It also\n        # publishes the 'path' command that can be used to discover output\n        # paths for build products.\n        #\n        build.require('root')\n\n        # use a function provided by another build script\n        #\n        dep_path = build.invoke('path', keyword)\n        build.invoke('function_from_external_dep', dep_path)\n\n        # provide a function for other build scripts\n        #\n        def example_function(*args, **kwargs):\n            # Log events using standard python logging semantics\n            #\n            build.log().debug(\"called with {} {}\".format(args, kwargs))\n\n        build.publish(keyword + '_func', example_function) \n\n        # Return the path to the built dependency.\n        # Return None if there are no resultant artifacts(i.e. just publishing)\n        #\n        return dep_path\n```\n\nThere are already build scripts to handle several general cases:\n\n  - Unpacking a zip\n  - Checking out a git repository\n\nThese generic scripts can be extended to cover other dependencies by expanding\ntheir registration:\n\n```python\n\ndef bind(framework):\n\n    # ... #\n    @framework.provide('dependency1', 'dependency2', 'my_new_dependency')\n    def function body(build, keyword):\n        # ... #\n```\n\nThere are also examples for working with MSIs and exes.\n\n#### Configuration points ####\n\nThe build system exposes configuration to build scripts as a generic key-value\nstore. Configuration consolidates two configuration streams: a default\nconfiguration method(see below), and user-defined override. Should neither\nexist, the configuration system throws a KeyError, just like any other\nkey-value store. Build scripts can query configuration via the build context:\n\n```python\n\ndef bind(framework):\n\n    @framework.provide('example_dep')\n    def provide_config_example(build, keyword):\n\n        try:\n            value = build.config('example-key')\n        except KeyError:\n            value = {'default': ['to','some','definition']}\n\n        # ....\n\n```\n\n#### Default configuation ####\n\nRather than baking defaults into build scripts, it's better to explicity expose\nthem via the build system so that users can intuitively discover and manipulate\nthem. Default configurations are just functions that the build system invokes\nto produce a value. They are registered via decorator, just like build scripts:\n\n```python\n\ndef bind(framework):\n\n    @framework.default_config('repo_a', 'repo_b')\n    def config_repo_defaults(keyword):\n        '''\n        Provide the default url and commit to checkout for a repo\n        '''\n        return {'url': 'https://github.com/ExampleAccount/{}'.format(keyword),\n                'commit': 'master' }\n\n\n```\n\n\n### Including new dependencies in packages ###\n\nPackaging is actually just a build script that prepares an MSI via wix. It uses\na template 'package_template.json' that describes build elements to scan/import.\nSee the template for examples.\n\nNote that the template uses brace expansion '{dependency}' to inject dependency\npaths into the configuration.\n"
  },
  {
    "path": "packages/windows-wix/TODO.md",
    "content": "# TODOs #\n\nThings that really should be done to improve the windows experience, in no\nparticular order.\n\n  - Change the after-install mailpile launch to launch hidden\n  - Integrate signing\n  - Improve language support\n  - Better per-file management in packaging(hash-correlation per path)\n"
  },
  {
    "path": "packages/windows-wix/bin/launch-mailpile.bat",
    "content": "SET MAILPILE_ROOT=%~dp0\r\n\r\nStart \"\" \"%MAILPILE_ROOT%..\\Python27\\pythonw-mailpile.exe\" \"%MAILPILE_ROOT%\\with-mailpile-env.py\" -q \"%MAILPILE_ROOT%..\\Mailpile\\shared-data\\mailpile-gui\\mailpile-gui.py\""
  },
  {
    "path": "packages/windows-wix/bin/with-mailpile-env.py",
    "content": "\"\"\"\r\nThis script sets up a mailpile-compatible environment, then executes the\r\nspecified command. It localizes the majority of bootstrapping into python,\r\nso that top-level entry points (like start menu items) only need to know\r\nabout python and this script.\r\n\r\nExample: path\\\\to\\\\python with-mailpile-env.py mailpile-gui.py\r\n\"\"\"\r\n\r\nimport os\r\nimport sys\r\nimport subprocess\r\nimport argparse\r\nfrom functools import reduce\n\r\nlocate = ((\"Mailpile\", \"mailpile\"),\r\n          (\"gpg\", \"bin\", \"gpg.exe\"),\r\n          (\"gpg\", \"bin\", \"gpg-agent.exe\"),\r\n          (\"Mailpile\", \"submodules\", \"gui-o-matic\", \"gui_o_matic\"),\r\n          (\"tor\", \"Tor\", \"tor.exe\"),\r\n          (\"openssl\", \"openssl.exe\"))\r\n\r\ndef locate_parent( path_parts ):\r\n\r\n    if len( path_parts ) > 1:\r\n        path = reduce( os.path.join, path_parts )\r\n    else:\r\n        path = path_parts[ 0 ]\r\n    directory = os.path.abspath( __file__ )\r\n    while True:\r\n        test_path = os.path.join( directory, path )\r\n        if os.path.exists( test_path ):\r\n            return os.path.split( test_path )[ 0 ]\r\n        \r\n        parent = os.path.split( directory )[0]\r\n        if parent == directory:\r\n            raise ValueError( \"Cannot locate '{}'\".format( path ) )\r\n        else:\r\n            directory = parent\r\n\r\ndef split_args( args = sys.argv[1:] ):\r\n    '''\r\n    Split arguments into options and remainder at first non-option argument\r\n    '''\r\n    opts = []\r\n    \r\n    for arg in args:\r\n        if arg.startswith('-'):\r\n            opts.append( arg )\r\n        else:\r\n            break\r\n        \r\n    invoke = args[ len(opts): ]\r\n    return (opts, invoke)\r\n\r\nif __name__ == '__main__':\r\n    path_additions = set( map( locate_parent, locate ) )\r\n    python_dir = os.path.abspath( os.path.split( sys.executable )[0] )\r\n    path_additions.add( python_dir )\r\n    for key in (\"PATH\", \"PYTHONPATH\" ):\r\n        try:\r\n            paths = list( path_additions )\r\n            paths.extend( filter( lambda path: path not in path_additions,\r\n                                  os.environ[ key ].split( ';' ) ) )\r\n            os.environ[ key ] = ';'.join( paths )\r\n        except KeyError:\r\n            os.environ[ key ] = ';'.join( path_additions )\r\n\r\n    parser = argparse.ArgumentParser(description=\"Invokes a command with environment variables setup for mailpile\")\r\n    parser.add_argument( '--stdin', type = str,\r\n                         help= \"Redirect stdin from file\" )\r\n    parser.add_argument( '--stdout', type = str,\r\n                         help = \"Redirect stdout to file\" )\r\n    parser.add_argument( '--stderr', type = str,\r\n                         help = \"Redirect stderr to file\" )\r\n\r\n    parser.add_argument( '-q', '--quiet',\r\n                         action = 'store_true',\r\n                         help = \"Redirect stdin, stdout, stderr to devnull unless otherwise specified\" )\r\n\r\n    if len(sys.argv) <= 1:\r\n        parser.print_usage()\r\n        exit( -1 )\r\n        \r\n    opts, invoke = split_args()\r\n    args = parser.parse_args(opts)\r\n\r\n    redirects = {}\r\n\r\n    for (key, mode) in (('stdin','r'), ('stdout','w'), ('stderr','w')):\r\n        path = getattr( args, key )\r\n        if path:\r\n            redirects[ key ] = open( path, mode )\r\n        elif args.quiet:\r\n            redirects[ key ] = open( os.devnull, mode )\r\n\r\n    if invoke[0].endswith( '.py' ):\r\n        invoke = [ sys.executable ] + invoke\r\n        \r\n    try:\r\n        subprocess.check_call( invoke, shell = True, **redirects )\r\n    finally:\r\n        for handle in redirects.values():\r\n            handle.close()\r\n"
  },
  {
    "path": "packages/windows-wix/dependencies-windows.txt",
    "content": "pywin32\r\npil"
  },
  {
    "path": "packages/windows-wix/package.json",
    "content": "{\r\n  \"languages\": \"1033\", \r\n  \"version\": \"0.0.0\", \r\n  \"installer_version\": \"100\", \r\n  \"product_id\": \"19671260-92a2-437d-bb3a-d47e91e3cf23\",\r\n  \"codepage\": \"1252\", \r\n  \"product_code\": \"4685a239-2c80-4f51-8476-791316d2df3d\", \r\n  \"manufacturer\": \"Mailpile ehf.\",\r\n  \"product_icon\": \"mailpile_logo.ico\",\r\n  \"icons\": [\r\n\t\"assets\\\\mailpile_logo.ico\"\r\n  ],\r\n  \"ui\": {\r\n    \"flavor\": \"WixUI_InstallDir\",\r\n    \"variables\": {\r\n      \"WixUILicenseRtf\": \"assets\\\\LicenseText.rtf\",\r\n      \"WixUIDialogBmp\": \"assets\\\\WixUIDialog.bmp\",\r\n      \"WixUIBannerBmp\": \"assets\\\\WixUIBanner.bmp\"\r\n    }\r\n  },\r\n  \"groups\": {\r\n    \"python\": {\r\n      \"ignore\": [\r\n        \".*\\\\.py(?:c|o)$\", \r\n        \".*\\\\.git.*\"\r\n      ], \r\n      \"root\": \".\\\\tmp8wzuv8\\\\Python27\", \r\n      \"uuid\": \"06dfe53e-01c3-4cd0-b6b6-1983f217692f\"\r\n    }, \r\n    \"platform-scripts\": {\r\n      \"ignore\": [\r\n        \".*\\\\.py(?:c|o)$\", \r\n        \".*\\\\.git.*\", \r\n        \".*\\\\.msi$\"\r\n      ], \r\n      \"shortcuts\": {\r\n        \"bin\\\\launch-mailpile.bat\": {\r\n          \"Description\": \"GUI for Mailpile Email Client\", \r\n          \"Id\": \"MailpileGUI\", \r\n          \"WorkingDirectory\": \"MailpileClient\", \r\n          \"Name\": \"Mailpile GUI\",\r\n          \"Show\": \"minimized\",\r\n          \"Icon\": \"mailpile_logo.ico\"\r\n        }\r\n      }, \r\n      \"root\": \"c:\\\\Users\\\\ededa\\\\Documents\\\\Mailpile\\\\packages\\\\windows-wix\\\\bin\", \r\n      \"uuid\": \"0540bc0b-a521-4488-812a-1c430ef1d8b3\"\r\n    }, \r\n    \"gpg\": {\r\n      \"ignore\": [], \r\n      \"root\": \".\\\\tmp8wzuv8\\\\gpg\", \r\n      \"uuid\": \"a8014bc6-5282-4d7a-a46d-3b0d53519914\"\r\n    },\r\n    \"mailpile\": {\r\n      \"ignore\": [\r\n        \".*\\\\.git.*\", \r\n        \".*\\\\.msi$\", \r\n        \".*packages\\\\\\\\windows.*\"\r\n      ], \r\n      \"root\": \".\\\\tmp8wzuv8\\\\Mailpile\", \r\n      \"uuid\": \"62e900bd-7f53-4746-8ef9-f8d93848e89d\"\r\n    }, \r\n    \"gui-o-matic\": {\r\n      \"ignore\": [\r\n        \".*\\\\.git.*\"\r\n      ], \r\n      \"root\": \".\\\\tmp8wzuv8\\\\gui-o-matic\", \r\n      \"uuid\": \"0465f2d6-becc-4279-82af-b6d23c6bf033\"\r\n    },\r\n    \"tor\": {\r\n      \"root\": \".\\\\tmp8wzuv8\\\\tor\",\r\n      \"uuid\": \"b44d81df-26c5-468b-b9fe-348a9fd0d606\"\r\n    }\r\n  }\r\n}"
  },
  {
    "path": "packages/windows-wix/package.py",
    "content": "\"\"\"\r\nPackaging script for capturing a portable mailpile into an msi.\r\n\r\nAssumes use of the wix install system--generates XML to configure the process.\r\n\r\nComponents:\r\n    - Python\r\n        - (potentially separate downloaded pages at a later date)\r\n    - Mailpile\r\n    - gui-o-matic (likely bunded as site package)\r\n    - gpg + deps\r\n    - openssl + deps\r\n\r\nEach component needs a consistant UUID for update purposes\r\n\"\"\"\r\n\r\nimport xml.etree.ElementTree as ET\r\nfrom xml.dom import minidom\r\nimport os\r\nimport os.path\r\nimport shutil\r\nimport collections\r\nimport itertools\r\nimport sys\r\nimport json\r\nimport re\r\nimport uuid\r\nimport hashlib\r\nimport functools\r\n\r\nimport logging\r\nimport logging.handlers\r\n\r\nimport argparse\r\nimport subprocess\r\n\r\nlogger = logging.getLogger( __name__ )\r\n\r\ndef consume(iterator, n=None):\r\n    \"Advance the iterator n-steps ahead. If n is None, consume entirely.\"\r\n    # Use functions that consume iterators at C speed.\r\n    if n is None:\r\n        # feed the entire iterator into a zero-length deque\r\n        collections.deque(iterator, maxlen=0)\r\n    else:\r\n        # advance to the empty slice starting at position n\r\n        next(islice(iterator, n, n), None)\r\n\r\n\r\ndef xml_attrs( xml_element, **attrs ):\r\n    consume( itertools.starmap( xml_element.set, attrs.items() ) )\r\n\r\ndef xml_append( xml_parent, xml_element_type, **attrs ):\r\n    result = ET.SubElement( xml_parent, xml_element_type )\r\n    xml_attrs( result, **attrs )\r\n    return result\r\n\r\nclass WixConfig( object ):\r\n    '''\r\n    A mailpile-specific wix file generator.\r\n\r\n    Takes config that one might expect to modify centrally or express as\r\n    arguments to the packaging process, and uses them to expand a template\r\n    structure.\r\n\r\n    Note: UUIDs ***MUST*** remain consistent across versions. While rarely they\r\n    might change, assume anywhere a UUID is required, it must be archived.\r\n    '''\r\n\r\n    def __init__( self, config ):\r\n        self.config = config\r\n        self.dirs = {}\r\n        self.logical = {}\r\n                \r\n        self.root = ET.Element( 'Wix' )\r\n        self.root.set( 'xmlns', 'http://schemas.microsoft.com/wix/2006/wi' )\r\n        \r\n        self.product = ET.SubElement( self.root, 'Product' )\r\n        xml_attrs( self.product,\r\n                   Name = 'Mailpile Email Client {version}'.format( **self.config ),\r\n                   Language = config['languages'],\r\n                   Codepage = config['codepage'],\r\n                   Version = config['version'],\r\n                   Manufacturer = config['manufacturer'],\r\n                   Id = config['product_id'],\r\n                   UpgradeCode = config['product_code'] )\r\n\r\n        xml_append(self.product, 'Package',\r\n                   Id = '*',\r\n                   Keywords = 'Installer',\r\n                   Description = 'Mailpile {version} Installer'.format(**self.config),\r\n                   Comments = 'Mailpile is under the AGPL license',\r\n                   Manufacturer = config['manufacturer'],\r\n                   InstallerVersion = config['installer_version'],\r\n                   Languages = config['languages'],\r\n                   Compressed = 'yes',\r\n                   SummaryCodepage = config['codepage'] )\r\n\r\n        xml_append(self.product, 'Media',\r\n                   Id = '1',\r\n                   Cabinet = 'mailpile.cab',\r\n                   EmbedCab = 'yes',\r\n                   DiskPrompt='CD-ROM #1')\r\n\r\n        xml_append(self.product, 'Property',\r\n                   Id = 'DiskPrompt',\r\n                   Value = 'Mailpile {version} Media [1]'.format( **self.config ))\r\n\r\n        xml_append(self.product, 'MajorUpgrade',\r\n                   AllowDowngrades = 'no',\r\n                   DowngradeErrorMessage = \"Downgrades not supported!\",\r\n                   AllowSameVersionUpgrades = 'no')\r\n                   \r\n        self.logical_root( Id = 'TARGETDIR',\r\n                           Name = 'SourceDir' )\r\n        \r\n        self.feature = ET.SubElement( self.product, 'Feature' )\r\n        xml_attrs( self.feature,\r\n                   Id = 'Complete',\r\n                   Title = 'Mailpile {version}'.format( **self.config ),\r\n                   Description = 'Complete Mailpile Install' )\r\n\r\n        # Setup: <location>/<manufacturer>/<product>\r\n        #\r\n        self.logical_node( 'TARGETDIR',\r\n                           Id = 'ProgramFilesFolder', # windows token\r\n                           Name = 'ProgramFiles' )\r\n\r\n        self.logical_node( 'ProgramFilesFolder',\r\n                           Id = 'APPLICATIONFOLDER',\r\n                           Name = 'Mailpile ehf' )\r\n\r\n        self.logical_node( 'APPLICATIONFOLDER',\r\n                           Id = 'MailpileClient',\r\n                           Name = 'Mailpile Client {version}'.format( **self.config ) )\r\n\r\n        # Setup: Menu item directory\r\n        #\r\n        self.logical_node( 'TARGETDIR',\r\n                           Id = 'ProgramMenuFolder' )\r\n\r\n        self.logical_node( 'ProgramMenuFolder',\r\n                           Id = 'ProgramMenuDir',\r\n                           Name = 'Mailpile {version}'.format( **self.config ) )\r\n\r\n        self.menu_component = xml_append( self.logical['ProgramMenuDir'],\r\n                                          'Component',\r\n                                     Id = 'ProgramMenuDir',\r\n                                     Guid = self.uuid( '\\\\windows\\\\ProgramMenuDir' ))\r\n\r\n        menu_reg_key = xml_append( self.menu_component, 'RegistryValue',\r\n                                   Root = 'HKCU',\r\n                                   Key = 'Software\\\\[Manufacturer]\\\\[ProductName]\\\\StartMenuShortcut',\r\n                                   Type = 'string',\r\n                                   Value = '1',\r\n                                   KeyPath = 'yes' )\r\n\r\n        menu_cleanup = xml_append( self.menu_component, 'RemoveFolder',\r\n                                   Id = 'ProgramMenuDir',\r\n                                   On = 'uninstall' )\r\n\r\n\r\n        xml_append( self.feature, 'ComponentRef',\r\n                    Id = 'ProgramMenuDir' )\r\n\r\n        # Setup: Desktop shortcut directory\r\n        #\r\n        self.logical_node( 'TARGETDIR',\r\n                           Id = 'DesktopFolder' )\r\n\r\n        self.desktop_component = xml_append( self.logical['DesktopFolder' ],\r\n                                             'Component',\r\n                                             Id = 'DesktopFolderDir',\r\n                                             Guid = self.uuid( '\\\\windows\\\\DesktopFolder' ))\r\n\r\n        desktop_reg_key = xml_append( self.desktop_component, 'RegistryValue',\r\n                                   Root = 'HKCU',\r\n                                   Key = 'Software\\\\[Manufacturer]\\\\[ProductName]\\\\DesktopShortcut',\r\n                                   Type = 'string',\r\n                                   Value = '1',\r\n                                   KeyPath = 'yes' )\r\n\r\n        desktop_cleanup = xml_append( self.desktop_component, 'RemoveFolder',\r\n                                   Id = 'DesktopFolderDir',\r\n                                   On = 'uninstall' )\r\n\r\n        xml_append( self.feature, 'ComponentRef',\r\n                    Id = 'DesktopFolderDir' )\r\n\r\n        # Setup: unpack groups\r\n        #\r\n        for key, group in self.config['groups'].items():\r\n            self.scan_group( key, **group )\r\n\r\n        try:\r\n            self.config_ui( **self.config['ui'] )\r\n        except KeyError:\r\n            logger.warn( \"No UI specified, configuring MSI without dialogs.\" )\r\n\r\n        for icon in self.config.get( 'icons', [] ):\r\n            xml_append( self.product, \"Icon\",\r\n                        Id = os.path.basename( icon ),\r\n                        SourceFile = icon )\r\n\r\n        try:\r\n            xml_append( self.product, 'Property',\r\n                        Id = 'ARPPRODUCTICON',\r\n                        Value = self.config['product_icon'] )\r\n        except KeyError:\r\n            raise\r\n\r\n    def post_install( self ):\r\n        '''\r\n        post_install actions\r\n        '''\r\n        \r\n\r\n    # These properties align folder relocation behavior with implicit structure\r\n    #\r\n    flavor_properties = {\r\n        'WixUI_InstallDir': {\r\n            'WIXUI_INSTALLDIR': 'APPLICATIONFOLDER'\r\n        },\r\n        'WixUI_Advanced': {\r\n            'ApplicationFolderName': 'Mailpile ehf',\r\n            'WixAppFolder': 'WixPerMachineFolder'\r\n        }\r\n    }\r\n\r\n    def config_ui( self, flavor = 'WixUI_Advanced', variables = {} ):\r\n        '''\r\n        configure wix UI\r\n        '''\r\n        logger.info( \"Configuring UI flavor '{}'\".format( flavor ) )\r\n        ui = xml_append( self.product, 'UI' )\r\n        xml_append( ui, 'UIRef', Id = flavor )\r\n        xml_append( ui, 'UIRef', Id = 'WixUI_ErrorProgressText' )\r\n\r\n        if flavor not in self.flavor_properties:\r\n            logger.warn( \"Unrecognized wix UI type: {}\".format( flavor ) )\r\n\r\n        for key, value in self.flavor_properties.get( flavor, {} ).items():\r\n            xml_append( self.product, 'Property',\r\n                        Id = key,\r\n                        Value = value )\r\n\r\n        for key, value in variables.items():\r\n            xml_append( self.product, 'WixVariable',\r\n                        Id = key,\r\n                        Value = value )\r\n        \r\n        node = xml_append( ui, 'Publish',\r\n                           Dialog = 'WelcomeDlg',\r\n                           Control = 'Next',\r\n                           Event = 'NewDialog',\r\n                           Value = 'InstallDirDlg',\r\n                           Order = '2' )\r\n        node.text = \"1\"\r\n        node = xml_append( ui, 'Publish',\r\n                           Dialog = 'InstallDirDlg',\r\n                           Control = 'Back',\r\n                           Event = 'NewDialog',\r\n                           Value = 'WelcomeDlg',\r\n                           Order = '2' )\r\n        node.text = \"1\"\r\n\r\n        # Launch mailpile on exit.\r\n        #\r\n        xml_append( self.product, 'Property',\r\n                    Id = 'WixShellExecTarget',\r\n                    Value = '[#{}]'.format( self.file_id( 'bin\\\\launch-mailpile.bat' ) ) )\r\n\r\n        xml_append( self.product, 'CustomAction',\r\n                    Id = 'LaunchApplication',\r\n                    BinaryKey = 'WixCA',\r\n                    DllEntry = 'WixShellExec',\r\n                    Impersonate = 'yes' )\r\n\r\n        node = xml_append( ui, 'Publish',\r\n                           Dialog = 'ExitDialog',\r\n                           Control = 'Finish',\r\n                           Event = 'DoAction',\r\n                           Value = 'LaunchApplication' )\r\n\r\n        node.text = 'NOT Installed'\r\n\r\n        # Kill GPG Agent if this MSI is installed.\r\n        #\r\n        xml_append( self.product, 'SetProperty',\r\n                    Id = 'WixQuietExecCmdLine',\r\n                    Sequence = 'execute',\r\n                    Before = 'GPGAgent.TaskKill',\r\n                    Value = '\"[WindowsFolder]\\\\System32\\\\taskkill.exe\" /F /IM gpg-agent.exe' )\r\n\r\n        '''\r\n        xml_append( self.product, 'Property',\r\n                    Id = 'WixQuietExecCmdLine',\r\n                    Value = '\"[WindowsFolder]\\\\System32\\\\taskkill.exe\" /F /IM gpg-agent.exe' )\r\n        '''\r\n        \r\n        xml_append( self.product, 'CustomAction',\r\n                    Id = 'GPGAgent.TaskKill',\r\n                    BinaryKey = 'WixCA',\r\n                    DllEntry = 'WixQuietExec',\r\n                    Execute = 'immediate',\r\n                    Return = 'ignore',\r\n                    Impersonate = 'yes' )\r\n\r\n        installSeq = xml_append( self.product, 'InstallExecuteSequence' )\r\n        node = xml_append( installSeq, 'Custom',\r\n                           Action = 'GPGAgent.TaskKill',\r\n                           Before = 'InstallValidate' )\r\n\r\n        node.text = 'Installed'\r\n\r\n    def logical_node( self, parent_id, **attrs ):\r\n        '''\r\n        Append a logical directory to the specified parent\r\n        '''\r\n        self.logical[ attrs['Id'] ] = xml_append( self.logical[ parent_id ],\r\n                                                  'Directory',\r\n                                                  **attrs )\r\n\r\n    def logical_root( self, **attrs ):\r\n        '''\r\n        Create a new top level logical object\r\n        '''\r\n        self.logical[ attrs['Id'] ] = xml_append( self.product,\r\n                                                  'Directory',\r\n                                                  **attrs )\r\n\r\n    def uuid( self, path, local_path = None ):\r\n        '''\r\n        get or create an appropriate uuid for the specified path.\r\n\r\n        Use UUIDv5 to create namespace/sha1 based uuids(see uuid RFC for\r\n        background about UUID v5). Namespaces are organized as follows:\r\n\r\n            product_id -> path -> binary contents of file\r\n\r\n        The first is easy:\r\n\r\n            root_namespace = self.config['product_id']\r\n            path_namespace = uuid.uuid5(root_namespace, path)\r\n\r\n        Sadly, uuid.uuid5() doesn't support generators, so we cannot write:\r\n\r\n            with open(localpath, 'rb') as handle:\r\n                guid = uuid.uuid5(path_namespace, handle)\r\n\r\n        Instead, we do the work ourselves:\r\n        \r\n            with open(localpath, 'rb') as handle:\r\n                digest = hashlib.sha1()\r\n                digest.update(path_namespace.bytes)\r\n                generator = functools.partial(handle.read, 4096)\r\n                        \r\n                for chunk in iter(generator, b''):\r\n                    digest.update(chunk)\r\n\r\n                guid = uuid.UUID(bytes = digest.digest()[:16], version = 5)\r\n\r\n        Where the last line overwrites the appropriate uuid version bits.\r\n\r\n        :path: path for uuid lookup\r\n        :local path: file to digest.\r\n        '''\r\n\r\n        root_namespace = uuid.UUID(self.config['product_id'])\r\n        path_namespace = uuid.uuid5(root_namespace, path.encode())\r\n\r\n        if local_path:\r\n            with open(local_path, 'rb') as handle:\r\n                digest = hashlib.sha1()\r\n                digest.update(path_namespace.bytes)\r\n                generator = functools.partial(handle.read, 4096)\r\n\r\n                for chunk in iter(generator, b''):\r\n                    digest.update(chunk)\r\n\r\n                guid = uuid.UUID(bytes = digest.digest()[:16], version = 5)\r\n        else:\r\n            guid = path_namespace\r\n\r\n\r\n        logger.debug(\"Using guid {} for path '{}'\".format(guid, path))\r\n        return str(guid)\r\n\r\n    def directory( self, path ):\r\n        '''\r\n        Get the xml element for the specified directory\r\n        \r\n        :path: directory path to lookup\r\n        '''\r\n        stack = []\r\n        while True:\r\n            try:\r\n                parent = self.dirs[ path ]\r\n                break\r\n            except KeyError:\r\n                parts = os.path.split( path )\r\n                if parts[0] == path:\r\n                    parent = self.logical['MailpileClient']\r\n                    break\r\n                else:\r\n                    path = parts[ 0 ]\r\n                    stack.append( parts[ 1 ] )\r\n\r\n        for part in reversed( stack ):\r\n            path = os.path.join( path, part )\r\n            parent = xml_append( parent, 'Directory',\r\n                                Id = self.directory_id( path ),\r\n                                Name = part )\r\n            self.dirs[ path ] = parent\r\n\r\n        return parent\r\n\r\n    def id_str( self, use, name ):\r\n        '''\r\n        Generate a unique ID string less than 72 characters long.\r\n        IDs must start with [a-zA-z_], and may contain dot '.'.\r\n\r\n        :use: context that allows names to be used for multiple elements.\r\n        :name: element identifier to mangle.\r\n        '''\r\n        #attempt = use + '_' + re.sub( '([-\\W])', '_', name )\r\n        #size = len( attempt )\r\n        #if size > 72:\r\n        #    attempt = use + '_' + hashlib.sha1( name ).hexdigest()\r\n        return use + '_' + hashlib.sha1( name.encode() ).hexdigest()\r\n\r\n    def directory_id( self, path ):\r\n        return self.id_str( 'Directory', path )\r\n\r\n    def component_id( self, name ):\r\n        return self.id_str( 'Component', name )\r\n    \r\n    def file_id( self, name ):\r\n        return self.id_str( 'File', name )\r\n\r\n    def mask_path( self, mask, path ):\r\n        '''\r\n        Mask off the local part of a path\r\n        '''\r\n        return path[ len(mask) + 1: ]\r\n\r\n    def scan_group( self, name, uuid, root, ignore = [], shortcuts = {} ):\r\n        '''\r\n        Scan an install root, adding files and generating uuids.\r\n\r\n        The specified root directory and it's contents are moved into the\r\n        install program files directory:\r\n\r\n        %ProgramFiles%/<Manufacturer>/<Product>/<root>\r\n\r\n        :name: name of this group\r\n        :uuid: uuid for this group\r\n        :root: local filesystem root directory\r\n        :ignore: sequence of regular expressions to suppress files.\r\n        '''\r\n        logger.info( \"Scanning install root '{}' uuid '{}'\".format( name, uuid ) )\r\n        mask = os.path.split( root )[0]\r\n        \r\n        def ignore_path( path ):\r\n            for expr in ignore:\r\n                if re.match( expr, path ):\r\n                    return True\r\n            return False\r\n\r\n        for parent, dirs, files in os.walk( root ):\r\n            for filename in files:\r\n                local_path = os.path.join( parent, filename )\r\n                path = self.mask_path( mask, local_path )\r\n                \r\n                if ignore_path( path ):\r\n                    logger.debug( 'Ignoring \"{}\"'.format( path ) )\r\n                    continue\r\n                else:\r\n                    logger.debug( 'Processing file \"{}\"'.format( path ) )\r\n\r\n\r\n                parent_dir = self.directory( self.mask_path( mask, parent ) )                \r\n                component_id = self.component_id( path )\r\n                component = xml_append( parent_dir, 'Component',\r\n                                Id = component_id,\r\n                                Guid = self.uuid( path, local_path ) )\r\n                \r\n                file = xml_append( component, 'File',\r\n                                   Id = self.file_id( path ),\r\n                                   Name = filename,\r\n                                   DiskId = '1',\r\n                                   Source = local_path,\r\n                                   KeyPath = 'yes' )\r\n\r\n                try:\r\n                    shortcut = dict( shortcuts[path] )\r\n                    name = shortcut.pop( 'Id' )\r\n                    xml_append( self.menu_component, 'Shortcut',\r\n                                Target = '[#{}]'.format( self.file_id( path ) ),\r\n                                Id = name + 'MenuItem',\r\n                                **shortcut )\r\n\r\n                    xml_append( self.desktop_component, 'Shortcut',\r\n                                Target = '[#{}]'.format( self.file_id( path ) ),\r\n                                Id = name + 'DesktopShortcut',\r\n                                **shortcut )\r\n                                \r\n                    logger.info( \"Created shortcut for '{}'\".format( path ) )\r\n                except KeyError:\r\n                    pass\r\n\r\n                xml_append( self.feature, 'ComponentRef', Id = component_id )\r\n        \r\n\r\n    def save( self, path, indent = 2 ):\r\n        dense = ET.tostring( self.root, encoding='utf-8' )\r\n        reparsed = minidom.parseString( dense )\r\n        pretty = reparsed.toprettyxml( indent = ' ' * indent, encoding = 'utf-8' ) \r\n        with open( path, 'wb' ) as handle:\r\n            handle.write( pretty )\r\n\r\n\r\nif __name__ == '__main__':\r\n    logging.basicConfig()\r\n\r\n    import argparse\r\n    import sys\r\n    basedir = os.path.dirname( sys.argv[ 0 ] )\r\n\r\n    parser = argparse.ArgumentParser( \"Generates the wix configuration from a\"\r\n                                      \" high level configuration file. Also\"\r\n                                      \" provides machinery for consistent uuids\" )\r\n\r\n    parser.add_argument( '--candle',\r\n                         default = os.path.join( basedir, \"tools\\\\wix311-binaries\\\\candle.exe {}\" ),\r\n                         help = \"template for invoking candle, using brace {} notation\" )\r\n\r\n\r\n    parser.add_argument( '--light',\r\n                         default = os.path.join( basedir, \"tools\\\\wix311-binaries\\\\light.exe -ext WixUIExtension -ext WixUtilExtension {} -cc cache -reusecab\" ),\r\n                         help = \"template for invoking light, using brace {} notation\" )\r\n\r\n    parser.add_argument( 'config',\r\n                         help = 'config file to inflate' )\r\n\r\n\r\n    parser.add_argument( '-l', '--log-level', help = \"Logging verbosity\" )\r\n\r\n    args = parser.parse_args()\r\n\r\n    if args.log_level:\r\n        logger.setLevel( getattr( logging, args.log_level ) )\r\n    \r\n    with open( args.config, 'r' ) as handle:\r\n        config = json.load( handle )\r\n\r\n    wix = WixConfig( config, 'mailpile.uuid.json' )\r\n    wix.save( 'mailpile' )\r\n\r\n    import subprocess\r\n    import shlex\r\n    cmd = shlex.split( args.candle.format( \"mailpile.wxs\" ).replace( '\\\\', '\\\\\\\\' ) )\r\n    logger.info( \"build step '{}'\".format( cmd ) )\r\n    subprocess.check_call( cmd )\r\n    cmd = shlex.split( args.light.format( \"mailpile.wixobj\" ).replace( '\\\\', '\\\\\\\\' ) )\r\n    logger.info( \"build step '{}'\".format( cmd ) )\r\n    subprocess.check_call( cmd )\r\n    '''candle mailpile.wxs'''\r\n    '''light -ext WixUIExtension mailpile.wixobj'''\r\n"
  },
  {
    "path": "packages/windows-wix/package_template.json",
    "content": "{\n  \"languages\": \"1033\", \n  \"version\": \"{version}\", \n  \"installer_version\": \"100\", \n  \"product_id\": \"19671260-92a2-437d-bb3a-d47e91e3cf23\",\n  \"codepage\": \"1252\", \n  \"product_code\": \"4685a239-2c80-4f51-8476-791316d2df3d\", \n  \"manufacturer\": \"Mailpile ehf.\",\n  \"product_icon\": \"mailpile_logo.ico\",\n  \"icons\": [\n\t\"{mailpile}\\\\packages\\\\windows-wix\\\\assets\\\\mailpile_logo.ico\"\n  ],\n  \"ui\": {\n    \"flavor\": \"WixUI_InstallDir\",\n    \"variables\": {\n      \"WixUILicenseRtf\": \"{mailpile}\\\\packages\\\\windows-wix\\\\assets\\\\LicenseText.rtf\",\n      \"WixUIDialogBmp\": \"{mailpile}\\\\packages\\\\windows-wix\\\\assets\\\\WixUIDialog.bmp\",\n      \"WixUIBannerBmp\": \"{mailpile}\\\\packages\\\\windows-wix\\\\assets\\\\WixUIBanner.bmp\"\n    }\n  },\n  \"groups\": {\n    \"python\": {\n      \"ignore\": [\n        \".*\\\\.py(?:c|o)$\", \n        \".*\\\\.git.*\"\n      ], \n      \"root\": \"{python27}\", \n      \"uuid\": \"06dfe53e-01c3-4cd0-b6b6-1983f217692f\"\n    }, \n    \"platform-scripts\": {\n      \"ignore\": [\n        \".*\\\\.py(?:c|o)$\", \n        \".*\\\\.git.*\", \n        \".*\\\\.msi$\"\n      ], \n      \"shortcuts\": {\n        \"bin\\\\launch-mailpile.bat\": {\n          \"Description\": \"Mailpile Email Client\", \n          \"Id\": \"MailpileShortcut\", \n          \"WorkingDirectory\": \"MailpileClient\", \n          \"Name\": \"Mailpile\",\n          \"Show\": \"minimized\",\n          \"Icon\": \"mailpile_logo.ico\"\n        }\n      }, \n      \"root\": \"{mailpile}\\\\packages\\\\windows-wix\\\\bin\", \n      \"uuid\": \"0540bc0b-a521-4488-812a-1c430ef1d8b3\"\n    }, \n    \"gpg\": {\n      \"ignore\": [], \n      \"root\": \"{gpg}\", \n      \"uuid\": \"a8014bc6-5282-4d7a-a46d-3b0d53519914\"\n    },\n    \"mailpile\": {\n      \"ignore\": [\n        \".*\\\\.git.*\", \n        \".*\\\\.msi$\", \n        \".*packages\\\\\\\\windows.*\"\n      ], \n      \"root\": \"{mailpile}\", \n      \"uuid\": \"62e900bd-7f53-4746-8ef9-f8d93848e89d\"\n    },\n    \"tor\": {\n      \"root\": \"{tor}\",\n      \"uuid\": \"b44d81df-26c5-468b-b9fe-348a9fd0d606\"\n    },\n    \"openssl\": {\n      \"root\": \"{openssl}\",\n      \"uuid\": \"827d52f4-62be-40c6-8fcf-42297fd99f79\"\n    }\n  }\n}"
  },
  {
    "path": "packages/windows-wix/provide/__init__.py",
    "content": ""
  },
  {
    "path": "packages/windows-wix/provide/__main__.py",
    "content": "from __future__ import absolute_import\nimport os\nimport os.path\nimport time\nimport argparse\nimport datetime\nimport json\nimport logging\nimport logging.handlers\n\nlogger = logging.getLogger(__name__)\nlogging.basicConfig()\n\nif 'DEBUG' in os.environ:\n    logging.getLogger().setLevel(logging.DEBUG)\n\nfrom . import cache\nfrom . import default\n\npackage_dir = os.path.dirname(__file__)\n\nparser = argparse.ArgumentParser()\ndefault_log = datetime.datetime.utcnow().strftime('provide-%Y%m%d-%H%M%S.log')\nparser.add_argument('-l', '--log_file', default=default_log,\n                    help=\"Log file, default build-<isotime>.log\")\nparser.add_argument('-v', '--log_level', default='WARN', help=\"Log level\")\nparser.add_argument('-i', '--input', help='input config file')\nparser.add_argument('-c', '--cache',\n                    default=cache.Cache.default_cache_dir(),\n                    help='cache directory location')\nparser.add_argument('-r', '--resources',\n                    default=os.path.join(package_dir, '..\\\\resources.json'),\n                    help='resources file location')\n\ndefault.build.parser(parser)\nargs = parser.parse_args()\nconfig = default.build.parse_config(args)\n\nif args.input:\n    with open(args.input, 'r') as handle:\n        config.update(json.load(handle))\n\nif args.log_file:\n    handler = logging.FileHandler(args.log_file)\n    logging.getLogger().addHandler(handler)\n\nif args.log_level:\n    logging.getLogger().setLevel(getattr(logging, args.log_level))\n\nresources = cache.SemanticCache.load(args.resources, cache.Cache(args.cache))\n\nlogger.debug(\"Using configuration: {}\".format(config))\nwith default.build.context(resources, config) as build:\n    build.depend('bootstrap')\n    build.depend('export')\n"
  },
  {
    "path": "packages/windows-wix/provide/build.py",
    "content": "import subprocess\nimport os.path\nimport tempfile\nimport argparse\nimport datetime\nimport logging\nimport json\nimport itertools\n\nlogger = logging.getLogger(__name__)\n\n\nclass Build(object):\n    '''\n    Construct a build environment where providers can be invoked to\n    compose build artifacts.\n    '''\n\n    class Invoker(object):\n        '''\n        Utility class for invoking external binaries. Captures stderr on error,\n        otherwise returns stdout. The intention is for the invocation to be\n        mostly a pythonic function call.\n        '''\n\n        def __init__(self, build, exe, method=subprocess.check_output):\n            '''\n            Configure the execution target and invocation method.\n            '''\n            self.build = build\n            self.exe = exe\n            self.method = method\n\n        def __call__(self, *args):\n            '''\n            Invoke the command, proxying arguments. Also surpress displaying\n            windows when possible.\n            '''\n\n            cmdline = (self.exe,) + tuple(args)\n            self.build.log().debug(\"Running command line: {}\".format(cmdline))\n            si = subprocess.STARTUPINFO()\n            si.dwFlags |= subprocess.STARTF_USESHOWWINDOW\n            with tempfile.TemporaryFile('ab+') as error_file:\n                try:\n                    return self.method(cmdline,\n                                       stderr=error_file,\n                                       startupinfo=si,\n                                       universal_newlines=True)\n                except subprocess.CalledProcessError as e:\n                    error_file.seek(0)\n                    error_text = error_file.read()\n                    self.build.log().critical(\"stderr from exception:\\n\" + error_text.decode())\n                    self.build.log().critical(\"stdout from exception:\\n\" + e.output)\n                    e.stderr = error_text\n                    raise\n\n    class Context(object):\n        '''\n        Build context that's disposed of after a build\n        '''\n\n        def __init__(self, build, cache, config):\n            '''\n            Attach the context to the parent build, and configure initial\n            properties.\n            '''\n\n            self._build = build\n            self._built = {}\n            self._cmds = {}\n            self._config = config\n            self._cache = cache\n            self._cleanup = []\n            self._log = [logger]  # TODO: Proxy things for clarity\n\n        def cache(self):\n            '''\n            API for accessing the SemanticCache for this build.\n            '''\n\n            return self._cache\n\n        def log(self):\n            '''\n            API for accessing the logger for this build.\n            '''\n\n            return self._log[-1]\n\n        def depend(self, keyword):\n            '''\n            Return the path to the specified build dependency, building it if\n            needed.\n            '''\n\n            try:\n                return self._built[keyword]\n            except KeyError:\n                self.log().info(\"Inflating dependency {}\".format(keyword))\n                log_name = '{}.{}@{}'.format(__name__, keyword, len(self._log))\n                self._log.append(logging.getLogger(log_name))\n                provider = self._build._providers[keyword]\n                result = provider(self, keyword)\n                self._built[keyword] = result\n                self._log.pop()\n                return result\n\n        def cleanup(self, action):\n            '''\n            Add a cleanup action to the build context.\n            '''\n\n            self._cleanup.append(action)\n\n        def publish(self, keyword, invoker):\n            '''\n            Provide an invokable for other build scripts to use. Can either be\n            a callable, or an absolute path to an executable. See Build.Invoker.\n            '''\n\n            if not callable(invoker):\n                invoker = self._build.Invoker(self, invoker)\n            self.log().debug('registering invokable {}'.format(keyword))\n            self._cmds[keyword] = invoker\n\n        def invoke(self, keyword, *args, **kwargs):\n            '''\n            Call a published interface with the specified arguments and kwargs\n            '''\n\n            return self._cmds[keyword](*args, **kwargs)\n\n        def clear(self):\n            '''\n            Delete everything in the build context.\n            '''\n\n            for action in self._cleanup:\n                try:\n                    action()\n                except:\n                    self.log().exception(\"Error during cleanup!\")\n\n        def config(self, keyword):\n            '''\n            Query a key-value config item.\n            '''\n\n            try:\n                result = self._config[keyword]\n            except KeyError:\n                result = self._build._defaults[keyword](keyword)\n                self.log().info(\"Using default '{}' config '{}'\".format(keyword, result))\n            return result\n\n        def __enter__(self):\n            self._begin = datetime.datetime.utcnow()\n            self.log().info('Entering build context at {}'.format(self._begin.isoformat()))\n            return self\n\n        def __exit__(self, type, value, traceback):\n            end = datetime.datetime.utcnow()\n            self.log().info('Exiting build context at {}'.format(end.isoformat()))\n\n            if traceback:\n                self.log().exception(\"Build failed!\")\n            self.clear()\n            self.log().info('Elapsed build time: {}'.format(end-self._begin))\n\n    def __init__(self):\n        '''\n        Create a new build framework\n        '''\n\n        self._providers = {}\n        self._defaults = {}\n        self._options = {}\n\n    def provide(self, *keywords, **extra):\n        '''\n        decorator that registers a provider for a keyword\n        '''\n\n        def decorator(provider):\n            for keyword in keywords:\n                self._providers[keyword] = provider\n            return provider\n        return decorator\n\n    def default_config(self, *keywords, **extra):\n        '''\n        decorator that registers a provider for a default configuration\n        '''\n\n        def decorator(callback):\n            for keyword in keywords:\n                self._defaults[keyword] = callback\n            return callback\n        return decorator\n\n    def define_config(self, keyword, doc):\n        '''\n        Configuration value without default.\n        '''\n        self._options[keyword] = doc\n\n    def context(self, cache, config={}):\n        '''\n        Create a new build context\n        '''\n        return self.Context(self, cache, config)\n\n    def parser(self, parser=None):\n        '''\n        Create an argument parser for this build\n        '''\n        parser = parser or argparse.ArgumentParser()\n        for keyword, func in self._defaults.items():\n            help_str = repr(func(keyword))\n            if func.__doc__:\n                desc = func.__doc__.strip()\n                if not desc.endswith('.'):\n                    desc += '.'\n                help_str = '{} Default: {}'.format(desc, help_str)\n                \n            parser.add_argument('--config_' + keyword.replace('-', '_'),\n                                help=help_str)\n\n        for keyword, desc in self._options.items():                   \n            parser.add_argument('--config_' + keyword.replace('-', '_'),\n                                help=desc)\n        return parser\n\n    def parse_config(self, args):\n        '''\n        create a context from argv\n        '''\n        config = {}\n        for key in itertools.chain(self._defaults.keys(), self._options.keys()):\n            test = 'config_' + key.replace('-', '_')\n            value = getattr(args, test)\n            if value is not None:\n                try:\n                    value = json.loads(value)\n                except:\n                    pass\n\n                config[key] = value\n\n        return config\n"
  },
  {
    "path": "packages/windows-wix/provide/cache.py",
    "content": "from __future__ import print_function\nimport os.path\ntry:\n    import urllib2\nexcept ImportError:\n    import urllib.request as urllib2\nimport logging\nimport hashlib\nimport tempfile\nimport datetime\nimport json\nimport ssl\n\nlogger = logging.getLogger(__name__)\n\n\nclass Cache(object):\n    '''\n    Cache for various file access methods. Only use default python packages to\n    allow bootstrapping.\n    '''\n\n    @staticmethod\n    def chunk_stream(src_read, dst_write, chunk_size=4096):\n        while True:\n            chunk = src_read(chunk_size)\n            if len(chunk):\n                dst_write(chunk)\n            else:\n                break\n\n    @staticmethod\n    def default_cache_dir():\n        package = os.path.dirname(__file__)\n        parent = os.path.abspath(os.path.dirname(package))\n        return os.path.join(parent, 'download_cache')\n\n    def __init__(self, cache_dir=None):\n        self.cache_dir = cache_dir or self.default_cache_dir()\n        if not os.path.isdir(self.cache_dir):\n            logger.info(\"Creating cache directory '{}'\".format(self.cache_dir))\n            os.mkdir(self.cache_dir)\n\n    @classmethod\n    def download(cls, url, writer, **kwargs):\n        logger.debug(\"Downloading '{}'...\".format(url))\n        context = ssl.create_default_context()\n        try:\n            try:\n                source = urllib2.urlopen(url, context=context)\n            except urllib2.URLError as e:\n                if \"CERTIFICATE_VERIFY_FAILED\" in str(e):\n                    logger.warning(\n                        \"Cannot verify ssl cert for '{}', delegating authenticity to digest...\".format(url))\n                    context = ssl._create_unverified_context()\n                    source = urllib2.urlopen(url, context=context)\n                else:\n                    raise\n            cls.chunk_stream(source.read, writer, **kwargs)\n        except:\n            logging.exception(\"Failed to fetch URL {}\".format(url))\n            raise\n\n    @classmethod\n    def sha1_handle(cls, handle, **kwargs):\n        algo = hashlib.sha1()\n        cls.chunk_stream(handle.read, algo.update, **kwargs)\n        return algo.hexdigest().lower()\n\n    @classmethod\n    def sha1_file(cls, target, **kwargs):\n        with open(target, 'rb') as handle:\n            return cls.sha1_handle(handle, **kwargs)\n\n    def __paths_for(self, url, sha1):\n        dst_dir = os.path.join(self.cache_dir, sha1)\n        dst_file = os.path.join(dst_dir, os.path.basename(url))\n        dst_meta = os.path.join(dst_dir, \"metadata.json\")\n        return (dst_dir, dst_file, dst_meta)\n\n    def __cache_handle(self, url, sha1, handle, **kwargs):\n        (dst_dir, dst_file, dst_meta) = self.__paths_for(url, digest)\n        os.mkdir(dst_dir)\n\n        with open(dst_file, 'wb') as dst_handle:\n            self.chunk_stream(handle.read, dst_handle.write, **kwargs)\n\n        metadata = {\n            \"timestamp\": datetime.datetime.utcnow().isoformat(),\n            \"url\": url,\n            \"sha1\": sha1,\n            \"filename\": os.path.basename(dst_file)\n        }\n        with open(dst_meta, 'w') as handle:\n            json.dump(metadata, handle)\n\n        return (dst_file, digest)\n\n    def __fetch(self, url, sha1, **kwargs):\n        logger.debug(\"attempting to cache {} at {}\".format(sha1, url))\n        with tempfile.TemporaryFile() as temp:\n            self.download(url, temp.write, **kwargs)\n            temp.seek(0)\n            digest = self.sha1_handle(temp)\n            if sha1 is not None and sha1 != digest.lower():\n                raise ValueError(\n                    \"Mismatched digest: {} {} for url '{}'\".format(sha1, digest, url))\n\n            temp.seek(0)\n\n            (dst_dir, dst_file, dst_meta) = self.__paths_for(url, digest)\n            os.mkdir(dst_dir)\n\n            with open(dst_file, 'wb') as handle:\n                self.chunk_stream(temp.read, handle.write, **kwargs)\n\n            metadata = {\n                \"timestamp\": datetime.datetime.utcnow().isoformat(),\n                \"url\": url,\n                \"sha1\": sha1,\n                \"filename\": os.path.basename(dst_file)\n            }\n            with open(dst_meta, 'w') as handle:\n                json.dump(metadata, handle)\n\n            return (dst_file, digest)\n\n    def __open(self, url, sha1, **kwargs):\n        logger.debug(\"inspecting cache for {} from {}\".format(sha1, url))\n        (dst_dir, dst_file, dst_meta) = self.__paths_for(url, sha1)\n\n        with open(dst_meta, 'r') as handle:\n            metadata = json.load(handle)\n\n        cached_file = os.path.join(dst_dir, metadata['filename'])\n        if cached_file != dst_file:\n            logger.warn(\n                \"Cached filename {} doesn't match {}\".format(cached_file, url))\n        if metadata['url'] != url:\n            logger.warn(\"Cached file from different url '{}' != '{}'\".format(\n                url, metadata['url']))\n\n        logger.info(\"Using cached file '{}' with sha1 '{}' for url '{}'\".format(\n            cached_file, sha1, url))\n        return cached_file\n\n    def resolve(self, url, sha1, **kwargs):\n        try:\n            return self.__open(url, sha1, **kwargs)\n        except IOError:\n            logger.info(\"Url {}  is not cached.\".format(url))\n            return self.__fetch(url, sha1)[0]\n\n    def insert(self, url, **kwargs):\n        return self.__fetch(url, None, **kwargs)\n\n\nclass SemanticCache(object):\n    '''\n    Operates at a resource-level of abstraction on top of a regular cache\n    '''\n\n    def __init__(self, resources, cache=None):\n        '''\n        Create semantic cache with the specified resource dictionary.\n        '''\n        self.resources = resources\n        self.cache = cache or Cache()\n\n    def resource(self, name):\n        '''\n        Get the path to a resource by name, fetching it if neccessary.\n        '''\n        entry = self.resources[name]\n        return self.cache.resolve(url = entry['url'], sha1 = entry['sha1'])\n\n    def insert(self, key, url, comment = None):\n        '''\n        insert a resource--hash is computed on insert.\n        '''\n        path, sha1 = self.cache.insert(url)\n        entry = {'url': url, 'sha1': sha1}\n        if comment:\n            entry['comment'] = comment\n        try:\n            self.resources[key].update(entry)\n        except KeyError:\n            self.resources[key] = entry\n        self.resources[key] = {'url': url, 'sha1': sha1}\n\n    def save(self, path, indent=2):\n        '''\n        save to json\n        '''\n        with open(path, 'w') as handle:\n            json.dump(self.resources, handle, indent=indent)\n\n    def preload(self):\n        '''\n        preload all resources.\n        '''\n        for key in self.resources.key():\n            self.resource(key)\n\n    @classmethod\n    def load(cls, path, cache=None):\n        '''\n        create a new semantic cache from the specified path\n        '''\n        with open(path, 'r') as handle:\n            return cls(json.load(handle), cache)\n\n\nif __name__ == '__main__':\n    logging.basicConfig()\n    import argparse\n\n    parser = argparse.ArgumentParser('Download cache')\n    parser.add_argument('-c', '--cache', type=str, help=\"Cache location\")\n    parser.add_argument('json', type=str, help=\"Json of urls to cache\")\n    parser.add_argument('-r', '--resource', type=str, help=\"file to lookup\")\n    parser.add_argument('-l', '--log-level', type=str, help=\"logging level\")\n    parser.add_argument('-i', '--insert', type=str,\n                        help='url to insert as [\"key\", \"url\", \"comment\"]')\n    parser.add_argument('-a', '--all', action='store_true',\n                        help='fetch all resources')\n    args = parser.parse_args()\n\n    if args.log_level:\n        logger.setLevel(getattr(logging, args.log_level))\n\n    cache = SemanticCache.load(args.json, Cache(args.cache))\n\n    if args.insert:\n        cache.insert(*json.loads(args.insert))\n        cache.save(args.json)\n\n    if args.resource:\n        print(cache.resource(args.resource))\n\n    if args.all:\n        cache.preload()\n"
  },
  {
    "path": "packages/windows-wix/provide/default.py",
    "content": "from __future__ import absolute_import\nfrom . import build\nimport importlib\nimport glob\nimport os.path\nimport logging\n\nlogger = logging.getLogger(__name__)\n\nbuild = build.Build()\n\n\n# Detect/handle being invoked with '-m'\n#\nmodule_root = __name__.split('.')[:-1]\nimport_module = '.'.join(module_root)\n\nif import_module:\n    def import_file(name):\n        '''\n        When invoked with '-m', perform a relative import.\n        '''\n        return importlib.import_module('.' + name, import_module)\nelse:\n    def import_file(name):\n        '''\n        When invoked without '-m', perform an absolute import.\n        '''\n        return importlib.import_module(name)\n\nscript_dir = os.path.join(os.path.dirname(__file__), 'scripts')\n\nfor script_path in glob.iglob(os.path.join(script_dir, '*.py')):\n    script_name = 'scripts.' + os.path.basename(script_path).split('.')[0]\n    if script_name.endswith('__'):\n        continue\n    logger.debug(\"adding script: '{}'\".format(script_name))\n    script = import_file(script_name)\n    script.bind(build)\n"
  },
  {
    "path": "packages/windows-wix/provide/scripts/__init__.py",
    "content": ""
  },
  {
    "path": "packages/windows-wix/provide/scripts/bootstrap.py",
    "content": "import os.path\nimport json\nimport sys\nimport logging\n'''\nBootstrap execution in an entirely clean checkout to ensure we use build scripts\nmatched to the specified build target.\n'''\n\ndef bind(framework):\n    '''\n    Attach build config+scripts to the build framework\n    '''\n\n    @framework.default_config('bootstrap')\n    def config_bootstrap(keyword):\n        '''\n        Inner configuration JSON for a fully bootstrapped build. All options\n        passed here are forwarded to the checked out build. The bootstrap env\n        will inherit this build's cache directory and log level. A second log\n        file will be created for this build.\n        '''\n        \n        return None\n\n    @framework.provide('bootstrap')\n    def provide_bootstrap(build, keyword):\n        '''\n        bootstrap execution of a native build.\n        '''\n        if not build.config('bootstrap'):\n            return\n        \n        build.depend('python27')\n        mailpile_dir = build.depend('mailpile')\n\n        log_level = logging.getLogger().getEffectiveLevel()\n        for attr in dir(logging):\n            if getattr(logging,attr) == log_level:\n                log_config = attr\n                break\n\n        util = build.depend('util')\n        with util.temporary_scope() as scope:\n            with scope.named_file() as handle:\n                json.dump(build.config('bootstrap'), handle)\n                config_file = handle.name\n                \n            devtool_path = os.path.join(mailpile_dir, 'packages\\\\windows-wix')\n            build.log().info(\"Entering bootstrap build(this could take a while)\")\n            build.invoke('python', 'provide',\n                         '--log_level={}'.format(log_config),\n                         '--input={}'.format(config_file),\n                         '--cache={}'.format(build.cache().cache.cache_dir))\n            build.log().info(\"Exiting bootstrap build\")\n"
  },
  {
    "path": "packages/windows-wix/provide/scripts/export.py",
    "content": "import shutil\n\n\ndef bind(build):\n\n    @build.provide('export')\n    def provide_export(build, keyword):\n        '''\n        Export build artifacts to arbitrary locations. Configured via a dictionary\n        of { dependency: export_path }.\n        '''\n\n        for dependency, export_path in build.config(keyword).items():\n            build.log().info(\"Exporting '{}' to '{}'...\".format(dependency,\n                                                                export_path))\n            dep_path = build.depend(dependency)\n            shutil.copytree(dep_path, export_path)\n\n    @build.default_config('export')\n    def config_export(keyword):\n        '''\n        Default is to export nothing.\n        '''\n\n        return {}\n"
  },
  {
    "path": "packages/windows-wix/provide/scripts/external.py",
    "content": "import os\nimport os.path\nimport itertools\n\ndef bind(build):\n\n    @build.default_config('git', 'signtool')\n    def default_config_env(keyword):\n        '''\n        Commands expected to exist on PATH.\n        '''\n        return keyword + '.exe'\n\n    @build.default_config('signtool')\n    def default_config_sdk(keyword):\n        '''\n        Try to discover tools from the windows sdk, otherwise expect on PATH.\n        '''\n        win_sdk_root = 'C:\\\\Program Files (x86)\\\\Windows Kits\\\\10\\\\bin'\n        versions = os.listdir(win_sdk_root)\n        versions.sort(reverse=True)\n        template = win_sdk_root + '\\\\{}\\\\{}\\\\{}.exe'\n        for version, platform in itertools.product(versions,('x86','x64')):\n            path = template.format(version, platform, keyword)\n            if os.path.exists(path):\n                return path\n\n        return keyword + '.exe'\n\n    @build.provide('git', 'signtool')\n    def provide_from_config(build, keyword):\n        '''\n        Trivial wrapper to inject invokables from config\n        '''\n        exe_path = build.config(keyword)\n\n        build.publish(keyword, exe_path)\n        return None\n"
  },
  {
    "path": "packages/windows-wix/provide/scripts/extract_msi.py",
    "content": "#!/usr/bin/python\n\n'''\nHelper for unpacking MSIs in various ways. Generic high-level API:\n\nextract( msi_path, out_path, features = ALL )\n'''\n\nALL = '***ALL***'\n\nimport subprocess\nimport logging\nimport tempfile\nimport shutil\nimport os.path\nimport os\n\nlogger = logging.getLogger(__name__)\n\n\nclass LessMSI(object):\n    '''\n    Use lessmsi to extract the msi. TODO: handle features\n    '''\n\n    def __init__(self, lessmsi_path):\n        self.lessmsi = lessmsi_path\n\n    def __call__(self, msi_path, out_path, features=ALL):\n        logger.info('Extracting {} to {} (features:{}) with lessmsi'.format(\n            msi_path, out_path, features))\n        if features != ALL:\n            logger.warning(\"LessMSI doesn't implement feature selection(yet)!\")\n\n        temp_dir = tempfile.mkdtemp()\n\n        if not temp_dir.endswith('\\\\'):\n            temp_dir += '\\\\'\n\n        args = (self.lessmsi, 'x', msi_path, temp_dir)\n        try:\n            si = subprocess.STARTUPINFO()\n            si.dwFlags |= subprocess.STARTF_USESHOWWINDOW\n            subprocess.check_call(args, startupinfo=si)\n            sub_dirs = os.listdir(temp_dir)\n            if len(sub_dirs) == 1:\n                src_dir = os.path.join(temp_dir, sub_dirs[0])\n            else:\n                logger.warning(\"Ambiguous MSI root dir {}\".format(msi_path))\n                src_dir = temp_dir\n\n            if not os.path.exists(out_path):\n                shutil.move(src_dir, out_path)\n            else:\n                raise OSError(\"Path '{}' already exists\".format(out_path))\n        finally:\n            def log_error(func, path, exec_info):\n                logger.error(\"Error cleaning up {}: {} {}\".format(msi_path, func, path),\n                             exec_info=exec_info)\n            shutil.rmtree(temp_dir, ignore_errors=True, onerror=log_error)\n\n\ndef bind(build):\n\n    @build.provide('extract_msi')\n    def provide_extract_msi(build, keyword):\n        lessmsi_dir = build.depend('lessmsi')\n        lessmsi_path = os.path.join(build.depend('lessmsi'), 'lessmsi.exe')\n        extractor = LessMSI(lessmsi_path)\n        return extractor\n"
  },
  {
    "path": "packages/windows-wix/provide/scripts/git_checkout.py",
    "content": "import os.path\nimport shutil\n\ndef bind(build):\n\n    def repo_root():\n        '''\n        Find the root of the current checkout\n        '''\n        repo_path = os.path.abspath(os.path.dirname(__file__))\n        while not os.path.exists(os.path.join(repo_path,'.git')):\n            parts = os.path.split(repo_path)\n            if parts[0] == repo_path:\n                raise ValueError(\"Cannot find root of git checkout!\")\n            else:\n                repo_path = parts[0]\n        return repo_path\n\n    @build.default_config('mailpile')\n    def config_repo(keyword):\n        '''\n        Configure git checkout url and commit/branch. Either a json like\n        {\"commit\": \"<hash or branch>\", \"repo\": \"<url>\" } or path to local\n        checkout to copy.\n        '''\n        return repo_root() #{'commit': 'master',\n                #'repo': 'https://github.com/mailpile/{}'.format(keyword)}\n\n    def copy_local_repo(build, src, dst):\n        '''\n        Copy the repo from on disk\n        '''\n        build.depend('git')\n        util = build.depend('util')\n\n        build.log().info(\"copying local repo: '{}'\".format(src))\n        shutil.copytree(src, dst)\n        with util.pushdir(dst):\n            build.invoke('git', 'clean', '-xdf')\n\n    def clone_remote_repo(build, config, dst):\n        '''\n        clone the repo from a dict of 'repo' url and 'commit'\n        '''\n        build.depend('git')\n        util = build.depend('util')\n        \n        build.log().info(\"cloning remote repo: '{}'\".format(config))        \n        build.invoke('git', 'clone', config['repo'], dst, '--recursive')\n        with util.pushdir(dst):\n            build.invoke('git', 'checkout', config['commit'])\n\n    @build.provide('mailpile')\n    def provide_checkout(build, keyword):\n        '''\n        Checkout the specified git repository to the specified commit/branch and\n        delete git files.\n        '''\n        build.depend('git')\n        build.depend('root')\n        dep_path = build.invoke('path', keyword)\n        util = build.depend('util')\n        config = build.config(keyword)\n        \n        if isinstance( config, str ):\n            checkout_method = copy_local_repo\n        else:\n            checkout_method = clone_remote_repo\n\n        checkout_method(build, config, dep_path)\n        with util.pushdir(dep_path):\n            util.rmtree('.git')\n        return dep_path\n\n"
  },
  {
    "path": "packages/windows-wix/provide/scripts/msi_package.py",
    "content": "import sys\nimport os.path\n\n# Hack to import somewhat relatively\n#\nlib_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))\nsys.path.insert(0, lib_dir)\nimport package\nsys.path.pop(0)\n\nimport json\nimport copy\n\ntry:\n    check = unicode\nexcept NameError:\n    unicode = str\n\n\ndef format_pod(template, **kwargs):\n    '''\n    apply str.format to all str elements of a simple object tree template\n    '''\n    if isinstance(template, dict):\n        template = {format_pod(key, **kwargs): format_pod(value, **kwargs)\n                    for (key, value) in template.items()}\n    elif isinstance(template, str) or isinstance(template, unicode):\n        template = template.format(**kwargs)\n    elif isinstance(template, list):\n        template = [format_pod(value, **kwargs) for value in template]\n    elif callable(template):\n        template = format_pod(template(), **kwargs)\n    else:\n        # Maybe raise an error instead?\n        #\n        template = copy.copy(template)\n\n    return template\n\n\ndef bind(build):\n\n    @build.default_config('package_template')\n    def config_package_jsons(keyword):\n        '''\n        json configuration files for packaging\n        '''\n        return os.path.join(lib_dir, keyword + '.json')\n\n    @build.default_config('package_cultures')\n    def config_package_lang(keyword):\n        '''\n        Wix 'cultures' for which to produce packages.\n        '''\n        return ['en-us']\n\n    @build.provide('package')\n    def provide_msi(build, keyword):\n        '''\n        Build an MSI with all the mailpile dependencies\n        '''\n\n        build.depend('root')\n        dep_path = build.invoke('path', keyword)\n\n        if not os.path.exists(dep_path):\n            os.mkdir(dep_path)\n\n        content_keys = ('tor',\n                        'mailpile',\n                        'python27',\n                        'openssl',\n                        'gpg',\n                        'version')\n\n        content_paths = {key: build.depend(key) for key in content_keys}\n\n        # pre-cache mailpile\n        build.invoke('python',\n                     os.path.join(content_paths['mailpile'],\n                                  'packages\\\\windows-wix\\\\bin\\\\with-mailpile-env.py'),\n                     os.path.join(content_paths['mailpile'],\n                                  'shared-data\\\\mailpile-gui\\\\mailpile-gui.py'),\n                     '--compile')\n\n        tool_keys = ('wix',\n                     'sign_tree')\n\n        tool_paths = {key: build.depend(key) for key in tool_keys}\n\n        # sign binary content\n        #\n        for path in content_paths.values():\n            if os.path.exists(path):\n                build.invoke('sign_tree', path)\n\n        # create the template for building the wix config\n        #\n        with open(build.config('package_template'), 'r') as handle:\n            package_template = json.load(handle)\n            package_config = format_pod(package_template, **content_paths)\n\n        with open(os.path.join(dep_path, 'mailpile.package.json'), 'w') as handle:\n            json.dump(package_config, handle, indent=2)\n\n        wix = package.WixConfig(package_config)\n\n        # TODO: Split uuid and wix config saving\n        wix_config_path = os.path.join(dep_path, 'mailpile')\n        wix.save(wix_config_path + '.wxs')\n\n        # Package everything using WIX\n        #\n        build.invoke('candle', wix_config_path + '.wxs',\n                     '-out', os.path.join(dep_path, 'mailpile.wixobj'))\n\n        for lang in build.config('package_cultures'):\n            msi_name = 'mailpile-{}-{}.msi'.format(content_paths['version'],\n                                                   lang)\n            build.log().info(\"Building msi for culture: \" + lang)\n            build.invoke('light',\n                         '-ext', 'WixUIExtension',\n                         '-ext', 'WixUtilExtension',\n                         '-cultures:' + lang,\n                         wix_config_path + '.wixobj',\n                         '-out', os.path.join(dep_path, msi_name))\n\n        # Sign the generated files\n        #\n        build.invoke('sign_tree', dep_path)\n        \n        return dep_path\n"
  },
  {
    "path": "packages/windows-wix/provide/scripts/python27.py",
    "content": "import os.path\nimport glob\nimport subprocess\n\n\ndef bind(framework):\n\n    @framework.provide('python27')\n    def provide_python27(build, keyword):\n        '''\n        provide python27 prepared for mailpile\n        '''\n\n        build.depend('root')\n        dep_path = build.invoke('path', keyword)\n\n        extractor = build.depend('extract_msi')\n        extractor(build.cache().resource(keyword), dep_path)\n\n        util = build.depend('util')\n        util.publish_exes(build, dep_path)\n\n        # Rebrand with resource hacker--very finicky\n        # https://www.askvg.com/tutorial-all-about-resource-hacker-in-a-brief-tutorial/\n        #\n        mailpile_dir = build.depend('mailpile')\n        assets_dir = os.path.join(\n            mailpile_dir, 'packages\\\\windows-wix\\\\assets')\n        resource_hacker = os.path.join(build.depend('resource_hacker'),\n                                       'ResourceHacker.exe')\n        for exe in ('python.exe', 'pythonw.exe'):\n            exe_path = os.path.join(dep_path, exe)\n            update_path = os.path.join(\n                dep_path, exe.replace('.', '-mailpile.'))\n            cmd = ('-open', exe_path,\n                   '-save', update_path,\n                   '-action', 'addoverwrite',\n                   '-resource', os.path.join(assets_dir, 'mailpile_logo.ico'),\n                   '-mask', 'ICONGROUP,1,0')\n            build.invoke('ResourceHacker', *cmd)\n            build.publish(exe.split('.')[0],\n                          framework.Invoker(build, update_path))\n            util.rmtree(exe_path)\n\n        # TODO: Prune unwanted files--either via features or manual delete.\n        #\n        for item in ('tcl','Lib\\\\test'):\n            build.log().info(\"Removing directory '{}'\".format(item))\n            util.rmtree(os.path.join(dep_path,item))\n\n        # Manually bootstrap pip.\n        # https://stackoverflow.com/questions/36132350/install-python-wheel-file-without-using-pip\n        #\n        build.log().debug('Bootstrapping pip from bundled wheels')\n        bundle_dir = os.path.join(dep_path, 'Lib\\\\ensurepip\\\\_bundled')\n        pip_wheel = next(glob.iglob(os.path.join(bundle_dir, 'pip*.whl')))\n        setup_wheel = next(glob.iglob(os.path.join(bundle_dir, 'setup*.whl')))\n\n        tmp_pip = os.path.join(pip_wheel, 'pip')\n        build.invoke('python', tmp_pip, 'install', setup_wheel)\n        build.invoke('python', tmp_pip, 'install', pip_wheel)\n\n        # Use pip to install dependencies\n        # TODO: Cache/statically version packages.\n        #\n        pip_deps = os.path.join(mailpile_dir, 'requirements.txt')\n        build.invoke('python', '-m', 'pip', 'install', '-r', pip_deps)\n\n        # TODO: import requirements from gui-o-matic\n        #\n        build.invoke('python', '-m', 'pip', 'install', 'pywin32')\n\n        return dep_path\n"
  },
  {
    "path": "packages/windows-wix/provide/scripts/root.py",
    "content": "import os.path\nimport tempfile\n\n\ndef bind(build):\n\n    @build.provide('root')\n    def provide_root(build, keyword):\n        '''\n        Provide build root temporary directory and 'path' invokable.\n        '''\n        util = build.depend('util')\n\n        root = tempfile.mkdtemp()\n        build.log().info(\"Initialized build root at '{}'\".format(root))\n\n        def cleanup_root():\n            build.log().info(\"Removing build root at '{}'\".format(root))\n            util.rmtree(root)\n\n        build.cleanup(cleanup_root)\n\n        def dependency_path(dependency):\n            return os.path.join(root, dependency)\n\n        build.publish('path', dependency_path)\n\n        return root\n"
  },
  {
    "path": "packages/windows-wix/provide/scripts/sign_tree.py",
    "content": "import os\nimport os.path\n\ndef is_signable(path):\n    '''\n    Check if a file is in the PE format by looking for the 'MZ' magic.\n    '''\n    try:\n        # FIXME--not the correct check...\n        with open(path, 'rb') as handle:\n            magic = handle.read(2)\n            if magic == \"\\x7F\\x45\":\n                return True\n    except IOError:\n        pass\n\n    recognized_formats = ('.msi', '.exe', '.dll', '.pyd')\n    return any( map( path.endswith, recognized_formats) )\n\ndef bind(build):\n    @build.default_config('timestamp_server')\n    def config_signing_timestamp_server(keyword):\n        '''\n        Time server for signing execubles\n        '''\n        return 'http://timestamp.digicert.com'\n\n    build.define_config('signing_key',\n                        'Key for signing PE files (pkcs12 format).')\n    build.define_config('signing_passwd',\n                        'Password for signing key.')\n    \n    @build.provide('sign_tree')\n    def provide_sign_tree(build, keyword):\n        '''\n        Provide tool to sign all executables in the specified build path\n        '''\n        build.depend('signtool')\n        try:\n            key = os.path.abspath(build.config('signing_key'))\n            build.log().debug('Using signing key {}'.format(key))\n            password = build.config('signing_passwd')\n\n            def sign_file(path):\n                '''\n                Sign a single file, if it's in PE format\n                '''\n                if is_signable(path):\n                    build.log().info(\"Signing '{}'\".format(path))\n                    build.invoke('signtool', 'sign',\n                                 '/f', key,\n                                 '/p', password,\n                                 '/tr', build.config('timestamp_server'),\n                                 '/td', 'sha512',\n                                 '/fd', 'sha512',\n                                 path)\n\n            # TODO: publish a recursive scanner.\n            def sign_tree(path):\n                '''\n                Sign all detected PE files in the specified path\n                '''\n                build.log().debug(\"Scanning '{}' for signable files...\".format(path))\n                assert(os.path.exists(path))\n                for root, dirs, files in os.walk(path):\n                    for name in files:\n                        path = os.path.join(root,name)\n                        sign_file(path)\n                        \n        except KeyError:\n            build.log().warning('No signing key configured--outputs will not be signed')\n\n            def sign_tree(path):\n                '''\n                Stub to support debug/test unsigned builds.\n                '''\n                build.log().warn(\"Ignoring request to sign tree '{}'\".format(path))\n\n        build.publish(keyword, sign_tree)\n\n        return None\n"
  },
  {
    "path": "packages/windows-wix/provide/scripts/util.py",
    "content": "import tempfile\nimport os.path\nimport glob\nimport contextlib\nimport shutil\nimport stat\nimport logging\nimport sys\n\nlogger = logging.getLogger(__name__)\n\nclass TemporaryScope(object):\n    '''\n    Context for tracking multiple temporary files.\n\n    Principly combats nested contexts:\n\n        with tempfile() as x:\n            with tempfile() as y:\n                with tempdir() as z:\n                    with tempsomething as q:\n                        ...\n    translates to:\n\n        with TemporaryScope() as context:\n\n            with context.named_file(...) as x:\n                ...\n\n            with context.named_file(...) as Y:\n                ...\n\n            z = context.named_dir(...):\n                ...\n\n    so that named resouces may be sequentially constructed and automatically\n    cleaned up.\n    '''\n\n    def __init__(self, deleter):\n        self.deleter = deleter\n\n    def __enter__(self):\n        self.paths = []\n        return self\n\n    def __exit__(self, *ignored):\n        for path in self.paths:\n            self.deleter(path)\n\n        del self.paths\n\n    def named_file(self, *args, **kwargs):\n        kwargs['delete'] = False\n        result = tempfile.NamedTemporaryFile(*args, **kwargs)\n        self.paths.append(result.name)\n        return result\n\n    def named_dir(self, *args, **kwargs):\n        result = tempfile.mkdtemp(*args, **kwargs)\n        self.paths.append(result)\n        return result\n\n\nclass Util(object):\n    '''\n    Utility functions for builds\n    '''\n\n    @staticmethod\n    def rmtree(path):\n        '''\n        Remove an entire directory tree rm -rf style, logging any errors\n        '''\n\n        def retry_log(func, path, exc_info):\n            try:\n                os.chmod(path, stat.S_IWUSR | stat.S_IRUSR)\n                func(path)\n            except:\n                logger.error(\"Unable perform action {}: {} {}\".format(path, func, path),\n                         exc_info=exc_info)\n                \n        if os.path.isdir(path):\n            shutil.rmtree(path, onerror=retry_log)\n        else:\n            try:\n                os.unlink(path)\n            except:\n                log_error('os.unlink', path, sys.exc_info())\n\n    @classmethod\n    def temporary_scope(cls):\n        '''\n        Create a temporary scope that uses rmtree as deleter\n        '''\n\n        return TemporaryScope(cls.rmtree)\n\n    @staticmethod\n    def publish_exes(build, path):\n        '''\n        Publish all exes found on the path\n        '''\n\n        for exe in glob.glob(os.path.join(path, '*.exe')):\n            cmd = os.path.basename(exe).split('.')[0]\n            build.publish(cmd, exe)\n\n    @staticmethod\n    @contextlib.contextmanager\n    def tempdir(*args, **kwargs):\n        '''\n        Temporary directory context: tempdir is deleted at exit.\n        returns temp dir absolute path\n        '''\n\n        path = tempfile.mkdtemp(*args, **kwargs)\n        try:\n            yield path\n        finally:\n            rmtree_log_error(path)\n\n    @staticmethod\n    @contextlib.contextmanager\n    def pushdir(path):\n        '''\n        Pushdir as a context manager. Restores previous working directory on exit.\n        '''\n        cwd = os.path.abspath(os.getcwd())\n        os.chdir(path)\n        try:\n            yield\n        finally:\n            os.chdir(cwd)\n\n\ndef bind(build):\n\n    @build.provide('util')\n    def provide_util(build, keyword):\n        return Util\n"
  },
  {
    "path": "packages/windows-wix/provide/scripts/version.py",
    "content": "def bind(framework):\n\n    @framework.provide('version')\n    def provide_version(build, keyword):\n        mailpile_dir = build.depend('mailpile')\n        util = build.depend('util')\n        build.depend('python27')\n\n        with util.pushdir(mailpile_dir):\n            version = build.invoke('python', 'scripts\\\\version.py').strip()\n            version = version.split('~')\n            build.log().info(\"Version string is: {}\".format(version))\n\n        return version[0]\n"
  },
  {
    "path": "packages/windows-wix/provide/scripts/win4gpg.py",
    "content": "import textwrap\nimport os\n\n\ndef bind(build):\n\n    @build.provide('gpg')\n    def provide_gpg(build, keyword):\n        '''\n        install GPG in a temporary location and use mkportable to create a\n        portable GPG for the build. Requires ADMIN and that gpg4win *is not*\n        installed.\n        '''\n        build.depend('root')\n        dep_path = build.invoke('path', keyword)\n\n        util = build.depend('util')\n\n        build.depend('python27')\n        gpg_installer = build.cache().resource('gpg')\n\n        gpg_ini = textwrap.dedent('''\n            [gpg4win]\n                inst_gpgol = false\n                inst_gpgex = false\n                inst_kleopatra = false\n                inst_gpa = false\n                inst_claws_mail = false\n                inst_compendium = false\n                inst_start_menu = false\n                inst_desktop = false\n                inst_quick_launch_bar = false\n            ''')\n\n        # Use the built python to elevate if needed\n        # https://stackoverflow.com/questions/130763/request-uac-elevation-from-within-a-python-script\n        build_template = textwrap.dedent('''\n            #!/usr/bin/python\n            import sys\n            import ctypes\n            import subprocess\n            import os.path\n            import win32com.shell.shell as shell\n            import win32com.shell.shellcon as shellcon\n            import win32event\n            import win32process\n            import win32api\n            import win32con\n            import socket\n            import random\n            import traceback\n\n            def make_portable_gpg():\n                install_cmd = (\"{installer_path}\", \"/S\",\n                               \"/C={config}\",\n                               \"/D={target}\")\n                uninstall_cmd = (\"{uninstaller_path}\", \"/S\")\n                portable_cmd = (\"{mkportable_path}\", \"{build}\")\n                \n                si = subprocess.STARTUPINFO()\n                si.dwFlags |= subprocess.STARTF_USESHOWWINDOW\n                \n                subprocess.check_call( install_cmd, startupinfo = si )\n                subprocess.check_call( portable_cmd, startupinfo = si )\n                subprocess.check_call( uninstall_cmd, startupinfo = si )\n\n            def ellevate_and_wait( sock, timeout ):\n                sock.settimeout( timeout )\n                while True:\n                    try:\n                        port = random.randint(1000, 2**16-1)\n                        sock.bind( (\"localhost\", port) )\n                        break\n                    except socket.error:\n                        continue\n\n                parameters = '\"{{}}\" {{}}'.format( os.path.abspath( __file__ ), port )\n                result = shell.ShellExecuteEx(fMask = shellcon.SEE_MASK_NOCLOSEPROCESS,\n                                              lpVerb = \"runas\",\n                                              lpFile = sys.executable,\n                                              lpParameters = parameters,\n                                              nShow = win32con.SW_HIDE )\n\n                handle = result['hProcess']\n                status = win32event.WaitForSingleObject(handle, -1)\n                win32api.CloseHandle( handle )\n                return sock.recv( 4096*1024 )\n\n            def signal_result_and_exit( sock, error_message = '' ):\n                if len( sys.argv ) > 1:\n                    port = int( sys.argv[1] )\n                    sock.sendto(error_message.encode(), (\"localhost\", port))\n                else:\n                    sys.stderr.write(error_message)\n                sys.exit( len( error_message ) )\n\n            try:\n                sock = socket.socket( socket.AF_INET, socket.SOCK_DGRAM )    \n                if ctypes.windll.shell32.IsUserAnAdmin():\n                    try:\n                        make_portable_gpg()\n                        result = ''\n                    except:\n                        result = traceback.format_exc()\n                else:\n                    result = ellevate_and_wait( sock, 14 )\n\n                signal_result_and_exit( sock, result )\n\n            finally:\n                sock.close()\n            ''')\n\n        def script_escape(value):\n            '''\n            Escape text literals for generated script\n            '''\n\n            return value.replace('\\\\', '\\\\\\\\')\n\n        os.mkdir(dep_path)\n\n        with util.temporary_scope() as scope:\n            tmp_path = scope.named_dir()\n            uninstaller = os.path.join(tmp_path, \"gpg4win-uninstall.exe\")\n            mkportable = os.path.join(tmp_path, \"bin\\\\mkportable.exe\")\n\n            script_vars = {'installer_path': script_escape(gpg_installer),\n                           'uninstaller_path': script_escape(uninstaller),\n                           'mkportable_path': script_escape(mkportable),\n                           'build': script_escape(dep_path),\n                           'target': script_escape(tmp_path)}\n\n            with scope.named_file() as ini_file:\n                ini_file.write(gpg_ini.encode())\n                script_vars['config'] = script_escape(ini_file.name)\n\n            with scope.named_file() as build_script:\n                build_script.write(\n                    build_template.format(**script_vars).encode())\n                #print build_template.format( **script_vars )\n                script_path = build_script.name\n\n            build.invoke('python', script_path)\n\n        return dep_path\n"
  },
  {
    "path": "packages/windows-wix/provide/scripts/zip.py",
    "content": "import zipfile\n\n\ndef bind(build):\n\n    @build.provide('wix', 'tor', 'lessmsi', 'openssl', 'resource_hacker')\n    def provide_zip(build, keyword):\n        '''\n        Inflate zip files into the build path\n        '''\n\n        build.depend('root')\n        util = build.depend('util')\n        dep_path = build.invoke('path', keyword)\n        zip_path = build.cache().resource(keyword)\n        archive = zipfile.ZipFile(zip_path)\n        archive.extractall(dep_path)\n        util.publish_exes(build, dep_path)\n        return dep_path\n"
  },
  {
    "path": "packages/windows-wix/provide-example-bootstrap-local.json",
    "content": "{\n  \"bootstrap\": {\n    \"export\": {\"package\": \".\\\\package\"},\n    \"package_cultures\": [\"fr-fr\",\"en-us\"],\n  }\n}"
  },
  {
    "path": "packages/windows-wix/provide-example-bootstrap-remote.json",
    "content": "{\n  \"bootstrap\": {\n    \"export\": {\"package\": \".\\\\package\"},\n    \"package_cultures\": [\"fr-fr\",\"en-us\"]\n  },\n  \"mailpile\": {\n    \"commit\": \"master\",\n    \"repo\": \"https://github.com/AlexanderHaase/mailpile\"\n  }\n}"
  },
  {
    "path": "packages/windows-wix/provide-example-fork-local.json",
    "content": "{\n  \"export\": {\"package\": \".\\\\package\"}\n}"
  },
  {
    "path": "packages/windows-wix/provide-example-fork-remote.json",
    "content": "{\n  \"export\": {\"package\": \".\\\\package\"},\n  \"mailpile\": {\n    \"commit\": \"master\",\n    \"repo\": \"https://github.com/AlexanderHaase/mailpile\"\n  }\n}"
  },
  {
    "path": "packages/windows-wix/provide.json",
    "content": "{\n  \"export\": {\"package\": \".\\\\package\"}\n}"
  },
  {
    "path": "packages/windows-wix/resources.json",
    "content": "{\n  \"tor\": {\n    \"url\": \"https://www.mailpile.is/files/windows-deps/20180901/tor-win32-0.3.3.7.zip\",\n    \"sha1\": \"f06efe990bbe3f4e434ef60f57f1c59e9a7808cd\",\n    \"source\": \"https://www.torproject.org/dist/torbrowser/7.5.6/tor-win32-0.3.3.7.zip\",\n    \"comment\": \"Official Tor project build.\"\n  },\n  \"gpg\": {\n    \"url\": \"https://www.mailpile.is/files/windows-deps/20180901/gpg4win-3.1.0.exe\",\n    \"sha1\": \"fb9f39199d7b096d1397dad4aa0719de5ff05b31\",\n    \"source\": \"https://files.gpg4win.org/gpg4win-3.1.0.exe\",\n    \"comment\": \"Long-standing windows GPG provider.\"\n  },\n  \"openssl\": {\n    \"url\": \"https://www.mailpile.is/files/windows-deps/20180901/openssl-1.0.2o-i386-win32.zip\",\n    \"sha1\": \"681c35bca5a505614755514495e2943b8f5ae0b7\",\n    \"source\": \"https://indy.fulgan.com/SSL/openssl-1.0.2o-i386-win32.zip\",\n    \"comment\": \"OpenSSL from a third-party maintainer. See list at https://wiki.openssl.org/index.php/Binaries\"\n  },\n  \"resource_hacker\": {\n    \"url\": \"https://www.mailpile.is/files/windows-deps/20180901/resource_hacker.zip\",\n    \"sha1\": \"a23837b4b326c04f9a36c4443538c0c7f932e8d3\",\n    \"source\": \"http://www.angusj.com/resourcehacker/resource_hacker.zip\",\n    \"comment\": \"Official build from project.\"\n  },\n  \"lessmsi\": {\n    \"url\": \"https://www.mailpile.is/files/windows-deps/20180901/lessmsi-v1.6.1.zip\",\n    \"sha1\": \"233a464ba7a9ff9ed37964b516d9fa017ce442eb\",\n    \"source\": \"https://github.com/activescott/lessmsi/releases/download/v1.6.1/lessmsi-v1.6.1.zip\",\n    \"comment\": \"Official build from lessmsi.\"\n  },\n  \"python27\": {\n    \"url\": \"https://www.mailpile.is/files/windows-deps/20180901/python-2.7.14.msi\",\n    \"sha1\": \"a84cb11bdae3e1cb76cf45aa96838d80b9dcd160\",\n    \"source\": \"https://www.python.org/ftp/python/2.7.14/python-2.7.14.msi\",\n    \"comment\": \"Official build from python.\"\n  },\n  \"wix\": {\n    \"url\": \"https://www.mailpile.is/files/windows-deps/20180901/wix311-binaries.zip\",\n    \"sha1\": \"79c2184c80a8b4c70f090ee25f709a056971ac80\",\n    \"source\": \"https://github.com/wixtoolset/wix3/releases/download/wix3111rtm/wix311-binaries.zip\",\n    \"comment\": \"Official build from wixtoolset.\"\n  }\n}\n"
  },
  {
    "path": "requirements-dev.txt",
    "content": "appdirs\nsetuptools>=11.3\ncryptography>=1.3.4\nlxml>=2.3.2\nimgsize\nJinja2\nselenium>=2.40.0\nmarkupsafe\nnose\nmock>=1.0.1\npycrypto\npyDNS\nPySocks\npgpdump\npillow\npbr\nfasteners\nwin_inet_pton; sys_platform == 'win32'\nstem>=1.4\nicalendar\n"
  },
  {
    "path": "requirements-with-deps.txt",
    "content": "appdirs\ncryptography>=1.3.4\nlxml>=2.3.2\nimgsize\nJinja2\nspambayes>=1.1b1\nmarkupsafe\npyDNS\nPySocks\npgpdump\npillow\npbr\nidna\nasn1crypto\nsix\nenum34\nipaddress\ncffi\nlockfile\npyasn1\npycparser\nfasteners\nmonotonic\nstem>=1.4\n"
  },
  {
    "path": "requirements.txt",
    "content": "appdirs\nsetuptools>=11.3\ncryptography>=1.3.4\nlxml>=2.3.2\nimgsize\nJinja2\nmarkupsafe\npy3DNS\nPySocks\npgpdump\npillow\npbr\nfasteners\nstem>=1.4\nicalendar\n"
  },
  {
    "path": "scripts/__init__.py",
    "content": ""
  },
  {
    "path": "scripts/add-gpgme-and-gnupg-to-venv",
    "content": "#!/bin/bash\n#\n# See also: https://www.gnupg.org/download/integrity_check.html\n#\n[ \"$VIRTUAL_ENV\" == \"\" ] && {\n    echo \"Please activate your virtualenv first!\"\n    exit 1\n}\n\nGNUPG_DEPS=\"\\\n    https://gnupg.org/ftp/gcrypt/npth/npth-1.3.tar.bz2=1b21507cfa3f58bdd19ef2f6800ab4cb67729972 \\\n    https://gnupg.org/ftp/gcrypt/libgpg-error/libgpg-error-1.26.tar.bz2=9a926e7ee6309e539313443555535d49a2a5c9f1 \\\n    https://gnupg.org/ftp/gcrypt/libksba/libksba-1.3.5.tar.bz2=a98385734a0c3f5b713198e8d6e6e4aeb0b76fde \\\n    https://gnupg.org/ftp/gcrypt/libassuan/libassuan-2.4.3.tar.bz2=27391cf4a820b5350ea789c30661830c9a271518 \\\n    https://gnupg.org/ftp/gcrypt/libgcrypt/libgcrypt-1.7.5.tar.bz2=fa485d854748fc06ea041b3057b2de2f12fbc17f \\\n    https://gnupg.org/ftp/gcrypt/gpgme/gpgme-1.8.0.tar.bz2=efa043064dbf675fd713228c6fcfcc4116feb221\"\nINSTALLING=\"GPGME 1.8.0\"\n\nif [ \"$1\" = \"2.1\" -o \"$1\" = \"\" ]; then\n    GNUPG_DEPS=\"$GNUPG_DEPS \\\n        https://gnupg.org/ftp/gcrypt/pinentry/pinentry-1.0.0.tar.bz2=85d9ac81ebad3fb082514c505c90c39a0456f1f6 \\\n        https://gnupg.org/ftp/gcrypt/gnupg/gnupg-2.1.17.tar.bz2=d83ab893faab35f37ace772ca29b939e6a5aa6a7\"\n    INSTALLING=\"$INSTALLING and GnuPG 2.1.17\"\nfi\nif [ \"$1\" = \"2.0\" ]; then\n    GNUPG_DEPS=\"$GNUPG_DEPS \\\n        https://gnupg.org/ftp/gcrypt/pinentry/pinentry-1.0.0.tar.bz2=85d9ac81ebad3fb082514c505c90c39a0456f1f6 \\\n        https://gnupg.org/ftp/gcrypt/gnupg/gnupg-2.0.30.tar.bz2=a9f024588c356a55e2fd413574bfb55b2e18794a\"\n    INSTALLING=\"$INSTALLING and GnuPG 2.0.30\"\nfi\nif [ \"$1\" = \"1.4\" ]; then\n    GNUPG_DEPS=\"$GNUPG_DEPS \\\n        https://gnupg.org/ftp/gcrypt/gnupg/gnupg-1.4.21.tar.bz2=e3bdb585026f752ae91360f45c28e76e4a15d338\"\n    INSTALLING=\"$INSTALLING and GnuPG 1.4.21\"\nfi\n\necho\necho \"About to download, build and install $INSTALLING\"\necho \"into: $VIRTUAL_ENV\"\necho\necho \"Press ENTER to continue.\"\"\"\nread\n\nset -x\nset -e\n\nmkdir -p \"$VIRTUAL_ENV/gnupg-build\"\ncd \"$VIRTUAL_ENV/gnupg-build\"\n\nfor packageandsum in $GNUPG_DEPS; do\n    package=$(echo \"$packageandsum\" |cut -f1 -d=)\n    shasum=$(echo $packageandsum |cut -f2 -d=)\n    tarball=$(basename \"$package\")\n    srcdir=$(echo \"$tarball\" |sed -e s/.tar.bz2//)\n\n    if [ ! -e \"$tarball\" ]; then\n        rm -f dl.tmp\n        wget \"$package\" -O dl.tmp\n        mv -f dl.tmp \"$tarball\"\n    fi\n    if [ \"$(sha1sum \"$tarball\" |cut -f1 -d\\ )\" != \"$shasum\" ]; then\n        set +x\n        echo\n        echo \"SHA1 checksum incorrect for $tarball\"\n        echo \"Nuke the virtualenv from orbit and try again?\"\n        echo\n        exit 99\n    fi\n\n    [ -e \"$srcdir\" ] || tar xfj \"$tarball\"\n    pushd \"$srcdir\"\n    ./configure --prefix=\"$VIRTUAL_ENV\"\n    make\n    make install\n    popd\ndone\n\nset +e\nset +x\n\necho\necho \"Whew! If you would like to clean up, run this:\"\necho\necho \"  rm -rf '$VIRTUAL_ENV/gnupg-build'\"\necho\necho ====================================================================\necho \"WARNING!    *** TAKE CARE OF YOUR MAIN KEYCHAIN! ***\"\necho\necho \"  GnuPG version 2.1 likes to upgrade the keychain storage format\"\necho \"  to something GnuPG 2.0 and 1.4 cannot read. So take care to set\"\necho \"  GNUPGHOME or backup your keychain! The scripts/setup-test.sh is\"\necho \"  good for self-contained disposable development test runs.\"\necho ====================================================================\necho\necho \"You will also need to set the following environment variable:\"\necho\necho \"  export LD_LIBRARY_PATH=$VIRTUAL_ENV/lib\"\necho\necho \"If you would like this added to the activate script, press ENTER.\"\necho \"Note that will BREAK the deactivate function because this script is\"\necho \"a lame hack. Press CTRL-C to skip this...\"\nread\necho \"export LD_LIBRARY_PATH=\\$VIRTUAL_ENV/lib\" >>\"$VIRTUAL_ENV/bin/activate\"\necho\necho \"Great. All done! Now rerun activate and have fun.\"\n\n"
  },
  {
    "path": "scripts/clear-cache.sh",
    "content": "#!/bin/bash\n#\n# For testing, this clears the Linux VM cache so we can get real numbers\n#\nsync\necho 3 | sudo tee /proc/sys/vm/drop_caches >/dev/null\n"
  },
  {
    "path": "scripts/colorprints.py",
    "content": "#!/usr/bin/env python2.7\n#\n# This is a small experiment in assigning colors to key fingerprints (or\n# any other fingerprint).\n#\n# The goal of this might be to make it easier to recognize two fingerprints\n# as being the same (or different), and maybe also make something pretty\n# that we can use to identify users instead of gravatar icons.\n#\n# The colors are assigned in such a way that the color shifts are gradual,\n# and the ends converge on the same values so the whole thing can be made\n# into a circle/wheel if necessary. By omitting one slice, a keyhole or\n# angel is created, which fits nicely with ideas of identity and privacy.\n#\n# The color blending used makes the end result more visually appealing,\n# and a bit less \"random\" - creating patterns which help with recall. The\n# downside (aside from color-blindness related issues), is we lose about\n# 50% of the bits of the fingerprint. To compensate for this, the low-order\n# bits of each hex pair are used to vary the hight of each color slice.\n# Another strategy would be to double the number of slices.\n#\n# Finally, to ensure that fingerprints which differ only slightly (a bit\n# flipped here and there) are obviously different, the entire color space\n# is shifted by a fixed value derived from the MD5 of the original\n# fingerprint.\n#\nfrom __future__ import print_function\nimport datetime\nimport os\nimport hashlib\nimport subprocess\n\ncanvastest = \"\"\"\n<!DOCTYPE html>\n<html><head>\n  <style>\n    body { background: #fff; }\n  </style>\n  <script>\n  function getWheel(box, points) {\n    var c = document.createElement(\"canvas\");\n    c.setAttribute(\"width\", box);\n    c.setAttribute(\"height\", box);\n    var ctx = c.getContext(\"2d\");\n    var step = (2 * Math.PI) / points.length;\n    var shift = (0.5 * Math.PI) + (step/2);\n    var maxwidth = box/3;\n    var pair = function(char, color, scale) {\n      var sh = shift;\n      var beg = sh + char*step;\n      var height = scale * maxwidth;\n      ctx.beginPath();\n      ctx.arc(box/2, box/2, box/2 - maxwidth + height/2, beg, beg + step);\n      ctx.lineWidth = height;\n      ctx.strokeStyle = color;\n      ctx.stroke();\n    };\n    ctx.clearRect(0, 0, c.width, c.height);\n    for (var i = 0; i < points.length-1; i++) {\n      pair(i, points[i][0], points[i][1]);\n    }\n    return c.toDataURL();\n  }\n  </script>\n</head><body>\n  <h2>Visualizing PGP fingerprints as colored keyholes/angels...</h2>\n\n\"\"\"\n\ndef colorprint(fingerprint, md5shift=True, mixer=[4, 8, 4]):\n    fingerprint = fingerprint.replace(' ', '').upper()\n    while 0 != ((len(fingerprint)//2) % 3):\n        fingerprint += '  '\n    l = len(fingerprint)//2\n\n    # Use an MD5 to shift the colorspace randomly, so even bitflips\n    # or other small changes become very noticable.\n    if md5shift:\n        digest = hashlib.md5(fingerprint).hexdigest()\n        sr, sg, sb = [int(d, 16) for d in (digest[0:3], digest[3:6], digest[6:9])]\n    else:\n        digest = '77'\n        sr = sg = sb = 0\n    def _r(v): return (sr + v) & 0xff;\n    def _g(v): return (sg + v) & 0xff;\n    def _b(v): return (sb + v) & 0xff;\n\n    # This assigns each hex pair a color, where the RGB values depend on\n    # itself and the other pairs around it. Changes are gradual, taking\n    # half the color value from immediate neighbors, with those once\n    # removed in either direction, mixing in at a quarter each.\n    def _int(v):\n        if v == '  ':\n            return int(digest[-2:], 16)\n        return int(v, 16)\n    pairs = []\n    m1, m2, m3 = mixer\n    for i in range(0, len(fingerprint)//2):\n        if (i % 3) == 1:\n            rgb = [((o+i) % l) for o in (-4, -3, -2, -1,  0,  1,  2,  3,  4)]\n        if (i % 3) == 2:\n            rgb = [((o+i) % l) for o in (-2, -4, -3,  1, -1,  0,  4,  2,  3)]\n        if (i % 3) == 0:\n            rgb = [((o+i) % l) for o in (-3, -2, -4,  0,  1, -1,  3,  4,  2)]\n \n        rgb = [_int(fingerprint[c*2:c*2+2]) for c in rgb]\n        r3 = (m1*_r(rgb[0]) + m2*_r(rgb[3]) + m3*_r(rgb[6])) // 16\n        g3 = (m1*_g(rgb[1]) + m2*_g(rgb[4]) + m3*_g(rgb[7])) // 16\n        b3 = (m1*_b(rgb[2]) + m2*_b(rgb[5]) + m3*_b(rgb[8])) // 16\n        r1 = '%2.2x' % _r(rgb[3])\n        g1 = '%2.2x' % _g(rgb[4])\n        b1 = '%2.2x' % _b(rgb[5])\n\n        fr, fg, fb = ['%2.2x' % ((4*c)/5 +0x00) for c in (r3, g3, b3)]\n        br, bg, bb = ['%2.2x' % ((4*c)/5 +0x33) for c in (r3, g3, b3)]\n        rr, gg, bb = ['%2.2x' % c for c in (r3, g3, b3)]\n\n        pairs.append(['#%s%s%s' % (rr, gg, bb),  # Mixed\n                      '#%s%s%s' % (r1, g1, b1),  # RGB: Plain\n                      '#%s%s%s' % (fr, fg, fb),  # RGB: Foreground\n                      '#%s%s%s' % (br, bg, bb),  # RGB: Background\n                      _int(fingerprint[i*2:i*2+2]),\n                      fingerprint[i*2:i*2+2]])\n\n    return pairs\n\ndef colorprint2(fingerprint, md5shift=True):\n    fingerprint = fingerprint.replace(' ', '').upper()\n    while 0 != ((len(fingerprint)//2) % 3):\n        fingerprint += '  '\n    l = len(fingerprint)//2\n\n    # Use an MD5 to shift the colorspace randomly, so even bitflips\n    # or other small changes become very noticable.\n    if md5shift:\n        digest = hashlib.md5(fingerprint).hexdigest()\n        sr, sg, sb = [int(d, 16) for d in (digest[0:2],\n                                           digest[2:4],\n                                           digest[4:6])]\n    else:\n        digest = '77'\n        sr = sg = sb = 0\n    def _r(v): return (sr + v) & 0xff;\n    def _g(v): return (sg + v) & 0xff;\n    def _b(v): return (sb + v) & 0xff;\n\n    # This assigns each hex pair a color, where the RGB values depend on\n    # itself and the other pairs around it. Changes are gradual, taking\n    # half the color value from immediate neighbors, with those once\n    # removed in either direction, mixing in at a quarter each.\n    def _int(v):\n        if v == '  ':\n            return int(digest[-2:], 16)\n        return int(v, 16)\n    pairs = []\n    for i in range(0, len(fingerprint)//2):\n        val = _int(fingerprint[i*2:i*2+2])\n\n        if (i % 3) == 0:\n            rgb = [((o+i) % l) for o in ( 0,  1, -1,  0,  1,  2)]\n            val = _r(val) \n        if (i % 3) == 1:\n            rgb = [((o+i) % l) for o in (-1,  0,  1,  2,  0,  1)]\n            val = _g(val) \n        if (i % 3) == 2:\n            rgb = [((o+i) % l) for o in ( 1, -1,  0,  1,  2,  0)]\n            val = _b(val) \n\n        rgb = [_int(fingerprint[c*2:c*2+2]) for c in rgb]\n        r1 = _r(rgb[0]) & 0xe0  # 0xe0 == 3 bits per channel\n        g1 = _g(rgb[1]) & 0xe0\n        b1 = _b(rgb[2]) & 0xe0\n        rm = (r1 + _r(rgb[3])) // 2\n        gm = (g1 + _g(rgb[4])) // 2\n        bm = (b1 + _b(rgb[5])) // 2\n\n        # Reverse the bits of val, so low order bits get more importance.\n        # This exposes what would be too subtle a color change, as a spacial\n        # change instead.\n        rval = 0\n        for j in range(0, 8):\n            rval += (0x80 >> j) if (val & (1 << j)) else 0\n\n        sbtR = (val & 0xb0)\n        sbtG = (val & 0x30) << 2\n        sbtB = (val & 0x0b) << 4\n\n        pairs.append(['#%2.2x%2.2x%2.2x' % (sbtR, sbtG, sbtB),  # sixbytwo\n                      '#%2.2x%2.2x%2.2x' % (r1, g1, b1),        # RGB\n                      '#%2.2x%2.2x%2.2x' % (rm, gm, bm),        # Mixed\n                      val,\n                      rval & 0xe0,        # 0xc0 == 2 bits per channel\n                      val & 0x18,         # These bits are orphans!\n                      fingerprint[i*2:i*2+2]])\n\n    return pairs\n\n\ndef tohtml(cprint, brk=False):\n    pairs = []\n    for mixed, rgb, fg, bg, val, hexchars in cprint:\n        pairs.append(('<span style=\"background: %s\">%s</span>'\n                      ) % (rgb, hexchars))\n        if brk and ((i % 4) == 3):\n            pairs.append('<br>')\n    # Fixed width font, so all our rainbows have the same proportions.\n    return '<tt>%s</tt>\\n' % ''.join(pairs)\n\ndef toangel(slices, fingerprint, copy=True, size=64):\n    return ''.join(['<span class=angel title=\"%s\">' % fingerprint,\n                    ('<script>document.write(\"<img '\n                     'src=\\'\"+getWheel(%s, %s)+\"\\'>\");</script>'\n                     ) % (size, slices, ),\n                    (('<script>document.write(\"<input type=text '\n                      'value=\\'\"+getWheel(%s, %s)+\"\\'>\");</script>'\n                      ) % (size, slices, )) if copy else '',\n                    '</span>\\n'])\n\nif os.getenv('HTTP_METHOD'):\n   print('Content-Type: text/html')\n   print()\nprint(canvastest)\n\nfingerprint = \"A3C4 F0F9 79CA A22C DBA8  F512 EE8C BC9E 886D DD89\"\ncprint = colorprint(fingerprint, md5shift=False)\nprint('<b>Channels:</b> ', tohtml(cprint), '<br>')\n\nprint('<h4>Channels, +mixing, +sizes, +md5shift</h4>')\nprint(toangel([[rgb, 1] for mixed, rgb, fg, bg, val, hc in cprint],\n              fingerprint))\nprint(toangel([[mixed, 1] for mixed, rgb, fg, bg, val, hc in cprint],\n              fingerprint), '<br>')\nprint(toangel([[mixed, 0.5 + float(val % 32)/64]\n                for mixed, rgb, fg, bg, val, hc in cprint],\n              fingerprint))\ncprint = colorprint(fingerprint, md5shift=True)\nprint(toangel([[mixed, 0.5 + float(val % 32)/64]\n                for mixed, rgb, fg, bg, val, hc in cprint],\n              fingerprint), '<br>')\n\nprint('<h4>1 bit flipped, w/o or with md5shift</h4>')\nfingerprint = \"A3C4 F0F8 79CA A22C DBA8  F512 EE8C BC9E 886D DD89\"\ncprint = colorprint(fingerprint, md5shift=False)\nprint(toangel([[mixed, 0.5 + float(val % 32)/64]\n                for mixed, rgb, fg, bg, val, hc in cprint],\n              fingerprint))\ncprint = colorprint(fingerprint, md5shift=True)\nprint(toangel([[mixed, 0.5 + float(val % 32)/64]\n                for mixed, rgb, fg, bg, val, hc in cprint],\n              fingerprint), '<br>')\n\nprint('<h4>New style colorprinting tests: 6x2 palette, channels, mixed</h4>')\nfingerprint = \"A3C4 F0F9 79CA A22C DBA8  F512 EE8C BC9E 886D DD89\"\ncprint2 = colorprint2(fingerprint, md5shift=False)\nprint(toangel([[sixtimestwo, 1.0 - float(rval)/512]\n                for sixtimestwo, rgb, mixed, val, rval, dr, hc in cprint2],\n              fingerprint))\nprint(toangel([[rgb, 1.0 - float(rval)/512]\n                for sixtimestwo, rgb, mixed, val, rval, dr, hc in cprint2],\n              fingerprint))\nprint(toangel([[mixed, 1.0 - float(rval)/512]\n                for sixtimestwo, rgb, mixed, val, rval, dr, hc in cprint2],\n              fingerprint), '<br>')\n\nprint('<h4>Adding md5shift, checking bit flips</h4>')\ncprint2 = colorprint2(fingerprint, md5shift=True)\nprint(toangel([[mixed, 1.0 - float(rval)/512]\n                for sixtimestwo, rgb, mixed, val, rval, dr, hc in cprint2],\n              fingerprint))\nfingerprint = \"A3C4 F0F8 79CA A22C DBA8  F512 EE8C BC9E 886D DD89\"\ncprint2b = colorprint2(fingerprint, md5shift=True)\nprint(toangel([[mixed, 1.0 - float(rval)/512]\n                for sixtimestwo, rgb, mixed, val, rval, dr, hc in cprint2b],\n              fingerprint), '<br>')\n\nprint(\"<hr><h3>More samples (new style, full features)</h3>\")\ncount = 0\nfor line in \"\"\"\\\n      Key fingerprint = A3C4 F0F9 79CA A22C DBA8  F512 EE8C BC9E 886D DD89\n      Key fingerprint = 61A0 1576 3D28 D410 A87B  1973 2819 1D9B 3B41 99B4\n      Key fingerprint = B221 6FD2 779A E5B5 9D79  743C D5DC 2A79 C2E4 AE92\n      Key fingerprint = C8F9 EBDA 2167 CC5F 4D2B  EDA1 F5C2 529F C903 BEF1\n      Key fingerprint = 8779 4923 97B2 0AA4 998C  0EA6 AED2 48B1 C7B2 CAC3\n      Key fingerprint = 7670 B684 846E C70E 61EF  FB7F 07AA A4D9 5F3D 6695\n      Key fingerprint = 7960 1CDC 85C9 0B63 8D9F  DD89 3BD8 FF2B 807B 17C1\n      Key fingerprint = 2139 09EF B5EC 48DC 9129  D778 53C8 B358 6156 D52D\n      Key fingerprint = 228F AD20 3DE9 AE7D 84E2  5265 CF9A 6F91 4193 A197\n      Key fingerprint = 6F10 0BDF B3B6 A1EA 31E8  54D1 46D0 A5AD 7050 73B5\n      Key fingerprint = D95C 3204 2EE5 4FFD B25E  C348 9F27 33F4 0928 D23A\n      Key fingerprint = E413 80A8 4EB7 30BB 8E5B  8355 B327 24B9 61FE DFBA\n      Key fingerprint = 3D6A 08E9 1262 3E9A 00B2  1BDC 067F 4920 98CF 2762\n      Key fingerprint = 20FF 4163 4DF6 F90E CA44  5220 3CAC 6CA6 E4D9 C373\n      Key fingerprint = B906 EA4B 8A28 15C4 F859  6F9F 47C1 3F3F ED73 5179\n\"\"\".splitlines():\n    if 'Key fingerprint' in line:\n        fingerprint = line.split(' = ', 1)[1].strip()\n        cprint = colorprint2(fingerprint)\n        print(toangel([[mixed, 1.0 - float(rval)/512]\n                       for sixXtwo, rgb, mixed, val, rval, dr, hc in cprint],\n                      fingerprint, copy=False, size=128))\n        count += 1\n        if count % 5 == 0:\n            print('<br>')\n\nprint('<hr>')\nprint('Generated: %s' % datetime.datetime.now().ctime())\nprint('</body></html>')\n\n"
  },
  {
    "path": "scripts/compile-messages.sh",
    "content": "#!/bin/bash\nset -e\ncd \"$(dirname $0)\"/..\nfor L in $(find shared-data/locale -type d |grep \"LC_MESSAGES\"); do\n    echo -e -n \"$(echo $L |sed -e s,.*locale/,, -e s,/LC_.*,,)\\t\"\n    msgfmt -c --statistics $L/mailpile.po -o $L/mailpile.mo || rm -f $L/mailpile.mo\ndone;\necho\necho \"WARNING: Fuzzy strings are NOT included in the final catalogues.\"\necho \"         Our take: English is better than a wrong translation.\"\necho\n"
  },
  {
    "path": "scripts/create-debian-changelog.py",
    "content": "#!/usr/bin/env python2.7\n#This script builds a DCH changelog from the git commit log\nfrom __future__ import print_function\nfrom subprocess import check_output, call\nfrom multiprocessing import Pool\nimport os\n\ndef getLogMessage(commitSHA):\n    \"\"\"Get the log message for a given commit hash\"\"\"\n    output = check_output([\"git\",\"log\",\"--format=%B\",\"-n\",\"1\",commitSHA])\n    return output.strip()\n\ndef versionFromCommitNo(commitNo):\n    \"\"\"Generate a version string from a numerical commit no\"\"\"\n    return \"0.0.0-dev%d\" % commitNo\n\n#Execute git rev-list $(git rev-parse HEAD) to get list of revisions\nhead = check_output([\"git\",\"rev-parse\",\"HEAD\"]).strip()\nrevisions = check_output([\"git\",\"rev-list\",head]).strip().split(\"\\n\")\n#Revisions now contains rev identifiers, newest revisions first.\nprint(\"Found %d revisions\" % len(revisions))\nrevisions.reverse() #In-place reverse, to make oldest revision first\n#Map the revisions to their log msgs\nprint(\"Mapping revisions to log messages\")\nthreadpool = p = Pool(10)\nrevLogMsgs = threadpool.map(getLogMessage, revisions)\n#(Re)create the changelog for the first revision (= the oldest one)\ntry:\n    os.unlink(\"debian/changelog\")\nexcept OSError:\n    pass #Don't care if the file does not exist\nfirstCommitMsg = revLogMsgs[0]\ncall([\"dch\",\"--create\",\"-v\",versionFromCommitNo(0),\"--package\",\"mailpile\",firstCommitMsg])\n#Create the changelog entry for all other commits\nfor i in range(1, len(revisions)):\n    print(\"Generating changelog for revision %d\" % i)\n    commitMsg = revLogMsgs[i]\n    call([\"dch\",\"-v\",versionFromCommitNo(i),\"--package\",\"mailpile\",commitMsg])\n"
  },
  {
    "path": "scripts/docker-dev/down",
    "content": "#!/bin/bash\ndocker-compose -f docker-compose.dev.yml down\n"
  },
  {
    "path": "scripts/docker-dev/shell",
    "content": "#!/bin/bash\nCONTAINER_NAME=mailpile_dev\nCONTAINER_ID=$(docker ps --filter \"name=${CONTAINER_NAME}\" --quiet)\nif [[ ! -z  $CONTAINER_ID ]]; then\n  echo \"Connecting to docker container ${CONTAINER_NAME}:${CONTAINER_ID}\"\n  docker attach $CONTAINER_NAME\nelse\n  echo \"Docker container ${CONTAINER_NAME} does not seem to be running. Start it with './scripts/docker-dev/up'\"\nfi\n"
  },
  {
    "path": "scripts/docker-dev/up",
    "content": "#!/bin/bash\ndocker-compose -f docker-compose.dev.yml up --build --remove-orphans\n"
  },
  {
    "path": "scripts/email-parsing-test.py",
    "content": "#!/usr/bin/env python2.7\n#\n# This is code which tries very hard to interpret the From:, To: and Cc:\n# lines found in real-world e-mail addresses and make sense of them.\n#\n# The general strategy of this script is to:\n#    1. parse header into tokens\n#    2. group tokens together into address + name constructs\n#    3. normalize each group to a standard format\n#\n# In practice, we do this in two passes - first a strict pass where we try\n# to parse things semi-sensibly.  If that fails, there is a second pass\n# where we try to cope with certain types of weirdness we've seen in the\n# wild. The wild can be pretty wild.\n#\n# This parser is NOT fully RFC2822 compliant - in particular it will get\n# confused by nested comments (see FIXME in tests below).\n#\nfrom __future__ import print_function\nimport sys\nimport traceback\n\nfrom mailpile.mailutils import AddressHeaderParser as AHP\n\n\nahp_tests = AHP(AHP.TEST_HEADER_DATA)\nprint('_tokens: %s' % ahp_tests._tokens)\nprint('_groups: %s' % ahp_tests._groups)\nprint('%s' % ahp_tests)\nprint('normalized: %s' % ahp_tests.normalized())\n\n\nheaders, header, inheader = {}, None, False\nfor line in sys.stdin:\n    if inheader:\n        if line in ('\\n', '\\r\\n'):\n            for hdr in ('from', 'to', 'cc'):\n                val = headers.get(hdr, '').replace('\\n', ' ').strip()\n                if val:\n                    try:\n                        nv = AHP(val, _raise=True).normalized()\n                        if '\\\\' in nv:\n                            print('ESCAPED: %s: %s (was %s)' % (hdr, nv, val))\n                        else:\n                            print('%s' % (nv,))\n                    except ValueError:\n                        print('FAILED: %s: %s -- %s' % (hdr, val,\n                            traceback.format_exc().replace('\\n', '  ')))\n            headers, header, inheader = {}, None, False\n        elif line[:1] in (' ', '\\t') and header:\n            headers[header] = headers[header].rstrip() + line[1:]\n        else:\n            try:\n                header, value = line.split(': ', 1)\n                header = header.lower()\n                headers[header] = headers.get(header, '') + ' ' + value\n            except ValueError:\n                headers, header, inheader = {}, None, False\n    else:\n        if line.startswith('From '):\n            inheader = True\n"
  },
  {
    "path": "scripts/gitwhere.sh",
    "content": "#!/bin/bash\necho $(git status --porcelain -b |head -1 |cut -f1 -d.)@$(git rev-parse HEAD|cut -b1-12) |cut -f 2 -d\" \"\n"
  },
  {
    "path": "scripts/gpg",
    "content": "#!/bin/bash\ncat >/tmp/gpg-input.$$\n/usr/bin/gpg \"$@\" </tmp/gpg-input.$$ >/tmp/gpg-output.$$\ncat /tmp/gpg-output.$$\n"
  },
  {
    "path": "scripts/less-compiler.in",
    "content": "PATH:=$(PATH):/Applications/CodeKit.app/Contents/Resources/engines/less/bin/\n\nall: shared-data/default-theme/css/default.css \\\n     shared-data/default-theme/css/print.css\n\t@true\n\nshared-data/default-theme/css/default.css: .less-deps\n\t@(cd shared-data/default-theme/less && \\\n            lessc default.less \\\n              |perl -npe 's,(libraries|app)/,,g' \\\n              |perl -npe 's,/shared-data/,../,g' \\\n              |perl -npe 's,[\\./]+/bower_components/select2/,../css/,g' \\\n\t    >../css/default.css)\n\t@ls -l shared-data/default-theme/css/default.css\n\nshared-data/default-theme/css/print.css: .less-deps\n\t@(cd shared-data/default-theme/less && lessc -x print.less >../css/print.css)\n\t@ls -l shared-data/default-theme/css/print.css\n\n.less-deps: scripts/less-compiler.in \\\n"
  },
  {
    "path": "scripts/mailpile",
    "content": "#!/usr/bin/env python2.7\nimport sys, os\n\n# Make imports work without PYTHONPATH\nmailpile_root = os.path.dirname(     # Mailpile root\n    os.path.dirname(                 # scripts/\n        os.path.realpath(__file__))) # this script\n\nsys.path = [mailpile_root, os.path.join(mailpile_root, 'lib')] + sys.path\n\nfrom mailpile.app import Main\nMain(sys.argv[1:])\n"
  },
  {
    "path": "scripts/mailpile-admin",
    "content": "#!/bin/bash\nexec python2.7 \"$(dirname $0)\"/../shared-data/multipile/mailpile-admin.py \"$@\"\n"
  },
  {
    "path": "scripts/mailpile-decrypt.py",
    "content": "#!/usr/bin/python2.7\n\"\"\"\nThis tool will (attempt to) decrypt files encrypted by Mailpile.\n\nTwo argument form:\n    mailpile-decrypt.py encrypted.mep decrypted.mep\n\nMultiple argument form:\n    mailpile-decrypt.py encrypted1.mep encrypted2.mep path/to/directory/\n\nLoading keys from a non-standard location:\n    export MAILPILE_HOME=/path/to/Mailpile/data\n    mailpile-decrypt.py ...\n\nIn the multiple argument form, the tool will name output files the same\nas input files, with `.txt` appended. The tool will refuse to overwrite\npre-existing files in both cases, printing a warning and skipping that\ninput file.\n\n\"\"\"\nimport os\nimport sys\nfrom mailpile.config.defaults import CONFIG_RULES\nfrom mailpile.config.manager import ConfigManager\nfrom mailpile.i18n import gettext as _\nfrom mailpile.ui import Session, UserInteraction\nfrom mailpile.util import decrypt_and_parse_lines\nfrom mailpile.auth import VerifyAndStorePassphrase\n\n\n# Check our arguments\ninfiles = outfiles = []\ntry:\n    infiles = sys.argv[1:-1]\n    outpath = sys.argv[-1]\nexcept:\n    pass\nif ((not infiles or not outpath) or\n        (os.path.exists(outpath) and not os.path.isdir(outpath)) or\n        (len(infiles) > 1 and not os.path.isdir(outpath))):\n    sys.stderr.write(__doc__)\n    sys.exit(1)\n\n\n# Basic app bootstrapping\nconfig = ConfigManager(rules=CONFIG_RULES)\nsession = Session(config)\nsession.ui = UserInteraction(config)\n\n\n# Get the password, verify it, decrypt config\nfails = 0\nfor tries in range(1, 4):\n    try:\n        VerifyAndStorePassphrase(\n            config, passphrase=session.ui.get_password(_('Your password: ')))\n        break\n    except:\n        if tries < 3:\n            sys.stderr.write('Incorrect, try again?')\n        fails = tries\nif fails == tries:\n    sys.exit(1)\nsys.stderr.write('\\n')\n\n\n# Go decrypt stuff! Or try at least.\nfor in_fn in infiles:\n    if os.path.isdir(outpath):\n        out_fn = os.path.join(outpath, os.path.basename(in_fn) + '.txt')\n    else:\n        out_fn = outpath\n\n    if os.path.exists(out_fn):\n        sys.stderr.write('SKIPPED, already exists: %s\\n' % out_fn)\n    else:\n        try:\n            in_fd = open(in_fn, 'rb')\n        except:\n            sys.stderr.write('SKIPPED, open failed: %s\\n' % in_fn)\n \n        with open(out_fn, 'wb') as out_fd:\n            def parser(lines):\n                for line in lines:\n                    out_fd.write(line.encode('utf-8'))\n            decrypt_and_parse_lines(in_fd, parser, config, newlines=True)\n            sys.stderr.write(\n                'Decrypted %s => %s\\n' % (os.path.basename(in_fn), out_fn))\n\n        in_fd.close()\n"
  },
  {
    "path": "scripts/mailpile-pyside.py",
    "content": "#!/usr/bin/env python2.7\n#\n# This is a proof-of-concept quick hack, copy-pasted from code found here:\n#\n#   http://agateau.com/2012/02/03/pyqtwebkit-experiments-part-2-debugging/\n#\nimport sys\nfrom PySide.QtCore import *\nfrom PySide.QtGui import *\nfrom PySide.QtWebKit import *\n\nclass Window(QWidget):\n    def __init__(self):\n        super(Window, self).__init__()\n        self.view = QWebView(self)\n\n        self.setupInspector()\n\n        self.splitter = QSplitter(self)\n        self.splitter.setOrientation(Qt.Vertical)\n\n        layout = QVBoxLayout(self)\n        #layout.setMargin(0)\n        layout.addWidget(self.splitter)\n\n        self.splitter.addWidget(self.view)\n        self.splitter.addWidget(self.webInspector)\n\n    def setupInspector(self):\n        page = self.view.page()\n        page.settings().setAttribute(QWebSettings.DeveloperExtrasEnabled, True)\n        self.webInspector = QWebInspector(self)\n        self.webInspector.setPage(page)\n\n        #shortcut = QShortcut(self)\n        #shortcut.setKey(Qt.Key_F12)\n        #shortcut.activated.connect(self.toggleInspector)\n        self.webInspector.setVisible(True)\n\n    def toggleInspector(self):\n        self.webInspector.setVisible(not self.webInspector.isVisible())\n\ndef main():\n    app = QApplication(sys.argv)\n    window = Window()\n    window.show()\n    window.view.load('http://localhost:33511/bjarni/')\n    return app.exec_()\n\nsys.exit(main())\n"
  },
  {
    "path": "scripts/mailpile-test.py",
    "content": "#!/usr/bin/env python2.7\n#\n# This script runs a set of black-box tests on Mailpile using the test\n# messages found in `testing/`.\n#\n# If run with -i as the first argument, it will then drop to an interactive\n# python shell for experimenting and manual testing.\n#\nfrom __future__ import print_function\nimport os\nimport sys\nimport time\nimport traceback\n\n\n# Set up some paths\nmailpile_root = os.path.join(os.path.dirname(__file__), '..')\nmailpile_test = os.path.join(mailpile_root, 'mailpile', 'tests', 'data')\nmailpile_send = os.path.join(mailpile_root, 'scripts', 'test-sendmail.sh')\nmailpile_home = os.path.join(mailpile_test, 'tmp')\nmailpile_gpgh = os.path.join(mailpile_test, 'gpg-keyring')\nmailpile_sent = os.path.join(mailpile_home, 'sent.mbx')\n\n# Set the GNUGPHOME variable to our test key\nos.environ['GNUPGHOME'] = mailpile_gpgh\n\n# Add the root to our import path, import API and demo plugins\nsys.path.append(mailpile_root)\nimport mailpile.app\nfrom mailpile import Mailpile\nfrom mailpile.mail_source.local import LocalMailSource\n\n\n##[ Black-box test script ]###################################################\n\nFROM_BRE = [u'from:r\\xfanar', u'from:bjarni']\nICELANDIC = u'r\\xfanar'\nIS_CHARS = (u'\\xe1\\xe9\\xed\\xf3\\xfa\\xfd\\xfe\\xe6\\xf6\\xf0\\xc1\\xc9\\xcd\\xd3'\n            u'\\xda\\xdd\\xde\\xc6\\xd6\\xd0')\nMY_FROM = 'team+testing@mailpile.is'\nMY_NAME = 'Mailpile Team'\nMY_KEYID = '0x7848252F'\n\n# First, we set up a pristine Mailpile\nos.system('rm -rf %s' % mailpile_home)\nmp = Mailpile(workdir=mailpile_home)\ncfg = config = mp._session.config\nui = mp._session.ui\n\nif '-v' not in sys.argv:\n    from mailpile.ui import SilentInteraction\n    mp._session.ui = SilentInteraction(config)\n\ncfg.plugins.load('demos', process_manifest=True)\ncfg.plugins.load('hacks', process_manifest=True)\ncfg.plugins.load('experiments', process_manifest=True)\ncfg.plugins.load('smtp_server', process_manifest=True)\n\n\ndef contents(fn):\n    return open(fn, 'r').read()\n\n\ndef grep(w, fn):\n    return '\\n'.join([l for l in open(fn, 'r').readlines() if w in l])\n\n\ndef grepv(w, fn):\n    return '\\n'.join([l for l in open(fn, 'r').readlines() if w not in l])\n\n\ndef say(stuff):\n    mp._session.ui.mark(stuff)\n    mp._session.ui.reset_marks()\n    if '-v' not in sys.argv:\n        sys.stderr.write('.')\n\n\ndef do_setup():\n    # Set it first to avoid interactive prompt for a passphrase\n    config.passphrases['DEFAULT'].set_passphrase('mailpile')\n    # Set up initial tags and such\n    mp.setup()\n    mp.profiles_add(MY_FROM, '=', MY_NAME)\n    mp.rescan('vcards:gpg')\n\n    # Setup GPG access credentials and TELL EVERYONE!\n    config.sys.login_banner = 'Pssst! The password is: mailpile'\n    #config.gnupg_passphrase.set_passphrase('mailpile')\n    #config.prefs.gpg_recipient = '3D955B5D7848252F'\n\n    config.vcards.get(MY_FROM).fn = MY_NAME\n    config.prefs.default_email = MY_FROM\n    config.prefs.encrypt_index = True\n    config.prefs.index_encrypted = True\n    config.prefs.inline_pgp = False\n\n    # Configure our fake mail sending setup\n    config.sys.http_port = 33414\n    config.sys.smtpd.host = 'localhost'\n    config.sys.smtpd.port = 33415\n    config.prefs.openpgp_header = 'encrypt'\n    config.prefs.crypto_policy = 'openpgp-sign'\n\n    if '-v' in sys.argv:\n        config.sys.debug = 'log http vcard rescan sendmail log compose'\n\n    # Set up dummy conctact importer for testing, disable Gravatar\n    mp.set('prefs/vcard/importers/demo/0/name = Mr. Rogers')\n    mp.set('prefs/vcard/importers/gravatar/0/active = false')\n    mp.set('prefs/vcard/importers/gpg/0/active = false')\n\n    # Make sure that actually worked\n    assert(not mp._config.prefs.vcard.importers.gpg[0].active)\n    assert(not mp._config.prefs.vcard.importers.gravatar[0].active)\n\n    # Copy the test Maildir...\n    for mailbox in ('Maildir', 'Maildir2'):\n        path = os.path.join(mailpile_home, mailbox)\n        os.system('cp -a %s/Maildir %s' % (mailpile_test, path))\n\n    # Add the test mailboxes\n    for mailbox in ('tests.mbx', ):\n        mp.add(os.path.join(mailpile_test, mailbox))\n    mp.add(os.path.join(mailpile_home, 'Maildir'))\n\n\ndef test_vcards():\n    say(\"Testing vcards\")\n\n    # Do we have a Mr. Rogers contact?\n    mp.rescan('vcards')\n    assert(mp.contacts_view('mr@rogers.com'\n                            ).result['contact']['fn'] == u'Mr. Rogers')\n    assert(len(mp.contacts('rogers').result['contacts']) == 1)\n\n\ndef test_load_save_rescan():\n    say(\"Testing load/save/rescan\")\n    mp.rescan('mailboxes')\n\n    # Save and load the index, just for kicks\n    messages = len(mp._config.index.INDEX)\n    assert(messages > 5)\n    mp._config.index.save(mp._session)\n    mp._session.ui.reset_marks()\n    mp._config.index.load(mp._session)\n    mp._session.ui.reset_marks()\n    assert(len(mp._config.index.INDEX) == messages)\n\n    # Rescan AGAIN, so we can test for the presence of duplicates and\n    # verify that the move-detection code actually works.\n    os.system('rm -f %s/Maildir/*/*' % mailpile_home)\n    mp.add(os.path.join(mailpile_home, 'Maildir2'))\n    mp.rescan('mailboxes')\n\n    # Search for things, there should be exactly one match for each.\n    mp.order('rev-date')\n    for search in (FROM_BRE,\n                   ['agirorn'],\n                   ['subject:emerging'],\n                   ['from:twitter', 'brennan'],\n                   ['dates:2013-09-17', 'feministinn'],\n                   ['mailbox:tests.mbx'] + FROM_BRE,\n                   ['att:jpg', 'fimmtudaginn'],\n                   ['subject:Moderation', 'kde-isl', '-is:unread'],\n                   ['from:bjarni', 'subject:testing', 'subject:encryption',\n                    'should', 'encrypted', 'message', 'tag:mp_enc-decrypted'],\n                   ['from:bjarni', 'subject:inline', 'subject:encryption',\n                    'grand', 'tag:mp_enc-mixed-decrypted'],\n                   ['from:bjarni', 'subject:signatures', '-is:unread',\n                    'tag:mp_sig-expired'],\n                   ['from:brennan', 'subject:encrypted',\n                    'testing', 'purposes', 'only', 'tag:mp_enc-decrypted'],\n                   ['from:brennan', 'subject:signed',\n                    'tag:mp_sig-expired'],\n                   ['from:barnaby', 'subject:testing', 'soup',\n                    'tag:mp_sig-unknown', 'tag:mp_enc-decrypted'],\n                   ['from:square', 'subject:here', '-has:attachment'],\n                   [u'subject:' + IS_CHARS, 'subject:8859'],\n                   [u'subject:' + IS_CHARS, 'subject:UTF'],\n                   ['use_libusb', 'unsubscribe', 'vger'],\n                   ):\n        say('Searching for: %s' % search)\n        results = mp.search(*search)\n        if results.result['stats']['count'] != 1:\n            raise AssertionError(\n                'Count = %s != 1' % results.result['stats']['count'])\n\n    say('Checking size of inbox')\n    mp.order('flat-date')\n    assert(mp.search('tag:inbox').result['stats']['count'] == 20)\n\n    say('FIXME: Make sure message signatures verified')\n\ndef test_message_data():\n    say(\"Testing message contents\")\n\n    # Load up a message and take a look at it...\n    search_md = mp.search('subject:emerging').result\n    result_md = search_md['data']['metadata'][search_md['thread_ids'][0]]\n    view_md = mp.view('=%s' % result_md['mid']).result\n\n    # That loaded?\n    message_md = view_md['data']['messages'][result_md['mid']]\n    assert('athygli' in message_md['text_parts'][0]['data'])\n\n    # Load up another message and take a look at it...\n    search_bre = mp.search(*FROM_BRE).result\n    result_bre = search_bre['data']['metadata'][search_bre['thread_ids'][0]]\n    view_bre = mp.view('=%s' % result_bre['mid']).result\n\n    # Make sure message threading is working (there are message-ids and\n    # references in the test data).\n    assert(len(view_bre['thread_ids']) == 3)\n\n    # Make sure we are decoding weird headers correctly\n    metadata_bre = view_bre['data']['metadata'][view_bre['message_ids'][0]]\n    message_bre = view_bre['data']['messages'][view_bre['message_ids'][0]]\n    from_bre = search_bre['data']['addresses'][metadata_bre['from']['aid']]\n    say('Checking encoding: %s' % from_bre)\n    assert('=C3' not in from_bre['fn'])\n    assert('=C3' not in from_bre['address'])\n    for key, val in message_bre['header_list']:\n        if key.lower() not in ('from', 'to', 'cc'):\n            continue\n        say('Checking encoding: %s: %s' % (key, val))\n        assert('utf' not in val)\n\n    # This message broke our HTML engine that one time\n    search_md = mp.search('from:heretic', 'subject:outcome').result\n    result_md = search_md['data']['metadata'][search_md['thread_ids'][0]]\n    view_md = mp.view('=%s' % result_md['mid'])\n    assert('Outcome' in view_md.as_html())\n\n\ndef test_composition():\n    say(\"Testing composition\")\n\n    # Create a message...\n    new_mid = mp.message_compose().result['thread_ids'][0]\n    assert(mp.search('tag:drafts').result['stats']['count'] == 0)\n    assert(mp.search('tag:blank').result['stats']['count'] == 1)\n    assert(mp.search('tag:sent').result['stats']['count'] == 0)\n    assert(not os.path.exists(mailpile_sent))\n\n    # Edit the message (moves from Blank to Draft, not findable in index)\n    msg_data = {\n        'to': ['%s#%s' % (MY_FROM, MY_KEYID)],\n        'bcc': ['secret@test.com#%s' % MY_KEYID],\n        'mid': [new_mid],\n        'subject': ['This the TESTMSG subject'],\n        'body': ['Hello world!'],\n        'attach-pgp-pubkey': ['yes']\n    }\n    mp.message_update(**msg_data)\n    assert(mp.search('tag:drafts').result['stats']['count'] == 1)\n    assert(mp.search('tag:blank').result['stats']['count'] == 0)\n    assert(mp.search('TESTMSG').result['stats']['count'] == 1)\n    assert(not os.path.exists(mailpile_sent))\n\n    # Send the message (moves from Draft to Sent, is findable via. search)\n    del msg_data['subject']\n    msg_data['body'] = [\n        ('Hello world... thisisauniquestring :) '+ICELANDIC)\n    ]\n    mp.message_update_send(**msg_data)\n    assert(mp.search('tag:drafts').result['stats']['count'] == 0)\n    assert(mp.search('tag:blank').result['stats']['count'] == 0)\n\n    # First attempt to send should fail & record failure to event log\n    config.prefs.default_messageroute = 'default'\n    config.routes['default'] = {\"command\": '/no/such/file'}\n    mp.sendmail()\n    events = mp.eventlog('source=mailpile.plugins.compose.Sendit',\n                         'data_mid=%s' % new_mid).result['events']\n    assert(len(events) == 1)\n    assert(events[0]['flags'] == 'i')\n    assert(len(mp.eventlog('incomplete').result['events']) == 1)\n\n    # Second attempt should succeed!\n    config.routes.default.command = '%s -i %%(rcpt)s' % mailpile_send\n    mp.sendmail()\n    events = mp.eventlog('source=mailpile.plugins.compose.Sendit',\n                         'data_mid=%s' % new_mid).result['events']\n    assert(len(events) == 1)\n    assert(events[0]['flags'] == 'c')\n    assert(len(mp.eventlog('incomplete').result['events']) == 0)\n\n    # Verify that it actually got sent correctly\n    assert('the TESTMSG subject' in contents(mailpile_sent))\n    # This is the base64 encoding of thisisauniquestring\n    assert('dGhpc2lzYXVuaXF1ZXN0cmluZ' in contents(mailpile_sent))\n    assert('encryption: ' not in contents(mailpile_sent).lower())\n    assert('attach-pgp-pubkey: ' not in contents(mailpile_sent).lower())\n    assert('x-mailpile-' not in contents(mailpile_sent))\n    assert(MY_KEYID not in contents(mailpile_sent))\n    assert(MY_FROM in grep('X-Args', mailpile_sent))\n    assert('secret@test.com' in grep('X-Args', mailpile_sent))\n    assert('secret@test.com' not in grepv('X-Args', mailpile_sent))\n    for search in (['tag:sent'],\n                   ['bcc:secret@test.com'],\n                   ['thisisauniquestring'],\n                   ['thisisauniquestring'] + MY_FROM.split(),\n                   ['thisisauniquestring',\n                    'in:mp_sig-verified', 'in:mp_enc-none', 'in:sent'],\n                   ['subject:TESTMSG']):\n        say('Searching for: %s' % search)\n        assert(mp.search(*search).result['stats']['count'] == 1)\n    # This is the base64 encoding of thisisauniquestring\n    assert('dGhpc2lzYXVuaXF1ZXN0cmluZ' in contents(mailpile_sent))\n    assert('OpenPGP: id=CF5E' in contents(mailpile_sent))\n    assert('Encryption key for' in contents(mailpile_sent))\n    assert('; preference=encrypt' in contents(mailpile_sent))\n    assert('secret@test.com' not in grepv('X-Args', mailpile_sent))\n    os.remove(mailpile_sent)\n\n    # Test the send method's \"bounce\" capability\n    mp.message_send(mid=[new_mid], to=['nasty@test.com'])\n    mp.sendmail()\n    # This is the base64 encoding of thisisauniquestring\n    assert('dGhpc2lzYXVuaXF1ZXN0cmluZ' in contents(mailpile_sent))\n    assert('OpenPGP: id=CF5E' in contents(mailpile_sent))\n    assert('; preference=encrypt' in contents(mailpile_sent))\n    assert('secret@test.com' not in grepv('X-Args', mailpile_sent))\n    assert('-i nasty@test.com' in contents(mailpile_sent))\n\n\ndef test_smtp():\n    config.prepare_workers(mp._session, daemons=True)\n    new_mid = mp.message_compose().result['thread_ids'][0]\n    msg_data = {\n        'from': ['%s#%s' % (MY_FROM, MY_KEYID)],\n        'mid': [new_mid],\n        'subject': ['This the OTHER TESTMSG...'],\n        'body': ['Hello SMTP world!']\n    }\n    config.prefs.default_messageroute = 'default'\n    config.prefs.always_bcc_self = False\n    config.routes['default'] = {\n        'protocol': 'smtp',\n        'host': 'localhost',\n        'port': 33415\n    }\n    mp.message_update(**msg_data)\n    mp.message_send(mid=[new_mid], to=['nasty@test.com'])\n    mp.sendmail()\n    config.stop_workers()\n\ndef test_html():\n    say(\"Testing HTML\")\n\n    mp.output(\"jhtml\")\n    assert('&lt;bang&gt;' in '%s' % mp.search('in:inbox').as_html())\n    mp.output(\"text\")\n\n\ntry:\n    do_setup()\n    if '-n' not in sys.argv:\n        test_vcards()\n        test_load_save_rescan()\n        test_message_data()\n        test_html()\n        test_composition()\n        test_smtp()\n        if '-v' not in sys.argv:\n            sys.stderr.write(\"\\nTests passed, woot!\\n\")\n        else:\n            say(\"Tests passed, woot!\")\nexcept:\n    sys.stderr.write(\"\\nTests FAILED!\\n\")\n    print()\n    traceback.print_exc()\n\n\n##[ Interactive mode ]########################################################\n\nif '-i' in sys.argv:\n    mp.set('prefs/vcard/importers/gravatar/0/active = true')\n    mp.set('prefs/vcard/importers/gpg/0/active = true')\n    mp._session.ui = ui\n    print('%s' % mp.help_splash())\n    mp.Interact()\n\n\n##[ Cleanup ]#################################################################\nconfig.stop_workers()\nos.system('rm -rf %s' % mailpile_home)\nos.system('git checkout %s' % mailpile_test)\n"
  },
  {
    "path": "scripts/make-messages.sh",
    "content": "#!/bin/bash\nset -x\nset -e\ncd \"$(dirname $0)\"/..\n\nexport PYTHONPATH=$(pwd)\n\npybabel extract --project=mailpile \\\n    -F babel.cfg \\\n    -o shared-data/locale/mailpile.pot.tmp \\\n    .\n\nsed -e 's/ORGANIZATION/Mailpile ehf/' \\\n    -e 's/FIRST AUTHOR <EMAIL@ADDRESS>/Mailpile Team <team@mailpile.is>/' \\\n    < shared-data/locale/mailpile.pot.tmp \\\n    > shared-data/locale/mailpile.pot \\\n    && rm -f shared-data/locale/mailpile.pot.tmp\n\nfor L in $(find shared-data/locale -type d |grep \"LC_MESSAGES\"); do\n    msgmerge -U $L/mailpile.po shared-data/locale/mailpile.pot\ndone\n"
  },
  {
    "path": "scripts/mbox-minimal.py",
    "content": "#!/usr/bin/env python2.7\nfrom __future__ import print_function\nimport sys\nimport re\n\nmsgid_re = re.compile('(?im)^message-id:')\nmessages = []\nmsgids = []\nbuf = '\\n'\npos = -1\n\ndef find_message_id(buf, LE, LFLF):\n    be = buf.find(LFLF)\n    mp = buf[:be].find('\\nMessage-I')\n    if mp >= 0 and buf[mp + 10:mp + 12].lower() == 'd:':\n        return buf[mp + 1:mp + buf[mp + 1:mp + 150].find(LE) + 1]\n\n    mp = buf[:be].lower().find('\\nmessage-id:')\n    if mp >= 0:\n        return buf[mp + 1:mp + buf[mp + 1:mp + 150].find(LE) + 1]\n\n    return None\n\n\nDELIM = '\\nFrom '\nREADS = 4 * 64 * 1024\nwith open(sys.argv[1], 'rb') as fd:\n    chunk = fd.read(READS)\n    LE, LFLF = ('\\r', '\\r\\n\\r\\n') if ('\\r\\n' in chunk) else ('\\n', '\\n\\n')\n    while len(chunk) > 0:\n        buf += chunk\n        if msgids and not msgids[-1]:\n            # Search for Message-ID again if it wasn't found on last pass\n            msgids[-1] = find_message_id(buf, LE, LFLF)\n\n        # Make a note of all messages in this buffer...\n        splits = buf.split(DELIM)\n        buf = splits.pop(0)\n        for split in splits:\n            pos += len(buf) + len(DELIM) \n            buf = split\n            messages.append(pos - len(DELIM) + 1)\n            msgids.append(find_message_id(buf, LE, LFLF))\n\n        pos += len(buf) - min(128, len(buf))\n        buf = buf[-128:]\n        assert(fd.tell() == pos + len(buf))\n        chunk = fd.read(READS)\n\n\nprint(('Done, found %d messages, %d msgids'\n       ) % (len(messages), len([1 for mi in msgids if mi])))\nfor i in range(0, 20):\n    print('%d/%d = %s' % (i * 13, messages[i], msgids[i * 13]))\n"
  },
  {
    "path": "scripts/minimize-pgp-key.py",
    "content": "#!/usr/bin/python\n#\n# Standalone script for minimizing PGP keys, using the same logic as\n# we use for Autocrypt.\n#\nfrom mailpile.crypto.autocrypt import get_minimal_PGP_key\nimport os\n\nif __name__ == \"__main__\":\n    default_file = os.path.dirname(os.path.abspath(__file__))\n    default_file = os.path.abspath(default_file + '/../tests/data/pub.key')\n\n    print\n    print 'Default key file:', default_file\n    print\n\n    key_file_path = raw_input('Enter key file path or <Enter> for default: ')\n    if key_file_path == '':\n        key_file_path = default_file\n\n    user_id = raw_input('Enter email address: ')\n    if user_id == '':\n        user_id = None\n\n    subkey_id = raw_input('Enter subkey_id: ')\n    if subkey_id == '':\n        subkey_id = None\n\n    with open(key_file_path, 'r') as keyfile:\n        keydata = bytearray( keyfile.read() )\n\n    print 'Key length:', len(keydata)\n\n    newkey, u, i = get_minimal_PGP_key(\n        keydata, user_id=user_id, subkey_id=subkey_id, binary_out=True)\n\n    print 'User ID:', u\n    print 'Subkey ID:', i\n    print 'Minimal key length:', len(newkey)\n    key_file_path += '.min.gpg'\n    print 'Minimal key output file:', key_file_path\n\n    with open(key_file_path, 'w') as keyfile:\n        keyfile.write(newkey)\n"
  },
  {
    "path": "scripts/mk-credits.py",
    "content": "#!/usr/bin/env python2.7\nfrom __future__ import print_function\nimport os\nimport re\nimport subprocess\n\nos.chdir(os.path.join(os.path.dirname(__file__), '..'))\n\nEMAIL_MAPPER = {\n    '<smari@immi.is>': ('Smari McCarthy', '<smari@mailpile.is>'),\n    '<smari@mailpile.is>': ('Smari McCarthy', '<smari@mailpile.is>'),\n    '<git@pagekite.net>': ('Bjarni R. Einarsson', '<bre@mailpile.is>'),\n    '<bre@klaki.net>': ('Bjarni R. Einarsson', '<bre@mailpile.is>'),\n    '<hi@brennannovak.com>': ('Brennan Novak', '<bnvk@mailpile.is>'),\n    '<hi@bnvk.me>': ('Brennan Novak', '<bnvk@mailpile.is>'),\n    '<alexandre@alexandreviau.net>': ('Alexandre Viau', '<alexandre@alexandreviau.net>'),\n}\n\nauthors = {}\ntranslators = {}\n\n# Get coders from git log\ngit_log = subprocess.Popen(['git', 'log'], stdout=subprocess.PIPE)\nfor line in git_log.stdout:\n    if line.startswith('Author: '):\n        author, email = line[8:].strip().rsplit(' ', 1)\n        author, email = EMAIL_MAPPER.get(email, (author, email))\n        if email.endswith('@mailpile.is>'):\n            continue\n        info = authors.get(author, [email, 0])\n        info[1] += 1\n        authors[author] = info\ngit_log.wait()\nauthors = [(c, n, e) for n, (e, c) in authors.iteritems()]\n\n# Get translators from .po files\nfor lang in os.listdir('shared-data/locale'):\n    po = 'shared-data/locale/%s/LC_MESSAGES/mailpile.po' % lang\n    tr = translators[lang] = ['', []]\n    try:\n        with open(po, 'r') as fd:\n            for line in fd:\n                if not line.strip():\n                    break    \n                elif line.startswith('#') and '@' in line:\n                    name = line[2:].strip().split(',')[0]\n                    if name not in tr[1]:\n                        tr[1].append(name)\n                elif line.startswith('\"Language-Team: '):\n                    tr[0] = line.split(': ')[1].split(' (')[0]   \n    except:\n        pass\n\ncode = 'shared-data/default-theme/html/page/release-notes/credits-code.html'\ni18n = 'shared-data/default-theme/html/page/release-notes/credits-i18n.html'\n\n# Our threshold for inclusion on the coders list is >= 1% of the total\n# commit count.\nthreshold = 0.01 * sum(c for c, n, e in authors)\nwith open(code, 'w') as fd:\n    authors.sort(key=lambda a: a[1])\n    fd.write('\\n'.join('<li class=\"commits-%s\">%s</li>' % (a[0], a[1])\n                       for a in authors if a[0] >= threshold))\n\nwith open(i18n, 'w') as fd:\n    email = re.compile(r'\\s+<[^>]+>')\n    first = True\n    langs = translators.keys()\n    langs.sort(key=lambda l: translators[l][0].lower())\n    for lang in langs:\n        language, tlist = translators[lang]\n        tlist.sort(key=lambda n: n.lower())\n        if language:\n            if not first:\n                fd.write('</ul>\\n')\n            fd.write('<li class=\"language\"><b>%s</b></li><ul>\\n' % language)\n            fd.write(''.join('  <li>%s</li>\\n' % re.sub(email, '', n)\n                             for n in tlist))\n            first = False\n        elif translators[lang][1]:\n            print('wtf: %s' % translators[lang])\n    if not first:\n        fd.write('</ul>\\n')\n\nos.system('ls -l %s %s' % (code, i18n))\n"
  },
  {
    "path": "scripts/nginx.conf",
    "content": "server {\n    listen       8888;\n    server_name  localhost;\n    location /mailpile {\n        rewrite /mailpile/(.*) /$1 break;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header Host $http_host;\n        proxy_set_header X-NginX-Proxy true;\n\n        proxy_pass http://127.0.0.1:33411;\n        proxy_redirect off;\n    }\n}\n\n"
  },
  {
    "path": "scripts/python-lint.sh",
    "content": "#!/bin/bash\ncommand -v pep8 >/dev/null 2>&1 || {\n    echo >&2 \"pep8 not found.\";\n    exit 1;\n}\npep8 --ignore W191 ./mailpile\nif [ $? -eq 0 ]; then\n   echo \"pep8 returned with success\"\n   exit 0\nelse\n   echo \"pep8 returned with failure\"\n   exit 1\nfi\n"
  },
  {
    "path": "scripts/reset-mac-mailpile.sh",
    "content": "#!/bin/bash\n\nCLEANUP=Mailpile.$(date +%Y%m%d-%H%M).$$\ncd \nmkdir -p $CLEANUP\nmv -v 'Library/Application Support/Mailpile' .gnupg $CLEANUP\necho \" \"\necho \"Moved Mailpile and GnuPG data to: $CLEANUP\"\necho \" \"\necho \"Feel free to delete that folder if you are sure nothing\"\necho \"important was lost!\"\n"
  },
  {
    "path": "scripts/setup-test.sh",
    "content": "#!/bin/bash\n\nexport MAILPILE_HOME=\"$(pwd)/setup-tmp\"\nif [ \"$1\" = \"--mygpg\" ]; then\n    shift\nelse\n    export GNUPGHOME=\"$MAILPILE_HOME\"\nfi\n\nif [ \"$VIRTUAL_ENV\" -a -e \"$VIRTUAL_ENV/bin/gpg2\" ]; then\n    GPG_BINARY=\"$VIRTUAL_ENV/bin/gpg2\"\nelse\n    if [ -d '/usr/local/Cellar/gnupg/1.4.16/bin' ]; then\n        export PATH=/usr/local/Cellar/gnupg/1.4.16/bin:$PATH\n    fi\n    GPG_BINARY=$(which gpg)\nfi\n\nif [ \"$1\" = \"--gpg\" ]; then\n    shift\n    exec $GPG_BINARY \"$@\"\nfi\nif [ \"$1\" = \"--dbg\" ]; then\n    shift\n    ulimit -c unlimited\n    PYTHON=python2.7-dbg\nelse\n    PYTHON=python2.7\nfi\n\nif [ \"$1\" = \"--keep\" ]; then\n    shift\nelse\n    rm -rf \"$MAILPILE_HOME\"\nfi\n\n#mkdir -p \"$MAILPILE_HOME\"\n#chmod 700 \"$MAILPILE_HOME\"\n\nif [ \"$1\" = \"--cleanup\" ]; then\n    shift\n    CLEANUP=1\nfi\n\n[ \"$1\" = \"\" ] && IA=\"--interact\" || IA=\"\"\n$PYTHON -OOR ./mp \\\n             --set 'sys.debug = log http' \\\n             --set \"sys.gpg_binary = $GPG_BINARY\" \\\n             --pidfile \"$MAILPILE_HOME/mailpile.pid\" \\\n             --www 'localhost:33433' \\\n             \"$@\" $IA\n\n[ \"xCLEANUP\" = \"1\" ] && rm -rf \"$MAILPILE_HOME\"\n"
  },
  {
    "path": "scripts/test-sendmail.sh",
    "content": "#!/bin/bash\nOUTPUT=\"$(dirname $0)/../mailpile/tests/data/tmp/sent.mbx\"\nmkdir -p \"$(dirname $OUTPUT)\"\ntouch \"$OUTPUT\"\nif [ -f \"$OUTPUT\" ]; then\n  echo \"From fake@localhost $(date)\" >> \"$OUTPUT\"\n  echo \"X-Args: $@\" >> \"$OUTPUT\"\n  cat >>\"$OUTPUT\"\n  echo >>\"$OUTPUT\"\n  sync\nelse\n  echo \"UGH: $OUTPUT\"\n  exit 1\nfi\n"
  },
  {
    "path": "scripts/unsent-mail-finder.py",
    "content": "#!/usr/bin/env python2.7\n\nfrom __future__ import print_function\nimport json\nimport sys\n\nprint((\"\"\"I am an unsent mail finder, due to buggy bug bugness.\nRun me like so:\n\n    cat /home/USER/.mailpile/logs/* | %s\n\n... and I might tell you which messages didn't get sent. Adjust the\npath above to match where your mailpile really is. Sorry this is so\nlame!  If you ran me wrong, press CTRL+C to abort right about now.\n\n\"\"\") % sys.argv[0])\n\nsendits = {}\nfor line in sys.stdin.readlines():\n  try:\n    data = json.loads(line)\n\n    d, eid, status, msg, cls = data[:5]\n    if cls == '.plugins.compose.Sendit':\n        if eid not in sendits and 'mid' in data[5]:\n            sendits[eid] = data\n        elif msg.startswith('Connecting'):\n            sendits[eid][5]['OK'] = True\n  except ValueError:\n    print('Unparsable: %s' % line)\n\nfor eid, data in sendits.iteritems():\n    if 'OK' not in data[5]:\n        print('On %s, failed to send %s' % (data[0], data[5]['mid']))\n"
  },
  {
    "path": "scripts/version.py",
    "content": "#!/usr/bin/env python2.7\nfrom __future__ import print_function\nimport datetime\nimport os\nimport re\nimport time\n\n\nROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), '..'))\ntry:\n    GIT_HEAD = open('%s/.git/HEAD' % ROOT).read().strip().split('/')[-1]\n    BRANCH = {\n       'master': 'dev',\n       'release': ''\n    }.get(GIT_HEAD, GIT_HEAD)\nexcept (OSError, IOError):\n    BRANCH = None\n\n\nnow = os.getenv('SOURCE_DATE_EPOCH', time.time())\ntoday = datetime.datetime.fromtimestamp(float(now))\n\nif BRANCH:\n    ts = str(today).replace('-', '').replace(' ', '').replace(':', '')\n    BRANCHVER = '~%s%s' % (BRANCH, ts[:12])\nelse:\n    BRANCHVER = ''\n\n\nAPPVER = '%s%s' % (next(\n    line.strip() for line in open('%s/mailpile/config/defaults.py' % ROOT, 'r')\n    if re.match(r'^APPVER\\s*=', line)\n).split('\"')[1], BRANCHVER)\n\n\n# Tweak the appver to make upgrades less of a concern.\nAPPVER = APPVER.replace('1.0.0rc', '0.99.')\n\n\nif __name__ == \"__main__\":\n    print('%s' % APPVER)\n"
  },
  {
    "path": "setup.cfg",
    "content": "[metadata]\nname = mailpile\nlicense = AGPLv3+\nauthor = Mailpile ehf.\nauthor_email = team@mailpile.is\nurl = https://www.mailpile.is/\nsummary = An e-mail search engine and webmail client, with easy encryption and privacy.\ndescription-file = README.md\nclassifier = \n    Development Status :: 4 - Beta\n    Programming Language :: Python :: 2.7\n    Programming Language :: JavaScript\n    License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)\n    Intended Audience :: End Users/Desktop\n    Topic :: Communications :: Email :: Email Clients (MUA)\n    Topic :: Security :: Cryptography\n    Operating System :: POSIX\n    Environment :: Console\n    Environment :: Web Environment\nkeywords =\n    email\n    webmail\n    search\n    pgp\n\n[files]\npackages = mailpile\ndata_files =\n    share/mailpile = shared-data/*\n\n[entry_points]\nconsole_scripts =\n    mailpile = mailpile.__main__:main\n\n[global]\nsetup-hooks =\n    install_hooks.symlink_develop\n"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python2.7\nfrom __future__ import print_function\nfrom datetime import date\nfrom setuptools import setup, find_packages\nfrom setuptools.command.build_py import build_py\nimport datetime\nimport os\nimport re\nimport subprocess\nfrom glob import glob\n\nfrom scripts.version import APPVER\n\nhere = os.path.abspath(os.path.dirname(__file__))\n\n########################################################\n##################### PBR Fix ##########################\n## Issue: https://bugs.launchpad.net/pbr/+bug/1530867 ##\n## PR: https://review.openstack.org/#/c/263297/ ########\n########################################################\n\nimport pbr.git\n\ndef _get_submodules(git_dir):\n    submodules = pbr.git._run_git_command(['submodule', 'status'], git_dir)\n    submodules = [s.strip().split(' ')[1]\n                  for s in submodules.split('\\n')\n                  if s != '']\n    return submodules\n\ndef _find_git_files(dirname='', git_dir=None):\n    \"\"\"Behave like a file finder entrypoint plugin.\n\n    We don't actually use the entrypoints system for this because it runs\n    at absurd times. We only want to do this when we are building an sdist.\n    \"\"\"\n    file_list = []\n    if git_dir is None:\n        git_dir = pbr.git._run_git_functions()\n    if git_dir:\n        file_list = pbr.git._run_git_command(['ls-files', '-z'], git_dir)\n        file_list += pbr.git._run_git_command(\n            ['submodule', 'foreach', '--quiet', 'ls-files', '-z'],\n            git_dir\n        )\n        # Users can fix utf8 issues locally with a single commit, so we are\n        # strict here.\n        file_list = file_list.split(b'\\x00'.decode('utf-8'))\n        submodules = _get_submodules(git_dir)\n    return [f for f in file_list if f and f not in submodules]\n\npbr.git._find_git_files = _find_git_files\n\n########## end of pbr fix ######\n\n\n## Cleanup ###################################################################\ntry:\n    assert(0 == subprocess.call(['make', 'clean'], cwd=here))\nexcept:\n    print(\"Faild to run 'make clean'. Bailing out.\")\n    exit(1)\n\n\n## Install ###################################################################\n\nclass Builder(build_py):\n    def run(self):\n        try:\n            assert(0 == subprocess.call(['make', 'bdist-prep'], cwd=here))\n        except:\n            print(\"Error building package. Try running 'make'.\")\n            exit(1)\n        else:\n            build_py.run(self)\n\n\n## \"Main\" ####################################################################\n\nsetup(\n    setup_requires=['pbr'],\n    version=APPVER,\n    pbr=True,\n    cmdclass={'build_py': Builder},\n)\n"
  },
  {
    "path": "shared-data/contrib/README.md",
    "content": "## Plugins\n\nThese are the default plugins that we ship with Mailpile.\n\nDevelopers: Check out the `demos` plugin and see our github wiki for\ndetails on how to write your own.\n\nPer-user plugins may go in `$MAILPILE_HOME/$MAILPILE_PROFILE/plugins`.\n\n"
  },
  {
    "path": "shared-data/contrib/autoajax/autoajax.js",
    "content": "/*\n * This code seamlessly upgrades any <a href=...> which would cause a search\n * to happen, to instead trigger an AJAX load.  This should make searches\n * feel almost instantaneous, while allowing us to keep all the template\n * logic in the back-end as Jinja2.\n *\n * FIXME:\n *   - Invoke any logic pertaining to display modes before updating\n *   - Use pushstate to update the browser history and location bar\n */\n\nvar get_now;\nvar clear_selection_state;\nvar can_refresh;\nvar update_using_jhtml;\nvar prepare_new_content;\nvar refresh_from_cache;\n\nvar refresh_history = {};\nvar U = Mailpile.API.U;\n\nvar refresh_timer;\nvar backup_refresh_interval = (30 * 1000) + (Math.random() * 2000);\nvar refresh_interval = backup_refresh_interval;\n\n\najaxable_url = function(url) {\n    return (url &&\n            ((url.indexOf(U(\"/in/\")) == 0) ||\n             (url.indexOf(U(\"/browse/\")) == 0) ||\n             (url.indexOf(U(\"/thread/\")) == 0) ||\n             (url.indexOf(U(\"/profiles/\")) == 0) ||\n             (url.indexOf(U(\"/message/compose/\")) == 0) ||\n             (url.indexOf(U(\"/settings/\")) == 0) ||\n             (url.indexOf(U(\"/crypto/tls/getcert/\")) == 0) ||\n             (url.indexOf(U(\"/logs/\")) == 0) ||\n             (url.indexOf(U(\"/page/\")) == 0) ||\n             (url.indexOf(U(\"/search/\")) == 0)\n             ) && url);\n};\n\n_outerHTML = function(elem) {\n    return $('<div />').append(elem.clone()).html();\n};\n\n_scroll_up = function(elem, scrollto) {\n    setTimeout(function() {\n      $(elem).find('div, table, tbody, p').scrollTop(0);\n      $('#content-view, #content-tall-view').eq(0).scrollTop(scrollto || 0);\n    }, 10);\n};\n\nget_now = function() {\n    if (Date.now) return Date.now();\n    return new Date().getTime()\n};\n\nclear_selection_state = function() {\n  Mailpile.UI.Selection.select_none();\n};\n\nget_selection_state = function() {\n  var selected = Mailpile.UI.Selection.selected('.pile-results');\n  var elements = {};\n  $.each(selected, function() {\n    if (this != '!all') {\n      elements[this] = $('.pile-results .pile-message-' + this).eq(0).clone();\n    }\n  });\n  return {\n    selected: selected,\n    elements: elements\n  };\n};\n\nrestore_selection_state = function(sstate) {\n  if (sstate.selected.length) {\n    $.each(sstate.selected.reverse(), function() {\n      if (this != '!all') {\n        if ($('.pile-results .pile-message-' + this).length < 1) {\n          var elem = sstate.elements[this];\n          if (elem && $(elem).find('.message-container').length < 1) {\n            $('.pile-results .pile-message').eq(0).parent().prepend(elem);\n          }\n        }\n      }\n      Mailpile.pile_action_select($('.pile-results .pile-message-' + this), 'partial');\n    });\n    Mailpile.bulk_actions_update_ui();\n  }\n};\n\ncan_refresh = function(cid) {\n    // Disable checks below (experimental)\n    return (Mailpile.ui_in_action < 1);\n\n    // By default we disable all refreshes of the UI if the user is busy\n    // selecting or rearranging or dragging... .\n    // FIXME: Hmm, seems other parts of the app should be able to block\n    //        updates somewhat granularly.\n    // FIXME: Should we just monitor for mouse/keyboard activity and only\n    //        update if user isn't doing anything in particular?\n    return ((Mailpile.ui_in_action < 1) &&\n            ($('.pile-results input[type=checkbox]:checked').length < 1));\n};\n\nautoajax_go = function(url, message, jhtml, noblank, noscroll, selrestore) {\n    url = Mailpile.fix_url(url);\n    if (jhtml === undefined) jhtml = ajaxable_url(url);\n\n    // Provide UI feedback if this takes time\n    var done = Mailpile.notify_working(message, (noblank) ? 250 : 1500, 'blank');\n\n    // Attempt to preserve selections cross-refresh.\n    var selected = get_selection_state();\n\n    // If noscroll is requested, try to preserve scroll position.\n    if (noscroll) {\n      scrollto = $('#content-view, #content-tall-view').eq(0).scrollTop();\n    }\n    else scrollto = 0;\n\n    // Called after the page is updated\n    var scroll_and_done = function(stuff) {\n      done();\n      if (selrestore) {\n        restore_selection_state(selected);\n      }\n      else {\n        clear_selection_state(selected);\n      }\n      return _scroll_up(stuff, scrollto);\n    }\n\n    // If we have any composers on the page, save contents\n    // before continuing - whether we're JHTMLing or not!\n    Mailpile.Composer.AutosaveAll(0, function() {\n        if (!(jhtml && update_using_jhtml(url, scroll_and_done, done, noblank))) {\n            document.location.href = url;\n        }\n    });\n};\n\nprepare_new_content = function(selector) {\n    $(selector).find('a').each(function(idx, elem) {\n        // FIXME: Should we add some majick to avoid dup click handlers?\n        var url = $(elem).attr('href');\n        var jhtml = ajaxable_url(url);\n        if (url &&\n                (url.indexOf('#') != 0) &&\n                (url.indexOf('mailto:') != 0) &&\n                (url.indexOf('javascript:') != 0) &&\n                (elem.target != '_blank') &&\n                (elem.className.indexOf('auto-modal') == -1)) {\n            $(elem).click(function(ev) {\n                // We don't hijack events that spawn new tabs/windows etc.\n                if (!(ev.ctrlKey || ev.altKey || ev.shiftKey)) {\n                    ev.preventDefault();\n                    var $elem = $(elem);\n                    autoajax_go(url, undefined, jhtml,\n                                $elem.data('noblank') ? true : false,\n                                $elem.data('noscroll') ? true : false,\n                                $elem.data('keep-selection') ? true : false);\n                }\n            });\n        }\n    });\n    var $form = $(selector).find('form#form-search');\n    $form.submit(function(ev) {\n        // We don't hijack events that spawn new tabs/windows etc.\n        if (!(ev.ctrlKey || ev.altKey || ev.shiftKey)) {\n            var selected = get_selection_state();\n            if (update_using_jhtml(U(\"/search/?\") + $form.serialize(),\n                                   function(stuff) {\n                // Always restore selection state on search, as searching\n                // is actually a moderately advanced behaviour.\n                restore_selection_state(selected);\n                return _scroll_up(stuff);\n            })) {\n                ev.preventDefault();\n            }\n        }\n    });\n};\nMailpile.UI.content_setup.push(prepare_new_content);\n\nrender_result = function(data, cv, html) {\n    var cv = cv || $('#content-view, #content-tall-view').parent();\n\n    if (data) Mailpile.update_title(data['message']);\n    cv.replaceWith(html || data['result']).show();\n\n    clear_selection_state();\n    cv = $('#content-view, #content-tall-view').parent();\n    Mailpile.UI.prepare_new_content(cv);\n    Mailpile.render();\n    // Work around bugs in drag/drop lib, nuke artefacts\n    $('div.ui-draggable-dragging').remove();\n\n    return cv;\n};\n\nrestore_state = function(ev) {\n    if (ev.state && ev.state.autoajax) {\n        update_using_jhtml(ev.state.url, function(cv) {\n                 // Success!\n            }, function(cv) {\n                 // Error?\n            }, false, true\n        );\n    }\n};\n\nupdate_using_jhtml = function(original_url, callback, error_callback,\n                              noblank, nohistory) {\n    if (ajaxable_url(document.location.pathname)) {\n        var cv = $('#content-view, #content-tall-view').parent();\n        if (!noblank) cv.css({\"opacity\": 0.25});\n        if (!nohistory)\n            history.replaceState({autoajax: true, url: document.location.href},\n                                 document.title);\n        return $.ajax({\n            url: Mailpile.API.jhtml_url(original_url, 'content'),\n            // These are user actions, timeouts barely make sense. Extend.\n            timeout: (Mailpile.ajax_timeout * 12),\n            type: 'GET',\n            success: function(data) {\n                if (!nohistory)\n                    history.pushState({autoajax: true, url: original_url},\n                                      data['message'], original_url);\n                shown = render_result(data, cv)\n                if (callback) { callback(shown) };\n            },\n            error: function() {\n                if (error_callback) error_callback(cv);\n                cv.css({\"opacity\": \"\"});\n            }\n        });\n    }\n    return false;\n};\n\nrefresh_from_cache = function(cid) {\n    var $inpage = $('.content-'+cid);\n    if ($inpage.length > 0 && ($inpage.closest('#modal-full').length < 1)) {\n        console.log('Updating from cache: ' + cid);\n        refresh_history[cid] = -1; // Avoid thrashing\n        Mailpile.API.cached_get({\n            id: cid,\n            _output: Mailpile.API.jhtml_url($inpage.data('template')\n                                            || 'as.html')\n        }, function(json) {\n             if (json.result) {\n                 var cid = json.state.cache_id;\n                 if (can_refresh(cid)) {\n                     var selected = get_selection_state();\n                     $('.content-'+cid).replaceWith(json.result.trim());\n                     Mailpile.UI.prepare_new_content('.content-'+cid);\n                     restore_selection_state(selected);\n                     refresh_history[cid] = get_now();\n                     // Work around bugs in drag/drop lib, nuke artefacts\n                     $('div.ui-draggable-dragging').remove();\n                 }\n                 else {\n                     // Result discarded, mark as needing a refresh!\n                     console.log('Result discarded, will try again for ' + cid);\n                     refresh_history[cid] = 0;\n                 }\n             }\n             else {\n                 console.log('Failed to load from cache!');\n             }\n        });\n        return true;\n    }\n    else if (refresh_history[cid]) {\n        delete refresh_history[cid];\n    }\n    return false;\n};\n\n$(document).ready(function() {\n    // Set up our onpopstate handler\n    window.onpopstate = restore_state;\n\n    Mailpile.go = autoajax_go;\n\n    // Figure out which elements on the page exist in cache, initialized\n    // our refresh_history timers to match...\n    $('.cached').each(function(i, elem) {\n        var classlist = elem.className.split(/\\s+/);\n        for (var i in classlist) {\n            var cn = classlist[i];\n            if (cn.indexOf('content-') == 0) {\n                var cid = cn.substring(8);\n                refresh_history[cid] = get_now();\n                console.log('refresh_history['+cid+'] = ' + refresh_history[cid]);\n            }\n        }\n    });\n\n    // Subscribe to the event-log, to freshen up UI elements as soon as\n    // possible.\n    EventLog.subscribe('.command_cache.CommandCache', function(ev) {\n        var now = get_now();\n        for (var cidx in ev.data.cache_ids) {\n            var cid = ev.data.cache_ids[cidx];\n            refresh_history[cid] = 0; // Mark as needing a refresh!\n            // If the event log is reporting things actively, lower the\n            // force-refresh interval as it's probably not needed.\n            if (refresh_interval < 120000) {\n                refresh_interval += 1000 * Math.random();\n            }\n        }\n    });\n\n    // As a backup, attempt to refresh all UI elements periodically\n    refresh_timer = $.timer(function() {\n        var now = get_now();\n        for (var cid in refresh_history) {\n            if ((refresh_history[cid] >= 0) &&\n                    (refresh_history[cid] < now - refresh_interval)) {\n                if (can_refresh(cid)) {\n                    refresh_from_cache(cid);\n                    return;\n                }\n            }\n        }\n        // If we get this far, nothing was refreshed, might need to\n        // force things.\n        if (refresh_interval > backup_refresh_interval) {\n            refresh_interval -= 1000 * Math.random();\n        }\n    });\n    refresh_timer.set({ time: 750, autostart: true });\n});\n\nreturn {\n    'timer': refresh_timer,\n    'history': refresh_history,\n    'update_using_jhtml': update_using_jhtml,\n    'refresh_from_cache': refresh_from_cache,\n    'prepare_new_content': prepare_new_content\n}\n"
  },
  {
    "path": "shared-data/contrib/autoajax/manifest.json",
    "content": "# This is an experimental plugin that uses Mailpile's JSON-wrapped-HTML\n# result rendering mode, to display search results without needing a full\n# page refresh.\n{\n    \"name\": \"autoajax\",\n    \"author\": \"The Mailpile Team <team@mailpile.is>\",\n\n    \"code\": {\n        \"javascript\": [\"autoajax.js\"]\n    }\n}\n"
  },
  {
    "path": "shared-data/contrib/datadig/datadig-modal.html",
    "content": "<div class=\"modal-dialog\">\n <form class=\"datadig-form\" action=\"{{ config.sys.http_path }}/datadig/as.csv\"\n       method=\"GET\" target=\"datadig-post-target\">\n  <div class=\"modal-content\">\n    <div class=\"modal-header\">\n      <button type=\"button\" class=\"close button-info\" data-dismiss=\"modal\"\n              aria-hidden=\"true\">&times;</button>\n      <h4 class=\"modal-title\">\n        <span class=\"icon-spreadsheet\"></span>\n        {{_(\"Extract CSV data from e-mail\")}}\n      </h4>\n    </div>\n    <div class=\"modal-body clearfix\">\n      <p class=\"datadig-help\">\n        {{_(\"This tool will extract data from the selected e-mails into a CSV file, which you can load into a spreadsheet application such as Microsoft Excel or LibreOffice Calc.\")}}\n      </p>\n      <p class=\"message paragraph-important\">\n        <span class=\"icon-settings\"></span> {{_(\"Choose what data to extract\")}}\n      </p>\n      <p class='datadig-columns text-center'\n         onclick=\"javascript:Mailpile.plugins.datadig.show_hints();\">\n        <b>{{_(\"Columns\")}}:</b> &nbsp; <span class=\"datadig-data-terms\">\n          {# The following div gets extracted and used as a sub-template! #}\n          <span class=\"datadig-data-term\">\n            <span><input type=\"text\" name=\"term\" placeholder=\"{{_(\"Data Source\")}}\" size=12 value=\"\"> &nbsp;</span>\n          </span>\n        </span>\n        <a onclick=\"javascript:Mailpile.plugins.datadig.add_column();\">\n          <span class=\"icon-plus\"></span> Add\n        </a>\n      </p>\n      <div class=\"datadig-hints\">\n        <table width=\"100%\">\n          <tr>\n            <th width=\"40%\">{{_(\"Data Source\")}}</th>\n            <th>{{_(\"Explanation\")}}</th>\n            <th width=\"1%\"></th>\n          </tr>\n          <tr class='datadig-hint'>\n            <td class='datadig-cspec'>Sender</td>\n            <td>{{_(\"The message sender\")}}</td>\n            <td><i>{{_(\"fast\")}}</i></td>\n          <tr class='datadig-hint'>\n            <td class='datadig-cspec'>Subject</td>\n            <td>{{_(\"The message subject\")}}</td>\n            <td><i>{{_(\"fast\")}}</i></td>\n          </tr>\n          <tr class='datadig-hint'>\n            <td class='datadig-cspec'>To</td>\n            <td>{{_(\"The message To header\")}}</td>\n            <td>&nbsp;</td>\n          </tr>\n          <tr class='datadig-hint'>\n            <td class='datadig-cspec'>Date</td>\n            <td>{{_(\"The message Date header\")}}</td>\n            <td>&nbsp;</td>\n          </tr>\n          </tr>\n          <tr class='datadig-hint'>\n            <td class='datadig-cspec'>URL=text:(https?:\\S+[\\w/])</td>\n            <td>{{_(\"Search for an URL\")}}</td>\n            <td><i>{{_(\"slow\")}}</i></td>\n          </tr>\n          <tr class='datadig-hint'>\n            <td class='datadig-cspec'>Money=text:\\$(\\d+)</td>\n            <td>{{_(\"Search for a dollar amount\")}}</td>\n            <td><i>{{_(\"slow\")}}</i></td>\n          </tr>\n        </table>\n        <i class='right'>{{_(\"Click an example to use it as a column\")}}</i>\n        <br>\n      </div>\n      <div class=\"datadig-preview-area hide\">\n        <p class=\"message paragraph-important\">\n          <span class=\"icon-search\"></span> {{_(\"Preview\")}}\n          <b onclick=\"javascript:Mailpile.plugins.datadig.show_hints();\"\n             class='right'>&times;</b>\n        </p>\n        <div class=\"datadig-working text-center hide\">\n          {% include(\"../img/loading-ellipsis.svg\") %}\n        </div>\n        <div class=\"datadig-downloading text-center hide\">\n          <i>{{_(\"Generating CSV file.\")}}\n             {{_(\"Please wait, this may take a while...\")}}</i><br>\n          <span id='datadig-downloading-progress'></span>\n        </div>\n        <div id=\"datadig-preview\" style=\"max-height: 200px; overflow: scroll;\"></div>\n      </div>\n    </div>\n    <div class=\"modal-footer\">\n      <ul class='left' style=\"display: inline-block; text-align: left;\">\n        <li><input type=\"checkbox\" name=\"header\" value=\"yes\" checked> Include header\n        <li><input type=\"checkbox\" name=\"no-mid\" value=\"yes\"> Omit Metadata-ID\n      </ul>\n      <div class=\"right\">\n        <button class=\"button-secondary right\"\n                onclick=\"javascript:Mailpile.plugins.datadig.download();\">\n          <span class=\"icon-download\"></span>\n          {{_(\"Download CSV\")}}\n        </button> &nbsp;\n      </div>\n      <div class=\"right\">\n        <button class=\"button-info\" type=\"button\"\n                onclick=\"javascript:Mailpile.plugins.datadig.preview();\">\n          <span class=\"icon-search\"></span>\n          {{_(\"Preview\")}}\n        </button>\n      </div>\n    </div>\n  </div>\n </form>\n <iframe class=\"hide\" name=\"datadig-post-target\"></iframe>\n</div>\n"
  },
  {
    "path": "shared-data/contrib/datadig/datadig.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block content %}\n{% if result %}\n  <table id=\"datadig-results\" class=\"{{ config.web.display_density }}\">\n    <tbody>\n    {% for row in result %}\n      <tr class=\"result\" data-mid=\"{{row[0]}}\">\n      {% for col in row %}\n        <td>{{col}}</td>\n      {% endfor %}\n      </tr>\n    {% endfor %}\n    </tbody>\n  </table>\n{% else %}\n  <div class=\"add-top add-bottom text-center\">\n    <h2 class=\"add-top center\">{{_(\"Hrm, We Could Not Find Anything\")}}</h2>\n  </div>\n{% endif %}\n{% endblock %}\n"
  },
  {
    "path": "shared-data/contrib/datadig/datadig.js",
    "content": "var disabled = false;\n\nfunction preview() {\n  if (disabled) return false;\n  $('#modal-full .datadig-hints').hide();\n  $('#modal-full #datadig-preview').hide();\n  $('#modal-full .datadig-working').show();\n  $('#modal-full .datadig-preview-area').show();\n  Mailpile.API.datadig_get({\n    _output: 'as.jhtml!minimal',\n    _serialized: $('#modal-full .datadig-form').serialize() + '&timeout=3'\n  }, function(data) {\n    $('#modal-full .datadig-working').hide();\n    $('#modal-full #datadig-preview').html(data['result']).fadeIn();\n  });\n  return false;\n}\n\nvar column_html = '';\nfunction add_column() {\n  if (disabled) return false;\n  $('#modal-full .datadig-data-terms').append($(column_html));\n  $('#modal-full .datadig-data-terms input').last().focus();\n  return false;\n}\n\nfunction set_column_from_hint(i, elem) {\n  if (disabled) return false;\n  var cspec = $(this).find('.datadig-cspec').html();\n  $('#modal-full .datadig-columns input').last().attr('value', cspec);\n}\n\nfunction show_hints() {\n  if (disabled) return false;\n  $('#modal-full .datadig-hints').show();\n  $('#modal-full .datadig-preview-area').hide();\n}\n\nfunction download() {\n  if (disabled) return false;\n  disabled = true;\n\n  // Add hidden form-field for tracking progress\n  var track_id = '@' + (new Date()).getTime();\n  var input = $('<input class=\"datadig-track-id\" type=\"hidden\" name=\"track-id\" value=\"' + track_id + '\">');\n  mf.find('.datadig-track-id').remove();\n  mf.find('.datadig-form').append(input);\n\n  // Add form-field for search context\n  var input = $('<input class=\"datadig-search-context\" type=\"hidden\" name=\"context\" value=\"' + $('#search-query').data('context') + '\">');\n  mf.find('.datadig-search-context').remove();\n  mf.find('.datadig-form').append(input);\n\n  // Change state of UI...\n  $('#modal-full .datadig-hints').hide();\n  $('#modal-full #datadig-preview').hide();\n  $('#modal-full .datadig-working').show();\n  $('#modal-full .datadig-downloading').show();\n  $('#modal-full .datadig-preview-area').show();\n  $('#modal-full .modal-footer').css('opacity', 0.5);\n\n  // This changes the state of the modal to show the progress of our\n  // data extraction, by temporarily subscribing to the EventLog.\n  var ev_source = '.*datadig.dataDigCommand';\n  var watch_id = EventLog.subscribe(ev_source, function(ev) {\n    if (ev.private_data['track-id'] == track_id) {\n      if (ev.flags == \"c\") {\n        // Completed! Revert UI back to normal, unsubscribe from events\n        disabled = false;\n        $('#modal-full .datadig-hints').show();\n        $('#modal-full .datadig-working').hide();\n        $('#modal-full .datadig-downloading').hide();\n        $('#modal-full .datadig-preview-area').hide();\n        $('#modal-full .modal-footer').css('opacity', 1.0);\n        EventLog.unsubscribe(ev_source, watch_id);\n      }\n      else {\n        // Otherwise, report progress!\n        $('#modal-full #datadig-downloading-progress').html(\n          ev.private_data.progress + ' / ' + ev.private_data.total\n        );\n      }\n    }\n  });\n}\n\n// Display the datadig widget!\n$(document).on('click', '.bulk-action-datadig', function() {\n  var $context = $(this);\n  Mailpile.API.with_template('datadig-modal', function(modal) {\n    mf = $('#modal-full').html(modal({\n      context: $('#search-query').data('context')\n    }));\n\n    // Extract our data-term template\n    column_html = mf.find('.datadig-data-term').html();\n\n    // Add hidden form-fields for all the message metadata-IDs\n    $.each(Mailpile.UI.Selection.selected($context), function(key, mid) {\n      var input = $('<input type=\"hidden\" name=\"mid\" value=\"' + mid + '\">');\n      mf.find('.datadig-form').append(input);\n    });\n\n    // Make the hints clickable\n    mf.find('.datadig-hints .datadig-hint').click(set_column_from_hint);\n\n    disabled = false;\n    mf.modal(Mailpile.UI.ModalOptions);\n    mf.find('.datadig-data-terms input').last().focus();\n  });\n});\n\n// Expose these methods as Mailpile.plugins.datadig.*\nreturn {\n  'preview': preview,\n  'show_hints': show_hints,\n  'add_column': add_column,\n  'download': download\n}\n"
  },
  {
    "path": "shared-data/contrib/datadig/datadig.py",
    "content": "import re\nimport time\n\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.commands import Command\nfrom mailpile.mailutils.emails import Email\nfrom mailpile.util import truthy\n\n# FIXME: Perhaps this plugin should be named ESQL and implement an SQL\n#        syntax for extracting data from e-mails...\n#\n#  SELECT from,to,body=Regards\\,(.*) FROM search=to:bre\n#\n\n\nclass dataDigCommand(Command):\n    \"\"\"Extract tables of structured data from e-mail content\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'datadig', 'datadig', '<terms ...> -- <messages ...>')\n    HTTP_CALLABLE = ('GET', )\n    HTTP_QUERY_VARS = {\n        'track-id': 'tracking ID for event log',\n        'timeout': 'runtime in seconds',\n        'header': 'include header',\n        'no-mid': 'omit metadata-ID column',\n        'term': 'extraction term',\n        'mid': 'metadata-ID'\n    }\n\n    class CommandResult(Command.CommandResult):\n        def as_text(self):\n            if self.result:\n                return '\\n'.join(['\\t'.join([unicode(cell) for cell in row])\n                                  for row in self.result])\n            else:\n                return _(\"Nothing Happened\")\n\n    def _filter(self, idx, e, celldata, filters):\n        if not isinstance(celldata, (list, tuple)):\n            celldata = [celldata]\n        for fltr in filters:\n            # FIXME!\n            celldata = celldata\n        return celldata\n\n    def _cell(self, idx, e, cellspec):\n        filters = cellspec.split('||')\n        cspec, lcspec = filters[0], filters[0].lower()\n        lcspec = lcspec.split(':', 1)[0].split('=', 1)[-1]\n\n        if lcspec == 'sender':\n            return self._filter(idx, e, e.get_msg_info(field=idx.MSG_FROM),\n                                filters)\n\n        if lcspec == 'subject':\n            return self._filter(idx, e, e.get_msg_info(field=idx.MSG_SUBJECT),\n                                filters)\n\n        if lcspec in ('from', 'to', 'cc', 'bcc', 'date'):\n            return self._filter(idx, e, e.get_msg()[lcspec], filters)\n\n        if lcspec in ('text', ):\n            rxp = cspec.split(':', 1)[1]\n            if lcspec == 'text':\n                body = e.get_editing_strings()['body']\n                mobj = re.search(rxp, body)\n            if mobj:\n                return self._filter(idx, e, list(mobj.groups()), filters)\n\n        return ['']\n\n    def command(self):\n        session, config, idx = self.session, self.session.config, self._idx()\n\n        # Command-line arguments...\n        msgs = list(self.args)\n        timeout = -1\n        tracking_id = None\n        with_header = False\n        without_mid = False\n        columns = []\n        while msgs and msgs[0].lower() != '--':\n            arg = msgs.pop(0)\n            if arg.startswith('--timeout='):\n                timeout = float(arg[10:])\n            elif arg.startswith('--header'):\n                with_header = True\n            elif arg.startswith('--no-mid'):\n                without_mid = True\n            else:\n                columns.append(arg)\n        if msgs and msgs[0].lower() == '--':\n            msgs.pop(0)\n\n        # Form arguments...\n        timeout = float(self.data.get('timeout', [timeout])[0])\n        with_header |= truthy(self.data.get('header', [''])[0])\n        without_mid |= truthy(self.data.get('no-mid', [''])[0])\n        tracking_id = self.data.get('track-id', [tracking_id])[0]\n        columns.extend(self.data.get('term', []))\n        msgs.extend(['=%s' % mid.replace('=', '')\n                     for mid in self.data.get('mid', [])])\n\n        # Add a header to the CSV if requested\n        if with_header:\n            results = [[col.split('||')[0].split(':', 1)[0].split('=', 1)[0]\n                        for col in columns]]\n            if not without_mid:\n                results[0] = ['MID'] + results[0]\n        else:\n            results = []\n\n        deadline = (time.time() + timeout) if (timeout > 0) else None\n        msg_idxs = self._choose_messages(msgs)\n        progress = []\n        for msg_idx in msg_idxs:\n            e = Email(idx, msg_idx)\n            if self.event and tracking_id:\n                progress.append(msg_idx)\n                self.event.private_data = {\"progress\": len(progress),\n                                           \"track-id\": tracking_id,\n                                           \"total\": len(msg_idxs),\n                                           \"reading\": e.msg_mid()}\n                self.event.message = _('Digging into =%s') % e.msg_mid()\n                self._update_event_state(self.event.RUNNING, log=True)\n            else:\n                session.ui.mark(_('Digging into =%s') % e.msg_mid())\n            row = [] if without_mid else ['%s' % e.msg_mid()]\n            for cellspec in columns:\n                row.extend(self._cell(idx, e, cellspec))\n            results.append(row)\n            if deadline and deadline < time.time():\n                break\n\n        return self._success(_('Found %d rows in %d messages'\n                               ) % (len(results), len(msg_idxs)), results)\n\n"
  },
  {
    "path": "shared-data/contrib/datadig/manifest.json",
    "content": "# This is a Mailpile plugin manifest, describing the `datadig` plugin.\n#\n{\n    \"name\": \"datadig\",\n    \"author\": \"The Mailpile Team <team@mailpile.is>\",\n    \"description\": \"Poor-man's data-mining! This plugin adds a selection action for extracting data from e-mail. The extracted data can be downloaded as CSV and imported into standard spreadsheet applications.\",\n    \"display\": true,\n    \"public\": true,\n\n    \"config\": {},\n    \"code\": {\n        \"python\": [\"datadig.py\"],\n        \"javascript\": [\"datadig.js\"]\n    },\n\n    # This section defines URL routes and MIME types.\n    \"routes\": {\n        \"/datadig/\": {\"file\": \"datadig.html\"},\n        \"/jsapi/templates/datadig-modal.html\": {\"file\": \"datadig-modal.html\"}\n    },\n\n    \"user_interface\": {\n        # Actions that can be performed on selected items\n        \"selection_actions\": [\n            {\n                \"context\": [\"/search/\"],\n                \"name\": \"datadig\",\n                \"text\": \"CSV Data\",\n                \"description\": \"Extract tables of data from selected messages\",\n                \"icon\": \"spreadsheet\"\n            }\n        ]\n    },\n\n    # These are our Python-related hooks\n    \"commands\": [\n        {\n            # Native API commands (controllers)\n            \"class\": \"dataDigCommand\",\n            \"url\": \"datadig\",\n            \"name\": \"datadig\"\n        }\n    ]\n}\n"
  },
  {
    "path": "shared-data/contrib/demos/demos.css",
    "content": "/* This could be some CSS */\n"
  },
  {
    "path": "shared-data/contrib/demos/demos.js",
    "content": "/* This is the demo plugin's javascript code!\n   The name of the returned class will be `mailpile.plugins.demos`.\n */\nreturn {\n    new_tool_click: function() {\n        alert('Are you ready to see a whole new world?');\n        return false;\n    },\n    new_tool_setup: function(element) {\n        $(element).click(this.new_tool_click);\n    },\n    new_tool: function() {\n        $('#sidebar').fadeOut();\n        $('#content').fadeOut();\n        $('#header').after('<div id=\"extreme-mashup\" style=\"position: relative; top: 200px\" class=\"text-center\"><h1>ALL YOUR MAILBOX ARE BELONG TO ME</h1><p>This could be an extreme plugin that renders a completely new feature or tool or mashup</p><p><button id=\"no-extreme\">Go Back</button></p></div>');\n        $('#no-extreme').on('click', Mailpile.plugins.demos.new_tool_hide);\n        return false;\n    },\n    new_tool_hide: function() {\n        $('#extreme-mashup').hide();\n        $('#sidebar').fadeIn();\n        $('#content').fadeIn();\n        return false;\n    },\n    /* These methods are exposed to the app for various things. */\n    earthquake_click: function() {\n        alert('Can you feel the rumble?');\n        return false;\n    },\n    earthquake_setup: function(element) {\n        /* Setup code for our activity launcher goes here. This function\n           gets run after the DOM has created the element, so we can\n           'enhance' it. Here we just give it a click handler, but fancier\n           plugins could set up event listeners and update the element\n           itself based on other app activities. */\n        $(element).click(Mailpile.plugins.demos.earthquake_click);\n    },\n    earthquake: function(element) {\n      $(\".boxy\").animate({\"margin-left\": -5}, 40)\n                .animate({\"margin-left\": 5}, 40)\n                .animate({\"margin-left\": -5}, 40)\n                .animate({\"margin-left\": 5}, 40)\n                .animate({\"margin-left\": -5}, 40)\n                .animate({\"margin-left\": 5}, 40)\n                .animate({\"margin-left\": -5}, 40)\n                .animate({\"margin-left\": 5}, 40)\n                .animate({\"margin-left\": -5}, 40)\n                .animate({\"margin-left\": 5}, 40)\n                .animate({\"margin-left\": -5}, 40)\n                .animate({\"margin-left\": 5}, 40);\n    },\n    tag_list: function() {\n        list_html = '';\n        for (var i=0; i < 11; i++) {\n          list_html += 'Demo Tag List ' + i + '<hr>';\n        }\n        $('#tags-list').html(list_html);\n        $('#tags-archived-list').hide();\n        return false;\n    }\n};\n"
  },
  {
    "path": "shared-data/contrib/demos/demos.py",
    "content": "# This is a collection of very short demo-plugins to illustrate how\n# to create and register hooks into the various parts of Mailpile\n#\n# To start creating a new plugin, it may make sense to copy this file,\n# globally search/replace the word \"Demo\" with your preferred plugin\n# name and then go delete sections you aren't going to use.\n#\n# Happy hacking!\n\nfrom gettext import gettext as _\nfrom mailpile.plugins import PluginManager\n\n\n##[ Pluggable configuration ]#################################################\n\n# FIXME\n\n\n##[ Pluggable keyword extractors ]############################################\n\n# FIXME\n\n\n##[ Pluggable search terms ]##################################################\n\n# Pluggable search terms allow plugins to enhance the behavior of the\n# search engine in various ways. Examples of basic enhanced search terms\n# are the date: and size: keywords, which accept human-friendly ranges\n# and input, and convert those to a list of \"low level\" keywords to\n# actually search for.\n\n# FIXME\n\n\n##[ Pluggable vcard functions ]###############################################\nfrom mailpile.vcard import *\n\n\nclass DemoVCardImporter(VCardImporter):\n    \"\"\"\n    This VCard importer simply generates VCards based on data in the\n    configuration. This is not particularly useful, but it demonstrates\n    how each importer can define (and use) its own settings.\n    \"\"\"\n    FORMAT_NAME = _('Demo Contacts')\n    FORMAT_DESCRPTION = _('This is the demo importer')\n    SHORT_NAME = 'demo'\n    CONFIG_RULES = {\n        'active': [_('Activate demo importer'), bool, True],\n        'name': [_('Contact name'), str, 'Mr. Rogers'],\n        'email': [_('Contact email'), 'email', 'mr@rogers.com']\n    }\n\n    def get_vcards(self):\n        \"\"\"Returns just a single contact, based on data from the config.\"\"\"\n        # Notes to implementors:\n        #\n        #  - It is important to only return one card per (set of)\n        #    e-mail addresses, as internal overwriting may cause\n        #    unexpected results.\n        #  - If data is to be deleted from the contact list, it\n        #    is important to return a VCard for that e-mail address\n        #    which has the relevant data removed.\n        #\n        if not self.config.active:\n            return []\n        return [MailpileVCard(\n            VCardLine(name='fn', value=self.config.name),\n            VCardLine(name='email', value=self.config.email)\n        )]\n\n\n##[ Pluggable cron jobs ]#####################################################\n\ndef TickJob(session):\n    \"\"\"\n    This is a very minimal cron job - just a function that runs within\n    a session.\n\n    Note that generally it is a better pattern to create a Command which\n    is then invoked by the cron job, so power users can access the\n    functionality directly.  It is also a good idea to make the interval\n    configurable by registering a setting and referencing that instead of\n    a fixed number.  See compose.py for an example of how this is done.\n    \"\"\"\n    session.ui.notify('Tick!')\n\n\n##[ Pluggable commands and data views ]#######################################\n\nfrom mailpile.commands import Command\nfrom mailpile.util import md5_hex\n\n\nclass md5sumCommand(Command):\n    \"\"\"This command calculates MD5 sums\"\"\"\n    SYNOPSIS_ARGS = '[<data to hash>]'\n    SPLIT_ARG = False\n    HTTP_CALLABLE = ('GET', 'POST')\n    HTTP_QUERY_VARS = {\n       'data': 'Data to hash'\n    }\n\n    def command(self):\n        if 'data' in self.data:\n            data = self.data['data'][0]\n        else:\n            data = ''.join(self.args)\n\n        for gross in self.session.config.sys.md5sum_blacklist.split():\n            if gross in data or not data:\n                return self._error(_('I refuse to work with empty '\n                                     'or gross data'),\n                                   info={'data': data})\n\n        return self._success(_('I hashed your data for you, yay!'),\n                             result=md5_hex(data))\n\n\nclass md5sumWordyView(md5sumCommand):\n    \"\"\"Represent MD5 sums in a more wordy way.\"\"\"\n    @classmethod\n    def view(cls, result):\n        return 'MD5:%s' % result\n\n\ndef on_plugin_start(config):\n    \"\"\"Called once after plugin is loaded.\n\n    You can initialize external or expensive dependencies here.\n\n    Args:\n        config: The Mailpile configuration dictionary\n\n    Returns:\n        void\n    \"\"\"\n    # initialize some external dependencies\n    pass\n\n\ndef on_plugin_shutdown(config):\n    \"\"\"Called before plugin is stopped.\n\n    Shutdown external dependencies here, especially if they created some threads.\n\n    Args:\n        config: The Mailpile configuration dictionary\n\n    Returns:\n        void\n    \"\"\"\n    # properly shutdown external dependencies\n    pass\n\n\nclass DemoMailbox(object):\n    \"\"\"A dysfunctional demo mailbox. See mailpile.mailboxes.* for proper mailbox examples\"\"\"\n    @classmethod\n    def parse_path(cls, config, fn, create=False):\n        raise ValueError('This is only a demo mailbox class!')\n"
  },
  {
    "path": "shared-data/contrib/demos/manifest.json",
    "content": "# This is a Mailpile plugin manifest, describing the `demos` plugin.\n#\n# This file describes the resources associated with this plugin and how\n# they are meant to interact with the rest of the Mailpile application.\n#\n# NOTE: The Mailpile plugin manifest format is JSON, with the optional\n#       extension that comments are allowed.  Comments must be on lines of\n#       their own and begin with a '#' character optionally preceded by\n#       some whitespace.  No other comment styles are supported.\n#\n{\n    \"name\": \"demos\",\n    \"author\": \"The Mailpile Team <team@mailpile.is>\",\n\n    \"code\": {\n        # Python modules end up in the namespace mailpile.plugins.<plugin>.\n        # Generally, the file stuff.py becomes mailpile.plugins.demos.stuff,\n        # with the exception that demos.py goes straight to the root.  If\n        # a directory is given (including \".\" for the plugin root dir), an\n        # __init__.py file is loaded per normal Pythonic conventions.\n        \"python\": [\"demos.py\"],\n\n        # ...\n        \"javascript\": [\"demos.js\"],\n\n        # ...\n        \"css\": [\"demos.css\"]\n    },\n\n    # This section defines URL routes and MIME types.\n    \"routes\": {\n        # Routes associating templates with API methods.\n        #\n        # Note: To communicate which API version is being developed against,\n        #       please either use full /api/N/ URL paths, or provide an \"api\"\n        #       attribute (both styles are illustrated below). If no view\n        #       type (filename part) is specified in the URL, HTML is assumed.\n        #\n        \"/md5sum/\": {\"file\": \"md5sum.html\", \"api\": 0},\n        \"/md5sum/wordy.html\": {\"file\": \"md5sum-wordy.html\", \"api\": 0},\n        \"/api/0/md5sum/as.xml\": {\"file\": \"md5sum.xml\"},\n        \"/search/demos.html\": {\"file\": \"search.html\", \"api\": 0},\n\n        # Static content can be injected anywhere under the /static/\n        # namespace. Optionally, non-obvious mime-types can be specified.\n        \"/static/img/demos.png\": {\"file\": \"demos.png\"},\n        \"/static/data/demos.txt\": {\"file\": \"manifest.json\",\n                                   \"mimetype\": \"text/plain\"}\n    },\n\n    # Please see https://github.com/pagekite/Mailpile/wiki/Config for\n    # details about the configuration file syntax.\n    \"config\": {\n        \"sections\": {},\n        \"variables\": {\n            \"sys\": {\n                \"md5sum_blacklist\": [\"Words hated by the md5sum command\",\n                                     \"str\", \"gross\"]\n            }\n        }\n    },\n\n    # This section tells the current Mailpile theme where and how to make\n    # this plugin visible in the user interface.\n    # Most of these will only be applicable in one (or rarely more) contexts,\n    # which is communicated by listing API endpoints as versioned URLs.\n    \"user_interface\": {\n        # These are stand-alone activities that exist within a context they \n        # are coupled to a higher level command. These examples utilize the \n        # javascript_setup attribute which is a way to init whatever your\n        # plugins JS code does before / or when it is triggered\n        \"activities\": [\n            {\n                \"context\": [\"/\"],\n                \"name\": \"demo\",\n                \"text\": \"(demo)\",\n                \"icon\": \"/static/img/demos.png\",\n                \"description\": \"New Tool (demo)\",\n                \"url\": \"#could-be-a-fallback\",\n                \"javascript_setup\": \"new_tool_setup\",\n                \"javascript_events\": {\n                    \"click\": \"new_tool\"\n                }\n            },\n            {\n                \"context\": [\"/tag/list/\"],\n                \"name\": \"tag-list-demo-activity\",\n                \"text\": \"Earthquake (demo)\",\n                \"description\": \"This button makes your tags shake\",\n                \"url\": \"#useless\",\n                \"icon\": \"/static/img/demos.png\",\n                \"javascript_setup\": \"earthquake_setup\",\n                \"javascript_events\": {\n                    \"mouseover\": \"earthquake\"\n                }\n            }\n        ],\n        # These are different ways to render data of a command\n        \"display_modes\": [\n            {\n                \"context\": [\"/md5sum/\"],\n                \"name\": \"md5sum\",\n                \"text\": \"Default\",\n                \"description\": \"Default MD5 sum view\",\n                \"url\": \"/md5sum/\"\n            },\n            {\n                \"context\": [\"/md5sum/\"],\n                \"name\": \"md5sum/wordy\",\n                \"text\": \"Wordy\",\n                \"description\": \"Wordy MD5 sum view\",\n                \"url\": \"/md5sum/wordy.html\"\n            },\n            # Here is a mode triggered & rendered by JS\n            {\n                \"context\": [\"/tag/list/\"],\n                \"name\": \"tag-list-demo-mode\",\n                \"text\": \"Tag List (demo)\",\n                \"description\": \"This is a useless button\",\n                \"url\": \"#taglist\",\n                \"icon\": \"/static/img/demos.png\",\n                \"javascript_events\": {\n                    \"click\": \"tag_list\"\n                }\n            }\n        ],\n        # These are ways to refine a certain view (e.g. a search)\n        \"display_refiners\": [\n            {\n                \"context\": [\"/search/\"],\n                \"name\": \"demo_search\",\n                \"text\": \"Demo\",\n                \"description\": \"Demo search!!!\",\n                \"url_args_remove\": [[\"qr\", \"\"]],\n                \"url_args_add\": [[\"qr\", \"demo\"]],\n                \"icon\": \"/static/img/demos.png\"\n            }\n        ],\n        # Actions that can be performed on selected items\n        \"selection_actions\": []\n    },\n\n    # These are our Python-related hooks\n    \"commands\": [\n        {\n            # Native API commands (controllers)\n            \"class\": \"md5sumCommand\",\n            \"url\": \"md5sum\",\n            \"name\": \"md5sum\"\n        },\n        {\n            # Data views provide alternate data representations\n            \"class\": \"md5sumWordyView\",\n            \"input\": \"md5sum\",\n            \"name\": \"wordy\"\n        }\n    ],\n    # Register custom mailbox types\n    \"mailboxes\": [\n        {\n            \"class\": \"DemoMailbox\",\n            \"priority\": \"100\"\n        }\n    ],\n    # Transforms mutate messages; same format as the mailbox section\n    \"email_transforms\": {\n        \"outgoing_content\": [],\n        \"outgoing_crypto\": [],\n        \"incoming_crypto\": [],\n        \"incoming_content\": []\n    },\n    \"contacts\": {\n        \"importers\": [\"DemoVCardImporter\"],\n        \"exporters\": [],\n        \"context\": []\n    },\n    \"periodic_jobs\": {\n        \"fast\": [{\"interval\": 5, \"class\": \"TickJob\"}],\n        \"slow\": [{\"interval\": 15, \"class\": \"TickJob\"}]\n    },\n    \"keyword_extractors\": {\n        \"meta\": [],\n        \"text\": [],\n        \"data\": []\n    },\n    \"search_terms\": [\n    ],\n    \"filters\": {\n        \"pre\": [],\n        \"post\": []\n    },\n    # List method names to be called after plugin is loaded or before its unloaded\n    \"lifecycle\": {\n        \"startup\": [\"on_plugin_start\"],\n        \"shutdown\": [\"on_plugin_shutdown\"]\n    }\n}\n"
  },
  {
    "path": "shared-data/contrib/demos/md5sum-wordy.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block content %}\n{% set result = use_data_view('/api/0/md5sum/wordy/', result) %}\n\nResult: {{ result }}\n\n{% endblock %}\n"
  },
  {
    "path": "shared-data/contrib/demos/md5sum.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block content %}\n\nResult: {{ result }}\n\n{% endblock %}\n"
  },
  {
    "path": "shared-data/contrib/demos/md5sum.xml",
    "content": "<md5sum>\n  <status>{{ status }}</status>\n  <message>{{ message }}</message>\n  <result>{{ result }}</result>\n</md5sum>\n"
  },
  {
    "path": "shared-data/contrib/demos/search.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block content %}\n\nThis is an alternate search view. It is VERY, VERY boring.\n\n{% endblock %}\n"
  },
  {
    "path": "shared-data/contrib/experiments/experiments.py",
    "content": "import copy\nimport re\n\nfrom mailpile.util import *\n\n\n##[ Keyword experiments ]#####################################################\n\nRE_QUOTES = re.compile(r'^(>\\s*)+')\nRE_CLEANPARA = re.compile(r'[>\"\\*\\'\\s]')\n\ndef paragraph_id_extractor(index, msg, ctype, textpart, **kwargs):\n    \"\"\"Create search index terms to identify paragraphs.\"\"\"\n    kws = set([])\n    try:\n        if not ctype == 'text/plain':\n            return kws\n        if not index.config.prefs.get('experiment_para_kws'):\n            return kws\n\n        para = {'text': '', 'qlevel': 0}\n        def end_para():\n            txt = para.get('text', '')\n            if (len(txt) > 60 and\n                    not ('unsubscribe' in txt and 'http' in txt) and\n                    not ('@lists' in txt or '/mailman/' in txt) and\n                    not (txt.endswith(':'))):\n                txt = re.sub(RE_CLEANPARA, '', txt)[-120:]\n#               print 'PARA: %s' % txt\n                kws.add('%s:p' % md5_hex(txt))\n            para.update({'text': '', 'qlevel': 0})\n\n        for line in textpart.splitlines():\n            if line in ('-- ', '- -- ', '- --'):\n                return kws\n\n            # Find the quote markers...\n            markers = re.match(RE_QUOTES, line)\n            ql = len((markers.group(0) if markers else '').strip())\n\n            # Paragraphs end when...\n            if ((ql == 0 and line.endswith(':')) or  # new quote starts\n                    (ql != para['qlevel']) or        # quote level changes\n                    (ql == len(line)) or             # blank lines\n                    (line[:2] == '--')):             # on -- dividers\n                end_para()\n\n            para['qlevel'] = ql\n            if not line[:2] in ('--', ):\n                para['text'] += line\n        end_para()\n    except: # AttributeError:\n        import traceback\n        traceback.print_exc()\n        pass\n    return kws\n"
  },
  {
    "path": "shared-data/contrib/experiments/manifest.json",
    "content": "# This is a Mailpile plugin manifest, describing the `hacks` plugin.\n{\n    \"name\": \"experiments\",\n    \"author\": \"The Mailpile Team <team@mailpile.is>\",\n    \"code\": {\n        \"python\": [\"experiments.py\"],\n        \"javascript\": [],\n        \"css\": []\n    },\n\n    # This section defines URL routes and MIME types.\n    \"routes\": {},\n\n    # Please see https://github.com/pagekite/Mailpile/wiki/Config for\n    # details about the configuration file syntax.\n    \"config\": {\n        \"variables\": {\n            \"prefs\": {\n                \"experiment_dkg_hdrs\": [\"This experiment is obsolete\",\n                                        \"str\", \"\"],\n                \"experiment_para_kws\": [\"Make paragraphs searchable by hash\",\n                                        \"bool\", \"False\"]\n            }\n        }\n    },\n\n    \"keyword_extractors\": {\n        \"text\": [\"paragraph_id_extractor\"]\n    },\n\n    # These are our Python-related hooks\n    \"commands\": [\n    ]\n}\n"
  },
  {
    "path": "shared-data/contrib/forcegrapher/README.md",
    "content": "Use The Force, Grapher\n======================\n\nVisualizes your Inbox or any Mailpile search result as a helpful force-directed graph\n"
  },
  {
    "path": "shared-data/contrib/forcegrapher/d3.drasl.js",
    "content": "!function() {\n  var d3 = {\n    version: \"3.4.6\"\n  };\n  if (!Date.now) Date.now = function() {\n    return +new Date();\n  };\n  var d3_arraySlice = [].slice, d3_array = function(list) {\n    return d3_arraySlice.call(list);\n  };\n  var d3_document = document, d3_documentElement = d3_document.documentElement, d3_window = window;\n  try {\n    d3_array(d3_documentElement.childNodes)[0].nodeType;\n  } catch (e) {\n    d3_array = function(list) {\n      var i = list.length, array = new Array(i);\n      while (i--) array[i] = list[i];\n      return array;\n    };\n  }\n  try {\n    d3_document.createElement(\"div\").style.setProperty(\"opacity\", 0, \"\");\n  } catch (error) {\n    var d3_element_prototype = d3_window.Element.prototype, d3_element_setAttribute = d3_element_prototype.setAttribute, d3_element_setAttributeNS = d3_element_prototype.setAttributeNS, d3_style_prototype = d3_window.CSSStyleDeclaration.prototype, d3_style_setProperty = d3_style_prototype.setProperty;\n    d3_element_prototype.setAttribute = function(name, value) {\n      d3_element_setAttribute.call(this, name, value + \"\");\n    };\n    d3_element_prototype.setAttributeNS = function(space, local, value) {\n      d3_element_setAttributeNS.call(this, space, local, value + \"\");\n    };\n    d3_style_prototype.setProperty = function(name, value, priority) {\n      d3_style_setProperty.call(this, name, value + \"\", priority);\n    };\n  }\n  d3.ascending = d3_ascending;\n  function d3_ascending(a, b) {\n    return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;\n  }\n  d3.descending = function(a, b) {\n    return b < a ? -1 : b > a ? 1 : b >= a ? 0 : NaN;\n  };\n  d3.min = function(array, f) {\n    var i = -1, n = array.length, a, b;\n    if (arguments.length === 1) {\n      while (++i < n && !((a = array[i]) != null && a <= a)) a = undefined;\n      while (++i < n) if ((b = array[i]) != null && a > b) a = b;\n    } else {\n      while (++i < n && !((a = f.call(array, array[i], i)) != null && a <= a)) a = undefined;\n      while (++i < n) if ((b = f.call(array, array[i], i)) != null && a > b) a = b;\n    }\n    return a;\n  };\n  d3.max = function(array, f) {\n    var i = -1, n = array.length, a, b;\n    if (arguments.length === 1) {\n      while (++i < n && !((a = array[i]) != null && a <= a)) a = undefined;\n      while (++i < n) if ((b = array[i]) != null && b > a) a = b;\n    } else {\n      while (++i < n && !((a = f.call(array, array[i], i)) != null && a <= a)) a = undefined;\n      while (++i < n) if ((b = f.call(array, array[i], i)) != null && b > a) a = b;\n    }\n    return a;\n  };\n  d3.extent = function(array, f) {\n    var i = -1, n = array.length, a, b, c;\n    if (arguments.length === 1) {\n      while (++i < n && !((a = c = array[i]) != null && a <= a)) a = c = undefined;\n      while (++i < n) if ((b = array[i]) != null) {\n        if (a > b) a = b;\n        if (c < b) c = b;\n      }\n    } else {\n      while (++i < n && !((a = c = f.call(array, array[i], i)) != null && a <= a)) a = undefined;\n      while (++i < n) if ((b = f.call(array, array[i], i)) != null) {\n        if (a > b) a = b;\n        if (c < b) c = b;\n      }\n    }\n    return [ a, c ];\n  };\n  d3.sum = function(array, f) {\n    var s = 0, n = array.length, a, i = -1;\n    if (arguments.length === 1) {\n      while (++i < n) if (!isNaN(a = +array[i])) s += a;\n    } else {\n      while (++i < n) if (!isNaN(a = +f.call(array, array[i], i))) s += a;\n    }\n    return s;\n  };\n  function d3_number(x) {\n    return x != null && !isNaN(x);\n  }\n  d3.mean = function(array, f) {\n    var s = 0, n = array.length, a, i = -1, j = n;\n    if (arguments.length === 1) {\n      while (++i < n) if (d3_number(a = array[i])) s += a; else --j;\n    } else {\n      while (++i < n) if (d3_number(a = f.call(array, array[i], i))) s += a; else --j;\n    }\n    return j ? s / j : undefined;\n  };\n  d3.quantile = function(values, p) {\n    var H = (values.length - 1) * p + 1, h = Math.floor(H), v = +values[h - 1], e = H - h;\n    return e ? v + e * (values[h] - v) : v;\n  };\n  d3.median = function(array, f) {\n    if (arguments.length > 1) array = array.map(f);\n    array = array.filter(d3_number);\n    return array.length ? d3.quantile(array.sort(d3_ascending), .5) : undefined;\n  };\n  function d3_bisector(compare) {\n    return {\n      left: function(a, x, lo, hi) {\n        if (arguments.length < 3) lo = 0;\n        if (arguments.length < 4) hi = a.length;\n        while (lo < hi) {\n          var mid = lo + hi >>> 1;\n          if (compare(a[mid], x) < 0) lo = mid + 1; else hi = mid;\n        }\n        return lo;\n      },\n      right: function(a, x, lo, hi) {\n        if (arguments.length < 3) lo = 0;\n        if (arguments.length < 4) hi = a.length;\n        while (lo < hi) {\n          var mid = lo + hi >>> 1;\n          if (compare(a[mid], x) > 0) hi = mid; else lo = mid + 1;\n        }\n        return lo;\n      }\n    };\n  }\n  var d3_bisect = d3_bisector(d3_ascending);\n  d3.bisectLeft = d3_bisect.left;\n  d3.bisect = d3.bisectRight = d3_bisect.right;\n  d3.bisector = function(f) {\n    return d3_bisector(f.length === 1 ? function(d, x) {\n      return d3_ascending(f(d), x);\n    } : f);\n  };\n  d3.shuffle = function(array) {\n    var m = array.length, t, i;\n    while (m) {\n      i = Math.random() * m-- | 0;\n      t = array[m], array[m] = array[i], array[i] = t;\n    }\n    return array;\n  };\n  d3.permute = function(array, indexes) {\n    var i = indexes.length, permutes = new Array(i);\n    while (i--) permutes[i] = array[indexes[i]];\n    return permutes;\n  };\n  d3.pairs = function(array) {\n    var i = 0, n = array.length - 1, p0, p1 = array[0], pairs = new Array(n < 0 ? 0 : n);\n    while (i < n) pairs[i] = [ p0 = p1, p1 = array[++i] ];\n    return pairs;\n  };\n  d3.zip = function() {\n    if (!(n = arguments.length)) return [];\n    for (var i = -1, m = d3.min(arguments, d3_zipLength), zips = new Array(m); ++i < m; ) {\n      for (var j = -1, n, zip = zips[i] = new Array(n); ++j < n; ) {\n        zip[j] = arguments[j][i];\n      }\n    }\n    return zips;\n  };\n  function d3_zipLength(d) {\n    return d.length;\n  }\n  d3.transpose = function(matrix) {\n    return d3.zip.apply(d3, matrix);\n  };\n  d3.keys = function(map) {\n    var keys = [];\n    for (var key in map) keys.push(key);\n    return keys;\n  };\n  d3.values = function(map) {\n    var values = [];\n    for (var key in map) values.push(map[key]);\n    return values;\n  };\n  d3.entries = function(map) {\n    var entries = [];\n    for (var key in map) entries.push({\n      key: key,\n      value: map[key]\n    });\n    return entries;\n  };\n  d3.merge = function(arrays) {\n    var n = arrays.length, m, i = -1, j = 0, merged, array;\n    while (++i < n) j += arrays[i].length;\n    merged = new Array(j);\n    while (--n >= 0) {\n      array = arrays[n];\n      m = array.length;\n      while (--m >= 0) {\n        merged[--j] = array[m];\n      }\n    }\n    return merged;\n  };\n  var abs = Math.abs;\n  d3.range = function(start, stop, step) {\n    if (arguments.length < 3) {\n      step = 1;\n      if (arguments.length < 2) {\n        stop = start;\n        start = 0;\n      }\n    }\n    if ((stop - start) / step === Infinity) throw new Error(\"infinite range\");\n    var range = [], k = d3_range_integerScale(abs(step)), i = -1, j;\n    start *= k, stop *= k, step *= k;\n    if (step < 0) while ((j = start + step * ++i) > stop) range.push(j / k); else while ((j = start + step * ++i) < stop) range.push(j / k);\n    return range;\n  };\n  function d3_range_integerScale(x) {\n    var k = 1;\n    while (x * k % 1) k *= 10;\n    return k;\n  }\n  function d3_class(ctor, properties) {\n    try {\n      for (var key in properties) {\n        Object.defineProperty(ctor.prototype, key, {\n          value: properties[key],\n          enumerable: false\n        });\n      }\n    } catch (e) {\n      ctor.prototype = properties;\n    }\n  }\n  d3.map = function(object) {\n    var map = new d3_Map();\n    if (object instanceof d3_Map) object.forEach(function(key, value) {\n      map.set(key, value);\n    }); else for (var key in object) map.set(key, object[key]);\n    return map;\n  };\n  function d3_Map() {}\n  d3_class(d3_Map, {\n    has: d3_map_has,\n    get: function(key) {\n      return this[d3_map_prefix + key];\n    },\n    set: function(key, value) {\n      return this[d3_map_prefix + key] = value;\n    },\n    remove: d3_map_remove,\n    keys: d3_map_keys,\n    values: function() {\n      var values = [];\n      this.forEach(function(key, value) {\n        values.push(value);\n      });\n      return values;\n    },\n    entries: function() {\n      var entries = [];\n      this.forEach(function(key, value) {\n        entries.push({\n          key: key,\n          value: value\n        });\n      });\n      return entries;\n    },\n    size: d3_map_size,\n    empty: d3_map_empty,\n    forEach: function(f) {\n      for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) f.call(this, key.substring(1), this[key]);\n    }\n  });\n  var d3_map_prefix = \"\\x00\", d3_map_prefixCode = d3_map_prefix.charCodeAt(0);\n  function d3_map_has(key) {\n    return d3_map_prefix + key in this;\n  }\n  function d3_map_remove(key) {\n    key = d3_map_prefix + key;\n    return key in this && delete this[key];\n  }\n  function d3_map_keys() {\n    var keys = [];\n    this.forEach(function(key) {\n      keys.push(key);\n    });\n    return keys;\n  }\n  function d3_map_size() {\n    var size = 0;\n    for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) ++size;\n    return size;\n  }\n  function d3_map_empty() {\n    for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) return false;\n    return true;\n  }\n  d3.nest = function() {\n    var nest = {}, keys = [], sortKeys = [], sortValues, rollup;\n    function map(mapType, array, depth) {\n      if (depth >= keys.length) return rollup ? rollup.call(nest, array) : sortValues ? array.sort(sortValues) : array;\n      var i = -1, n = array.length, key = keys[depth++], keyValue, object, setter, valuesByKey = new d3_Map(), values;\n      while (++i < n) {\n        if (values = valuesByKey.get(keyValue = key(object = array[i]))) {\n          values.push(object);\n        } else {\n          valuesByKey.set(keyValue, [ object ]);\n        }\n      }\n      if (mapType) {\n        object = mapType();\n        setter = function(keyValue, values) {\n          object.set(keyValue, map(mapType, values, depth));\n        };\n      } else {\n        object = {};\n        setter = function(keyValue, values) {\n          object[keyValue] = map(mapType, values, depth);\n        };\n      }\n      valuesByKey.forEach(setter);\n      return object;\n    }\n    function entries(map, depth) {\n      if (depth >= keys.length) return map;\n      var array = [], sortKey = sortKeys[depth++];\n      map.forEach(function(key, keyMap) {\n        array.push({\n          key: key,\n          values: entries(keyMap, depth)\n        });\n      });\n      return sortKey ? array.sort(function(a, b) {\n        return sortKey(a.key, b.key);\n      }) : array;\n    }\n    nest.map = function(array, mapType) {\n      return map(mapType, array, 0);\n    };\n    nest.entries = function(array) {\n      return entries(map(d3.map, array, 0), 0);\n    };\n    nest.key = function(d) {\n      keys.push(d);\n      return nest;\n    };\n    nest.sortKeys = function(order) {\n      sortKeys[keys.length - 1] = order;\n      return nest;\n    };\n    nest.sortValues = function(order) {\n      sortValues = order;\n      return nest;\n    };\n    nest.rollup = function(f) {\n      rollup = f;\n      return nest;\n    };\n    return nest;\n  };\n  d3.set = function(array) {\n    var set = new d3_Set();\n    if (array) for (var i = 0, n = array.length; i < n; ++i) set.add(array[i]);\n    return set;\n  };\n  function d3_Set() {}\n  d3_class(d3_Set, {\n    has: d3_map_has,\n    add: function(value) {\n      this[d3_map_prefix + value] = true;\n      return value;\n    },\n    remove: function(value) {\n      value = d3_map_prefix + value;\n      return value in this && delete this[value];\n    },\n    values: d3_map_keys,\n    size: d3_map_size,\n    empty: d3_map_empty,\n    forEach: function(f) {\n      for (var value in this) if (value.charCodeAt(0) === d3_map_prefixCode) f.call(this, value.substring(1));\n    }\n  });\n  d3.behavior = {};\n  d3.rebind = function(target, source) {\n    var i = 1, n = arguments.length, method;\n    while (++i < n) target[method = arguments[i]] = d3_rebind(target, source, source[method]);\n    return target;\n  };\n  function d3_rebind(target, source, method) {\n    return function() {\n      var value = method.apply(source, arguments);\n      return value === source ? target : value;\n    };\n  }\n  function d3_vendorSymbol(object, name) {\n    if (name in object) return name;\n    name = name.charAt(0).toUpperCase() + name.substring(1);\n    for (var i = 0, n = d3_vendorPrefixes.length; i < n; ++i) {\n      var prefixName = d3_vendorPrefixes[i] + name;\n      if (prefixName in object) return prefixName;\n    }\n  }\n  var d3_vendorPrefixes = [ \"webkit\", \"ms\", \"moz\", \"Moz\", \"o\", \"O\" ];\n  function d3_noop() {}\n  d3.dispatch = function() {\n    var dispatch = new d3_dispatch(), i = -1, n = arguments.length;\n    while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch);\n    return dispatch;\n  };\n  function d3_dispatch() {}\n  d3_dispatch.prototype.on = function(type, listener) {\n    var i = type.indexOf(\".\"), name = \"\";\n    if (i >= 0) {\n      name = type.substring(i + 1);\n      type = type.substring(0, i);\n    }\n    if (type) return arguments.length < 2 ? this[type].on(name) : this[type].on(name, listener);\n    if (arguments.length === 2) {\n      if (listener == null) for (type in this) {\n        if (this.hasOwnProperty(type)) this[type].on(name, null);\n      }\n      return this;\n    }\n  };\n  function d3_dispatch_event(dispatch) {\n    var listeners = [], listenerByName = new d3_Map();\n    function event() {\n      var z = listeners, i = -1, n = z.length, l;\n      while (++i < n) if (l = z[i].on) l.apply(this, arguments);\n      return dispatch;\n    }\n    event.on = function(name, listener) {\n      var l = listenerByName.get(name), i;\n      if (arguments.length < 2) return l && l.on;\n      if (l) {\n        l.on = null;\n        listeners = listeners.slice(0, i = listeners.indexOf(l)).concat(listeners.slice(i + 1));\n        listenerByName.remove(name);\n      }\n      if (listener) listeners.push(listenerByName.set(name, {\n        on: listener\n      }));\n      return dispatch;\n    };\n    return event;\n  }\n  d3.event = null;\n  function d3_eventPreventDefault() {\n    d3.event.preventDefault();\n  }\n  function d3_eventSource() {\n    var e = d3.event, s;\n    while (s = e.sourceEvent) e = s;\n    return e;\n  }\n  function d3_eventDispatch(target) {\n    var dispatch = new d3_dispatch(), i = 0, n = arguments.length;\n    while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch);\n    dispatch.of = function(thiz, argumentz) {\n      return function(e1) {\n        try {\n          var e0 = e1.sourceEvent = d3.event;\n          e1.target = target;\n          d3.event = e1;\n          dispatch[e1.type].apply(thiz, argumentz);\n        } finally {\n          d3.event = e0;\n        }\n      };\n    };\n    return dispatch;\n  }\n  d3.requote = function(s) {\n    return s.replace(d3_requote_re, \"\\\\$&\");\n  };\n  var d3_requote_re = /[\\\\\\^\\$\\*\\+\\?\\|\\[\\]\\(\\)\\.\\{\\}]/g;\n  var d3_subclass = {}.__proto__ ? function(object, prototype) {\n    object.__proto__ = prototype;\n  } : function(object, prototype) {\n    for (var property in prototype) object[property] = prototype[property];\n  };\n  function d3_selection(groups) {\n    d3_subclass(groups, d3_selectionPrototype);\n    return groups;\n  }\n  var d3_select = function(s, n) {\n    return n.querySelector(s);\n  }, d3_selectAll = function(s, n) {\n    return n.querySelectorAll(s);\n  }, d3_selectMatcher = d3_documentElement[d3_vendorSymbol(d3_documentElement, \"matchesSelector\")], d3_selectMatches = function(n, s) {\n    return d3_selectMatcher.call(n, s);\n  };\n  if (typeof Sizzle === \"function\") {\n    d3_select = function(s, n) {\n      return Sizzle(s, n)[0] || null;\n    };\n    d3_selectAll = Sizzle;\n    d3_selectMatches = Sizzle.matchesSelector;\n  }\n  d3.selection = function() {\n    return d3_selectionRoot;\n  };\n  var d3_selectionPrototype = d3.selection.prototype = [];\n  d3_selectionPrototype.select = function(selector) {\n    var subgroups = [], subgroup, subnode, group, node;\n    selector = d3_selection_selector(selector);\n    for (var j = -1, m = this.length; ++j < m; ) {\n      subgroups.push(subgroup = []);\n      subgroup.parentNode = (group = this[j]).parentNode;\n      for (var i = -1, n = group.length; ++i < n; ) {\n        if (node = group[i]) {\n          subgroup.push(subnode = selector.call(node, node.__data__, i, j));\n          if (subnode && \"__data__\" in node) subnode.__data__ = node.__data__;\n        } else {\n          subgroup.push(null);\n        }\n      }\n    }\n    return d3_selection(subgroups);\n  };\n  function d3_selection_selector(selector) {\n    return typeof selector === \"function\" ? selector : function() {\n      return d3_select(selector, this);\n    };\n  }\n  d3_selectionPrototype.selectAll = function(selector) {\n    var subgroups = [], subgroup, node;\n    selector = d3_selection_selectorAll(selector);\n    for (var j = -1, m = this.length; ++j < m; ) {\n      for (var group = this[j], i = -1, n = group.length; ++i < n; ) {\n        if (node = group[i]) {\n          subgroups.push(subgroup = d3_array(selector.call(node, node.__data__, i, j)));\n          subgroup.parentNode = node;\n        }\n      }\n    }\n    return d3_selection(subgroups);\n  };\n  function d3_selection_selectorAll(selector) {\n    return typeof selector === \"function\" ? selector : function() {\n      return d3_selectAll(selector, this);\n    };\n  }\n  var d3_nsPrefix = {\n    svg: \"http://www.w3.org/2000/svg\",\n    xhtml: \"http://www.w3.org/1999/xhtml\",\n    xlink: \"http://www.w3.org/1999/xlink\",\n    xml: \"http://www.w3.org/XML/1998/namespace\",\n    xmlns: \"http://www.w3.org/2000/xmlns/\"\n  };\n  d3.ns = {\n    prefix: d3_nsPrefix,\n    qualify: function(name) {\n      var i = name.indexOf(\":\"), prefix = name;\n      if (i >= 0) {\n        prefix = name.substring(0, i);\n        name = name.substring(i + 1);\n      }\n      return d3_nsPrefix.hasOwnProperty(prefix) ? {\n        space: d3_nsPrefix[prefix],\n        local: name\n      } : name;\n    }\n  };\n  d3_selectionPrototype.attr = function(name, value) {\n    if (arguments.length < 2) {\n      if (typeof name === \"string\") {\n        var node = this.node();\n        name = d3.ns.qualify(name);\n        return name.local ? node.getAttributeNS(name.space, name.local) : node.getAttribute(name);\n      }\n      for (value in name) this.each(d3_selection_attr(value, name[value]));\n      return this;\n    }\n    return this.each(d3_selection_attr(name, value));\n  };\n  function d3_selection_attr(name, value) {\n    name = d3.ns.qualify(name);\n    function attrNull() {\n      this.removeAttribute(name);\n    }\n    function attrNullNS() {\n      this.removeAttributeNS(name.space, name.local);\n    }\n    function attrConstant() {\n      this.setAttribute(name, value);\n    }\n    function attrConstantNS() {\n      this.setAttributeNS(name.space, name.local, value);\n    }\n    function attrFunction() {\n      var x = value.apply(this, arguments);\n      if (x == null) this.removeAttribute(name); else this.setAttribute(name, x);\n    }\n    function attrFunctionNS() {\n      var x = value.apply(this, arguments);\n      if (x == null) this.removeAttributeNS(name.space, name.local); else this.setAttributeNS(name.space, name.local, x);\n    }\n    return value == null ? name.local ? attrNullNS : attrNull : typeof value === \"function\" ? name.local ? attrFunctionNS : attrFunction : name.local ? attrConstantNS : attrConstant;\n  }\n  function d3_collapse(s) {\n    return s.trim().replace(/\\s+/g, \" \");\n  }\n  d3_selectionPrototype.classed = function(name, value) {\n    if (arguments.length < 2) {\n      if (typeof name === \"string\") {\n        var node = this.node(), n = (name = d3_selection_classes(name)).length, i = -1;\n        if (value = node.classList) {\n          while (++i < n) if (!value.contains(name[i])) return false;\n        } else {\n          value = node.getAttribute(\"class\");\n          while (++i < n) if (!d3_selection_classedRe(name[i]).test(value)) return false;\n        }\n        return true;\n      }\n      for (value in name) this.each(d3_selection_classed(value, name[value]));\n      return this;\n    }\n    return this.each(d3_selection_classed(name, value));\n  };\n  function d3_selection_classedRe(name) {\n    return new RegExp(\"(?:^|\\\\s+)\" + d3.requote(name) + \"(?:\\\\s+|$)\", \"g\");\n  }\n  function d3_selection_classes(name) {\n    return name.trim().split(/^|\\s+/);\n  }\n  function d3_selection_classed(name, value) {\n    name = d3_selection_classes(name).map(d3_selection_classedName);\n    var n = name.length;\n    function classedConstant() {\n      var i = -1;\n      while (++i < n) name[i](this, value);\n    }\n    function classedFunction() {\n      var i = -1, x = value.apply(this, arguments);\n      while (++i < n) name[i](this, x);\n    }\n    return typeof value === \"function\" ? classedFunction : classedConstant;\n  }\n  function d3_selection_classedName(name) {\n    var re = d3_selection_classedRe(name);\n    return function(node, value) {\n      if (c = node.classList) return value ? c.add(name) : c.remove(name);\n      var c = node.getAttribute(\"class\") || \"\";\n      if (value) {\n        re.lastIndex = 0;\n        if (!re.test(c)) node.setAttribute(\"class\", d3_collapse(c + \" \" + name));\n      } else {\n        node.setAttribute(\"class\", d3_collapse(c.replace(re, \" \")));\n      }\n    };\n  }\n  d3_selectionPrototype.style = function(name, value, priority) {\n    var n = arguments.length;\n    if (n < 3) {\n      if (typeof name !== \"string\") {\n        if (n < 2) value = \"\";\n        for (priority in name) this.each(d3_selection_style(priority, name[priority], value));\n        return this;\n      }\n      if (n < 2) return d3_window.getComputedStyle(this.node(), null).getPropertyValue(name);\n      priority = \"\";\n    }\n    return this.each(d3_selection_style(name, value, priority));\n  };\n  function d3_selection_style(name, value, priority) {\n    function styleNull() {\n      this.style.removeProperty(name);\n    }\n    function styleConstant() {\n      this.style.setProperty(name, value, priority);\n    }\n    function styleFunction() {\n      var x = value.apply(this, arguments);\n      if (x == null) this.style.removeProperty(name); else this.style.setProperty(name, x, priority);\n    }\n    return value == null ? styleNull : typeof value === \"function\" ? styleFunction : styleConstant;\n  }\n  d3_selectionPrototype.property = function(name, value) {\n    if (arguments.length < 2) {\n      if (typeof name === \"string\") return this.node()[name];\n      for (value in name) this.each(d3_selection_property(value, name[value]));\n      return this;\n    }\n    return this.each(d3_selection_property(name, value));\n  };\n  function d3_selection_property(name, value) {\n    function propertyNull() {\n      delete this[name];\n    }\n    function propertyConstant() {\n      this[name] = value;\n    }\n    function propertyFunction() {\n      var x = value.apply(this, arguments);\n      if (x == null) delete this[name]; else this[name] = x;\n    }\n    return value == null ? propertyNull : typeof value === \"function\" ? propertyFunction : propertyConstant;\n  }\n  d3_selectionPrototype.text = function(value) {\n    return arguments.length ? this.each(typeof value === \"function\" ? function() {\n      var v = value.apply(this, arguments);\n      this.textContent = v == null ? \"\" : v;\n    } : value == null ? function() {\n      this.textContent = \"\";\n    } : function() {\n      this.textContent = value;\n    }) : this.node().textContent;\n  };\n  d3_selectionPrototype.html = function(value) {\n    return arguments.length ? this.each(typeof value === \"function\" ? function() {\n      var v = value.apply(this, arguments);\n      this.innerHTML = v == null ? \"\" : v;\n    } : value == null ? function() {\n      this.innerHTML = \"\";\n    } : function() {\n      this.innerHTML = value;\n    }) : this.node().innerHTML;\n  };\n  d3_selectionPrototype.append = function(name) {\n    name = d3_selection_creator(name);\n    return this.select(function() {\n      return this.appendChild(name.apply(this, arguments));\n    });\n  };\n  function d3_selection_creator(name) {\n    return typeof name === \"function\" ? name : (name = d3.ns.qualify(name)).local ? function() {\n      return this.ownerDocument.createElementNS(name.space, name.local);\n    } : function() {\n      return this.ownerDocument.createElementNS(this.namespaceURI, name);\n    };\n  }\n  d3_selectionPrototype.insert = function(name, before) {\n    name = d3_selection_creator(name);\n    before = d3_selection_selector(before);\n    return this.select(function() {\n      return this.insertBefore(name.apply(this, arguments), before.apply(this, arguments) || null);\n    });\n  };\n  d3_selectionPrototype.remove = function() {\n    return this.each(function() {\n      var parent = this.parentNode;\n      if (parent) parent.removeChild(this);\n    });\n  };\n  d3_selectionPrototype.data = function(value, key) {\n    var i = -1, n = this.length, group, node;\n    if (!arguments.length) {\n      value = new Array(n = (group = this[0]).length);\n      while (++i < n) {\n        if (node = group[i]) {\n          value[i] = node.__data__;\n        }\n      }\n      return value;\n    }\n    function bind(group, groupData) {\n      var i, n = group.length, m = groupData.length, n0 = Math.min(n, m), updateNodes = new Array(m), enterNodes = new Array(m), exitNodes = new Array(n), node, nodeData;\n      if (key) {\n        var nodeByKeyValue = new d3_Map(), dataByKeyValue = new d3_Map(), keyValues = [], keyValue;\n        for (i = -1; ++i < n; ) {\n          keyValue = key.call(node = group[i], node.__data__, i);\n          if (nodeByKeyValue.has(keyValue)) {\n            exitNodes[i] = node;\n          } else {\n            nodeByKeyValue.set(keyValue, node);\n          }\n          keyValues.push(keyValue);\n        }\n        for (i = -1; ++i < m; ) {\n          keyValue = key.call(groupData, nodeData = groupData[i], i);\n          if (node = nodeByKeyValue.get(keyValue)) {\n            updateNodes[i] = node;\n            node.__data__ = nodeData;\n          } else if (!dataByKeyValue.has(keyValue)) {\n            enterNodes[i] = d3_selection_dataNode(nodeData);\n          }\n          dataByKeyValue.set(keyValue, nodeData);\n          nodeByKeyValue.remove(keyValue);\n        }\n        for (i = -1; ++i < n; ) {\n          if (nodeByKeyValue.has(keyValues[i])) {\n            exitNodes[i] = group[i];\n          }\n        }\n      } else {\n        for (i = -1; ++i < n0; ) {\n          node = group[i];\n          nodeData = groupData[i];\n          if (node) {\n            node.__data__ = nodeData;\n            updateNodes[i] = node;\n          } else {\n            enterNodes[i] = d3_selection_dataNode(nodeData);\n          }\n        }\n        for (;i < m; ++i) {\n          enterNodes[i] = d3_selection_dataNode(groupData[i]);\n        }\n        for (;i < n; ++i) {\n          exitNodes[i] = group[i];\n        }\n      }\n      enterNodes.update = updateNodes;\n      enterNodes.parentNode = updateNodes.parentNode = exitNodes.parentNode = group.parentNode;\n      enter.push(enterNodes);\n      update.push(updateNodes);\n      exit.push(exitNodes);\n    }\n    var enter = d3_selection_enter([]), update = d3_selection([]), exit = d3_selection([]);\n    if (typeof value === \"function\") {\n      while (++i < n) {\n        bind(group = this[i], value.call(group, group.parentNode.__data__, i));\n      }\n    } else {\n      while (++i < n) {\n        bind(group = this[i], value);\n      }\n    }\n    update.enter = function() {\n      return enter;\n    };\n    update.exit = function() {\n      return exit;\n    };\n    return update;\n  };\n  function d3_selection_dataNode(data) {\n    return {\n      __data__: data\n    };\n  }\n  d3_selectionPrototype.datum = function(value) {\n    return arguments.length ? this.property(\"__data__\", value) : this.property(\"__data__\");\n  };\n  d3_selectionPrototype.filter = function(filter) {\n    var subgroups = [], subgroup, group, node;\n    if (typeof filter !== \"function\") filter = d3_selection_filter(filter);\n    for (var j = 0, m = this.length; j < m; j++) {\n      subgroups.push(subgroup = []);\n      subgroup.parentNode = (group = this[j]).parentNode;\n      for (var i = 0, n = group.length; i < n; i++) {\n        if ((node = group[i]) && filter.call(node, node.__data__, i, j)) {\n          subgroup.push(node);\n        }\n      }\n    }\n    return d3_selection(subgroups);\n  };\n  function d3_selection_filter(selector) {\n    return function() {\n      return d3_selectMatches(this, selector);\n    };\n  }\n  d3_selectionPrototype.order = function() {\n    for (var j = -1, m = this.length; ++j < m; ) {\n      for (var group = this[j], i = group.length - 1, next = group[i], node; --i >= 0; ) {\n        if (node = group[i]) {\n          if (next && next !== node.nextSibling) next.parentNode.insertBefore(node, next);\n          next = node;\n        }\n      }\n    }\n    return this;\n  };\n  d3_selectionPrototype.sort = function(comparator) {\n    comparator = d3_selection_sortComparator.apply(this, arguments);\n    for (var j = -1, m = this.length; ++j < m; ) this[j].sort(comparator);\n    return this.order();\n  };\n  function d3_selection_sortComparator(comparator) {\n    if (!arguments.length) comparator = d3_ascending;\n    return function(a, b) {\n      return a && b ? comparator(a.__data__, b.__data__) : !a - !b;\n    };\n  }\n  d3_selectionPrototype.each = function(callback) {\n    return d3_selection_each(this, function(node, i, j) {\n      callback.call(node, node.__data__, i, j);\n    });\n  };\n  function d3_selection_each(groups, callback) {\n    for (var j = 0, m = groups.length; j < m; j++) {\n      for (var group = groups[j], i = 0, n = group.length, node; i < n; i++) {\n        if (node = group[i]) callback(node, i, j);\n      }\n    }\n    return groups;\n  }\n  d3_selectionPrototype.call = function(callback) {\n    var args = d3_array(arguments);\n    callback.apply(args[0] = this, args);\n    return this;\n  };\n  d3_selectionPrototype.empty = function() {\n    return !this.node();\n  };\n  d3_selectionPrototype.node = function() {\n    for (var j = 0, m = this.length; j < m; j++) {\n      for (var group = this[j], i = 0, n = group.length; i < n; i++) {\n        var node = group[i];\n        if (node) return node;\n      }\n    }\n    return null;\n  };\n  d3_selectionPrototype.size = function() {\n    var n = 0;\n    this.each(function() {\n      ++n;\n    });\n    return n;\n  };\n  function d3_selection_enter(selection) {\n    d3_subclass(selection, d3_selection_enterPrototype);\n    return selection;\n  }\n  var d3_selection_enterPrototype = [];\n  d3.selection.enter = d3_selection_enter;\n  d3.selection.enter.prototype = d3_selection_enterPrototype;\n  d3_selection_enterPrototype.append = d3_selectionPrototype.append;\n  d3_selection_enterPrototype.empty = d3_selectionPrototype.empty;\n  d3_selection_enterPrototype.node = d3_selectionPrototype.node;\n  d3_selection_enterPrototype.call = d3_selectionPrototype.call;\n  d3_selection_enterPrototype.size = d3_selectionPrototype.size;\n  d3_selection_enterPrototype.select = function(selector) {\n    var subgroups = [], subgroup, subnode, upgroup, group, node;\n    for (var j = -1, m = this.length; ++j < m; ) {\n      upgroup = (group = this[j]).update;\n      subgroups.push(subgroup = []);\n      subgroup.parentNode = group.parentNode;\n      for (var i = -1, n = group.length; ++i < n; ) {\n        if (node = group[i]) {\n          subgroup.push(upgroup[i] = subnode = selector.call(group.parentNode, node.__data__, i, j));\n          subnode.__data__ = node.__data__;\n        } else {\n          subgroup.push(null);\n        }\n      }\n    }\n    return d3_selection(subgroups);\n  };\n  d3_selection_enterPrototype.insert = function(name, before) {\n    if (arguments.length < 2) before = d3_selection_enterInsertBefore(this);\n    return d3_selectionPrototype.insert.call(this, name, before);\n  };\n  function d3_selection_enterInsertBefore(enter) {\n    var i0, j0;\n    return function(d, i, j) {\n      var group = enter[j].update, n = group.length, node;\n      if (j != j0) j0 = j, i0 = 0;\n      if (i >= i0) i0 = i + 1;\n      while (!(node = group[i0]) && ++i0 < n) ;\n      return node;\n    };\n  }\n  d3_selectionPrototype.transition = function() {\n    var id = d3_transitionInheritId || ++d3_transitionId, subgroups = [], subgroup, node, transition = d3_transitionInherit || {\n      time: Date.now(),\n      ease: d3_ease_cubicInOut,\n      delay: 0,\n      duration: 250\n    };\n    for (var j = -1, m = this.length; ++j < m; ) {\n      subgroups.push(subgroup = []);\n      for (var group = this[j], i = -1, n = group.length; ++i < n; ) {\n        if (node = group[i]) d3_transitionNode(node, i, id, transition);\n        subgroup.push(node);\n      }\n    }\n    return d3_transition(subgroups, id);\n  };\n  d3_selectionPrototype.interrupt = function() {\n    return this.each(d3_selection_interrupt);\n  };\n  function d3_selection_interrupt() {\n    var lock = this.__transition__;\n    if (lock) ++lock.active;\n  }\n  d3.select = function(node) {\n    var group = [ typeof node === \"string\" ? d3_select(node, d3_document) : node ];\n    group.parentNode = d3_documentElement;\n    return d3_selection([ group ]);\n  };\n  d3.selectAll = function(nodes) {\n    var group = d3_array(typeof nodes === \"string\" ? d3_selectAll(nodes, d3_document) : nodes);\n    group.parentNode = d3_documentElement;\n    return d3_selection([ group ]);\n  };\n  var d3_selectionRoot = d3.select(d3_documentElement);\n  d3_selectionPrototype.on = function(type, listener, capture) {\n    var n = arguments.length;\n    if (n < 3) {\n      if (typeof type !== \"string\") {\n        if (n < 2) listener = false;\n        for (capture in type) this.each(d3_selection_on(capture, type[capture], listener));\n        return this;\n      }\n      if (n < 2) return (n = this.node()[\"__on\" + type]) && n._;\n      capture = false;\n    }\n    return this.each(d3_selection_on(type, listener, capture));\n  };\n  function d3_selection_on(type, listener, capture) {\n    var name = \"__on\" + type, i = type.indexOf(\".\"), wrap = d3_selection_onListener;\n    if (i > 0) type = type.substring(0, i);\n    var filter = d3_selection_onFilters.get(type);\n    if (filter) type = filter, wrap = d3_selection_onFilter;\n    function onRemove() {\n      var l = this[name];\n      if (l) {\n        this.removeEventListener(type, l, l.$);\n        delete this[name];\n      }\n    }\n    function onAdd() {\n      var l = wrap(listener, d3_array(arguments));\n      onRemove.call(this);\n      this.addEventListener(type, this[name] = l, l.$ = capture);\n      l._ = listener;\n    }\n    function removeAll() {\n      var re = new RegExp(\"^__on([^.]+)\" + d3.requote(type) + \"$\"), match;\n      for (var name in this) {\n        if (match = name.match(re)) {\n          var l = this[name];\n          this.removeEventListener(match[1], l, l.$);\n          delete this[name];\n        }\n      }\n    }\n    return i ? listener ? onAdd : onRemove : listener ? d3_noop : removeAll;\n  }\n  var d3_selection_onFilters = d3.map({\n    mouseenter: \"mouseover\",\n    mouseleave: \"mouseout\"\n  });\n  d3_selection_onFilters.forEach(function(k) {\n    if (\"on\" + k in d3_document) d3_selection_onFilters.remove(k);\n  });\n  function d3_selection_onListener(listener, argumentz) {\n    return function(e) {\n      var o = d3.event;\n      d3.event = e;\n      argumentz[0] = this.__data__;\n      try {\n        listener.apply(this, argumentz);\n      } finally {\n        d3.event = o;\n      }\n    };\n  }\n  function d3_selection_onFilter(listener, argumentz) {\n    var l = d3_selection_onListener(listener, argumentz);\n    return function(e) {\n      var target = this, related = e.relatedTarget;\n      if (!related || related !== target && !(related.compareDocumentPosition(target) & 8)) {\n        l.call(target, e);\n      }\n    };\n  }\n  var d3_event_dragSelect = \"onselectstart\" in d3_document ? null : d3_vendorSymbol(d3_documentElement.style, \"userSelect\"), d3_event_dragId = 0;\n  function d3_event_dragSuppress() {\n    var name = \".dragsuppress-\" + ++d3_event_dragId, click = \"click\" + name, w = d3.select(d3_window).on(\"touchmove\" + name, d3_eventPreventDefault).on(\"dragstart\" + name, d3_eventPreventDefault).on(\"selectstart\" + name, d3_eventPreventDefault);\n    if (d3_event_dragSelect) {\n      var style = d3_documentElement.style, select = style[d3_event_dragSelect];\n      style[d3_event_dragSelect] = \"none\";\n    }\n    return function(suppressClick) {\n      w.on(name, null);\n      if (d3_event_dragSelect) style[d3_event_dragSelect] = select;\n      if (suppressClick) {\n        function off() {\n          w.on(click, null);\n        }\n        w.on(click, function() {\n          d3_eventPreventDefault();\n          off();\n        }, true);\n        setTimeout(off, 0);\n      }\n    };\n  }\n  d3.mouse = function(container) {\n    return d3_mousePoint(container, d3_eventSource());\n  };\n  function d3_mousePoint(container, e) {\n    if (e.changedTouches) e = e.changedTouches[0];\n    var svg = container.ownerSVGElement || container;\n    if (svg.createSVGPoint) {\n      var point = svg.createSVGPoint();\n      point.x = e.clientX, point.y = e.clientY;\n      point = point.matrixTransform(container.getScreenCTM().inverse());\n      return [ point.x, point.y ];\n    }\n    var rect = container.getBoundingClientRect();\n    return [ e.clientX - rect.left - container.clientLeft, e.clientY - rect.top - container.clientTop ];\n  }\n  d3.touches = function(container, touches) {\n    if (arguments.length < 2) touches = d3_eventSource().touches;\n    return touches ? d3_array(touches).map(function(touch) {\n      var point = d3_mousePoint(container, touch);\n      point.identifier = touch.identifier;\n      return point;\n    }) : [];\n  };\n  d3.behavior.drag = function() {\n    var event = d3_eventDispatch(drag, \"drag\", \"dragstart\", \"dragend\"), origin = null, mousedown = dragstart(d3_noop, d3.mouse, d3_behavior_dragMouseSubject, \"mousemove\", \"mouseup\"), touchstart = dragstart(d3_behavior_dragTouchId, d3.touch, d3_behavior_dragTouchSubject, \"touchmove\", \"touchend\");\n    function drag() {\n      this.on(\"mousedown.drag\", mousedown).on(\"touchstart.drag\", touchstart);\n    }\n    function dragstart(id, position, subject, move, end) {\n      return function() {\n        var that = this, target = d3.event.target, parent = that.parentNode, dispatch = event.of(that, arguments), dragged = 0, dragId = id(), dragName = \".drag\" + (dragId == null ? \"\" : \"-\" + dragId), dragOffset, dragSubject = d3.select(subject()).on(move + dragName, moved).on(end + dragName, ended), dragRestore = d3_event_dragSuppress(), position0 = position(parent, dragId);\n        if (origin) {\n          dragOffset = origin.apply(that, arguments);\n          dragOffset = [ dragOffset.x - position0[0], dragOffset.y - position0[1] ];\n        } else {\n          dragOffset = [ 0, 0 ];\n        }\n        dispatch({\n          type: \"dragstart\"\n        });\n        function moved() {\n          var position1 = position(parent, dragId), dx, dy;\n          if (!position1) return;\n          dx = position1[0] - position0[0];\n          dy = position1[1] - position0[1];\n          dragged |= dx | dy;\n          position0 = position1;\n          dispatch({\n            type: \"drag\",\n            x: position1[0] + dragOffset[0],\n            y: position1[1] + dragOffset[1],\n            dx: dx,\n            dy: dy\n          });\n        }\n        function ended() {\n          if (!position(parent, dragId)) return;\n          dragSubject.on(move + dragName, null).on(end + dragName, null);\n          dragRestore(dragged && d3.event.target === target);\n          dispatch({\n            type: \"dragend\"\n          });\n        }\n      };\n    }\n    drag.origin = function(x) {\n      if (!arguments.length) return origin;\n      origin = x;\n      return drag;\n    };\n    return d3.rebind(drag, event, \"on\");\n  };\n  function d3_behavior_dragTouchId() {\n    return d3.event.changedTouches[0].identifier;\n  }\n  function d3_behavior_dragTouchSubject() {\n    return d3.event.target;\n  }\n  function d3_behavior_dragMouseSubject() {\n    return d3_window;\n  }\n  var π = Math.PI, τ = 2 * π, halfπ = π / 2, ε = 1e-6, ε2 = ε * ε, d3_radians = π / 180, d3_degrees = 180 / π;\n  function d3_sgn(x) {\n    return x > 0 ? 1 : x < 0 ? -1 : 0;\n  }\n  function d3_cross2d(a, b, c) {\n    return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);\n  }\n  function d3_acos(x) {\n    return x > 1 ? 0 : x < -1 ? π : Math.acos(x);\n  }\n  function d3_asin(x) {\n    return x > 1 ? halfπ : x < -1 ? -halfπ : Math.asin(x);\n  }\n  function d3_sinh(x) {\n    return ((x = Math.exp(x)) - 1 / x) / 2;\n  }\n  function d3_cosh(x) {\n    return ((x = Math.exp(x)) + 1 / x) / 2;\n  }\n  function d3_tanh(x) {\n    return ((x = Math.exp(2 * x)) - 1) / (x + 1);\n  }\n  function d3_haversin(x) {\n    return (x = Math.sin(x / 2)) * x;\n  }\n  var ρ = Math.SQRT2, ρ2 = 2, ρ4 = 4;\n  d3.interpolateZoom = function(p0, p1) {\n    var ux0 = p0[0], uy0 = p0[1], w0 = p0[2], ux1 = p1[0], uy1 = p1[1], w1 = p1[2];\n    var dx = ux1 - ux0, dy = uy1 - uy0, d2 = dx * dx + dy * dy, d1 = Math.sqrt(d2), b0 = (w1 * w1 - w0 * w0 + ρ4 * d2) / (2 * w0 * ρ2 * d1), b1 = (w1 * w1 - w0 * w0 - ρ4 * d2) / (2 * w1 * ρ2 * d1), r0 = Math.log(Math.sqrt(b0 * b0 + 1) - b0), r1 = Math.log(Math.sqrt(b1 * b1 + 1) - b1), dr = r1 - r0, S = (dr || Math.log(w1 / w0)) / ρ;\n    function interpolate(t) {\n      var s = t * S;\n      if (dr) {\n        var coshr0 = d3_cosh(r0), u = w0 / (ρ2 * d1) * (coshr0 * d3_tanh(ρ * s + r0) - d3_sinh(r0));\n        return [ ux0 + u * dx, uy0 + u * dy, w0 * coshr0 / d3_cosh(ρ * s + r0) ];\n      }\n      return [ ux0 + t * dx, uy0 + t * dy, w0 * Math.exp(ρ * s) ];\n    }\n    interpolate.duration = S * 1e3;\n    return interpolate;\n  };\n  d3.behavior.zoom = function() {\n    var view = {\n      x: 0,\n      y: 0,\n      k: 1\n    }, translate0, center, size = [ 960, 500 ], scaleExtent = d3_behavior_zoomInfinity, mousedown = \"mousedown.zoom\", mousemove = \"mousemove.zoom\", mouseup = \"mouseup.zoom\", mousewheelTimer, touchstart = \"touchstart.zoom\", touchtime, event = d3_eventDispatch(zoom, \"zoomstart\", \"zoom\", \"zoomend\"), x0, x1, y0, y1;\n    function zoom(g) {\n      g.on(mousedown, mousedowned).on(d3_behavior_zoomWheel + \".zoom\", mousewheeled).on(mousemove, mousewheelreset).on(\"dblclick.zoom\", dblclicked).on(touchstart, touchstarted);\n    }\n    zoom.event = function(g) {\n      g.each(function() {\n        var dispatch = event.of(this, arguments), view1 = view;\n        if (d3_transitionInheritId) {\n          d3.select(this).transition().each(\"start.zoom\", function() {\n            view = this.__chart__ || {\n              x: 0,\n              y: 0,\n              k: 1\n            };\n            zoomstarted(dispatch);\n          }).tween(\"zoom:zoom\", function() {\n            var dx = size[0], dy = size[1], cx = dx / 2, cy = dy / 2, i = d3.interpolateZoom([ (cx - view.x) / view.k, (cy - view.y) / view.k, dx / view.k ], [ (cx - view1.x) / view1.k, (cy - view1.y) / view1.k, dx / view1.k ]);\n            return function(t) {\n              var l = i(t), k = dx / l[2];\n              this.__chart__ = view = {\n                x: cx - l[0] * k,\n                y: cy - l[1] * k,\n                k: k\n              };\n              zoomed(dispatch);\n            };\n          }).each(\"end.zoom\", function() {\n            zoomended(dispatch);\n          });\n        } else {\n          this.__chart__ = view;\n          zoomstarted(dispatch);\n          zoomed(dispatch);\n          zoomended(dispatch);\n        }\n      });\n    };\n    zoom.translate = function(_) {\n      if (!arguments.length) return [ view.x, view.y ];\n      view = {\n        x: +_[0],\n        y: +_[1],\n        k: view.k\n      };\n      rescale();\n      return zoom;\n    };\n    zoom.scale = function(_) {\n      if (!arguments.length) return view.k;\n      view = {\n        x: view.x,\n        y: view.y,\n        k: +_\n      };\n      rescale();\n      return zoom;\n    };\n    zoom.scaleExtent = function(_) {\n      if (!arguments.length) return scaleExtent;\n      scaleExtent = _ == null ? d3_behavior_zoomInfinity : [ +_[0], +_[1] ];\n      return zoom;\n    };\n    zoom.center = function(_) {\n      if (!arguments.length) return center;\n      center = _ && [ +_[0], +_[1] ];\n      return zoom;\n    };\n    zoom.size = function(_) {\n      if (!arguments.length) return size;\n      size = _ && [ +_[0], +_[1] ];\n      return zoom;\n    };\n    zoom.x = function(z) {\n      if (!arguments.length) return x1;\n      x1 = z;\n      x0 = z.copy();\n      view = {\n        x: 0,\n        y: 0,\n        k: 1\n      };\n      return zoom;\n    };\n    zoom.y = function(z) {\n      if (!arguments.length) return y1;\n      y1 = z;\n      y0 = z.copy();\n      view = {\n        x: 0,\n        y: 0,\n        k: 1\n      };\n      return zoom;\n    };\n    function location(p) {\n      return [ (p[0] - view.x) / view.k, (p[1] - view.y) / view.k ];\n    }\n    function point(l) {\n      return [ l[0] * view.k + view.x, l[1] * view.k + view.y ];\n    }\n    function scaleTo(s) {\n      view.k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], s));\n    }\n    function translateTo(p, l) {\n      l = point(l);\n      view.x += p[0] - l[0];\n      view.y += p[1] - l[1];\n    }\n    function rescale() {\n      if (x1) x1.domain(x0.range().map(function(x) {\n        return (x - view.x) / view.k;\n      }).map(x0.invert));\n      if (y1) y1.domain(y0.range().map(function(y) {\n        return (y - view.y) / view.k;\n      }).map(y0.invert));\n    }\n    function zoomstarted(dispatch) {\n      dispatch({\n        type: \"zoomstart\"\n      });\n    }\n    function zoomed(dispatch) {\n      rescale();\n      dispatch({\n        type: \"zoom\",\n        scale: view.k,\n        translate: [ view.x, view.y ]\n      });\n    }\n    function zoomended(dispatch) {\n      dispatch({\n        type: \"zoomend\"\n      });\n    }\n    function mousedowned() {\n      var that = this, target = d3.event.target, dispatch = event.of(that, arguments), dragged = 0, subject = d3.select(d3_window).on(mousemove, moved).on(mouseup, ended), location0 = location(d3.mouse(that)), dragRestore = d3_event_dragSuppress();\n      d3_selection_interrupt.call(that);\n      zoomstarted(dispatch);\n      function moved() {\n        dragged = 1;\n        translateTo(d3.mouse(that), location0);\n        zoomed(dispatch);\n      }\n      function ended() {\n        subject.on(mousemove, d3_window === that ? mousewheelreset : null).on(mouseup, null);\n        dragRestore(dragged && d3.event.target === target);\n        zoomended(dispatch);\n      }\n    }\n    function touchstarted() {\n      var that = this, dispatch = event.of(that, arguments), locations0 = {}, distance0 = 0, scale0, zoomName = \".zoom-\" + d3.event.changedTouches[0].identifier, touchmove = \"touchmove\" + zoomName, touchend = \"touchend\" + zoomName, target = d3.select(d3.event.target).on(touchmove, moved).on(touchend, ended), subject = d3.select(that).on(mousedown, null).on(touchstart, started), dragRestore = d3_event_dragSuppress();\n      d3_selection_interrupt.call(that);\n      started();\n      zoomstarted(dispatch);\n      function relocate() {\n        var touches = d3.touches(that);\n        scale0 = view.k;\n        touches.forEach(function(t) {\n          if (t.identifier in locations0) locations0[t.identifier] = location(t);\n        });\n        return touches;\n      }\n      function started() {\n        var changed = d3.event.changedTouches;\n        for (var i = 0, n = changed.length; i < n; ++i) {\n          locations0[changed[i].identifier] = null;\n        }\n        var touches = relocate(), now = Date.now();\n        if (touches.length === 1) {\n          if (now - touchtime < 500) {\n            var p = touches[0], l = locations0[p.identifier];\n            scaleTo(view.k * 2);\n            translateTo(p, l);\n            d3_eventPreventDefault();\n            zoomed(dispatch);\n          }\n          touchtime = now;\n        } else if (touches.length > 1) {\n          var p = touches[0], q = touches[1], dx = p[0] - q[0], dy = p[1] - q[1];\n          distance0 = dx * dx + dy * dy;\n        }\n      }\n      function moved() {\n        var touches = d3.touches(that), p0, l0, p1, l1;\n        for (var i = 0, n = touches.length; i < n; ++i, l1 = null) {\n          p1 = touches[i];\n          if (l1 = locations0[p1.identifier]) {\n            if (l0) break;\n            p0 = p1, l0 = l1;\n          }\n        }\n        if (l1) {\n          var distance1 = (distance1 = p1[0] - p0[0]) * distance1 + (distance1 = p1[1] - p0[1]) * distance1, scale1 = distance0 && Math.sqrt(distance1 / distance0);\n          p0 = [ (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2 ];\n          l0 = [ (l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2 ];\n          scaleTo(scale1 * scale0);\n        }\n        touchtime = null;\n        translateTo(p0, l0);\n        zoomed(dispatch);\n      }\n      function ended() {\n        if (d3.event.touches.length) {\n          var changed = d3.event.changedTouches;\n          for (var i = 0, n = changed.length; i < n; ++i) {\n            delete locations0[changed[i].identifier];\n          }\n          for (var identifier in locations0) {\n            return void relocate();\n          }\n        }\n        target.on(zoomName, null);\n        subject.on(mousedown, mousedowned).on(touchstart, touchstarted);\n        dragRestore();\n        zoomended(dispatch);\n      }\n    }\n    function mousewheeled() {\n      var dispatch = event.of(this, arguments);\n      if (mousewheelTimer) clearTimeout(mousewheelTimer); else d3_selection_interrupt.call(this), \n      zoomstarted(dispatch);\n      mousewheelTimer = setTimeout(function() {\n        mousewheelTimer = null;\n        zoomended(dispatch);\n      }, 50);\n      d3_eventPreventDefault();\n      var point = center || d3.mouse(this);\n      if (!translate0) translate0 = location(point);\n      scaleTo(Math.pow(2, d3_behavior_zoomDelta() * .002) * view.k);\n      translateTo(point, translate0);\n      zoomed(dispatch);\n    }\n    function mousewheelreset() {\n      translate0 = null;\n    }\n    function dblclicked() {\n      var dispatch = event.of(this, arguments), p = d3.mouse(this), l = location(p), k = Math.log(view.k) / Math.LN2;\n      zoomstarted(dispatch);\n      scaleTo(Math.pow(2, d3.event.shiftKey ? Math.ceil(k) - 1 : Math.floor(k) + 1));\n      translateTo(p, l);\n      zoomed(dispatch);\n      zoomended(dispatch);\n    }\n    return d3.rebind(zoom, event, \"on\");\n  };\n  var d3_behavior_zoomInfinity = [ 0, Infinity ];\n  var d3_behavior_zoomDelta, d3_behavior_zoomWheel = \"onwheel\" in d3_document ? (d3_behavior_zoomDelta = function() {\n    return -d3.event.deltaY * (d3.event.deltaMode ? 120 : 1);\n  }, \"wheel\") : \"onmousewheel\" in d3_document ? (d3_behavior_zoomDelta = function() {\n    return d3.event.wheelDelta;\n  }, \"mousewheel\") : (d3_behavior_zoomDelta = function() {\n    return -d3.event.detail;\n  }, \"MozMousePixelScroll\");\n  function d3_Color() {}\n  d3_Color.prototype.toString = function() {\n    return this.rgb() + \"\";\n  };\n  d3.hsl = function(h, s, l) {\n    return arguments.length === 1 ? h instanceof d3_Hsl ? d3_hsl(h.h, h.s, h.l) : d3_rgb_parse(\"\" + h, d3_rgb_hsl, d3_hsl) : d3_hsl(+h, +s, +l);\n  };\n  function d3_hsl(h, s, l) {\n    return new d3_Hsl(h, s, l);\n  }\n  function d3_Hsl(h, s, l) {\n    this.h = h;\n    this.s = s;\n    this.l = l;\n  }\n  var d3_hslPrototype = d3_Hsl.prototype = new d3_Color();\n  d3_hslPrototype.brighter = function(k) {\n    k = Math.pow(.7, arguments.length ? k : 1);\n    return d3_hsl(this.h, this.s, this.l / k);\n  };\n  d3_hslPrototype.darker = function(k) {\n    k = Math.pow(.7, arguments.length ? k : 1);\n    return d3_hsl(this.h, this.s, k * this.l);\n  };\n  d3_hslPrototype.rgb = function() {\n    return d3_hsl_rgb(this.h, this.s, this.l);\n  };\n  function d3_hsl_rgb(h, s, l) {\n    var m1, m2;\n    h = isNaN(h) ? 0 : (h %= 360) < 0 ? h + 360 : h;\n    s = isNaN(s) ? 0 : s < 0 ? 0 : s > 1 ? 1 : s;\n    l = l < 0 ? 0 : l > 1 ? 1 : l;\n    m2 = l <= .5 ? l * (1 + s) : l + s - l * s;\n    m1 = 2 * l - m2;\n    function v(h) {\n      if (h > 360) h -= 360; else if (h < 0) h += 360;\n      if (h < 60) return m1 + (m2 - m1) * h / 60;\n      if (h < 180) return m2;\n      if (h < 240) return m1 + (m2 - m1) * (240 - h) / 60;\n      return m1;\n    }\n    function vv(h) {\n      return Math.round(v(h) * 255);\n    }\n    return d3_rgb(vv(h + 120), vv(h), vv(h - 120));\n  }\n  d3.hcl = function(h, c, l) {\n    return arguments.length === 1 ? h instanceof d3_Hcl ? d3_hcl(h.h, h.c, h.l) : h instanceof d3_Lab ? d3_lab_hcl(h.l, h.a, h.b) : d3_lab_hcl((h = d3_rgb_lab((h = d3.rgb(h)).r, h.g, h.b)).l, h.a, h.b) : d3_hcl(+h, +c, +l);\n  };\n  function d3_hcl(h, c, l) {\n    return new d3_Hcl(h, c, l);\n  }\n  function d3_Hcl(h, c, l) {\n    this.h = h;\n    this.c = c;\n    this.l = l;\n  }\n  var d3_hclPrototype = d3_Hcl.prototype = new d3_Color();\n  d3_hclPrototype.brighter = function(k) {\n    return d3_hcl(this.h, this.c, Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1)));\n  };\n  d3_hclPrototype.darker = function(k) {\n    return d3_hcl(this.h, this.c, Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1)));\n  };\n  d3_hclPrototype.rgb = function() {\n    return d3_hcl_lab(this.h, this.c, this.l).rgb();\n  };\n  function d3_hcl_lab(h, c, l) {\n    if (isNaN(h)) h = 0;\n    if (isNaN(c)) c = 0;\n    return d3_lab(l, Math.cos(h *= d3_radians) * c, Math.sin(h) * c);\n  }\n  d3.lab = function(l, a, b) {\n    return arguments.length === 1 ? l instanceof d3_Lab ? d3_lab(l.l, l.a, l.b) : l instanceof d3_Hcl ? d3_hcl_lab(l.l, l.c, l.h) : d3_rgb_lab((l = d3.rgb(l)).r, l.g, l.b) : d3_lab(+l, +a, +b);\n  };\n  function d3_lab(l, a, b) {\n    return new d3_Lab(l, a, b);\n  }\n  function d3_Lab(l, a, b) {\n    this.l = l;\n    this.a = a;\n    this.b = b;\n  }\n  var d3_lab_K = 18;\n  var d3_lab_X = .95047, d3_lab_Y = 1, d3_lab_Z = 1.08883;\n  var d3_labPrototype = d3_Lab.prototype = new d3_Color();\n  d3_labPrototype.brighter = function(k) {\n    return d3_lab(Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1)), this.a, this.b);\n  };\n  d3_labPrototype.darker = function(k) {\n    return d3_lab(Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1)), this.a, this.b);\n  };\n  d3_labPrototype.rgb = function() {\n    return d3_lab_rgb(this.l, this.a, this.b);\n  };\n  function d3_lab_rgb(l, a, b) {\n    var y = (l + 16) / 116, x = y + a / 500, z = y - b / 200;\n    x = d3_lab_xyz(x) * d3_lab_X;\n    y = d3_lab_xyz(y) * d3_lab_Y;\n    z = d3_lab_xyz(z) * d3_lab_Z;\n    return d3_rgb(d3_xyz_rgb(3.2404542 * x - 1.5371385 * y - .4985314 * z), d3_xyz_rgb(-.969266 * x + 1.8760108 * y + .041556 * z), d3_xyz_rgb(.0556434 * x - .2040259 * y + 1.0572252 * z));\n  }\n  function d3_lab_hcl(l, a, b) {\n    return l > 0 ? d3_hcl(Math.atan2(b, a) * d3_degrees, Math.sqrt(a * a + b * b), l) : d3_hcl(NaN, NaN, l);\n  }\n  function d3_lab_xyz(x) {\n    return x > .206893034 ? x * x * x : (x - 4 / 29) / 7.787037;\n  }\n  function d3_xyz_lab(x) {\n    return x > .008856 ? Math.pow(x, 1 / 3) : 7.787037 * x + 4 / 29;\n  }\n  function d3_xyz_rgb(r) {\n    return Math.round(255 * (r <= .00304 ? 12.92 * r : 1.055 * Math.pow(r, 1 / 2.4) - .055));\n  }\n  d3.rgb = function(r, g, b) {\n    return arguments.length === 1 ? r instanceof d3_Rgb ? d3_rgb(r.r, r.g, r.b) : d3_rgb_parse(\"\" + r, d3_rgb, d3_hsl_rgb) : d3_rgb(~~r, ~~g, ~~b);\n  };\n  function d3_rgbNumber(value) {\n    return d3_rgb(value >> 16, value >> 8 & 255, value & 255);\n  }\n  function d3_rgbString(value) {\n    return d3_rgbNumber(value) + \"\";\n  }\n  function d3_rgb(r, g, b) {\n    return new d3_Rgb(r, g, b);\n  }\n  function d3_Rgb(r, g, b) {\n    this.r = r;\n    this.g = g;\n    this.b = b;\n  }\n  var d3_rgbPrototype = d3_Rgb.prototype = new d3_Color();\n  d3_rgbPrototype.brighter = function(k) {\n    k = Math.pow(.7, arguments.length ? k : 1);\n    var r = this.r, g = this.g, b = this.b, i = 30;\n    if (!r && !g && !b) return d3_rgb(i, i, i);\n    if (r && r < i) r = i;\n    if (g && g < i) g = i;\n    if (b && b < i) b = i;\n    return d3_rgb(Math.min(255, ~~(r / k)), Math.min(255, ~~(g / k)), Math.min(255, ~~(b / k)));\n  };\n  d3_rgbPrototype.darker = function(k) {\n    k = Math.pow(.7, arguments.length ? k : 1);\n    return d3_rgb(~~(k * this.r), ~~(k * this.g), ~~(k * this.b));\n  };\n  d3_rgbPrototype.hsl = function() {\n    return d3_rgb_hsl(this.r, this.g, this.b);\n  };\n  d3_rgbPrototype.toString = function() {\n    return \"#\" + d3_rgb_hex(this.r) + d3_rgb_hex(this.g) + d3_rgb_hex(this.b);\n  };\n  function d3_rgb_hex(v) {\n    return v < 16 ? \"0\" + Math.max(0, v).toString(16) : Math.min(255, v).toString(16);\n  }\n  function d3_rgb_parse(format, rgb, hsl) {\n    var r = 0, g = 0, b = 0, m1, m2, color;\n    m1 = /([a-z]+)\\((.*)\\)/i.exec(format);\n    if (m1) {\n      m2 = m1[2].split(\",\");\n      switch (m1[1]) {\n       case \"hsl\":\n        {\n          return hsl(parseFloat(m2[0]), parseFloat(m2[1]) / 100, parseFloat(m2[2]) / 100);\n        }\n\n       case \"rgb\":\n        {\n          return rgb(d3_rgb_parseNumber(m2[0]), d3_rgb_parseNumber(m2[1]), d3_rgb_parseNumber(m2[2]));\n        }\n      }\n    }\n    if (color = d3_rgb_names.get(format)) return rgb(color.r, color.g, color.b);\n    if (format != null && format.charAt(0) === \"#\" && !isNaN(color = parseInt(format.substring(1), 16))) {\n      if (format.length === 4) {\n        r = (color & 3840) >> 4;\n        r = r >> 4 | r;\n        g = color & 240;\n        g = g >> 4 | g;\n        b = color & 15;\n        b = b << 4 | b;\n      } else if (format.length === 7) {\n        r = (color & 16711680) >> 16;\n        g = (color & 65280) >> 8;\n        b = color & 255;\n      }\n    }\n    return rgb(r, g, b);\n  }\n  function d3_rgb_hsl(r, g, b) {\n    var min = Math.min(r /= 255, g /= 255, b /= 255), max = Math.max(r, g, b), d = max - min, h, s, l = (max + min) / 2;\n    if (d) {\n      s = l < .5 ? d / (max + min) : d / (2 - max - min);\n      if (r == max) h = (g - b) / d + (g < b ? 6 : 0); else if (g == max) h = (b - r) / d + 2; else h = (r - g) / d + 4;\n      h *= 60;\n    } else {\n      h = NaN;\n      s = l > 0 && l < 1 ? 0 : h;\n    }\n    return d3_hsl(h, s, l);\n  }\n  function d3_rgb_lab(r, g, b) {\n    r = d3_rgb_xyz(r);\n    g = d3_rgb_xyz(g);\n    b = d3_rgb_xyz(b);\n    var x = d3_xyz_lab((.4124564 * r + .3575761 * g + .1804375 * b) / d3_lab_X), y = d3_xyz_lab((.2126729 * r + .7151522 * g + .072175 * b) / d3_lab_Y), z = d3_xyz_lab((.0193339 * r + .119192 * g + .9503041 * b) / d3_lab_Z);\n    return d3_lab(116 * y - 16, 500 * (x - y), 200 * (y - z));\n  }\n  function d3_rgb_xyz(r) {\n    return (r /= 255) <= .04045 ? r / 12.92 : Math.pow((r + .055) / 1.055, 2.4);\n  }\n  function d3_rgb_parseNumber(c) {\n    var f = parseFloat(c);\n    return c.charAt(c.length - 1) === \"%\" ? Math.round(f * 2.55) : f;\n  }\n  var d3_rgb_names = d3.map({\n    aliceblue: 15792383,\n    antiquewhite: 16444375,\n    aqua: 65535,\n    aquamarine: 8388564,\n    azure: 15794175,\n    beige: 16119260,\n    bisque: 16770244,\n    black: 0,\n    blanchedalmond: 16772045,\n    blue: 255,\n    blueviolet: 9055202,\n    brown: 10824234,\n    burlywood: 14596231,\n    cadetblue: 6266528,\n    chartreuse: 8388352,\n    chocolate: 13789470,\n    coral: 16744272,\n    cornflowerblue: 6591981,\n    cornsilk: 16775388,\n    crimson: 14423100,\n    cyan: 65535,\n    darkblue: 139,\n    darkcyan: 35723,\n    darkgoldenrod: 12092939,\n    darkgray: 11119017,\n    darkgreen: 25600,\n    darkgrey: 11119017,\n    darkkhaki: 12433259,\n    darkmagenta: 9109643,\n    darkolivegreen: 5597999,\n    darkorange: 16747520,\n    darkorchid: 10040012,\n    darkred: 9109504,\n    darksalmon: 15308410,\n    darkseagreen: 9419919,\n    darkslateblue: 4734347,\n    darkslategray: 3100495,\n    darkslategrey: 3100495,\n    darkturquoise: 52945,\n    darkviolet: 9699539,\n    deeppink: 16716947,\n    deepskyblue: 49151,\n    dimgray: 6908265,\n    dimgrey: 6908265,\n    dodgerblue: 2003199,\n    firebrick: 11674146,\n    floralwhite: 16775920,\n    forestgreen: 2263842,\n    fuchsia: 16711935,\n    gainsboro: 14474460,\n    ghostwhite: 16316671,\n    gold: 16766720,\n    goldenrod: 14329120,\n    gray: 8421504,\n    green: 32768,\n    greenyellow: 11403055,\n    grey: 8421504,\n    honeydew: 15794160,\n    hotpink: 16738740,\n    indianred: 13458524,\n    indigo: 4915330,\n    ivory: 16777200,\n    khaki: 15787660,\n    lavender: 15132410,\n    lavenderblush: 16773365,\n    lawngreen: 8190976,\n    lemonchiffon: 16775885,\n    lightblue: 11393254,\n    lightcoral: 15761536,\n    lightcyan: 14745599,\n    lightgoldenrodyellow: 16448210,\n    lightgray: 13882323,\n    lightgreen: 9498256,\n    lightgrey: 13882323,\n    lightpink: 16758465,\n    lightsalmon: 16752762,\n    lightseagreen: 2142890,\n    lightskyblue: 8900346,\n    lightslategray: 7833753,\n    lightslategrey: 7833753,\n    lightsteelblue: 11584734,\n    lightyellow: 16777184,\n    lime: 65280,\n    limegreen: 3329330,\n    linen: 16445670,\n    magenta: 16711935,\n    maroon: 8388608,\n    mediumaquamarine: 6737322,\n    mediumblue: 205,\n    mediumorchid: 12211667,\n    mediumpurple: 9662683,\n    mediumseagreen: 3978097,\n    mediumslateblue: 8087790,\n    mediumspringgreen: 64154,\n    mediumturquoise: 4772300,\n    mediumvioletred: 13047173,\n    midnightblue: 1644912,\n    mintcream: 16121850,\n    mistyrose: 16770273,\n    moccasin: 16770229,\n    navajowhite: 16768685,\n    navy: 128,\n    oldlace: 16643558,\n    olive: 8421376,\n    olivedrab: 7048739,\n    orange: 16753920,\n    orangered: 16729344,\n    orchid: 14315734,\n    palegoldenrod: 15657130,\n    palegreen: 10025880,\n    paleturquoise: 11529966,\n    palevioletred: 14381203,\n    papayawhip: 16773077,\n    peachpuff: 16767673,\n    peru: 13468991,\n    pink: 16761035,\n    plum: 14524637,\n    powderblue: 11591910,\n    purple: 8388736,\n    red: 16711680,\n    rosybrown: 12357519,\n    royalblue: 4286945,\n    saddlebrown: 9127187,\n    salmon: 16416882,\n    sandybrown: 16032864,\n    seagreen: 3050327,\n    seashell: 16774638,\n    sienna: 10506797,\n    silver: 12632256,\n    skyblue: 8900331,\n    slateblue: 6970061,\n    slategray: 7372944,\n    slategrey: 7372944,\n    snow: 16775930,\n    springgreen: 65407,\n    steelblue: 4620980,\n    tan: 13808780,\n    teal: 32896,\n    thistle: 14204888,\n    tomato: 16737095,\n    turquoise: 4251856,\n    violet: 15631086,\n    wheat: 16113331,\n    white: 16777215,\n    whitesmoke: 16119285,\n    yellow: 16776960,\n    yellowgreen: 10145074\n  });\n  d3_rgb_names.forEach(function(key, value) {\n    d3_rgb_names.set(key, d3_rgbNumber(value));\n  });\n  function d3_functor(v) {\n    return typeof v === \"function\" ? v : function() {\n      return v;\n    };\n  }\n  d3.functor = d3_functor;\n  function d3_identity(d) {\n    return d;\n  }\n  d3.xhr = d3_xhrType(d3_identity);\n  function d3_xhrType(response) {\n    return function(url, mimeType, callback) {\n      if (arguments.length === 2 && typeof mimeType === \"function\") callback = mimeType, \n      mimeType = null;\n      return d3_xhr(url, mimeType, response, callback);\n    };\n  }\n  function d3_xhr(url, mimeType, response, callback) {\n    var xhr = {}, dispatch = d3.dispatch(\"beforesend\", \"progress\", \"load\", \"error\"), headers = {}, request = new XMLHttpRequest(), responseType = null;\n    if (d3_window.XDomainRequest && !(\"withCredentials\" in request) && /^(http(s)?:)?\\/\\//.test(url)) request = new XDomainRequest();\n    \"onload\" in request ? request.onload = request.onerror = respond : request.onreadystatechange = function() {\n      request.readyState > 3 && respond();\n    };\n    function respond() {\n      var status = request.status, result;\n      if (!status && request.responseText || status >= 200 && status < 300 || status === 304) {\n        try {\n          result = response.call(xhr, request);\n        } catch (e) {\n          dispatch.error.call(xhr, e);\n          return;\n        }\n        dispatch.load.call(xhr, result);\n      } else {\n        dispatch.error.call(xhr, request);\n      }\n    }\n    request.onprogress = function(event) {\n      var o = d3.event;\n      d3.event = event;\n      try {\n        dispatch.progress.call(xhr, request);\n      } finally {\n        d3.event = o;\n      }\n    };\n    xhr.header = function(name, value) {\n      name = (name + \"\").toLowerCase();\n      if (arguments.length < 2) return headers[name];\n      if (value == null) delete headers[name]; else headers[name] = value + \"\";\n      return xhr;\n    };\n    xhr.mimeType = function(value) {\n      if (!arguments.length) return mimeType;\n      mimeType = value == null ? null : value + \"\";\n      return xhr;\n    };\n    xhr.responseType = function(value) {\n      if (!arguments.length) return responseType;\n      responseType = value;\n      return xhr;\n    };\n    xhr.response = function(value) {\n      response = value;\n      return xhr;\n    };\n    [ \"get\", \"post\" ].forEach(function(method) {\n      xhr[method] = function() {\n        return xhr.send.apply(xhr, [ method ].concat(d3_array(arguments)));\n      };\n    });\n    xhr.send = function(method, data, callback) {\n      if (arguments.length === 2 && typeof data === \"function\") callback = data, data = null;\n      request.open(method, url, true);\n      if (mimeType != null && !(\"accept\" in headers)) headers[\"accept\"] = mimeType + \",*/*\";\n      if (request.setRequestHeader) for (var name in headers) request.setRequestHeader(name, headers[name]);\n      if (mimeType != null && request.overrideMimeType) request.overrideMimeType(mimeType);\n      if (responseType != null) request.responseType = responseType;\n      if (callback != null) xhr.on(\"error\", callback).on(\"load\", function(request) {\n        callback(null, request);\n      });\n      dispatch.beforesend.call(xhr, request);\n      request.send(data == null ? null : data);\n      return xhr;\n    };\n    xhr.abort = function() {\n      request.abort();\n      return xhr;\n    };\n    d3.rebind(xhr, dispatch, \"on\");\n    return callback == null ? xhr : xhr.get(d3_xhr_fixCallback(callback));\n  }\n  function d3_xhr_fixCallback(callback) {\n    return callback.length === 1 ? function(error, request) {\n      callback(error == null ? request : null);\n    } : callback;\n  }\n  d3.dsv = function(delimiter, mimeType) {\n    var reFormat = new RegExp('[\"' + delimiter + \"\\n]\"), delimiterCode = delimiter.charCodeAt(0);\n    function dsv(url, row, callback) {\n      if (arguments.length < 3) callback = row, row = null;\n      var xhr = d3_xhr(url, mimeType, row == null ? response : typedResponse(row), callback);\n      xhr.row = function(_) {\n        return arguments.length ? xhr.response((row = _) == null ? response : typedResponse(_)) : row;\n      };\n      return xhr;\n    }\n    function response(request) {\n      return dsv.parse(request.responseText);\n    }\n    function typedResponse(f) {\n      return function(request) {\n        return dsv.parse(request.responseText, f);\n      };\n    }\n    dsv.parse = function(text, f) {\n      var o;\n      return dsv.parseRows(text, function(row, i) {\n        if (o) return o(row, i - 1);\n        var a = new Function(\"d\", \"return {\" + row.map(function(name, i) {\n          return JSON.stringify(name) + \": d[\" + i + \"]\";\n        }).join(\",\") + \"}\");\n        o = f ? function(row, i) {\n          return f(a(row), i);\n        } : a;\n      });\n    };\n    dsv.parseRows = function(text, f) {\n      var EOL = {}, EOF = {}, rows = [], N = text.length, I = 0, n = 0, t, eol;\n      function token() {\n        if (I >= N) return EOF;\n        if (eol) return eol = false, EOL;\n        var j = I;\n        if (text.charCodeAt(j) === 34) {\n          var i = j;\n          while (i++ < N) {\n            if (text.charCodeAt(i) === 34) {\n              if (text.charCodeAt(i + 1) !== 34) break;\n              ++i;\n            }\n          }\n          I = i + 2;\n          var c = text.charCodeAt(i + 1);\n          if (c === 13) {\n            eol = true;\n            if (text.charCodeAt(i + 2) === 10) ++I;\n          } else if (c === 10) {\n            eol = true;\n          }\n          return text.substring(j + 1, i).replace(/\"\"/g, '\"');\n        }\n        while (I < N) {\n          var c = text.charCodeAt(I++), k = 1;\n          if (c === 10) eol = true; else if (c === 13) {\n            eol = true;\n            if (text.charCodeAt(I) === 10) ++I, ++k;\n          } else if (c !== delimiterCode) continue;\n          return text.substring(j, I - k);\n        }\n        return text.substring(j);\n      }\n      while ((t = token()) !== EOF) {\n        var a = [];\n        while (t !== EOL && t !== EOF) {\n          a.push(t);\n          t = token();\n        }\n        if (f && !(a = f(a, n++))) continue;\n        rows.push(a);\n      }\n      return rows;\n    };\n    dsv.format = function(rows) {\n      if (Array.isArray(rows[0])) return dsv.formatRows(rows);\n      var fieldSet = new d3_Set(), fields = [];\n      rows.forEach(function(row) {\n        for (var field in row) {\n          if (!fieldSet.has(field)) {\n            fields.push(fieldSet.add(field));\n          }\n        }\n      });\n      return [ fields.map(formatValue).join(delimiter) ].concat(rows.map(function(row) {\n        return fields.map(function(field) {\n          return formatValue(row[field]);\n        }).join(delimiter);\n      })).join(\"\\n\");\n    };\n    dsv.formatRows = function(rows) {\n      return rows.map(formatRow).join(\"\\n\");\n    };\n    function formatRow(row) {\n      return row.map(formatValue).join(delimiter);\n    }\n    function formatValue(text) {\n      return reFormat.test(text) ? '\"' + text.replace(/\\\"/g, '\"\"') + '\"' : text;\n    }\n    return dsv;\n  };\n  d3.csv = d3.dsv(\",\", \"text/csv\");\n  d3.tsv = d3.dsv(\"\t\", \"text/tab-separated-values\");\n  d3.touch = function(container, touches, identifier) {\n    if (arguments.length < 3) identifier = touches, touches = d3_eventSource().changedTouches;\n    if (touches) for (var i = 0, n = touches.length, touch; i < n; ++i) {\n      if ((touch = touches[i]).identifier === identifier) {\n        return d3_mousePoint(container, touch);\n      }\n    }\n  };\n  var d3_timer_queueHead, d3_timer_queueTail, d3_timer_interval, d3_timer_timeout, d3_timer_active, d3_timer_frame = d3_window[d3_vendorSymbol(d3_window, \"requestAnimationFrame\")] || function(callback) {\n    setTimeout(callback, 17);\n  };\n  d3.timer = function(callback, delay, then) {\n    var n = arguments.length;\n    if (n < 2) delay = 0;\n    if (n < 3) then = Date.now();\n    var time = then + delay, timer = {\n      c: callback,\n      t: time,\n      f: false,\n      n: null\n    };\n    if (d3_timer_queueTail) d3_timer_queueTail.n = timer; else d3_timer_queueHead = timer;\n    d3_timer_queueTail = timer;\n    if (!d3_timer_interval) {\n      d3_timer_timeout = clearTimeout(d3_timer_timeout);\n      d3_timer_interval = 1;\n      d3_timer_frame(d3_timer_step);\n    }\n  };\n  function d3_timer_step() {\n    var now = d3_timer_mark(), delay = d3_timer_sweep() - now;\n    if (delay > 24) {\n      if (isFinite(delay)) {\n        clearTimeout(d3_timer_timeout);\n        d3_timer_timeout = setTimeout(d3_timer_step, delay);\n      }\n      d3_timer_interval = 0;\n    } else {\n      d3_timer_interval = 1;\n      d3_timer_frame(d3_timer_step);\n    }\n  }\n  d3.timer.flush = function() {\n    d3_timer_mark();\n    d3_timer_sweep();\n  };\n  function d3_timer_mark() {\n    var now = Date.now();\n    d3_timer_active = d3_timer_queueHead;\n    while (d3_timer_active) {\n      if (now >= d3_timer_active.t) d3_timer_active.f = d3_timer_active.c(now - d3_timer_active.t);\n      d3_timer_active = d3_timer_active.n;\n    }\n    return now;\n  }\n  function d3_timer_sweep() {\n    var t0, t1 = d3_timer_queueHead, time = Infinity;\n    while (t1) {\n      if (t1.f) {\n        t1 = t0 ? t0.n = t1.n : d3_timer_queueHead = t1.n;\n      } else {\n        if (t1.t < time) time = t1.t;\n        t1 = (t0 = t1).n;\n      }\n    }\n    d3_timer_queueTail = t0;\n    return time;\n  }\n  function d3_format_precision(x, p) {\n    return p - (x ? Math.ceil(Math.log(x) / Math.LN10) : 1);\n  }\n  d3.round = function(x, n) {\n    return n ? Math.round(x * (n = Math.pow(10, n))) / n : Math.round(x);\n  };\n  var d3_formatPrefixes = [ \"y\", \"z\", \"a\", \"f\", \"p\", \"n\", \"µ\", \"m\", \"\", \"k\", \"M\", \"G\", \"T\", \"P\", \"E\", \"Z\", \"Y\" ].map(d3_formatPrefix);\n  d3.formatPrefix = function(value, precision) {\n    var i = 0;\n    if (value) {\n      if (value < 0) value *= -1;\n      if (precision) value = d3.round(value, d3_format_precision(value, precision));\n      i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10);\n      i = Math.max(-24, Math.min(24, Math.floor((i - 1) / 3) * 3));\n    }\n    return d3_formatPrefixes[8 + i / 3];\n  };\n  function d3_formatPrefix(d, i) {\n    var k = Math.pow(10, abs(8 - i) * 3);\n    return {\n      scale: i > 8 ? function(d) {\n        return d / k;\n      } : function(d) {\n        return d * k;\n      },\n      symbol: d\n    };\n  }\n  function d3_locale_numberFormat(locale) {\n    var locale_decimal = locale.decimal, locale_thousands = locale.thousands, locale_grouping = locale.grouping, locale_currency = locale.currency, formatGroup = locale_grouping ? function(value) {\n      var i = value.length, t = [], j = 0, g = locale_grouping[0];\n      while (i > 0 && g > 0) {\n        t.push(value.substring(i -= g, i + g));\n        g = locale_grouping[j = (j + 1) % locale_grouping.length];\n      }\n      return t.reverse().join(locale_thousands);\n    } : d3_identity;\n    return function(specifier) {\n      var match = d3_format_re.exec(specifier), fill = match[1] || \" \", align = match[2] || \">\", sign = match[3] || \"\", symbol = match[4] || \"\", zfill = match[5], width = +match[6], comma = match[7], precision = match[8], type = match[9], scale = 1, prefix = \"\", suffix = \"\", integer = false;\n      if (precision) precision = +precision.substring(1);\n      if (zfill || fill === \"0\" && align === \"=\") {\n        zfill = fill = \"0\";\n        align = \"=\";\n        if (comma) width -= Math.floor((width - 1) / 4);\n      }\n      switch (type) {\n       case \"n\":\n        comma = true;\n        type = \"g\";\n        break;\n\n       case \"%\":\n        scale = 100;\n        suffix = \"%\";\n        type = \"f\";\n        break;\n\n       case \"p\":\n        scale = 100;\n        suffix = \"%\";\n        type = \"r\";\n        break;\n\n       case \"b\":\n       case \"o\":\n       case \"x\":\n       case \"X\":\n        if (symbol === \"#\") prefix = \"0\" + type.toLowerCase();\n\n       case \"c\":\n       case \"d\":\n        integer = true;\n        precision = 0;\n        break;\n\n       case \"s\":\n        scale = -1;\n        type = \"r\";\n        break;\n      }\n      if (symbol === \"$\") prefix = locale_currency[0], suffix = locale_currency[1];\n      if (type == \"r\" && !precision) type = \"g\";\n      if (precision != null) {\n        if (type == \"g\") precision = Math.max(1, Math.min(21, precision)); else if (type == \"e\" || type == \"f\") precision = Math.max(0, Math.min(20, precision));\n      }\n      type = d3_format_types.get(type) || d3_format_typeDefault;\n      var zcomma = zfill && comma;\n      return function(value) {\n        var fullSuffix = suffix;\n        if (integer && value % 1) return \"\";\n        var negative = value < 0 || value === 0 && 1 / value < 0 ? (value = -value, \"-\") : sign;\n        if (scale < 0) {\n          var unit = d3.formatPrefix(value, precision);\n          value = unit.scale(value);\n          fullSuffix = unit.symbol + suffix;\n        } else {\n          value *= scale;\n        }\n        value = type(value, precision);\n        var i = value.lastIndexOf(\".\"), before = i < 0 ? value : value.substring(0, i), after = i < 0 ? \"\" : locale_decimal + value.substring(i + 1);\n        if (!zfill && comma) before = formatGroup(before);\n        var length = prefix.length + before.length + after.length + (zcomma ? 0 : negative.length), padding = length < width ? new Array(length = width - length + 1).join(fill) : \"\";\n        if (zcomma) before = formatGroup(padding + before);\n        negative += prefix;\n        value = before + after;\n        return (align === \"<\" ? negative + value + padding : align === \">\" ? padding + negative + value : align === \"^\" ? padding.substring(0, length >>= 1) + negative + value + padding.substring(length) : negative + (zcomma ? value : padding + value)) + fullSuffix;\n      };\n    };\n  }\n  var d3_format_re = /(?:([^{])?([<>=^]))?([+\\- ])?([$#])?(0)?(\\d+)?(,)?(\\.-?\\d+)?([a-z%])?/i;\n  var d3_format_types = d3.map({\n    b: function(x) {\n      return x.toString(2);\n    },\n    c: function(x) {\n      return String.fromCharCode(x);\n    },\n    o: function(x) {\n      return x.toString(8);\n    },\n    x: function(x) {\n      return x.toString(16);\n    },\n    X: function(x) {\n      return x.toString(16).toUpperCase();\n    },\n    g: function(x, p) {\n      return x.toPrecision(p);\n    },\n    e: function(x, p) {\n      return x.toExponential(p);\n    },\n    f: function(x, p) {\n      return x.toFixed(p);\n    },\n    r: function(x, p) {\n      return (x = d3.round(x, d3_format_precision(x, p))).toFixed(Math.max(0, Math.min(20, d3_format_precision(x * (1 + 1e-15), p))));\n    }\n  });\n  function d3_format_typeDefault(x) {\n    return x + \"\";\n  }\n  var d3_time = d3.time = {}, d3_date = Date;\n  function d3_date_utc() {\n    this._ = new Date(arguments.length > 1 ? Date.UTC.apply(this, arguments) : arguments[0]);\n  }\n  d3_date_utc.prototype = {\n    getDate: function() {\n      return this._.getUTCDate();\n    },\n    getDay: function() {\n      return this._.getUTCDay();\n    },\n    getFullYear: function() {\n      return this._.getUTCFullYear();\n    },\n    getHours: function() {\n      return this._.getUTCHours();\n    },\n    getMilliseconds: function() {\n      return this._.getUTCMilliseconds();\n    },\n    getMinutes: function() {\n      return this._.getUTCMinutes();\n    },\n    getMonth: function() {\n      return this._.getUTCMonth();\n    },\n    getSeconds: function() {\n      return this._.getUTCSeconds();\n    },\n    getTime: function() {\n      return this._.getTime();\n    },\n    getTimezoneOffset: function() {\n      return 0;\n    },\n    valueOf: function() {\n      return this._.valueOf();\n    },\n    setDate: function() {\n      d3_time_prototype.setUTCDate.apply(this._, arguments);\n    },\n    setDay: function() {\n      d3_time_prototype.setUTCDay.apply(this._, arguments);\n    },\n    setFullYear: function() {\n      d3_time_prototype.setUTCFullYear.apply(this._, arguments);\n    },\n    setHours: function() {\n      d3_time_prototype.setUTCHours.apply(this._, arguments);\n    },\n    setMilliseconds: function() {\n      d3_time_prototype.setUTCMilliseconds.apply(this._, arguments);\n    },\n    setMinutes: function() {\n      d3_time_prototype.setUTCMinutes.apply(this._, arguments);\n    },\n    setMonth: function() {\n      d3_time_prototype.setUTCMonth.apply(this._, arguments);\n    },\n    setSeconds: function() {\n      d3_time_prototype.setUTCSeconds.apply(this._, arguments);\n    },\n    setTime: function() {\n      d3_time_prototype.setTime.apply(this._, arguments);\n    }\n  };\n  var d3_time_prototype = Date.prototype;\n  function d3_time_interval(local, step, number) {\n    function round(date) {\n      var d0 = local(date), d1 = offset(d0, 1);\n      return date - d0 < d1 - date ? d0 : d1;\n    }\n    function ceil(date) {\n      step(date = local(new d3_date(date - 1)), 1);\n      return date;\n    }\n    function offset(date, k) {\n      step(date = new d3_date(+date), k);\n      return date;\n    }\n    function range(t0, t1, dt) {\n      var time = ceil(t0), times = [];\n      if (dt > 1) {\n        while (time < t1) {\n          if (!(number(time) % dt)) times.push(new Date(+time));\n          step(time, 1);\n        }\n      } else {\n        while (time < t1) times.push(new Date(+time)), step(time, 1);\n      }\n      return times;\n    }\n    function range_utc(t0, t1, dt) {\n      try {\n        d3_date = d3_date_utc;\n        var utc = new d3_date_utc();\n        utc._ = t0;\n        return range(utc, t1, dt);\n      } finally {\n        d3_date = Date;\n      }\n    }\n    local.floor = local;\n    local.round = round;\n    local.ceil = ceil;\n    local.offset = offset;\n    local.range = range;\n    var utc = local.utc = d3_time_interval_utc(local);\n    utc.floor = utc;\n    utc.round = d3_time_interval_utc(round);\n    utc.ceil = d3_time_interval_utc(ceil);\n    utc.offset = d3_time_interval_utc(offset);\n    utc.range = range_utc;\n    return local;\n  }\n  function d3_time_interval_utc(method) {\n    return function(date, k) {\n      try {\n        d3_date = d3_date_utc;\n        var utc = new d3_date_utc();\n        utc._ = date;\n        return method(utc, k)._;\n      } finally {\n        d3_date = Date;\n      }\n    };\n  }\n  d3_time.year = d3_time_interval(function(date) {\n    date = d3_time.day(date);\n    date.setMonth(0, 1);\n    return date;\n  }, function(date, offset) {\n    date.setFullYear(date.getFullYear() + offset);\n  }, function(date) {\n    return date.getFullYear();\n  });\n  d3_time.years = d3_time.year.range;\n  d3_time.years.utc = d3_time.year.utc.range;\n  d3_time.day = d3_time_interval(function(date) {\n    var day = new d3_date(2e3, 0);\n    day.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());\n    return day;\n  }, function(date, offset) {\n    date.setDate(date.getDate() + offset);\n  }, function(date) {\n    return date.getDate() - 1;\n  });\n  d3_time.days = d3_time.day.range;\n  d3_time.days.utc = d3_time.day.utc.range;\n  d3_time.dayOfYear = function(date) {\n    var year = d3_time.year(date);\n    return Math.floor((date - year - (date.getTimezoneOffset() - year.getTimezoneOffset()) * 6e4) / 864e5);\n  };\n  [ \"sunday\", \"monday\", \"tuesday\", \"wednesday\", \"thursday\", \"friday\", \"saturday\" ].forEach(function(day, i) {\n    i = 7 - i;\n    var interval = d3_time[day] = d3_time_interval(function(date) {\n      (date = d3_time.day(date)).setDate(date.getDate() - (date.getDay() + i) % 7);\n      return date;\n    }, function(date, offset) {\n      date.setDate(date.getDate() + Math.floor(offset) * 7);\n    }, function(date) {\n      var day = d3_time.year(date).getDay();\n      return Math.floor((d3_time.dayOfYear(date) + (day + i) % 7) / 7) - (day !== i);\n    });\n    d3_time[day + \"s\"] = interval.range;\n    d3_time[day + \"s\"].utc = interval.utc.range;\n    d3_time[day + \"OfYear\"] = function(date) {\n      var day = d3_time.year(date).getDay();\n      return Math.floor((d3_time.dayOfYear(date) + (day + i) % 7) / 7);\n    };\n  });\n  d3_time.week = d3_time.sunday;\n  d3_time.weeks = d3_time.sunday.range;\n  d3_time.weeks.utc = d3_time.sunday.utc.range;\n  d3_time.weekOfYear = d3_time.sundayOfYear;\n  function d3_locale_timeFormat(locale) {\n    var locale_dateTime = locale.dateTime, locale_date = locale.date, locale_time = locale.time, locale_periods = locale.periods, locale_days = locale.days, locale_shortDays = locale.shortDays, locale_months = locale.months, locale_shortMonths = locale.shortMonths;\n    function d3_time_format(template) {\n      var n = template.length;\n      function format(date) {\n        var string = [], i = -1, j = 0, c, p, f;\n        while (++i < n) {\n          if (template.charCodeAt(i) === 37) {\n            string.push(template.substring(j, i));\n            if ((p = d3_time_formatPads[c = template.charAt(++i)]) != null) c = template.charAt(++i);\n            if (f = d3_time_formats[c]) c = f(date, p == null ? c === \"e\" ? \" \" : \"0\" : p);\n            string.push(c);\n            j = i + 1;\n          }\n        }\n        string.push(template.substring(j, i));\n        return string.join(\"\");\n      }\n      format.parse = function(string) {\n        var d = {\n          y: 1900,\n          m: 0,\n          d: 1,\n          H: 0,\n          M: 0,\n          S: 0,\n          L: 0,\n          Z: null\n        }, i = d3_time_parse(d, template, string, 0);\n        if (i != string.length) return null;\n        if (\"p\" in d) d.H = d.H % 12 + d.p * 12;\n        var localZ = d.Z != null && d3_date !== d3_date_utc, date = new (localZ ? d3_date_utc : d3_date)();\n        if (\"j\" in d) date.setFullYear(d.y, 0, d.j); else if (\"w\" in d && (\"W\" in d || \"U\" in d)) {\n          date.setFullYear(d.y, 0, 1);\n          date.setFullYear(d.y, 0, \"W\" in d ? (d.w + 6) % 7 + d.W * 7 - (date.getDay() + 5) % 7 : d.w + d.U * 7 - (date.getDay() + 6) % 7);\n        } else date.setFullYear(d.y, d.m, d.d);\n        date.setHours(d.H + Math.floor(d.Z / 100), d.M + d.Z % 100, d.S, d.L);\n        return localZ ? date._ : date;\n      };\n      format.toString = function() {\n        return template;\n      };\n      return format;\n    }\n    function d3_time_parse(date, template, string, j) {\n      var c, p, t, i = 0, n = template.length, m = string.length;\n      while (i < n) {\n        if (j >= m) return -1;\n        c = template.charCodeAt(i++);\n        if (c === 37) {\n          t = template.charAt(i++);\n          p = d3_time_parsers[t in d3_time_formatPads ? template.charAt(i++) : t];\n          if (!p || (j = p(date, string, j)) < 0) return -1;\n        } else if (c != string.charCodeAt(j++)) {\n          return -1;\n        }\n      }\n      return j;\n    }\n    d3_time_format.utc = function(template) {\n      var local = d3_time_format(template);\n      function format(date) {\n        try {\n          d3_date = d3_date_utc;\n          var utc = new d3_date();\n          utc._ = date;\n          return local(utc);\n        } finally {\n          d3_date = Date;\n        }\n      }\n      format.parse = function(string) {\n        try {\n          d3_date = d3_date_utc;\n          var date = local.parse(string);\n          return date && date._;\n        } finally {\n          d3_date = Date;\n        }\n      };\n      format.toString = local.toString;\n      return format;\n    };\n    d3_time_format.multi = d3_time_format.utc.multi = d3_time_formatMulti;\n    var d3_time_periodLookup = d3.map(), d3_time_dayRe = d3_time_formatRe(locale_days), d3_time_dayLookup = d3_time_formatLookup(locale_days), d3_time_dayAbbrevRe = d3_time_formatRe(locale_shortDays), d3_time_dayAbbrevLookup = d3_time_formatLookup(locale_shortDays), d3_time_monthRe = d3_time_formatRe(locale_months), d3_time_monthLookup = d3_time_formatLookup(locale_months), d3_time_monthAbbrevRe = d3_time_formatRe(locale_shortMonths), d3_time_monthAbbrevLookup = d3_time_formatLookup(locale_shortMonths);\n    locale_periods.forEach(function(p, i) {\n      d3_time_periodLookup.set(p.toLowerCase(), i);\n    });\n    var d3_time_formats = {\n      a: function(d) {\n        return locale_shortDays[d.getDay()];\n      },\n      A: function(d) {\n        return locale_days[d.getDay()];\n      },\n      b: function(d) {\n        return locale_shortMonths[d.getMonth()];\n      },\n      B: function(d) {\n        return locale_months[d.getMonth()];\n      },\n      c: d3_time_format(locale_dateTime),\n      d: function(d, p) {\n        return d3_time_formatPad(d.getDate(), p, 2);\n      },\n      e: function(d, p) {\n        return d3_time_formatPad(d.getDate(), p, 2);\n      },\n      H: function(d, p) {\n        return d3_time_formatPad(d.getHours(), p, 2);\n      },\n      I: function(d, p) {\n        return d3_time_formatPad(d.getHours() % 12 || 12, p, 2);\n      },\n      j: function(d, p) {\n        return d3_time_formatPad(1 + d3_time.dayOfYear(d), p, 3);\n      },\n      L: function(d, p) {\n        return d3_time_formatPad(d.getMilliseconds(), p, 3);\n      },\n      m: function(d, p) {\n        return d3_time_formatPad(d.getMonth() + 1, p, 2);\n      },\n      M: function(d, p) {\n        return d3_time_formatPad(d.getMinutes(), p, 2);\n      },\n      p: function(d) {\n        return locale_periods[+(d.getHours() >= 12)];\n      },\n      S: function(d, p) {\n        return d3_time_formatPad(d.getSeconds(), p, 2);\n      },\n      U: function(d, p) {\n        return d3_time_formatPad(d3_time.sundayOfYear(d), p, 2);\n      },\n      w: function(d) {\n        return d.getDay();\n      },\n      W: function(d, p) {\n        return d3_time_formatPad(d3_time.mondayOfYear(d), p, 2);\n      },\n      x: d3_time_format(locale_date),\n      X: d3_time_format(locale_time),\n      y: function(d, p) {\n        return d3_time_formatPad(d.getFullYear() % 100, p, 2);\n      },\n      Y: function(d, p) {\n        return d3_time_formatPad(d.getFullYear() % 1e4, p, 4);\n      },\n      Z: d3_time_zone,\n      \"%\": function() {\n        return \"%\";\n      }\n    };\n    var d3_time_parsers = {\n      a: d3_time_parseWeekdayAbbrev,\n      A: d3_time_parseWeekday,\n      b: d3_time_parseMonthAbbrev,\n      B: d3_time_parseMonth,\n      c: d3_time_parseLocaleFull,\n      d: d3_time_parseDay,\n      e: d3_time_parseDay,\n      H: d3_time_parseHour24,\n      I: d3_time_parseHour24,\n      j: d3_time_parseDayOfYear,\n      L: d3_time_parseMilliseconds,\n      m: d3_time_parseMonthNumber,\n      M: d3_time_parseMinutes,\n      p: d3_time_parseAmPm,\n      S: d3_time_parseSeconds,\n      U: d3_time_parseWeekNumberSunday,\n      w: d3_time_parseWeekdayNumber,\n      W: d3_time_parseWeekNumberMonday,\n      x: d3_time_parseLocaleDate,\n      X: d3_time_parseLocaleTime,\n      y: d3_time_parseYear,\n      Y: d3_time_parseFullYear,\n      Z: d3_time_parseZone,\n      \"%\": d3_time_parseLiteralPercent\n    };\n    function d3_time_parseWeekdayAbbrev(date, string, i) {\n      d3_time_dayAbbrevRe.lastIndex = 0;\n      var n = d3_time_dayAbbrevRe.exec(string.substring(i));\n      return n ? (date.w = d3_time_dayAbbrevLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;\n    }\n    function d3_time_parseWeekday(date, string, i) {\n      d3_time_dayRe.lastIndex = 0;\n      var n = d3_time_dayRe.exec(string.substring(i));\n      return n ? (date.w = d3_time_dayLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;\n    }\n    function d3_time_parseMonthAbbrev(date, string, i) {\n      d3_time_monthAbbrevRe.lastIndex = 0;\n      var n = d3_time_monthAbbrevRe.exec(string.substring(i));\n      return n ? (date.m = d3_time_monthAbbrevLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;\n    }\n    function d3_time_parseMonth(date, string, i) {\n      d3_time_monthRe.lastIndex = 0;\n      var n = d3_time_monthRe.exec(string.substring(i));\n      return n ? (date.m = d3_time_monthLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;\n    }\n    function d3_time_parseLocaleFull(date, string, i) {\n      return d3_time_parse(date, d3_time_formats.c.toString(), string, i);\n    }\n    function d3_time_parseLocaleDate(date, string, i) {\n      return d3_time_parse(date, d3_time_formats.x.toString(), string, i);\n    }\n    function d3_time_parseLocaleTime(date, string, i) {\n      return d3_time_parse(date, d3_time_formats.X.toString(), string, i);\n    }\n    function d3_time_parseAmPm(date, string, i) {\n      var n = d3_time_periodLookup.get(string.substring(i, i += 2).toLowerCase());\n      return n == null ? -1 : (date.p = n, i);\n    }\n    return d3_time_format;\n  }\n  var d3_time_formatPads = {\n    \"-\": \"\",\n    _: \" \",\n    \"0\": \"0\"\n  }, d3_time_numberRe = /^\\s*\\d+/, d3_time_percentRe = /^%/;\n  function d3_time_formatPad(value, fill, width) {\n    var sign = value < 0 ? \"-\" : \"\", string = (sign ? -value : value) + \"\", length = string.length;\n    return sign + (length < width ? new Array(width - length + 1).join(fill) + string : string);\n  }\n  function d3_time_formatRe(names) {\n    return new RegExp(\"^(?:\" + names.map(d3.requote).join(\"|\") + \")\", \"i\");\n  }\n  function d3_time_formatLookup(names) {\n    var map = new d3_Map(), i = -1, n = names.length;\n    while (++i < n) map.set(names[i].toLowerCase(), i);\n    return map;\n  }\n  function d3_time_parseWeekdayNumber(date, string, i) {\n    d3_time_numberRe.lastIndex = 0;\n    var n = d3_time_numberRe.exec(string.substring(i, i + 1));\n    return n ? (date.w = +n[0], i + n[0].length) : -1;\n  }\n  function d3_time_parseWeekNumberSunday(date, string, i) {\n    d3_time_numberRe.lastIndex = 0;\n    var n = d3_time_numberRe.exec(string.substring(i));\n    return n ? (date.U = +n[0], i + n[0].length) : -1;\n  }\n  function d3_time_parseWeekNumberMonday(date, string, i) {\n    d3_time_numberRe.lastIndex = 0;\n    var n = d3_time_numberRe.exec(string.substring(i));\n    return n ? (date.W = +n[0], i + n[0].length) : -1;\n  }\n  function d3_time_parseFullYear(date, string, i) {\n    d3_time_numberRe.lastIndex = 0;\n    var n = d3_time_numberRe.exec(string.substring(i, i + 4));\n    return n ? (date.y = +n[0], i + n[0].length) : -1;\n  }\n  function d3_time_parseYear(date, string, i) {\n    d3_time_numberRe.lastIndex = 0;\n    var n = d3_time_numberRe.exec(string.substring(i, i + 2));\n    return n ? (date.y = d3_time_expandYear(+n[0]), i + n[0].length) : -1;\n  }\n  function d3_time_parseZone(date, string, i) {\n    return /^[+-]\\d{4}$/.test(string = string.substring(i, i + 5)) ? (date.Z = -string, \n    i + 5) : -1;\n  }\n  function d3_time_expandYear(d) {\n    return d + (d > 68 ? 1900 : 2e3);\n  }\n  function d3_time_parseMonthNumber(date, string, i) {\n    d3_time_numberRe.lastIndex = 0;\n    var n = d3_time_numberRe.exec(string.substring(i, i + 2));\n    return n ? (date.m = n[0] - 1, i + n[0].length) : -1;\n  }\n  function d3_time_parseDay(date, string, i) {\n    d3_time_numberRe.lastIndex = 0;\n    var n = d3_time_numberRe.exec(string.substring(i, i + 2));\n    return n ? (date.d = +n[0], i + n[0].length) : -1;\n  }\n  function d3_time_parseDayOfYear(date, string, i) {\n    d3_time_numberRe.lastIndex = 0;\n    var n = d3_time_numberRe.exec(string.substring(i, i + 3));\n    return n ? (date.j = +n[0], i + n[0].length) : -1;\n  }\n  function d3_time_parseHour24(date, string, i) {\n    d3_time_numberRe.lastIndex = 0;\n    var n = d3_time_numberRe.exec(string.substring(i, i + 2));\n    return n ? (date.H = +n[0], i + n[0].length) : -1;\n  }\n  function d3_time_parseMinutes(date, string, i) {\n    d3_time_numberRe.lastIndex = 0;\n    var n = d3_time_numberRe.exec(string.substring(i, i + 2));\n    return n ? (date.M = +n[0], i + n[0].length) : -1;\n  }\n  function d3_time_parseSeconds(date, string, i) {\n    d3_time_numberRe.lastIndex = 0;\n    var n = d3_time_numberRe.exec(string.substring(i, i + 2));\n    return n ? (date.S = +n[0], i + n[0].length) : -1;\n  }\n  function d3_time_parseMilliseconds(date, string, i) {\n    d3_time_numberRe.lastIndex = 0;\n    var n = d3_time_numberRe.exec(string.substring(i, i + 3));\n    return n ? (date.L = +n[0], i + n[0].length) : -1;\n  }\n  function d3_time_zone(d) {\n    var z = d.getTimezoneOffset(), zs = z > 0 ? \"-\" : \"+\", zh = ~~(abs(z) / 60), zm = abs(z) % 60;\n    return zs + d3_time_formatPad(zh, \"0\", 2) + d3_time_formatPad(zm, \"0\", 2);\n  }\n  function d3_time_parseLiteralPercent(date, string, i) {\n    d3_time_percentRe.lastIndex = 0;\n    var n = d3_time_percentRe.exec(string.substring(i, i + 1));\n    return n ? i + n[0].length : -1;\n  }\n  function d3_time_formatMulti(formats) {\n    var n = formats.length, i = -1;\n    while (++i < n) formats[i][0] = this(formats[i][0]);\n    return function(date) {\n      var i = 0, f = formats[i];\n      while (!f[1](date)) f = formats[++i];\n      return f[0](date);\n    };\n  }\n  d3.locale = function(locale) {\n    return {\n      numberFormat: d3_locale_numberFormat(locale),\n      timeFormat: d3_locale_timeFormat(locale)\n    };\n  };\n  var d3_locale_enUS = d3.locale({\n    decimal: \".\",\n    thousands: \",\",\n    grouping: [ 3 ],\n    currency: [ \"$\", \"\" ],\n    dateTime: \"%a %b %e %X %Y\",\n    date: \"%m/%d/%Y\",\n    time: \"%H:%M:%S\",\n    periods: [ \"AM\", \"PM\" ],\n    days: [ \"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\" ],\n    shortDays: [ \"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\" ],\n    months: [ \"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\" ],\n    shortMonths: [ \"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\" ]\n  });\n  d3.format = d3_locale_enUS.numberFormat;\n  d3.geo = {};\n  function d3_adder() {}\n  d3_adder.prototype = {\n    s: 0,\n    t: 0,\n    add: function(y) {\n      d3_adderSum(y, this.t, d3_adderTemp);\n      d3_adderSum(d3_adderTemp.s, this.s, this);\n      if (this.s) this.t += d3_adderTemp.t; else this.s = d3_adderTemp.t;\n    },\n    reset: function() {\n      this.s = this.t = 0;\n    },\n    valueOf: function() {\n      return this.s;\n    }\n  };\n  var d3_adderTemp = new d3_adder();\n  function d3_adderSum(a, b, o) {\n    var x = o.s = a + b, bv = x - a, av = x - bv;\n    o.t = a - av + (b - bv);\n  }\n  d3.geo.stream = function(object, listener) {\n    if (object && d3_geo_streamObjectType.hasOwnProperty(object.type)) {\n      d3_geo_streamObjectType[object.type](object, listener);\n    } else {\n      d3_geo_streamGeometry(object, listener);\n    }\n  };\n  function d3_geo_streamGeometry(geometry, listener) {\n    if (geometry && d3_geo_streamGeometryType.hasOwnProperty(geometry.type)) {\n      d3_geo_streamGeometryType[geometry.type](geometry, listener);\n    }\n  }\n  var d3_geo_streamObjectType = {\n    Feature: function(feature, listener) {\n      d3_geo_streamGeometry(feature.geometry, listener);\n    },\n    FeatureCollection: function(object, listener) {\n      var features = object.features, i = -1, n = features.length;\n      while (++i < n) d3_geo_streamGeometry(features[i].geometry, listener);\n    }\n  };\n  var d3_geo_streamGeometryType = {\n    Sphere: function(object, listener) {\n      listener.sphere();\n    },\n    Point: function(object, listener) {\n      object = object.coordinates;\n      listener.point(object[0], object[1], object[2]);\n    },\n    MultiPoint: function(object, listener) {\n      var coordinates = object.coordinates, i = -1, n = coordinates.length;\n      while (++i < n) object = coordinates[i], listener.point(object[0], object[1], object[2]);\n    },\n    LineString: function(object, listener) {\n      d3_geo_streamLine(object.coordinates, listener, 0);\n    },\n    MultiLineString: function(object, listener) {\n      var coordinates = object.coordinates, i = -1, n = coordinates.length;\n      while (++i < n) d3_geo_streamLine(coordinates[i], listener, 0);\n    },\n    Polygon: function(object, listener) {\n      d3_geo_streamPolygon(object.coordinates, listener);\n    },\n    MultiPolygon: function(object, listener) {\n      var coordinates = object.coordinates, i = -1, n = coordinates.length;\n      while (++i < n) d3_geo_streamPolygon(coordinates[i], listener);\n    },\n    GeometryCollection: function(object, listener) {\n      var geometries = object.geometries, i = -1, n = geometries.length;\n      while (++i < n) d3_geo_streamGeometry(geometries[i], listener);\n    }\n  };\n  function d3_geo_streamLine(coordinates, listener, closed) {\n    var i = -1, n = coordinates.length - closed, coordinate;\n    listener.lineStart();\n    while (++i < n) coordinate = coordinates[i], listener.point(coordinate[0], coordinate[1], coordinate[2]);\n    listener.lineEnd();\n  }\n  function d3_geo_streamPolygon(coordinates, listener) {\n    var i = -1, n = coordinates.length;\n    listener.polygonStart();\n    while (++i < n) d3_geo_streamLine(coordinates[i], listener, 1);\n    listener.polygonEnd();\n  }\n  d3.geo.area = function(object) {\n    d3_geo_areaSum = 0;\n    d3.geo.stream(object, d3_geo_area);\n    return d3_geo_areaSum;\n  };\n  var d3_geo_areaSum, d3_geo_areaRingSum = new d3_adder();\n  var d3_geo_area = {\n    sphere: function() {\n      d3_geo_areaSum += 4 * π;\n    },\n    point: d3_noop,\n    lineStart: d3_noop,\n    lineEnd: d3_noop,\n    polygonStart: function() {\n      d3_geo_areaRingSum.reset();\n      d3_geo_area.lineStart = d3_geo_areaRingStart;\n    },\n    polygonEnd: function() {\n      var area = 2 * d3_geo_areaRingSum;\n      d3_geo_areaSum += area < 0 ? 4 * π + area : area;\n      d3_geo_area.lineStart = d3_geo_area.lineEnd = d3_geo_area.point = d3_noop;\n    }\n  };\n  function d3_geo_areaRingStart() {\n    var λ00, φ00, λ0, cosφ0, sinφ0;\n    d3_geo_area.point = function(λ, φ) {\n      d3_geo_area.point = nextPoint;\n      λ0 = (λ00 = λ) * d3_radians, cosφ0 = Math.cos(φ = (φ00 = φ) * d3_radians / 2 + π / 4), \n      sinφ0 = Math.sin(φ);\n    };\n    function nextPoint(λ, φ) {\n      λ *= d3_radians;\n      φ = φ * d3_radians / 2 + π / 4;\n      var dλ = λ - λ0, sdλ = dλ >= 0 ? 1 : -1, adλ = sdλ * dλ, cosφ = Math.cos(φ), sinφ = Math.sin(φ), k = sinφ0 * sinφ, u = cosφ0 * cosφ + k * Math.cos(adλ), v = k * sdλ * Math.sin(adλ);\n      d3_geo_areaRingSum.add(Math.atan2(v, u));\n      λ0 = λ, cosφ0 = cosφ, sinφ0 = sinφ;\n    }\n    d3_geo_area.lineEnd = function() {\n      nextPoint(λ00, φ00);\n    };\n  }\n  function d3_geo_cartesian(spherical) {\n    var λ = spherical[0], φ = spherical[1], cosφ = Math.cos(φ);\n    return [ cosφ * Math.cos(λ), cosφ * Math.sin(λ), Math.sin(φ) ];\n  }\n  function d3_geo_cartesianDot(a, b) {\n    return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];\n  }\n  function d3_geo_cartesianCross(a, b) {\n    return [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ];\n  }\n  function d3_geo_cartesianAdd(a, b) {\n    a[0] += b[0];\n    a[1] += b[1];\n    a[2] += b[2];\n  }\n  function d3_geo_cartesianScale(vector, k) {\n    return [ vector[0] * k, vector[1] * k, vector[2] * k ];\n  }\n  function d3_geo_cartesianNormalize(d) {\n    var l = Math.sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);\n    d[0] /= l;\n    d[1] /= l;\n    d[2] /= l;\n  }\n  function d3_geo_spherical(cartesian) {\n    return [ Math.atan2(cartesian[1], cartesian[0]), d3_asin(cartesian[2]) ];\n  }\n  function d3_geo_sphericalEqual(a, b) {\n    return abs(a[0] - b[0]) < ε && abs(a[1] - b[1]) < ε;\n  }\n  d3.geo.bounds = function() {\n    var λ0, φ0, λ1, φ1, λ_, λ__, φ__, p0, dλSum, ranges, range;\n    var bound = {\n      point: point,\n      lineStart: lineStart,\n      lineEnd: lineEnd,\n      polygonStart: function() {\n        bound.point = ringPoint;\n        bound.lineStart = ringStart;\n        bound.lineEnd = ringEnd;\n        dλSum = 0;\n        d3_geo_area.polygonStart();\n      },\n      polygonEnd: function() {\n        d3_geo_area.polygonEnd();\n        bound.point = point;\n        bound.lineStart = lineStart;\n        bound.lineEnd = lineEnd;\n        if (d3_geo_areaRingSum < 0) λ0 = -(λ1 = 180), φ0 = -(φ1 = 90); else if (dλSum > ε) φ1 = 90; else if (dλSum < -ε) φ0 = -90;\n        range[0] = λ0, range[1] = λ1;\n      }\n    };\n    function point(λ, φ) {\n      ranges.push(range = [ λ0 = λ, λ1 = λ ]);\n      if (φ < φ0) φ0 = φ;\n      if (φ > φ1) φ1 = φ;\n    }\n    function linePoint(λ, φ) {\n      var p = d3_geo_cartesian([ λ * d3_radians, φ * d3_radians ]);\n      if (p0) {\n        var normal = d3_geo_cartesianCross(p0, p), equatorial = [ normal[1], -normal[0], 0 ], inflection = d3_geo_cartesianCross(equatorial, normal);\n        d3_geo_cartesianNormalize(inflection);\n        inflection = d3_geo_spherical(inflection);\n        var dλ = λ - λ_, s = dλ > 0 ? 1 : -1, λi = inflection[0] * d3_degrees * s, antimeridian = abs(dλ) > 180;\n        if (antimeridian ^ (s * λ_ < λi && λi < s * λ)) {\n          var φi = inflection[1] * d3_degrees;\n          if (φi > φ1) φ1 = φi;\n        } else if (λi = (λi + 360) % 360 - 180, antimeridian ^ (s * λ_ < λi && λi < s * λ)) {\n          var φi = -inflection[1] * d3_degrees;\n          if (φi < φ0) φ0 = φi;\n        } else {\n          if (φ < φ0) φ0 = φ;\n          if (φ > φ1) φ1 = φ;\n        }\n        if (antimeridian) {\n          if (λ < λ_) {\n            if (angle(λ0, λ) > angle(λ0, λ1)) λ1 = λ;\n          } else {\n            if (angle(λ, λ1) > angle(λ0, λ1)) λ0 = λ;\n          }\n        } else {\n          if (λ1 >= λ0) {\n            if (λ < λ0) λ0 = λ;\n            if (λ > λ1) λ1 = λ;\n          } else {\n            if (λ > λ_) {\n              if (angle(λ0, λ) > angle(λ0, λ1)) λ1 = λ;\n            } else {\n              if (angle(λ, λ1) > angle(λ0, λ1)) λ0 = λ;\n            }\n          }\n        }\n      } else {\n        point(λ, φ);\n      }\n      p0 = p, λ_ = λ;\n    }\n    function lineStart() {\n      bound.point = linePoint;\n    }\n    function lineEnd() {\n      range[0] = λ0, range[1] = λ1;\n      bound.point = point;\n      p0 = null;\n    }\n    function ringPoint(λ, φ) {\n      if (p0) {\n        var dλ = λ - λ_;\n        dλSum += abs(dλ) > 180 ? dλ + (dλ > 0 ? 360 : -360) : dλ;\n      } else λ__ = λ, φ__ = φ;\n      d3_geo_area.point(λ, φ);\n      linePoint(λ, φ);\n    }\n    function ringStart() {\n      d3_geo_area.lineStart();\n    }\n    function ringEnd() {\n      ringPoint(λ__, φ__);\n      d3_geo_area.lineEnd();\n      if (abs(dλSum) > ε) λ0 = -(λ1 = 180);\n      range[0] = λ0, range[1] = λ1;\n      p0 = null;\n    }\n    function angle(λ0, λ1) {\n      return (λ1 -= λ0) < 0 ? λ1 + 360 : λ1;\n    }\n    function compareRanges(a, b) {\n      return a[0] - b[0];\n    }\n    function withinRange(x, range) {\n      return range[0] <= range[1] ? range[0] <= x && x <= range[1] : x < range[0] || range[1] < x;\n    }\n    return function(feature) {\n      φ1 = λ1 = -(λ0 = φ0 = Infinity);\n      ranges = [];\n      d3.geo.stream(feature, bound);\n      var n = ranges.length;\n      if (n) {\n        ranges.sort(compareRanges);\n        for (var i = 1, a = ranges[0], b, merged = [ a ]; i < n; ++i) {\n          b = ranges[i];\n          if (withinRange(b[0], a) || withinRange(b[1], a)) {\n            if (angle(a[0], b[1]) > angle(a[0], a[1])) a[1] = b[1];\n            if (angle(b[0], a[1]) > angle(a[0], a[1])) a[0] = b[0];\n          } else {\n            merged.push(a = b);\n          }\n        }\n        var best = -Infinity, dλ;\n        for (var n = merged.length - 1, i = 0, a = merged[n], b; i <= n; a = b, ++i) {\n          b = merged[i];\n          if ((dλ = angle(a[1], b[0])) > best) best = dλ, λ0 = b[0], λ1 = a[1];\n        }\n      }\n      ranges = range = null;\n      return λ0 === Infinity || φ0 === Infinity ? [ [ NaN, NaN ], [ NaN, NaN ] ] : [ [ λ0, φ0 ], [ λ1, φ1 ] ];\n    };\n  }();\n  d3.geo.centroid = function(object) {\n    d3_geo_centroidW0 = d3_geo_centroidW1 = d3_geo_centroidX0 = d3_geo_centroidY0 = d3_geo_centroidZ0 = d3_geo_centroidX1 = d3_geo_centroidY1 = d3_geo_centroidZ1 = d3_geo_centroidX2 = d3_geo_centroidY2 = d3_geo_centroidZ2 = 0;\n    d3.geo.stream(object, d3_geo_centroid);\n    var x = d3_geo_centroidX2, y = d3_geo_centroidY2, z = d3_geo_centroidZ2, m = x * x + y * y + z * z;\n    if (m < ε2) {\n      x = d3_geo_centroidX1, y = d3_geo_centroidY1, z = d3_geo_centroidZ1;\n      if (d3_geo_centroidW1 < ε) x = d3_geo_centroidX0, y = d3_geo_centroidY0, z = d3_geo_centroidZ0;\n      m = x * x + y * y + z * z;\n      if (m < ε2) return [ NaN, NaN ];\n    }\n    return [ Math.atan2(y, x) * d3_degrees, d3_asin(z / Math.sqrt(m)) * d3_degrees ];\n  };\n  var d3_geo_centroidW0, d3_geo_centroidW1, d3_geo_centroidX0, d3_geo_centroidY0, d3_geo_centroidZ0, d3_geo_centroidX1, d3_geo_centroidY1, d3_geo_centroidZ1, d3_geo_centroidX2, d3_geo_centroidY2, d3_geo_centroidZ2;\n  var d3_geo_centroid = {\n    sphere: d3_noop,\n    point: d3_geo_centroidPoint,\n    lineStart: d3_geo_centroidLineStart,\n    lineEnd: d3_geo_centroidLineEnd,\n    polygonStart: function() {\n      d3_geo_centroid.lineStart = d3_geo_centroidRingStart;\n    },\n    polygonEnd: function() {\n      d3_geo_centroid.lineStart = d3_geo_centroidLineStart;\n    }\n  };\n  function d3_geo_centroidPoint(λ, φ) {\n    λ *= d3_radians;\n    var cosφ = Math.cos(φ *= d3_radians);\n    d3_geo_centroidPointXYZ(cosφ * Math.cos(λ), cosφ * Math.sin(λ), Math.sin(φ));\n  }\n  function d3_geo_centroidPointXYZ(x, y, z) {\n    ++d3_geo_centroidW0;\n    d3_geo_centroidX0 += (x - d3_geo_centroidX0) / d3_geo_centroidW0;\n    d3_geo_centroidY0 += (y - d3_geo_centroidY0) / d3_geo_centroidW0;\n    d3_geo_centroidZ0 += (z - d3_geo_centroidZ0) / d3_geo_centroidW0;\n  }\n  function d3_geo_centroidLineStart() {\n    var x0, y0, z0;\n    d3_geo_centroid.point = function(λ, φ) {\n      λ *= d3_radians;\n      var cosφ = Math.cos(φ *= d3_radians);\n      x0 = cosφ * Math.cos(λ);\n      y0 = cosφ * Math.sin(λ);\n      z0 = Math.sin(φ);\n      d3_geo_centroid.point = nextPoint;\n      d3_geo_centroidPointXYZ(x0, y0, z0);\n    };\n    function nextPoint(λ, φ) {\n      λ *= d3_radians;\n      var cosφ = Math.cos(φ *= d3_radians), x = cosφ * Math.cos(λ), y = cosφ * Math.sin(λ), z = Math.sin(φ), w = Math.atan2(Math.sqrt((w = y0 * z - z0 * y) * w + (w = z0 * x - x0 * z) * w + (w = x0 * y - y0 * x) * w), x0 * x + y0 * y + z0 * z);\n      d3_geo_centroidW1 += w;\n      d3_geo_centroidX1 += w * (x0 + (x0 = x));\n      d3_geo_centroidY1 += w * (y0 + (y0 = y));\n      d3_geo_centroidZ1 += w * (z0 + (z0 = z));\n      d3_geo_centroidPointXYZ(x0, y0, z0);\n    }\n  }\n  function d3_geo_centroidLineEnd() {\n    d3_geo_centroid.point = d3_geo_centroidPoint;\n  }\n  function d3_geo_centroidRingStart() {\n    var λ00, φ00, x0, y0, z0;\n    d3_geo_centroid.point = function(λ, φ) {\n      λ00 = λ, φ00 = φ;\n      d3_geo_centroid.point = nextPoint;\n      λ *= d3_radians;\n      var cosφ = Math.cos(φ *= d3_radians);\n      x0 = cosφ * Math.cos(λ);\n      y0 = cosφ * Math.sin(λ);\n      z0 = Math.sin(φ);\n      d3_geo_centroidPointXYZ(x0, y0, z0);\n    };\n    d3_geo_centroid.lineEnd = function() {\n      nextPoint(λ00, φ00);\n      d3_geo_centroid.lineEnd = d3_geo_centroidLineEnd;\n      d3_geo_centroid.point = d3_geo_centroidPoint;\n    };\n    function nextPoint(λ, φ) {\n      λ *= d3_radians;\n      var cosφ = Math.cos(φ *= d3_radians), x = cosφ * Math.cos(λ), y = cosφ * Math.sin(λ), z = Math.sin(φ), cx = y0 * z - z0 * y, cy = z0 * x - x0 * z, cz = x0 * y - y0 * x, m = Math.sqrt(cx * cx + cy * cy + cz * cz), u = x0 * x + y0 * y + z0 * z, v = m && -d3_acos(u) / m, w = Math.atan2(m, u);\n      d3_geo_centroidX2 += v * cx;\n      d3_geo_centroidY2 += v * cy;\n      d3_geo_centroidZ2 += v * cz;\n      d3_geo_centroidW1 += w;\n      d3_geo_centroidX1 += w * (x0 + (x0 = x));\n      d3_geo_centroidY1 += w * (y0 + (y0 = y));\n      d3_geo_centroidZ1 += w * (z0 + (z0 = z));\n      d3_geo_centroidPointXYZ(x0, y0, z0);\n    }\n  }\n  function d3_true() {\n    return true;\n  }\n  function d3_geo_clipPolygon(segments, compare, clipStartInside, interpolate, listener) {\n    var subject = [], clip = [];\n    segments.forEach(function(segment) {\n      if ((n = segment.length - 1) <= 0) return;\n      var n, p0 = segment[0], p1 = segment[n];\n      if (d3_geo_sphericalEqual(p0, p1)) {\n        listener.lineStart();\n        for (var i = 0; i < n; ++i) listener.point((p0 = segment[i])[0], p0[1]);\n        listener.lineEnd();\n        return;\n      }\n      var a = new d3_geo_clipPolygonIntersection(p0, segment, null, true), b = new d3_geo_clipPolygonIntersection(p0, null, a, false);\n      a.o = b;\n      subject.push(a);\n      clip.push(b);\n      a = new d3_geo_clipPolygonIntersection(p1, segment, null, false);\n      b = new d3_geo_clipPolygonIntersection(p1, null, a, true);\n      a.o = b;\n      subject.push(a);\n      clip.push(b);\n    });\n    clip.sort(compare);\n    d3_geo_clipPolygonLinkCircular(subject);\n    d3_geo_clipPolygonLinkCircular(clip);\n    if (!subject.length) return;\n    for (var i = 0, entry = clipStartInside, n = clip.length; i < n; ++i) {\n      clip[i].e = entry = !entry;\n    }\n    var start = subject[0], points, point;\n    while (1) {\n      var current = start, isSubject = true;\n      while (current.v) if ((current = current.n) === start) return;\n      points = current.z;\n      listener.lineStart();\n      do {\n        current.v = current.o.v = true;\n        if (current.e) {\n          if (isSubject) {\n            for (var i = 0, n = points.length; i < n; ++i) listener.point((point = points[i])[0], point[1]);\n          } else {\n            interpolate(current.x, current.n.x, 1, listener);\n          }\n          current = current.n;\n        } else {\n          if (isSubject) {\n            points = current.p.z;\n            for (var i = points.length - 1; i >= 0; --i) listener.point((point = points[i])[0], point[1]);\n          } else {\n            interpolate(current.x, current.p.x, -1, listener);\n          }\n          current = current.p;\n        }\n        current = current.o;\n        points = current.z;\n        isSubject = !isSubject;\n      } while (!current.v);\n      listener.lineEnd();\n    }\n  }\n  function d3_geo_clipPolygonLinkCircular(array) {\n    if (!(n = array.length)) return;\n    var n, i = 0, a = array[0], b;\n    while (++i < n) {\n      a.n = b = array[i];\n      b.p = a;\n      a = b;\n    }\n    a.n = b = array[0];\n    b.p = a;\n  }\n  function d3_geo_clipPolygonIntersection(point, points, other, entry) {\n    this.x = point;\n    this.z = points;\n    this.o = other;\n    this.e = entry;\n    this.v = false;\n    this.n = this.p = null;\n  }\n  function d3_geo_clip(pointVisible, clipLine, interpolate, clipStart) {\n    return function(rotate, listener) {\n      var line = clipLine(listener), rotatedClipStart = rotate.invert(clipStart[0], clipStart[1]);\n      var clip = {\n        point: point,\n        lineStart: lineStart,\n        lineEnd: lineEnd,\n        polygonStart: function() {\n          clip.point = pointRing;\n          clip.lineStart = ringStart;\n          clip.lineEnd = ringEnd;\n          segments = [];\n          polygon = [];\n        },\n        polygonEnd: function() {\n          clip.point = point;\n          clip.lineStart = lineStart;\n          clip.lineEnd = lineEnd;\n          segments = d3.merge(segments);\n          var clipStartInside = d3_geo_pointInPolygon(rotatedClipStart, polygon);\n          if (segments.length) {\n            if (!polygonStarted) listener.polygonStart(), polygonStarted = true;\n            d3_geo_clipPolygon(segments, d3_geo_clipSort, clipStartInside, interpolate, listener);\n          } else if (clipStartInside) {\n            if (!polygonStarted) listener.polygonStart(), polygonStarted = true;\n            listener.lineStart();\n            interpolate(null, null, 1, listener);\n            listener.lineEnd();\n          }\n          if (polygonStarted) listener.polygonEnd(), polygonStarted = false;\n          segments = polygon = null;\n        },\n        sphere: function() {\n          listener.polygonStart();\n          listener.lineStart();\n          interpolate(null, null, 1, listener);\n          listener.lineEnd();\n          listener.polygonEnd();\n        }\n      };\n      function point(λ, φ) {\n        var point = rotate(λ, φ);\n        if (pointVisible(λ = point[0], φ = point[1])) listener.point(λ, φ);\n      }\n      function pointLine(λ, φ) {\n        var point = rotate(λ, φ);\n        line.point(point[0], point[1]);\n      }\n      function lineStart() {\n        clip.point = pointLine;\n        line.lineStart();\n      }\n      function lineEnd() {\n        clip.point = point;\n        line.lineEnd();\n      }\n      var segments;\n      var buffer = d3_geo_clipBufferListener(), ringListener = clipLine(buffer), polygonStarted = false, polygon, ring;\n      function pointRing(λ, φ) {\n        ring.push([ λ, φ ]);\n        var point = rotate(λ, φ);\n        ringListener.point(point[0], point[1]);\n      }\n      function ringStart() {\n        ringListener.lineStart();\n        ring = [];\n      }\n      function ringEnd() {\n        pointRing(ring[0][0], ring[0][1]);\n        ringListener.lineEnd();\n        var clean = ringListener.clean(), ringSegments = buffer.buffer(), segment, n = ringSegments.length;\n        ring.pop();\n        polygon.push(ring);\n        ring = null;\n        if (!n) return;\n        if (clean & 1) {\n          segment = ringSegments[0];\n          var n = segment.length - 1, i = -1, point;\n          if (n > 0) {\n            if (!polygonStarted) listener.polygonStart(), polygonStarted = true;\n            listener.lineStart();\n            while (++i < n) listener.point((point = segment[i])[0], point[1]);\n            listener.lineEnd();\n          }\n          return;\n        }\n        if (n > 1 && clean & 2) ringSegments.push(ringSegments.pop().concat(ringSegments.shift()));\n        segments.push(ringSegments.filter(d3_geo_clipSegmentLength1));\n      }\n      return clip;\n    };\n  }\n  function d3_geo_clipSegmentLength1(segment) {\n    return segment.length > 1;\n  }\n  function d3_geo_clipBufferListener() {\n    var lines = [], line;\n    return {\n      lineStart: function() {\n        lines.push(line = []);\n      },\n      point: function(λ, φ) {\n        line.push([ λ, φ ]);\n      },\n      lineEnd: d3_noop,\n      buffer: function() {\n        var buffer = lines;\n        lines = [];\n        line = null;\n        return buffer;\n      },\n      rejoin: function() {\n        if (lines.length > 1) lines.push(lines.pop().concat(lines.shift()));\n      }\n    };\n  }\n  function d3_geo_clipSort(a, b) {\n    return ((a = a.x)[0] < 0 ? a[1] - halfπ - ε : halfπ - a[1]) - ((b = b.x)[0] < 0 ? b[1] - halfπ - ε : halfπ - b[1]);\n  }\n  function d3_geo_pointInPolygon(point, polygon) {\n    var meridian = point[0], parallel = point[1], meridianNormal = [ Math.sin(meridian), -Math.cos(meridian), 0 ], polarAngle = 0, winding = 0;\n    d3_geo_areaRingSum.reset();\n    for (var i = 0, n = polygon.length; i < n; ++i) {\n      var ring = polygon[i], m = ring.length;\n      if (!m) continue;\n      var point0 = ring[0], λ0 = point0[0], φ0 = point0[1] / 2 + π / 4, sinφ0 = Math.sin(φ0), cosφ0 = Math.cos(φ0), j = 1;\n      while (true) {\n        if (j === m) j = 0;\n        point = ring[j];\n        var λ = point[0], φ = point[1] / 2 + π / 4, sinφ = Math.sin(φ), cosφ = Math.cos(φ), dλ = λ - λ0, sdλ = dλ >= 0 ? 1 : -1, adλ = sdλ * dλ, antimeridian = adλ > π, k = sinφ0 * sinφ;\n        d3_geo_areaRingSum.add(Math.atan2(k * sdλ * Math.sin(adλ), cosφ0 * cosφ + k * Math.cos(adλ)));\n        polarAngle += antimeridian ? dλ + sdλ * τ : dλ;\n        if (antimeridian ^ λ0 >= meridian ^ λ >= meridian) {\n          var arc = d3_geo_cartesianCross(d3_geo_cartesian(point0), d3_geo_cartesian(point));\n          d3_geo_cartesianNormalize(arc);\n          var intersection = d3_geo_cartesianCross(meridianNormal, arc);\n          d3_geo_cartesianNormalize(intersection);\n          var φarc = (antimeridian ^ dλ >= 0 ? -1 : 1) * d3_asin(intersection[2]);\n          if (parallel > φarc || parallel === φarc && (arc[0] || arc[1])) {\n            winding += antimeridian ^ dλ >= 0 ? 1 : -1;\n          }\n        }\n        if (!j++) break;\n        λ0 = λ, sinφ0 = sinφ, cosφ0 = cosφ, point0 = point;\n      }\n    }\n    return (polarAngle < -ε || polarAngle < ε && d3_geo_areaRingSum < 0) ^ winding & 1;\n  }\n  var d3_geo_clipAntimeridian = d3_geo_clip(d3_true, d3_geo_clipAntimeridianLine, d3_geo_clipAntimeridianInterpolate, [ -π, -π / 2 ]);\n  function d3_geo_clipAntimeridianLine(listener) {\n    var λ0 = NaN, φ0 = NaN, sλ0 = NaN, clean;\n    return {\n      lineStart: function() {\n        listener.lineStart();\n        clean = 1;\n      },\n      point: function(λ1, φ1) {\n        var sλ1 = λ1 > 0 ? π : -π, dλ = abs(λ1 - λ0);\n        if (abs(dλ - π) < ε) {\n          listener.point(λ0, φ0 = (φ0 + φ1) / 2 > 0 ? halfπ : -halfπ);\n          listener.point(sλ0, φ0);\n          listener.lineEnd();\n          listener.lineStart();\n          listener.point(sλ1, φ0);\n          listener.point(λ1, φ0);\n          clean = 0;\n        } else if (sλ0 !== sλ1 && dλ >= π) {\n          if (abs(λ0 - sλ0) < ε) λ0 -= sλ0 * ε;\n          if (abs(λ1 - sλ1) < ε) λ1 -= sλ1 * ε;\n          φ0 = d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1);\n          listener.point(sλ0, φ0);\n          listener.lineEnd();\n          listener.lineStart();\n          listener.point(sλ1, φ0);\n          clean = 0;\n        }\n        listener.point(λ0 = λ1, φ0 = φ1);\n        sλ0 = sλ1;\n      },\n      lineEnd: function() {\n        listener.lineEnd();\n        λ0 = φ0 = NaN;\n      },\n      clean: function() {\n        return 2 - clean;\n      }\n    };\n  }\n  function d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1) {\n    var cosφ0, cosφ1, sinλ0_λ1 = Math.sin(λ0 - λ1);\n    return abs(sinλ0_λ1) > ε ? Math.atan((Math.sin(φ0) * (cosφ1 = Math.cos(φ1)) * Math.sin(λ1) - Math.sin(φ1) * (cosφ0 = Math.cos(φ0)) * Math.sin(λ0)) / (cosφ0 * cosφ1 * sinλ0_λ1)) : (φ0 + φ1) / 2;\n  }\n  function d3_geo_clipAntimeridianInterpolate(from, to, direction, listener) {\n    var φ;\n    if (from == null) {\n      φ = direction * halfπ;\n      listener.point(-π, φ);\n      listener.point(0, φ);\n      listener.point(π, φ);\n      listener.point(π, 0);\n      listener.point(π, -φ);\n      listener.point(0, -φ);\n      listener.point(-π, -φ);\n      listener.point(-π, 0);\n      listener.point(-π, φ);\n    } else if (abs(from[0] - to[0]) > ε) {\n      var s = from[0] < to[0] ? π : -π;\n      φ = direction * s / 2;\n      listener.point(-s, φ);\n      listener.point(0, φ);\n      listener.point(s, φ);\n    } else {\n      listener.point(to[0], to[1]);\n    }\n  }\n  function d3_geo_clipCircle(radius) {\n    var cr = Math.cos(radius), smallRadius = cr > 0, notHemisphere = abs(cr) > ε, interpolate = d3_geo_circleInterpolate(radius, 6 * d3_radians);\n    return d3_geo_clip(visible, clipLine, interpolate, smallRadius ? [ 0, -radius ] : [ -π, radius - π ]);\n    function visible(λ, φ) {\n      return Math.cos(λ) * Math.cos(φ) > cr;\n    }\n    function clipLine(listener) {\n      var point0, c0, v0, v00, clean;\n      return {\n        lineStart: function() {\n          v00 = v0 = false;\n          clean = 1;\n        },\n        point: function(λ, φ) {\n          var point1 = [ λ, φ ], point2, v = visible(λ, φ), c = smallRadius ? v ? 0 : code(λ, φ) : v ? code(λ + (λ < 0 ? π : -π), φ) : 0;\n          if (!point0 && (v00 = v0 = v)) listener.lineStart();\n          if (v !== v0) {\n            point2 = intersect(point0, point1);\n            if (d3_geo_sphericalEqual(point0, point2) || d3_geo_sphericalEqual(point1, point2)) {\n              point1[0] += ε;\n              point1[1] += ε;\n              v = visible(point1[0], point1[1]);\n            }\n          }\n          if (v !== v0) {\n            clean = 0;\n            if (v) {\n              listener.lineStart();\n              point2 = intersect(point1, point0);\n              listener.point(point2[0], point2[1]);\n            } else {\n              point2 = intersect(point0, point1);\n              listener.point(point2[0], point2[1]);\n              listener.lineEnd();\n            }\n            point0 = point2;\n          } else if (notHemisphere && point0 && smallRadius ^ v) {\n            var t;\n            if (!(c & c0) && (t = intersect(point1, point0, true))) {\n              clean = 0;\n              if (smallRadius) {\n                listener.lineStart();\n                listener.point(t[0][0], t[0][1]);\n                listener.point(t[1][0], t[1][1]);\n                listener.lineEnd();\n              } else {\n                listener.point(t[1][0], t[1][1]);\n                listener.lineEnd();\n                listener.lineStart();\n                listener.point(t[0][0], t[0][1]);\n              }\n            }\n          }\n          if (v && (!point0 || !d3_geo_sphericalEqual(point0, point1))) {\n            listener.point(point1[0], point1[1]);\n          }\n          point0 = point1, v0 = v, c0 = c;\n        },\n        lineEnd: function() {\n          if (v0) listener.lineEnd();\n          point0 = null;\n        },\n        clean: function() {\n          return clean | (v00 && v0) << 1;\n        }\n      };\n    }\n    function intersect(a, b, two) {\n      var pa = d3_geo_cartesian(a), pb = d3_geo_cartesian(b);\n      var n1 = [ 1, 0, 0 ], n2 = d3_geo_cartesianCross(pa, pb), n2n2 = d3_geo_cartesianDot(n2, n2), n1n2 = n2[0], determinant = n2n2 - n1n2 * n1n2;\n      if (!determinant) return !two && a;\n      var c1 = cr * n2n2 / determinant, c2 = -cr * n1n2 / determinant, n1xn2 = d3_geo_cartesianCross(n1, n2), A = d3_geo_cartesianScale(n1, c1), B = d3_geo_cartesianScale(n2, c2);\n      d3_geo_cartesianAdd(A, B);\n      var u = n1xn2, w = d3_geo_cartesianDot(A, u), uu = d3_geo_cartesianDot(u, u), t2 = w * w - uu * (d3_geo_cartesianDot(A, A) - 1);\n      if (t2 < 0) return;\n      var t = Math.sqrt(t2), q = d3_geo_cartesianScale(u, (-w - t) / uu);\n      d3_geo_cartesianAdd(q, A);\n      q = d3_geo_spherical(q);\n      if (!two) return q;\n      var λ0 = a[0], λ1 = b[0], φ0 = a[1], φ1 = b[1], z;\n      if (λ1 < λ0) z = λ0, λ0 = λ1, λ1 = z;\n      var δλ = λ1 - λ0, polar = abs(δλ - π) < ε, meridian = polar || δλ < ε;\n      if (!polar && φ1 < φ0) z = φ0, φ0 = φ1, φ1 = z;\n      if (meridian ? polar ? φ0 + φ1 > 0 ^ q[1] < (abs(q[0] - λ0) < ε ? φ0 : φ1) : φ0 <= q[1] && q[1] <= φ1 : δλ > π ^ (λ0 <= q[0] && q[0] <= λ1)) {\n        var q1 = d3_geo_cartesianScale(u, (-w + t) / uu);\n        d3_geo_cartesianAdd(q1, A);\n        return [ q, d3_geo_spherical(q1) ];\n      }\n    }\n    function code(λ, φ) {\n      var r = smallRadius ? radius : π - radius, code = 0;\n      if (λ < -r) code |= 1; else if (λ > r) code |= 2;\n      if (φ < -r) code |= 4; else if (φ > r) code |= 8;\n      return code;\n    }\n  }\n  function d3_geom_clipLine(x0, y0, x1, y1) {\n    return function(line) {\n      var a = line.a, b = line.b, ax = a.x, ay = a.y, bx = b.x, by = b.y, t0 = 0, t1 = 1, dx = bx - ax, dy = by - ay, r;\n      r = x0 - ax;\n      if (!dx && r > 0) return;\n      r /= dx;\n      if (dx < 0) {\n        if (r < t0) return;\n        if (r < t1) t1 = r;\n      } else if (dx > 0) {\n        if (r > t1) return;\n        if (r > t0) t0 = r;\n      }\n      r = x1 - ax;\n      if (!dx && r < 0) return;\n      r /= dx;\n      if (dx < 0) {\n        if (r > t1) return;\n        if (r > t0) t0 = r;\n      } else if (dx > 0) {\n        if (r < t0) return;\n        if (r < t1) t1 = r;\n      }\n      r = y0 - ay;\n      if (!dy && r > 0) return;\n      r /= dy;\n      if (dy < 0) {\n        if (r < t0) return;\n        if (r < t1) t1 = r;\n      } else if (dy > 0) {\n        if (r > t1) return;\n        if (r > t0) t0 = r;\n      }\n      r = y1 - ay;\n      if (!dy && r < 0) return;\n      r /= dy;\n      if (dy < 0) {\n        if (r > t1) return;\n        if (r > t0) t0 = r;\n      } else if (dy > 0) {\n        if (r < t0) return;\n        if (r < t1) t1 = r;\n      }\n      if (t0 > 0) line.a = {\n        x: ax + t0 * dx,\n        y: ay + t0 * dy\n      };\n      if (t1 < 1) line.b = {\n        x: ax + t1 * dx,\n        y: ay + t1 * dy\n      };\n      return line;\n    };\n  }\n  var d3_geo_clipExtentMAX = 1e9;\n  d3.geo.clipExtent = function() {\n    var x0, y0, x1, y1, stream, clip, clipExtent = {\n      stream: function(output) {\n        if (stream) stream.valid = false;\n        stream = clip(output);\n        stream.valid = true;\n        return stream;\n      },\n      extent: function(_) {\n        if (!arguments.length) return [ [ x0, y0 ], [ x1, y1 ] ];\n        clip = d3_geo_clipExtent(x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1]);\n        if (stream) stream.valid = false, stream = null;\n        return clipExtent;\n      }\n    };\n    return clipExtent.extent([ [ 0, 0 ], [ 960, 500 ] ]);\n  };\n  function d3_geo_clipExtent(x0, y0, x1, y1) {\n    return function(listener) {\n      var listener_ = listener, bufferListener = d3_geo_clipBufferListener(), clipLine = d3_geom_clipLine(x0, y0, x1, y1), segments, polygon, ring;\n      var clip = {\n        point: point,\n        lineStart: lineStart,\n        lineEnd: lineEnd,\n        polygonStart: function() {\n          listener = bufferListener;\n          segments = [];\n          polygon = [];\n          clean = true;\n        },\n        polygonEnd: function() {\n          listener = listener_;\n          segments = d3.merge(segments);\n          var clipStartInside = insidePolygon([ x0, y1 ]), inside = clean && clipStartInside, visible = segments.length;\n          if (inside || visible) {\n            listener.polygonStart();\n            if (inside) {\n              listener.lineStart();\n              interpolate(null, null, 1, listener);\n              listener.lineEnd();\n            }\n            if (visible) {\n              d3_geo_clipPolygon(segments, compare, clipStartInside, interpolate, listener);\n            }\n            listener.polygonEnd();\n          }\n          segments = polygon = ring = null;\n        }\n      };\n      function insidePolygon(p) {\n        var wn = 0, n = polygon.length, y = p[1];\n        for (var i = 0; i < n; ++i) {\n          for (var j = 1, v = polygon[i], m = v.length, a = v[0], b; j < m; ++j) {\n            b = v[j];\n            if (a[1] <= y) {\n              if (b[1] > y && d3_cross2d(a, b, p) > 0) ++wn;\n            } else {\n              if (b[1] <= y && d3_cross2d(a, b, p) < 0) --wn;\n            }\n            a = b;\n          }\n        }\n        return wn !== 0;\n      }\n      function interpolate(from, to, direction, listener) {\n        var a = 0, a1 = 0;\n        if (from == null || (a = corner(from, direction)) !== (a1 = corner(to, direction)) || comparePoints(from, to) < 0 ^ direction > 0) {\n          do {\n            listener.point(a === 0 || a === 3 ? x0 : x1, a > 1 ? y1 : y0);\n          } while ((a = (a + direction + 4) % 4) !== a1);\n        } else {\n          listener.point(to[0], to[1]);\n        }\n      }\n      function pointVisible(x, y) {\n        return x0 <= x && x <= x1 && y0 <= y && y <= y1;\n      }\n      function point(x, y) {\n        if (pointVisible(x, y)) listener.point(x, y);\n      }\n      var x__, y__, v__, x_, y_, v_, first, clean;\n      function lineStart() {\n        clip.point = linePoint;\n        if (polygon) polygon.push(ring = []);\n        first = true;\n        v_ = false;\n        x_ = y_ = NaN;\n      }\n      function lineEnd() {\n        if (segments) {\n          linePoint(x__, y__);\n          if (v__ && v_) bufferListener.rejoin();\n          segments.push(bufferListener.buffer());\n        }\n        clip.point = point;\n        if (v_) listener.lineEnd();\n      }\n      function linePoint(x, y) {\n        x = Math.max(-d3_geo_clipExtentMAX, Math.min(d3_geo_clipExtentMAX, x));\n        y = Math.max(-d3_geo_clipExtentMAX, Math.min(d3_geo_clipExtentMAX, y));\n        var v = pointVisible(x, y);\n        if (polygon) ring.push([ x, y ]);\n        if (first) {\n          x__ = x, y__ = y, v__ = v;\n          first = false;\n          if (v) {\n            listener.lineStart();\n            listener.point(x, y);\n          }\n        } else {\n          if (v && v_) listener.point(x, y); else {\n            var l = {\n              a: {\n                x: x_,\n                y: y_\n              },\n              b: {\n                x: x,\n                y: y\n              }\n            };\n            if (clipLine(l)) {\n              if (!v_) {\n                listener.lineStart();\n                listener.point(l.a.x, l.a.y);\n              }\n              listener.point(l.b.x, l.b.y);\n              if (!v) listener.lineEnd();\n              clean = false;\n            } else if (v) {\n              listener.lineStart();\n              listener.point(x, y);\n              clean = false;\n            }\n          }\n        }\n        x_ = x, y_ = y, v_ = v;\n      }\n      return clip;\n    };\n    function corner(p, direction) {\n      return abs(p[0] - x0) < ε ? direction > 0 ? 0 : 3 : abs(p[0] - x1) < ε ? direction > 0 ? 2 : 1 : abs(p[1] - y0) < ε ? direction > 0 ? 1 : 0 : direction > 0 ? 3 : 2;\n    }\n    function compare(a, b) {\n      return comparePoints(a.x, b.x);\n    }\n    function comparePoints(a, b) {\n      var ca = corner(a, 1), cb = corner(b, 1);\n      return ca !== cb ? ca - cb : ca === 0 ? b[1] - a[1] : ca === 1 ? a[0] - b[0] : ca === 2 ? a[1] - b[1] : b[0] - a[0];\n    }\n  }\n  function d3_geo_compose(a, b) {\n    function compose(x, y) {\n      return x = a(x, y), b(x[0], x[1]);\n    }\n    if (a.invert && b.invert) compose.invert = function(x, y) {\n      return x = b.invert(x, y), x && a.invert(x[0], x[1]);\n    };\n    return compose;\n  }\n  function d3_geo_conic(projectAt) {\n    var φ0 = 0, φ1 = π / 3, m = d3_geo_projectionMutator(projectAt), p = m(φ0, φ1);\n    p.parallels = function(_) {\n      if (!arguments.length) return [ φ0 / π * 180, φ1 / π * 180 ];\n      return m(φ0 = _[0] * π / 180, φ1 = _[1] * π / 180);\n    };\n    return p;\n  }\n  function d3_geo_conicEqualArea(φ0, φ1) {\n    var sinφ0 = Math.sin(φ0), n = (sinφ0 + Math.sin(φ1)) / 2, C = 1 + sinφ0 * (2 * n - sinφ0), ρ0 = Math.sqrt(C) / n;\n    function forward(λ, φ) {\n      var ρ = Math.sqrt(C - 2 * n * Math.sin(φ)) / n;\n      return [ ρ * Math.sin(λ *= n), ρ0 - ρ * Math.cos(λ) ];\n    }\n    forward.invert = function(x, y) {\n      var ρ0_y = ρ0 - y;\n      return [ Math.atan2(x, ρ0_y) / n, d3_asin((C - (x * x + ρ0_y * ρ0_y) * n * n) / (2 * n)) ];\n    };\n    return forward;\n  }\n  (d3.geo.conicEqualArea = function() {\n    return d3_geo_conic(d3_geo_conicEqualArea);\n  }).raw = d3_geo_conicEqualArea;\n  d3.geo.albers = function() {\n    return d3.geo.conicEqualArea().rotate([ 96, 0 ]).center([ -.6, 38.7 ]).parallels([ 29.5, 45.5 ]).scale(1070);\n  };\n  d3.geo.albersUsa = function() {\n    var lower48 = d3.geo.albers();\n    var alaska = d3.geo.conicEqualArea().rotate([ 154, 0 ]).center([ -2, 58.5 ]).parallels([ 55, 65 ]);\n    var hawaii = d3.geo.conicEqualArea().rotate([ 157, 0 ]).center([ -3, 19.9 ]).parallels([ 8, 18 ]);\n    var point, pointStream = {\n      point: function(x, y) {\n        point = [ x, y ];\n      }\n    }, lower48Point, alaskaPoint, hawaiiPoint;\n    function albersUsa(coordinates) {\n      var x = coordinates[0], y = coordinates[1];\n      point = null;\n      (lower48Point(x, y), point) || (alaskaPoint(x, y), point) || hawaiiPoint(x, y);\n      return point;\n    }\n    albersUsa.invert = function(coordinates) {\n      var k = lower48.scale(), t = lower48.translate(), x = (coordinates[0] - t[0]) / k, y = (coordinates[1] - t[1]) / k;\n      return (y >= .12 && y < .234 && x >= -.425 && x < -.214 ? alaska : y >= .166 && y < .234 && x >= -.214 && x < -.115 ? hawaii : lower48).invert(coordinates);\n    };\n    albersUsa.stream = function(stream) {\n      var lower48Stream = lower48.stream(stream), alaskaStream = alaska.stream(stream), hawaiiStream = hawaii.stream(stream);\n      return {\n        point: function(x, y) {\n          lower48Stream.point(x, y);\n          alaskaStream.point(x, y);\n          hawaiiStream.point(x, y);\n        },\n        sphere: function() {\n          lower48Stream.sphere();\n          alaskaStream.sphere();\n          hawaiiStream.sphere();\n        },\n        lineStart: function() {\n          lower48Stream.lineStart();\n          alaskaStream.lineStart();\n          hawaiiStream.lineStart();\n        },\n        lineEnd: function() {\n          lower48Stream.lineEnd();\n          alaskaStream.lineEnd();\n          hawaiiStream.lineEnd();\n        },\n        polygonStart: function() {\n          lower48Stream.polygonStart();\n          alaskaStream.polygonStart();\n          hawaiiStream.polygonStart();\n        },\n        polygonEnd: function() {\n          lower48Stream.polygonEnd();\n          alaskaStream.polygonEnd();\n          hawaiiStream.polygonEnd();\n        }\n      };\n    };\n    albersUsa.precision = function(_) {\n      if (!arguments.length) return lower48.precision();\n      lower48.precision(_);\n      alaska.precision(_);\n      hawaii.precision(_);\n      return albersUsa;\n    };\n    albersUsa.scale = function(_) {\n      if (!arguments.length) return lower48.scale();\n      lower48.scale(_);\n      alaska.scale(_ * .35);\n      hawaii.scale(_);\n      return albersUsa.translate(lower48.translate());\n    };\n    albersUsa.translate = function(_) {\n      if (!arguments.length) return lower48.translate();\n      var k = lower48.scale(), x = +_[0], y = +_[1];\n      lower48Point = lower48.translate(_).clipExtent([ [ x - .455 * k, y - .238 * k ], [ x + .455 * k, y + .238 * k ] ]).stream(pointStream).point;\n      alaskaPoint = alaska.translate([ x - .307 * k, y + .201 * k ]).clipExtent([ [ x - .425 * k + ε, y + .12 * k + ε ], [ x - .214 * k - ε, y + .234 * k - ε ] ]).stream(pointStream).point;\n      hawaiiPoint = hawaii.translate([ x - .205 * k, y + .212 * k ]).clipExtent([ [ x - .214 * k + ε, y + .166 * k + ε ], [ x - .115 * k - ε, y + .234 * k - ε ] ]).stream(pointStream).point;\n      return albersUsa;\n    };\n    return albersUsa.scale(1070);\n  };\n  var d3_geo_pathAreaSum, d3_geo_pathAreaPolygon, d3_geo_pathArea = {\n    point: d3_noop,\n    lineStart: d3_noop,\n    lineEnd: d3_noop,\n    polygonStart: function() {\n      d3_geo_pathAreaPolygon = 0;\n      d3_geo_pathArea.lineStart = d3_geo_pathAreaRingStart;\n    },\n    polygonEnd: function() {\n      d3_geo_pathArea.lineStart = d3_geo_pathArea.lineEnd = d3_geo_pathArea.point = d3_noop;\n      d3_geo_pathAreaSum += abs(d3_geo_pathAreaPolygon / 2);\n    }\n  };\n  function d3_geo_pathAreaRingStart() {\n    var x00, y00, x0, y0;\n    d3_geo_pathArea.point = function(x, y) {\n      d3_geo_pathArea.point = nextPoint;\n      x00 = x0 = x, y00 = y0 = y;\n    };\n    function nextPoint(x, y) {\n      d3_geo_pathAreaPolygon += y0 * x - x0 * y;\n      x0 = x, y0 = y;\n    }\n    d3_geo_pathArea.lineEnd = function() {\n      nextPoint(x00, y00);\n    };\n  }\n  var d3_geo_pathBoundsX0, d3_geo_pathBoundsY0, d3_geo_pathBoundsX1, d3_geo_pathBoundsY1;\n  var d3_geo_pathBounds = {\n    point: d3_geo_pathBoundsPoint,\n    lineStart: d3_noop,\n    lineEnd: d3_noop,\n    polygonStart: d3_noop,\n    polygonEnd: d3_noop\n  };\n  function d3_geo_pathBoundsPoint(x, y) {\n    if (x < d3_geo_pathBoundsX0) d3_geo_pathBoundsX0 = x;\n    if (x > d3_geo_pathBoundsX1) d3_geo_pathBoundsX1 = x;\n    if (y < d3_geo_pathBoundsY0) d3_geo_pathBoundsY0 = y;\n    if (y > d3_geo_pathBoundsY1) d3_geo_pathBoundsY1 = y;\n  }\n  function d3_geo_pathBuffer() {\n    var pointCircle = d3_geo_pathBufferCircle(4.5), buffer = [];\n    var stream = {\n      point: point,\n      lineStart: function() {\n        stream.point = pointLineStart;\n      },\n      lineEnd: lineEnd,\n      polygonStart: function() {\n        stream.lineEnd = lineEndPolygon;\n      },\n      polygonEnd: function() {\n        stream.lineEnd = lineEnd;\n        stream.point = point;\n      },\n      pointRadius: function(_) {\n        pointCircle = d3_geo_pathBufferCircle(_);\n        return stream;\n      },\n      result: function() {\n        if (buffer.length) {\n          var result = buffer.join(\"\");\n          buffer = [];\n          return result;\n        }\n      }\n    };\n    function point(x, y) {\n      buffer.push(\"M\", x, \",\", y, pointCircle);\n    }\n    function pointLineStart(x, y) {\n      buffer.push(\"M\", x, \",\", y);\n      stream.point = pointLine;\n    }\n    function pointLine(x, y) {\n      buffer.push(\"L\", x, \",\", y);\n    }\n    function lineEnd() {\n      stream.point = point;\n    }\n    function lineEndPolygon() {\n      buffer.push(\"Z\");\n    }\n    return stream;\n  }\n  function d3_geo_pathBufferCircle(radius) {\n    return \"m0,\" + radius + \"a\" + radius + \",\" + radius + \" 0 1,1 0,\" + -2 * radius + \"a\" + radius + \",\" + radius + \" 0 1,1 0,\" + 2 * radius + \"z\";\n  }\n  var d3_geo_pathCentroid = {\n    point: d3_geo_pathCentroidPoint,\n    lineStart: d3_geo_pathCentroidLineStart,\n    lineEnd: d3_geo_pathCentroidLineEnd,\n    polygonStart: function() {\n      d3_geo_pathCentroid.lineStart = d3_geo_pathCentroidRingStart;\n    },\n    polygonEnd: function() {\n      d3_geo_pathCentroid.point = d3_geo_pathCentroidPoint;\n      d3_geo_pathCentroid.lineStart = d3_geo_pathCentroidLineStart;\n      d3_geo_pathCentroid.lineEnd = d3_geo_pathCentroidLineEnd;\n    }\n  };\n  function d3_geo_pathCentroidPoint(x, y) {\n    d3_geo_centroidX0 += x;\n    d3_geo_centroidY0 += y;\n    ++d3_geo_centroidZ0;\n  }\n  function d3_geo_pathCentroidLineStart() {\n    var x0, y0;\n    d3_geo_pathCentroid.point = function(x, y) {\n      d3_geo_pathCentroid.point = nextPoint;\n      d3_geo_pathCentroidPoint(x0 = x, y0 = y);\n    };\n    function nextPoint(x, y) {\n      var dx = x - x0, dy = y - y0, z = Math.sqrt(dx * dx + dy * dy);\n      d3_geo_centroidX1 += z * (x0 + x) / 2;\n      d3_geo_centroidY1 += z * (y0 + y) / 2;\n      d3_geo_centroidZ1 += z;\n      d3_geo_pathCentroidPoint(x0 = x, y0 = y);\n    }\n  }\n  function d3_geo_pathCentroidLineEnd() {\n    d3_geo_pathCentroid.point = d3_geo_pathCentroidPoint;\n  }\n  function d3_geo_pathCentroidRingStart() {\n    var x00, y00, x0, y0;\n    d3_geo_pathCentroid.point = function(x, y) {\n      d3_geo_pathCentroid.point = nextPoint;\n      d3_geo_pathCentroidPoint(x00 = x0 = x, y00 = y0 = y);\n    };\n    function nextPoint(x, y) {\n      var dx = x - x0, dy = y - y0, z = Math.sqrt(dx * dx + dy * dy);\n      d3_geo_centroidX1 += z * (x0 + x) / 2;\n      d3_geo_centroidY1 += z * (y0 + y) / 2;\n      d3_geo_centroidZ1 += z;\n      z = y0 * x - x0 * y;\n      d3_geo_centroidX2 += z * (x0 + x);\n      d3_geo_centroidY2 += z * (y0 + y);\n      d3_geo_centroidZ2 += z * 3;\n      d3_geo_pathCentroidPoint(x0 = x, y0 = y);\n    }\n    d3_geo_pathCentroid.lineEnd = function() {\n      nextPoint(x00, y00);\n    };\n  }\n  function d3_geo_pathContext(context) {\n    var pointRadius = 4.5;\n    var stream = {\n      point: point,\n      lineStart: function() {\n        stream.point = pointLineStart;\n      },\n      lineEnd: lineEnd,\n      polygonStart: function() {\n        stream.lineEnd = lineEndPolygon;\n      },\n      polygonEnd: function() {\n        stream.lineEnd = lineEnd;\n        stream.point = point;\n      },\n      pointRadius: function(_) {\n        pointRadius = _;\n        return stream;\n      },\n      result: d3_noop\n    };\n    function point(x, y) {\n      context.moveTo(x, y);\n      context.arc(x, y, pointRadius, 0, τ);\n    }\n    function pointLineStart(x, y) {\n      context.moveTo(x, y);\n      stream.point = pointLine;\n    }\n    function pointLine(x, y) {\n      context.lineTo(x, y);\n    }\n    function lineEnd() {\n      stream.point = point;\n    }\n    function lineEndPolygon() {\n      context.closePath();\n    }\n    return stream;\n  }\n  function d3_geo_resample(project) {\n    var δ2 = .5, cosMinDistance = Math.cos(30 * d3_radians), maxDepth = 16;\n    function resample(stream) {\n      return (maxDepth ? resampleRecursive : resampleNone)(stream);\n    }\n    function resampleNone(stream) {\n      return d3_geo_transformPoint(stream, function(x, y) {\n        x = project(x, y);\n        stream.point(x[0], x[1]);\n      });\n    }\n    function resampleRecursive(stream) {\n      var λ00, φ00, x00, y00, a00, b00, c00, λ0, x0, y0, a0, b0, c0;\n      var resample = {\n        point: point,\n        lineStart: lineStart,\n        lineEnd: lineEnd,\n        polygonStart: function() {\n          stream.polygonStart();\n          resample.lineStart = ringStart;\n        },\n        polygonEnd: function() {\n          stream.polygonEnd();\n          resample.lineStart = lineStart;\n        }\n      };\n      function point(x, y) {\n        x = project(x, y);\n        stream.point(x[0], x[1]);\n      }\n      function lineStart() {\n        x0 = NaN;\n        resample.point = linePoint;\n        stream.lineStart();\n      }\n      function linePoint(λ, φ) {\n        var c = d3_geo_cartesian([ λ, φ ]), p = project(λ, φ);\n        resampleLineTo(x0, y0, λ0, a0, b0, c0, x0 = p[0], y0 = p[1], λ0 = λ, a0 = c[0], b0 = c[1], c0 = c[2], maxDepth, stream);\n        stream.point(x0, y0);\n      }\n      function lineEnd() {\n        resample.point = point;\n        stream.lineEnd();\n      }\n      function ringStart() {\n        lineStart();\n        resample.point = ringPoint;\n        resample.lineEnd = ringEnd;\n      }\n      function ringPoint(λ, φ) {\n        linePoint(λ00 = λ, φ00 = φ), x00 = x0, y00 = y0, a00 = a0, b00 = b0, c00 = c0;\n        resample.point = linePoint;\n      }\n      function ringEnd() {\n        resampleLineTo(x0, y0, λ0, a0, b0, c0, x00, y00, λ00, a00, b00, c00, maxDepth, stream);\n        resample.lineEnd = lineEnd;\n        lineEnd();\n      }\n      return resample;\n    }\n    function resampleLineTo(x0, y0, λ0, a0, b0, c0, x1, y1, λ1, a1, b1, c1, depth, stream) {\n      var dx = x1 - x0, dy = y1 - y0, d2 = dx * dx + dy * dy;\n      if (d2 > 4 * δ2 && depth--) {\n        var a = a0 + a1, b = b0 + b1, c = c0 + c1, m = Math.sqrt(a * a + b * b + c * c), φ2 = Math.asin(c /= m), λ2 = abs(abs(c) - 1) < ε || abs(λ0 - λ1) < ε ? (λ0 + λ1) / 2 : Math.atan2(b, a), p = project(λ2, φ2), x2 = p[0], y2 = p[1], dx2 = x2 - x0, dy2 = y2 - y0, dz = dy * dx2 - dx * dy2;\n        if (dz * dz / d2 > δ2 || abs((dx * dx2 + dy * dy2) / d2 - .5) > .3 || a0 * a1 + b0 * b1 + c0 * c1 < cosMinDistance) {\n          resampleLineTo(x0, y0, λ0, a0, b0, c0, x2, y2, λ2, a /= m, b /= m, c, depth, stream);\n          stream.point(x2, y2);\n          resampleLineTo(x2, y2, λ2, a, b, c, x1, y1, λ1, a1, b1, c1, depth, stream);\n        }\n      }\n    }\n    resample.precision = function(_) {\n      if (!arguments.length) return Math.sqrt(δ2);\n      maxDepth = (δ2 = _ * _) > 0 && 16;\n      return resample;\n    };\n    return resample;\n  }\n  d3.geo.path = function() {\n    var pointRadius = 4.5, projection, context, projectStream, contextStream, cacheStream;\n    function path(object) {\n      if (object) {\n        if (typeof pointRadius === \"function\") contextStream.pointRadius(+pointRadius.apply(this, arguments));\n        if (!cacheStream || !cacheStream.valid) cacheStream = projectStream(contextStream);\n        d3.geo.stream(object, cacheStream);\n      }\n      return contextStream.result();\n    }\n    path.area = function(object) {\n      d3_geo_pathAreaSum = 0;\n      d3.geo.stream(object, projectStream(d3_geo_pathArea));\n      return d3_geo_pathAreaSum;\n    };\n    path.centroid = function(object) {\n      d3_geo_centroidX0 = d3_geo_centroidY0 = d3_geo_centroidZ0 = d3_geo_centroidX1 = d3_geo_centroidY1 = d3_geo_centroidZ1 = d3_geo_centroidX2 = d3_geo_centroidY2 = d3_geo_centroidZ2 = 0;\n      d3.geo.stream(object, projectStream(d3_geo_pathCentroid));\n      return d3_geo_centroidZ2 ? [ d3_geo_centroidX2 / d3_geo_centroidZ2, d3_geo_centroidY2 / d3_geo_centroidZ2 ] : d3_geo_centroidZ1 ? [ d3_geo_centroidX1 / d3_geo_centroidZ1, d3_geo_centroidY1 / d3_geo_centroidZ1 ] : d3_geo_centroidZ0 ? [ d3_geo_centroidX0 / d3_geo_centroidZ0, d3_geo_centroidY0 / d3_geo_centroidZ0 ] : [ NaN, NaN ];\n    };\n    path.bounds = function(object) {\n      d3_geo_pathBoundsX1 = d3_geo_pathBoundsY1 = -(d3_geo_pathBoundsX0 = d3_geo_pathBoundsY0 = Infinity);\n      d3.geo.stream(object, projectStream(d3_geo_pathBounds));\n      return [ [ d3_geo_pathBoundsX0, d3_geo_pathBoundsY0 ], [ d3_geo_pathBoundsX1, d3_geo_pathBoundsY1 ] ];\n    };\n    path.projection = function(_) {\n      if (!arguments.length) return projection;\n      projectStream = (projection = _) ? _.stream || d3_geo_pathProjectStream(_) : d3_identity;\n      return reset();\n    };\n    path.context = function(_) {\n      if (!arguments.length) return context;\n      contextStream = (context = _) == null ? new d3_geo_pathBuffer() : new d3_geo_pathContext(_);\n      if (typeof pointRadius !== \"function\") contextStream.pointRadius(pointRadius);\n      return reset();\n    };\n    path.pointRadius = function(_) {\n      if (!arguments.length) return pointRadius;\n      pointRadius = typeof _ === \"function\" ? _ : (contextStream.pointRadius(+_), +_);\n      return path;\n    };\n    function reset() {\n      cacheStream = null;\n      return path;\n    }\n    return path.projection(d3.geo.albersUsa()).context(null);\n  };\n  function d3_geo_pathProjectStream(project) {\n    var resample = d3_geo_resample(function(x, y) {\n      return project([ x * d3_degrees, y * d3_degrees ]);\n    });\n    return function(stream) {\n      return d3_geo_projectionRadians(resample(stream));\n    };\n  }\n  d3.geo.transform = function(methods) {\n    return {\n      stream: function(stream) {\n        var transform = new d3_geo_transform(stream);\n        for (var k in methods) transform[k] = methods[k];\n        return transform;\n      }\n    };\n  };\n  function d3_geo_transform(stream) {\n    this.stream = stream;\n  }\n  d3_geo_transform.prototype = {\n    point: function(x, y) {\n      this.stream.point(x, y);\n    },\n    sphere: function() {\n      this.stream.sphere();\n    },\n    lineStart: function() {\n      this.stream.lineStart();\n    },\n    lineEnd: function() {\n      this.stream.lineEnd();\n    },\n    polygonStart: function() {\n      this.stream.polygonStart();\n    },\n    polygonEnd: function() {\n      this.stream.polygonEnd();\n    }\n  };\n  function d3_geo_transformPoint(stream, point) {\n    return {\n      point: point,\n      sphere: function() {\n        stream.sphere();\n      },\n      lineStart: function() {\n        stream.lineStart();\n      },\n      lineEnd: function() {\n        stream.lineEnd();\n      },\n      polygonStart: function() {\n        stream.polygonStart();\n      },\n      polygonEnd: function() {\n        stream.polygonEnd();\n      }\n    };\n  }\n  d3.geo.projection = d3_geo_projection;\n  d3.geo.projectionMutator = d3_geo_projectionMutator;\n  function d3_geo_projection(project) {\n    return d3_geo_projectionMutator(function() {\n      return project;\n    })();\n  }\n  function d3_geo_projectionMutator(projectAt) {\n    var project, rotate, projectRotate, projectResample = d3_geo_resample(function(x, y) {\n      x = project(x, y);\n      return [ x[0] * k + δx, δy - x[1] * k ];\n    }), k = 150, x = 480, y = 250, λ = 0, φ = 0, δλ = 0, δφ = 0, δγ = 0, δx, δy, preclip = d3_geo_clipAntimeridian, postclip = d3_identity, clipAngle = null, clipExtent = null, stream;\n    function projection(point) {\n      point = projectRotate(point[0] * d3_radians, point[1] * d3_radians);\n      return [ point[0] * k + δx, δy - point[1] * k ];\n    }\n    function invert(point) {\n      point = projectRotate.invert((point[0] - δx) / k, (δy - point[1]) / k);\n      return point && [ point[0] * d3_degrees, point[1] * d3_degrees ];\n    }\n    projection.stream = function(output) {\n      if (stream) stream.valid = false;\n      stream = d3_geo_projectionRadians(preclip(rotate, projectResample(postclip(output))));\n      stream.valid = true;\n      return stream;\n    };\n    projection.clipAngle = function(_) {\n      if (!arguments.length) return clipAngle;\n      preclip = _ == null ? (clipAngle = _, d3_geo_clipAntimeridian) : d3_geo_clipCircle((clipAngle = +_) * d3_radians);\n      return invalidate();\n    };\n    projection.clipExtent = function(_) {\n      if (!arguments.length) return clipExtent;\n      clipExtent = _;\n      postclip = _ ? d3_geo_clipExtent(_[0][0], _[0][1], _[1][0], _[1][1]) : d3_identity;\n      return invalidate();\n    };\n    projection.scale = function(_) {\n      if (!arguments.length) return k;\n      k = +_;\n      return reset();\n    };\n    projection.translate = function(_) {\n      if (!arguments.length) return [ x, y ];\n      x = +_[0];\n      y = +_[1];\n      return reset();\n    };\n    projection.center = function(_) {\n      if (!arguments.length) return [ λ * d3_degrees, φ * d3_degrees ];\n      λ = _[0] % 360 * d3_radians;\n      φ = _[1] % 360 * d3_radians;\n      return reset();\n    };\n    projection.rotate = function(_) {\n      if (!arguments.length) return [ δλ * d3_degrees, δφ * d3_degrees, δγ * d3_degrees ];\n      δλ = _[0] % 360 * d3_radians;\n      δφ = _[1] % 360 * d3_radians;\n      δγ = _.length > 2 ? _[2] % 360 * d3_radians : 0;\n      return reset();\n    };\n    d3.rebind(projection, projectResample, \"precision\");\n    function reset() {\n      projectRotate = d3_geo_compose(rotate = d3_geo_rotation(δλ, δφ, δγ), project);\n      var center = project(λ, φ);\n      δx = x - center[0] * k;\n      δy = y + center[1] * k;\n      return invalidate();\n    }\n    function invalidate() {\n      if (stream) stream.valid = false, stream = null;\n      return projection;\n    }\n    return function() {\n      project = projectAt.apply(this, arguments);\n      projection.invert = project.invert && invert;\n      return reset();\n    };\n  }\n  function d3_geo_projectionRadians(stream) {\n    return d3_geo_transformPoint(stream, function(x, y) {\n      stream.point(x * d3_radians, y * d3_radians);\n    });\n  }\n  function d3_geo_equirectangular(λ, φ) {\n    return [ λ, φ ];\n  }\n  (d3.geo.equirectangular = function() {\n    return d3_geo_projection(d3_geo_equirectangular);\n  }).raw = d3_geo_equirectangular.invert = d3_geo_equirectangular;\n  d3.geo.rotation = function(rotate) {\n    rotate = d3_geo_rotation(rotate[0] % 360 * d3_radians, rotate[1] * d3_radians, rotate.length > 2 ? rotate[2] * d3_radians : 0);\n    function forward(coordinates) {\n      coordinates = rotate(coordinates[0] * d3_radians, coordinates[1] * d3_radians);\n      return coordinates[0] *= d3_degrees, coordinates[1] *= d3_degrees, coordinates;\n    }\n    forward.invert = function(coordinates) {\n      coordinates = rotate.invert(coordinates[0] * d3_radians, coordinates[1] * d3_radians);\n      return coordinates[0] *= d3_degrees, coordinates[1] *= d3_degrees, coordinates;\n    };\n    return forward;\n  };\n  function d3_geo_identityRotation(λ, φ) {\n    return [ λ > π ? λ - τ : λ < -π ? λ + τ : λ, φ ];\n  }\n  d3_geo_identityRotation.invert = d3_geo_equirectangular;\n  function d3_geo_rotation(δλ, δφ, δγ) {\n    return δλ ? δφ || δγ ? d3_geo_compose(d3_geo_rotationλ(δλ), d3_geo_rotationφγ(δφ, δγ)) : d3_geo_rotationλ(δλ) : δφ || δγ ? d3_geo_rotationφγ(δφ, δγ) : d3_geo_identityRotation;\n  }\n  function d3_geo_forwardRotationλ(δλ) {\n    return function(λ, φ) {\n      return λ += δλ, [ λ > π ? λ - τ : λ < -π ? λ + τ : λ, φ ];\n    };\n  }\n  function d3_geo_rotationλ(δλ) {\n    var rotation = d3_geo_forwardRotationλ(δλ);\n    rotation.invert = d3_geo_forwardRotationλ(-δλ);\n    return rotation;\n  }\n  function d3_geo_rotationφγ(δφ, δγ) {\n    var cosδφ = Math.cos(δφ), sinδφ = Math.sin(δφ), cosδγ = Math.cos(δγ), sinδγ = Math.sin(δγ);\n    function rotation(λ, φ) {\n      var cosφ = Math.cos(φ), x = Math.cos(λ) * cosφ, y = Math.sin(λ) * cosφ, z = Math.sin(φ), k = z * cosδφ + x * sinδφ;\n      return [ Math.atan2(y * cosδγ - k * sinδγ, x * cosδφ - z * sinδφ), d3_asin(k * cosδγ + y * sinδγ) ];\n    }\n    rotation.invert = function(λ, φ) {\n      var cosφ = Math.cos(φ), x = Math.cos(λ) * cosφ, y = Math.sin(λ) * cosφ, z = Math.sin(φ), k = z * cosδγ - y * sinδγ;\n      return [ Math.atan2(y * cosδγ + z * sinδγ, x * cosδφ + k * sinδφ), d3_asin(k * cosδφ - x * sinδφ) ];\n    };\n    return rotation;\n  }\n  d3.geo.circle = function() {\n    var origin = [ 0, 0 ], angle, precision = 6, interpolate;\n    function circle() {\n      var center = typeof origin === \"function\" ? origin.apply(this, arguments) : origin, rotate = d3_geo_rotation(-center[0] * d3_radians, -center[1] * d3_radians, 0).invert, ring = [];\n      interpolate(null, null, 1, {\n        point: function(x, y) {\n          ring.push(x = rotate(x, y));\n          x[0] *= d3_degrees, x[1] *= d3_degrees;\n        }\n      });\n      return {\n        type: \"Polygon\",\n        coordinates: [ ring ]\n      };\n    }\n    circle.origin = function(x) {\n      if (!arguments.length) return origin;\n      origin = x;\n      return circle;\n    };\n    circle.angle = function(x) {\n      if (!arguments.length) return angle;\n      interpolate = d3_geo_circleInterpolate((angle = +x) * d3_radians, precision * d3_radians);\n      return circle;\n    };\n    circle.precision = function(_) {\n      if (!arguments.length) return precision;\n      interpolate = d3_geo_circleInterpolate(angle * d3_radians, (precision = +_) * d3_radians);\n      return circle;\n    };\n    return circle.angle(90);\n  };\n  function d3_geo_circleInterpolate(radius, precision) {\n    var cr = Math.cos(radius), sr = Math.sin(radius);\n    return function(from, to, direction, listener) {\n      var step = direction * precision;\n      if (from != null) {\n        from = d3_geo_circleAngle(cr, from);\n        to = d3_geo_circleAngle(cr, to);\n        if (direction > 0 ? from < to : from > to) from += direction * τ;\n      } else {\n        from = radius + direction * τ;\n        to = radius - .5 * step;\n      }\n      for (var point, t = from; direction > 0 ? t > to : t < to; t -= step) {\n        listener.point((point = d3_geo_spherical([ cr, -sr * Math.cos(t), -sr * Math.sin(t) ]))[0], point[1]);\n      }\n    };\n  }\n  function d3_geo_circleAngle(cr, point) {\n    var a = d3_geo_cartesian(point);\n    a[0] -= cr;\n    d3_geo_cartesianNormalize(a);\n    var angle = d3_acos(-a[1]);\n    return ((-a[2] < 0 ? -angle : angle) + 2 * Math.PI - ε) % (2 * Math.PI);\n  }\n  d3.geo.distance = function(a, b) {\n    var Δλ = (b[0] - a[0]) * d3_radians, φ0 = a[1] * d3_radians, φ1 = b[1] * d3_radians, sinΔλ = Math.sin(Δλ), cosΔλ = Math.cos(Δλ), sinφ0 = Math.sin(φ0), cosφ0 = Math.cos(φ0), sinφ1 = Math.sin(φ1), cosφ1 = Math.cos(φ1), t;\n    return Math.atan2(Math.sqrt((t = cosφ1 * sinΔλ) * t + (t = cosφ0 * sinφ1 - sinφ0 * cosφ1 * cosΔλ) * t), sinφ0 * sinφ1 + cosφ0 * cosφ1 * cosΔλ);\n  };\n  d3.geo.graticule = function() {\n    var x1, x0, X1, X0, y1, y0, Y1, Y0, dx = 10, dy = dx, DX = 90, DY = 360, x, y, X, Y, precision = 2.5;\n    function graticule() {\n      return {\n        type: \"MultiLineString\",\n        coordinates: lines()\n      };\n    }\n    function lines() {\n      return d3.range(Math.ceil(X0 / DX) * DX, X1, DX).map(X).concat(d3.range(Math.ceil(Y0 / DY) * DY, Y1, DY).map(Y)).concat(d3.range(Math.ceil(x0 / dx) * dx, x1, dx).filter(function(x) {\n        return abs(x % DX) > ε;\n      }).map(x)).concat(d3.range(Math.ceil(y0 / dy) * dy, y1, dy).filter(function(y) {\n        return abs(y % DY) > ε;\n      }).map(y));\n    }\n    graticule.lines = function() {\n      return lines().map(function(coordinates) {\n        return {\n          type: \"LineString\",\n          coordinates: coordinates\n        };\n      });\n    };\n    graticule.outline = function() {\n      return {\n        type: \"Polygon\",\n        coordinates: [ X(X0).concat(Y(Y1).slice(1), X(X1).reverse().slice(1), Y(Y0).reverse().slice(1)) ]\n      };\n    };\n    graticule.extent = function(_) {\n      if (!arguments.length) return graticule.minorExtent();\n      return graticule.majorExtent(_).minorExtent(_);\n    };\n    graticule.majorExtent = function(_) {\n      if (!arguments.length) return [ [ X0, Y0 ], [ X1, Y1 ] ];\n      X0 = +_[0][0], X1 = +_[1][0];\n      Y0 = +_[0][1], Y1 = +_[1][1];\n      if (X0 > X1) _ = X0, X0 = X1, X1 = _;\n      if (Y0 > Y1) _ = Y0, Y0 = Y1, Y1 = _;\n      return graticule.precision(precision);\n    };\n    graticule.minorExtent = function(_) {\n      if (!arguments.length) return [ [ x0, y0 ], [ x1, y1 ] ];\n      x0 = +_[0][0], x1 = +_[1][0];\n      y0 = +_[0][1], y1 = +_[1][1];\n      if (x0 > x1) _ = x0, x0 = x1, x1 = _;\n      if (y0 > y1) _ = y0, y0 = y1, y1 = _;\n      return graticule.precision(precision);\n    };\n    graticule.step = function(_) {\n      if (!arguments.length) return graticule.minorStep();\n      return graticule.majorStep(_).minorStep(_);\n    };\n    graticule.majorStep = function(_) {\n      if (!arguments.length) return [ DX, DY ];\n      DX = +_[0], DY = +_[1];\n      return graticule;\n    };\n    graticule.minorStep = function(_) {\n      if (!arguments.length) return [ dx, dy ];\n      dx = +_[0], dy = +_[1];\n      return graticule;\n    };\n    graticule.precision = function(_) {\n      if (!arguments.length) return precision;\n      precision = +_;\n      x = d3_geo_graticuleX(y0, y1, 90);\n      y = d3_geo_graticuleY(x0, x1, precision);\n      X = d3_geo_graticuleX(Y0, Y1, 90);\n      Y = d3_geo_graticuleY(X0, X1, precision);\n      return graticule;\n    };\n    return graticule.majorExtent([ [ -180, -90 + ε ], [ 180, 90 - ε ] ]).minorExtent([ [ -180, -80 - ε ], [ 180, 80 + ε ] ]);\n  };\n  function d3_geo_graticuleX(y0, y1, dy) {\n    var y = d3.range(y0, y1 - ε, dy).concat(y1);\n    return function(x) {\n      return y.map(function(y) {\n        return [ x, y ];\n      });\n    };\n  }\n  function d3_geo_graticuleY(x0, x1, dx) {\n    var x = d3.range(x0, x1 - ε, dx).concat(x1);\n    return function(y) {\n      return x.map(function(x) {\n        return [ x, y ];\n      });\n    };\n  }\n  function d3_source(d) {\n    return d.source;\n  }\n  function d3_target(d) {\n    return d.target;\n  }\n  d3.geo.greatArc = function() {\n    var source = d3_source, source_, target = d3_target, target_;\n    function greatArc() {\n      return {\n        type: \"LineString\",\n        coordinates: [ source_ || source.apply(this, arguments), target_ || target.apply(this, arguments) ]\n      };\n    }\n    greatArc.distance = function() {\n      return d3.geo.distance(source_ || source.apply(this, arguments), target_ || target.apply(this, arguments));\n    };\n    greatArc.source = function(_) {\n      if (!arguments.length) return source;\n      source = _, source_ = typeof _ === \"function\" ? null : _;\n      return greatArc;\n    };\n    greatArc.target = function(_) {\n      if (!arguments.length) return target;\n      target = _, target_ = typeof _ === \"function\" ? null : _;\n      return greatArc;\n    };\n    greatArc.precision = function() {\n      return arguments.length ? greatArc : 0;\n    };\n    return greatArc;\n  };\n  d3.geo.interpolate = function(source, target) {\n    return d3_geo_interpolate(source[0] * d3_radians, source[1] * d3_radians, target[0] * d3_radians, target[1] * d3_radians);\n  };\n  function d3_geo_interpolate(x0, y0, x1, y1) {\n    var cy0 = Math.cos(y0), sy0 = Math.sin(y0), cy1 = Math.cos(y1), sy1 = Math.sin(y1), kx0 = cy0 * Math.cos(x0), ky0 = cy0 * Math.sin(x0), kx1 = cy1 * Math.cos(x1), ky1 = cy1 * Math.sin(x1), d = 2 * Math.asin(Math.sqrt(d3_haversin(y1 - y0) + cy0 * cy1 * d3_haversin(x1 - x0))), k = 1 / Math.sin(d);\n    var interpolate = d ? function(t) {\n      var B = Math.sin(t *= d) * k, A = Math.sin(d - t) * k, x = A * kx0 + B * kx1, y = A * ky0 + B * ky1, z = A * sy0 + B * sy1;\n      return [ Math.atan2(y, x) * d3_degrees, Math.atan2(z, Math.sqrt(x * x + y * y)) * d3_degrees ];\n    } : function() {\n      return [ x0 * d3_degrees, y0 * d3_degrees ];\n    };\n    interpolate.distance = d;\n    return interpolate;\n  }\n  d3.geo.length = function(object) {\n    d3_geo_lengthSum = 0;\n    d3.geo.stream(object, d3_geo_length);\n    return d3_geo_lengthSum;\n  };\n  var d3_geo_lengthSum;\n  var d3_geo_length = {\n    sphere: d3_noop,\n    point: d3_noop,\n    lineStart: d3_geo_lengthLineStart,\n    lineEnd: d3_noop,\n    polygonStart: d3_noop,\n    polygonEnd: d3_noop\n  };\n  function d3_geo_lengthLineStart() {\n    var λ0, sinφ0, cosφ0;\n    d3_geo_length.point = function(λ, φ) {\n      λ0 = λ * d3_radians, sinφ0 = Math.sin(φ *= d3_radians), cosφ0 = Math.cos(φ);\n      d3_geo_length.point = nextPoint;\n    };\n    d3_geo_length.lineEnd = function() {\n      d3_geo_length.point = d3_geo_length.lineEnd = d3_noop;\n    };\n    function nextPoint(λ, φ) {\n      var sinφ = Math.sin(φ *= d3_radians), cosφ = Math.cos(φ), t = abs((λ *= d3_radians) - λ0), cosΔλ = Math.cos(t);\n      d3_geo_lengthSum += Math.atan2(Math.sqrt((t = cosφ * Math.sin(t)) * t + (t = cosφ0 * sinφ - sinφ0 * cosφ * cosΔλ) * t), sinφ0 * sinφ + cosφ0 * cosφ * cosΔλ);\n      λ0 = λ, sinφ0 = sinφ, cosφ0 = cosφ;\n    }\n  }\n  function d3_geo_azimuthal(scale, angle) {\n    function azimuthal(λ, φ) {\n      var cosλ = Math.cos(λ), cosφ = Math.cos(φ), k = scale(cosλ * cosφ);\n      return [ k * cosφ * Math.sin(λ), k * Math.sin(φ) ];\n    }\n    azimuthal.invert = function(x, y) {\n      var ρ = Math.sqrt(x * x + y * y), c = angle(ρ), sinc = Math.sin(c), cosc = Math.cos(c);\n      return [ Math.atan2(x * sinc, ρ * cosc), Math.asin(ρ && y * sinc / ρ) ];\n    };\n    return azimuthal;\n  }\n  var d3_geo_azimuthalEqualArea = d3_geo_azimuthal(function(cosλcosφ) {\n    return Math.sqrt(2 / (1 + cosλcosφ));\n  }, function(ρ) {\n    return 2 * Math.asin(ρ / 2);\n  });\n  (d3.geo.azimuthalEqualArea = function() {\n    return d3_geo_projection(d3_geo_azimuthalEqualArea);\n  }).raw = d3_geo_azimuthalEqualArea;\n  var d3_geo_azimuthalEquidistant = d3_geo_azimuthal(function(cosλcosφ) {\n    var c = Math.acos(cosλcosφ);\n    return c && c / Math.sin(c);\n  }, d3_identity);\n  (d3.geo.azimuthalEquidistant = function() {\n    return d3_geo_projection(d3_geo_azimuthalEquidistant);\n  }).raw = d3_geo_azimuthalEquidistant;\n  function d3_geo_conicConformal(φ0, φ1) {\n    var cosφ0 = Math.cos(φ0), t = function(φ) {\n      return Math.tan(π / 4 + φ / 2);\n    }, n = φ0 === φ1 ? Math.sin(φ0) : Math.log(cosφ0 / Math.cos(φ1)) / Math.log(t(φ1) / t(φ0)), F = cosφ0 * Math.pow(t(φ0), n) / n;\n    if (!n) return d3_geo_mercator;\n    function forward(λ, φ) {\n      if (F > 0) {\n        if (φ < -halfπ + ε) φ = -halfπ + ε;\n      } else {\n        if (φ > halfπ - ε) φ = halfπ - ε;\n      }\n      var ρ = F / Math.pow(t(φ), n);\n      return [ ρ * Math.sin(n * λ), F - ρ * Math.cos(n * λ) ];\n    }\n    forward.invert = function(x, y) {\n      var ρ0_y = F - y, ρ = d3_sgn(n) * Math.sqrt(x * x + ρ0_y * ρ0_y);\n      return [ Math.atan2(x, ρ0_y) / n, 2 * Math.atan(Math.pow(F / ρ, 1 / n)) - halfπ ];\n    };\n    return forward;\n  }\n  (d3.geo.conicConformal = function() {\n    return d3_geo_conic(d3_geo_conicConformal);\n  }).raw = d3_geo_conicConformal;\n  function d3_geo_conicEquidistant(φ0, φ1) {\n    var cosφ0 = Math.cos(φ0), n = φ0 === φ1 ? Math.sin(φ0) : (cosφ0 - Math.cos(φ1)) / (φ1 - φ0), G = cosφ0 / n + φ0;\n    if (abs(n) < ε) return d3_geo_equirectangular;\n    function forward(λ, φ) {\n      var ρ = G - φ;\n      return [ ρ * Math.sin(n * λ), G - ρ * Math.cos(n * λ) ];\n    }\n    forward.invert = function(x, y) {\n      var ρ0_y = G - y;\n      return [ Math.atan2(x, ρ0_y) / n, G - d3_sgn(n) * Math.sqrt(x * x + ρ0_y * ρ0_y) ];\n    };\n    return forward;\n  }\n  (d3.geo.conicEquidistant = function() {\n    return d3_geo_conic(d3_geo_conicEquidistant);\n  }).raw = d3_geo_conicEquidistant;\n  var d3_geo_gnomonic = d3_geo_azimuthal(function(cosλcosφ) {\n    return 1 / cosλcosφ;\n  }, Math.atan);\n  (d3.geo.gnomonic = function() {\n    return d3_geo_projection(d3_geo_gnomonic);\n  }).raw = d3_geo_gnomonic;\n  function d3_geo_mercator(λ, φ) {\n    return [ λ, Math.log(Math.tan(π / 4 + φ / 2)) ];\n  }\n  d3_geo_mercator.invert = function(x, y) {\n    return [ x, 2 * Math.atan(Math.exp(y)) - halfπ ];\n  };\n  function d3_geo_mercatorProjection(project) {\n    var m = d3_geo_projection(project), scale = m.scale, translate = m.translate, clipExtent = m.clipExtent, clipAuto;\n    m.scale = function() {\n      var v = scale.apply(m, arguments);\n      return v === m ? clipAuto ? m.clipExtent(null) : m : v;\n    };\n    m.translate = function() {\n      var v = translate.apply(m, arguments);\n      return v === m ? clipAuto ? m.clipExtent(null) : m : v;\n    };\n    m.clipExtent = function(_) {\n      var v = clipExtent.apply(m, arguments);\n      if (v === m) {\n        if (clipAuto = _ == null) {\n          var k = π * scale(), t = translate();\n          clipExtent([ [ t[0] - k, t[1] - k ], [ t[0] + k, t[1] + k ] ]);\n        }\n      } else if (clipAuto) {\n        v = null;\n      }\n      return v;\n    };\n    return m.clipExtent(null);\n  }\n  (d3.geo.mercator = function() {\n    return d3_geo_mercatorProjection(d3_geo_mercator);\n  }).raw = d3_geo_mercator;\n  var d3_geo_orthographic = d3_geo_azimuthal(function() {\n    return 1;\n  }, Math.asin);\n  (d3.geo.orthographic = function() {\n    return d3_geo_projection(d3_geo_orthographic);\n  }).raw = d3_geo_orthographic;\n  var d3_geo_stereographic = d3_geo_azimuthal(function(cosλcosφ) {\n    return 1 / (1 + cosλcosφ);\n  }, function(ρ) {\n    return 2 * Math.atan(ρ);\n  });\n  (d3.geo.stereographic = function() {\n    return d3_geo_projection(d3_geo_stereographic);\n  }).raw = d3_geo_stereographic;\n  function d3_geo_transverseMercator(λ, φ) {\n    return [ Math.log(Math.tan(π / 4 + φ / 2)), -λ ];\n  }\n  d3_geo_transverseMercator.invert = function(x, y) {\n    return [ -y, 2 * Math.atan(Math.exp(x)) - halfπ ];\n  };\n  (d3.geo.transverseMercator = function() {\n    var projection = d3_geo_mercatorProjection(d3_geo_transverseMercator), center = projection.center, rotate = projection.rotate;\n    projection.center = function(_) {\n      return _ ? center([ -_[1], _[0] ]) : (_ = center(), [ -_[1], _[0] ]);\n    };\n    projection.rotate = function(_) {\n      return _ ? rotate([ _[0], _[1], _.length > 2 ? _[2] + 90 : 90 ]) : (_ = rotate(), \n      [ _[0], _[1], _[2] - 90 ]);\n    };\n    return projection.rotate([ 0, 0 ]);\n  }).raw = d3_geo_transverseMercator;\n  d3.geom = {};\n  function d3_geom_pointX(d) {\n    return d[0];\n  }\n  function d3_geom_pointY(d) {\n    return d[1];\n  }\n  d3.geom.hull = function(vertices) {\n    var x = d3_geom_pointX, y = d3_geom_pointY;\n    if (arguments.length) return hull(vertices);\n    function hull(data) {\n      if (data.length < 3) return [];\n      var fx = d3_functor(x), fy = d3_functor(y), i, n = data.length, points = [], flippedPoints = [];\n      for (i = 0; i < n; i++) {\n        points.push([ +fx.call(this, data[i], i), +fy.call(this, data[i], i), i ]);\n      }\n      points.sort(d3_geom_hullOrder);\n      for (i = 0; i < n; i++) flippedPoints.push([ points[i][0], -points[i][1] ]);\n      var upper = d3_geom_hullUpper(points), lower = d3_geom_hullUpper(flippedPoints);\n      var skipLeft = lower[0] === upper[0], skipRight = lower[lower.length - 1] === upper[upper.length - 1], polygon = [];\n      for (i = upper.length - 1; i >= 0; --i) polygon.push(data[points[upper[i]][2]]);\n      for (i = +skipLeft; i < lower.length - skipRight; ++i) polygon.push(data[points[lower[i]][2]]);\n      return polygon;\n    }\n    hull.x = function(_) {\n      return arguments.length ? (x = _, hull) : x;\n    };\n    hull.y = function(_) {\n      return arguments.length ? (y = _, hull) : y;\n    };\n    return hull;\n  };\n  function d3_geom_hullUpper(points) {\n    var n = points.length, hull = [ 0, 1 ], hs = 2;\n    for (var i = 2; i < n; i++) {\n      while (hs > 1 && d3_cross2d(points[hull[hs - 2]], points[hull[hs - 1]], points[i]) <= 0) --hs;\n      hull[hs++] = i;\n    }\n    return hull.slice(0, hs);\n  }\n  function d3_geom_hullOrder(a, b) {\n    return a[0] - b[0] || a[1] - b[1];\n  }\n  d3.geom.polygon = function(coordinates) {\n    d3_subclass(coordinates, d3_geom_polygonPrototype);\n    return coordinates;\n  };\n  var d3_geom_polygonPrototype = d3.geom.polygon.prototype = [];\n  d3_geom_polygonPrototype.area = function() {\n    var i = -1, n = this.length, a, b = this[n - 1], area = 0;\n    while (++i < n) {\n      a = b;\n      b = this[i];\n      area += a[1] * b[0] - a[0] * b[1];\n    }\n    return area * .5;\n  };\n  d3_geom_polygonPrototype.centroid = function(k) {\n    var i = -1, n = this.length, x = 0, y = 0, a, b = this[n - 1], c;\n    if (!arguments.length) k = -1 / (6 * this.area());\n    while (++i < n) {\n      a = b;\n      b = this[i];\n      c = a[0] * b[1] - b[0] * a[1];\n      x += (a[0] + b[0]) * c;\n      y += (a[1] + b[1]) * c;\n    }\n    return [ x * k, y * k ];\n  };\n  d3_geom_polygonPrototype.clip = function(subject) {\n    var input, closed = d3_geom_polygonClosed(subject), i = -1, n = this.length - d3_geom_polygonClosed(this), j, m, a = this[n - 1], b, c, d;\n    while (++i < n) {\n      input = subject.slice();\n      subject.length = 0;\n      b = this[i];\n      c = input[(m = input.length - closed) - 1];\n      j = -1;\n      while (++j < m) {\n        d = input[j];\n        if (d3_geom_polygonInside(d, a, b)) {\n          if (!d3_geom_polygonInside(c, a, b)) {\n            subject.push(d3_geom_polygonIntersect(c, d, a, b));\n          }\n          subject.push(d);\n        } else if (d3_geom_polygonInside(c, a, b)) {\n          subject.push(d3_geom_polygonIntersect(c, d, a, b));\n        }\n        c = d;\n      }\n      if (closed) subject.push(subject[0]);\n      a = b;\n    }\n    return subject;\n  };\n  function d3_geom_polygonInside(p, a, b) {\n    return (b[0] - a[0]) * (p[1] - a[1]) < (b[1] - a[1]) * (p[0] - a[0]);\n  }\n  function d3_geom_polygonIntersect(c, d, a, b) {\n    var x1 = c[0], x3 = a[0], x21 = d[0] - x1, x43 = b[0] - x3, y1 = c[1], y3 = a[1], y21 = d[1] - y1, y43 = b[1] - y3, ua = (x43 * (y1 - y3) - y43 * (x1 - x3)) / (y43 * x21 - x43 * y21);\n    return [ x1 + ua * x21, y1 + ua * y21 ];\n  }\n  function d3_geom_polygonClosed(coordinates) {\n    var a = coordinates[0], b = coordinates[coordinates.length - 1];\n    return !(a[0] - b[0] || a[1] - b[1]);\n  }\n  var d3_geom_voronoiEdges, d3_geom_voronoiCells, d3_geom_voronoiBeaches, d3_geom_voronoiBeachPool = [], d3_geom_voronoiFirstCircle, d3_geom_voronoiCircles, d3_geom_voronoiCirclePool = [];\n  function d3_geom_voronoiBeach() {\n    d3_geom_voronoiRedBlackNode(this);\n    this.edge = this.site = this.circle = null;\n  }\n  function d3_geom_voronoiCreateBeach(site) {\n    var beach = d3_geom_voronoiBeachPool.pop() || new d3_geom_voronoiBeach();\n    beach.site = site;\n    return beach;\n  }\n  function d3_geom_voronoiDetachBeach(beach) {\n    d3_geom_voronoiDetachCircle(beach);\n    d3_geom_voronoiBeaches.remove(beach);\n    d3_geom_voronoiBeachPool.push(beach);\n    d3_geom_voronoiRedBlackNode(beach);\n  }\n  function d3_geom_voronoiRemoveBeach(beach) {\n    var circle = beach.circle, x = circle.x, y = circle.cy, vertex = {\n      x: x,\n      y: y\n    }, previous = beach.P, next = beach.N, disappearing = [ beach ];\n    d3_geom_voronoiDetachBeach(beach);\n    var lArc = previous;\n    while (lArc.circle && abs(x - lArc.circle.x) < ε && abs(y - lArc.circle.cy) < ε) {\n      previous = lArc.P;\n      disappearing.unshift(lArc);\n      d3_geom_voronoiDetachBeach(lArc);\n      lArc = previous;\n    }\n    disappearing.unshift(lArc);\n    d3_geom_voronoiDetachCircle(lArc);\n    var rArc = next;\n    while (rArc.circle && abs(x - rArc.circle.x) < ε && abs(y - rArc.circle.cy) < ε) {\n      next = rArc.N;\n      disappearing.push(rArc);\n      d3_geom_voronoiDetachBeach(rArc);\n      rArc = next;\n    }\n    disappearing.push(rArc);\n    d3_geom_voronoiDetachCircle(rArc);\n    var nArcs = disappearing.length, iArc;\n    for (iArc = 1; iArc < nArcs; ++iArc) {\n      rArc = disappearing[iArc];\n      lArc = disappearing[iArc - 1];\n      d3_geom_voronoiSetEdgeEnd(rArc.edge, lArc.site, rArc.site, vertex);\n    }\n    lArc = disappearing[0];\n    rArc = disappearing[nArcs - 1];\n    rArc.edge = d3_geom_voronoiCreateEdge(lArc.site, rArc.site, null, vertex);\n    d3_geom_voronoiAttachCircle(lArc);\n    d3_geom_voronoiAttachCircle(rArc);\n  }\n  function d3_geom_voronoiAddBeach(site) {\n    var x = site.x, directrix = site.y, lArc, rArc, dxl, dxr, node = d3_geom_voronoiBeaches._;\n    while (node) {\n      dxl = d3_geom_voronoiLeftBreakPoint(node, directrix) - x;\n      if (dxl > ε) node = node.L; else {\n        dxr = x - d3_geom_voronoiRightBreakPoint(node, directrix);\n        if (dxr > ε) {\n          if (!node.R) {\n            lArc = node;\n            break;\n          }\n          node = node.R;\n        } else {\n          if (dxl > -ε) {\n            lArc = node.P;\n            rArc = node;\n          } else if (dxr > -ε) {\n            lArc = node;\n            rArc = node.N;\n          } else {\n            lArc = rArc = node;\n          }\n          break;\n        }\n      }\n    }\n    var newArc = d3_geom_voronoiCreateBeach(site);\n    d3_geom_voronoiBeaches.insert(lArc, newArc);\n    if (!lArc && !rArc) return;\n    if (lArc === rArc) {\n      d3_geom_voronoiDetachCircle(lArc);\n      rArc = d3_geom_voronoiCreateBeach(lArc.site);\n      d3_geom_voronoiBeaches.insert(newArc, rArc);\n      newArc.edge = rArc.edge = d3_geom_voronoiCreateEdge(lArc.site, newArc.site);\n      d3_geom_voronoiAttachCircle(lArc);\n      d3_geom_voronoiAttachCircle(rArc);\n      return;\n    }\n    if (!rArc) {\n      newArc.edge = d3_geom_voronoiCreateEdge(lArc.site, newArc.site);\n      return;\n    }\n    d3_geom_voronoiDetachCircle(lArc);\n    d3_geom_voronoiDetachCircle(rArc);\n    var lSite = lArc.site, ax = lSite.x, ay = lSite.y, bx = site.x - ax, by = site.y - ay, rSite = rArc.site, cx = rSite.x - ax, cy = rSite.y - ay, d = 2 * (bx * cy - by * cx), hb = bx * bx + by * by, hc = cx * cx + cy * cy, vertex = {\n      x: (cy * hb - by * hc) / d + ax,\n      y: (bx * hc - cx * hb) / d + ay\n    };\n    d3_geom_voronoiSetEdgeEnd(rArc.edge, lSite, rSite, vertex);\n    newArc.edge = d3_geom_voronoiCreateEdge(lSite, site, null, vertex);\n    rArc.edge = d3_geom_voronoiCreateEdge(site, rSite, null, vertex);\n    d3_geom_voronoiAttachCircle(lArc);\n    d3_geom_voronoiAttachCircle(rArc);\n  }\n  function d3_geom_voronoiLeftBreakPoint(arc, directrix) {\n    var site = arc.site, rfocx = site.x, rfocy = site.y, pby2 = rfocy - directrix;\n    if (!pby2) return rfocx;\n    var lArc = arc.P;\n    if (!lArc) return -Infinity;\n    site = lArc.site;\n    var lfocx = site.x, lfocy = site.y, plby2 = lfocy - directrix;\n    if (!plby2) return lfocx;\n    var hl = lfocx - rfocx, aby2 = 1 / pby2 - 1 / plby2, b = hl / plby2;\n    if (aby2) return (-b + Math.sqrt(b * b - 2 * aby2 * (hl * hl / (-2 * plby2) - lfocy + plby2 / 2 + rfocy - pby2 / 2))) / aby2 + rfocx;\n    return (rfocx + lfocx) / 2;\n  }\n  function d3_geom_voronoiRightBreakPoint(arc, directrix) {\n    var rArc = arc.N;\n    if (rArc) return d3_geom_voronoiLeftBreakPoint(rArc, directrix);\n    var site = arc.site;\n    return site.y === directrix ? site.x : Infinity;\n  }\n  function d3_geom_voronoiCell(site) {\n    this.site = site;\n    this.edges = [];\n  }\n  d3_geom_voronoiCell.prototype.prepare = function() {\n    var halfEdges = this.edges, iHalfEdge = halfEdges.length, edge;\n    while (iHalfEdge--) {\n      edge = halfEdges[iHalfEdge].edge;\n      if (!edge.b || !edge.a) halfEdges.splice(iHalfEdge, 1);\n    }\n    halfEdges.sort(d3_geom_voronoiHalfEdgeOrder);\n    return halfEdges.length;\n  };\n  function d3_geom_voronoiCloseCells(extent) {\n    var x0 = extent[0][0], x1 = extent[1][0], y0 = extent[0][1], y1 = extent[1][1], x2, y2, x3, y3, cells = d3_geom_voronoiCells, iCell = cells.length, cell, iHalfEdge, halfEdges, nHalfEdges, start, end;\n    while (iCell--) {\n      cell = cells[iCell];\n      if (!cell || !cell.prepare()) continue;\n      halfEdges = cell.edges;\n      nHalfEdges = halfEdges.length;\n      iHalfEdge = 0;\n      while (iHalfEdge < nHalfEdges) {\n        end = halfEdges[iHalfEdge].end(), x3 = end.x, y3 = end.y;\n        start = halfEdges[++iHalfEdge % nHalfEdges].start(), x2 = start.x, y2 = start.y;\n        if (abs(x3 - x2) > ε || abs(y3 - y2) > ε) {\n          halfEdges.splice(iHalfEdge, 0, new d3_geom_voronoiHalfEdge(d3_geom_voronoiCreateBorderEdge(cell.site, end, abs(x3 - x0) < ε && y1 - y3 > ε ? {\n            x: x0,\n            y: abs(x2 - x0) < ε ? y2 : y1\n          } : abs(y3 - y1) < ε && x1 - x3 > ε ? {\n            x: abs(y2 - y1) < ε ? x2 : x1,\n            y: y1\n          } : abs(x3 - x1) < ε && y3 - y0 > ε ? {\n            x: x1,\n            y: abs(x2 - x1) < ε ? y2 : y0\n          } : abs(y3 - y0) < ε && x3 - x0 > ε ? {\n            x: abs(y2 - y0) < ε ? x2 : x0,\n            y: y0\n          } : null), cell.site, null));\n          ++nHalfEdges;\n        }\n      }\n    }\n  }\n  function d3_geom_voronoiHalfEdgeOrder(a, b) {\n    return b.angle - a.angle;\n  }\n  function d3_geom_voronoiCircle() {\n    d3_geom_voronoiRedBlackNode(this);\n    this.x = this.y = this.arc = this.site = this.cy = null;\n  }\n  function d3_geom_voronoiAttachCircle(arc) {\n    var lArc = arc.P, rArc = arc.N;\n    if (!lArc || !rArc) return;\n    var lSite = lArc.site, cSite = arc.site, rSite = rArc.site;\n    if (lSite === rSite) return;\n    var bx = cSite.x, by = cSite.y, ax = lSite.x - bx, ay = lSite.y - by, cx = rSite.x - bx, cy = rSite.y - by;\n    var d = 2 * (ax * cy - ay * cx);\n    if (d >= -ε2) return;\n    var ha = ax * ax + ay * ay, hc = cx * cx + cy * cy, x = (cy * ha - ay * hc) / d, y = (ax * hc - cx * ha) / d, cy = y + by;\n    var circle = d3_geom_voronoiCirclePool.pop() || new d3_geom_voronoiCircle();\n    circle.arc = arc;\n    circle.site = cSite;\n    circle.x = x + bx;\n    circle.y = cy + Math.sqrt(x * x + y * y);\n    circle.cy = cy;\n    arc.circle = circle;\n    var before = null, node = d3_geom_voronoiCircles._;\n    while (node) {\n      if (circle.y < node.y || circle.y === node.y && circle.x <= node.x) {\n        if (node.L) node = node.L; else {\n          before = node.P;\n          break;\n        }\n      } else {\n        if (node.R) node = node.R; else {\n          before = node;\n          break;\n        }\n      }\n    }\n    d3_geom_voronoiCircles.insert(before, circle);\n    if (!before) d3_geom_voronoiFirstCircle = circle;\n  }\n  function d3_geom_voronoiDetachCircle(arc) {\n    var circle = arc.circle;\n    if (circle) {\n      if (!circle.P) d3_geom_voronoiFirstCircle = circle.N;\n      d3_geom_voronoiCircles.remove(circle);\n      d3_geom_voronoiCirclePool.push(circle);\n      d3_geom_voronoiRedBlackNode(circle);\n      arc.circle = null;\n    }\n  }\n  function d3_geom_voronoiClipEdges(extent) {\n    var edges = d3_geom_voronoiEdges, clip = d3_geom_clipLine(extent[0][0], extent[0][1], extent[1][0], extent[1][1]), i = edges.length, e;\n    while (i--) {\n      e = edges[i];\n      if (!d3_geom_voronoiConnectEdge(e, extent) || !clip(e) || abs(e.a.x - e.b.x) < ε && abs(e.a.y - e.b.y) < ε) {\n        e.a = e.b = null;\n        edges.splice(i, 1);\n      }\n    }\n  }\n  function d3_geom_voronoiConnectEdge(edge, extent) {\n    var vb = edge.b;\n    if (vb) return true;\n    var va = edge.a, x0 = extent[0][0], x1 = extent[1][0], y0 = extent[0][1], y1 = extent[1][1], lSite = edge.l, rSite = edge.r, lx = lSite.x, ly = lSite.y, rx = rSite.x, ry = rSite.y, fx = (lx + rx) / 2, fy = (ly + ry) / 2, fm, fb;\n    if (ry === ly) {\n      if (fx < x0 || fx >= x1) return;\n      if (lx > rx) {\n        if (!va) va = {\n          x: fx,\n          y: y0\n        }; else if (va.y >= y1) return;\n        vb = {\n          x: fx,\n          y: y1\n        };\n      } else {\n        if (!va) va = {\n          x: fx,\n          y: y1\n        }; else if (va.y < y0) return;\n        vb = {\n          x: fx,\n          y: y0\n        };\n      }\n    } else {\n      fm = (lx - rx) / (ry - ly);\n      fb = fy - fm * fx;\n      if (fm < -1 || fm > 1) {\n        if (lx > rx) {\n          if (!va) va = {\n            x: (y0 - fb) / fm,\n            y: y0\n          }; else if (va.y >= y1) return;\n          vb = {\n            x: (y1 - fb) / fm,\n            y: y1\n          };\n        } else {\n          if (!va) va = {\n            x: (y1 - fb) / fm,\n            y: y1\n          }; else if (va.y < y0) return;\n          vb = {\n            x: (y0 - fb) / fm,\n            y: y0\n          };\n        }\n      } else {\n        if (ly < ry) {\n          if (!va) va = {\n            x: x0,\n            y: fm * x0 + fb\n          }; else if (va.x >= x1) return;\n          vb = {\n            x: x1,\n            y: fm * x1 + fb\n          };\n        } else {\n          if (!va) va = {\n            x: x1,\n            y: fm * x1 + fb\n          }; else if (va.x < x0) return;\n          vb = {\n            x: x0,\n            y: fm * x0 + fb\n          };\n        }\n      }\n    }\n    edge.a = va;\n    edge.b = vb;\n    return true;\n  }\n  function d3_geom_voronoiEdge(lSite, rSite) {\n    this.l = lSite;\n    this.r = rSite;\n    this.a = this.b = null;\n  }\n  function d3_geom_voronoiCreateEdge(lSite, rSite, va, vb) {\n    var edge = new d3_geom_voronoiEdge(lSite, rSite);\n    d3_geom_voronoiEdges.push(edge);\n    if (va) d3_geom_voronoiSetEdgeEnd(edge, lSite, rSite, va);\n    if (vb) d3_geom_voronoiSetEdgeEnd(edge, rSite, lSite, vb);\n    d3_geom_voronoiCells[lSite.i].edges.push(new d3_geom_voronoiHalfEdge(edge, lSite, rSite));\n    d3_geom_voronoiCells[rSite.i].edges.push(new d3_geom_voronoiHalfEdge(edge, rSite, lSite));\n    return edge;\n  }\n  function d3_geom_voronoiCreateBorderEdge(lSite, va, vb) {\n    var edge = new d3_geom_voronoiEdge(lSite, null);\n    edge.a = va;\n    edge.b = vb;\n    d3_geom_voronoiEdges.push(edge);\n    return edge;\n  }\n  function d3_geom_voronoiSetEdgeEnd(edge, lSite, rSite, vertex) {\n    if (!edge.a && !edge.b) {\n      edge.a = vertex;\n      edge.l = lSite;\n      edge.r = rSite;\n    } else if (edge.l === rSite) {\n      edge.b = vertex;\n    } else {\n      edge.a = vertex;\n    }\n  }\n  function d3_geom_voronoiHalfEdge(edge, lSite, rSite) {\n    var va = edge.a, vb = edge.b;\n    this.edge = edge;\n    this.site = lSite;\n    this.angle = rSite ? Math.atan2(rSite.y - lSite.y, rSite.x - lSite.x) : edge.l === lSite ? Math.atan2(vb.x - va.x, va.y - vb.y) : Math.atan2(va.x - vb.x, vb.y - va.y);\n  }\n  d3_geom_voronoiHalfEdge.prototype = {\n    start: function() {\n      return this.edge.l === this.site ? this.edge.a : this.edge.b;\n    },\n    end: function() {\n      return this.edge.l === this.site ? this.edge.b : this.edge.a;\n    }\n  };\n  function d3_geom_voronoiRedBlackTree() {\n    this._ = null;\n  }\n  function d3_geom_voronoiRedBlackNode(node) {\n    node.U = node.C = node.L = node.R = node.P = node.N = null;\n  }\n  d3_geom_voronoiRedBlackTree.prototype = {\n    insert: function(after, node) {\n      var parent, grandpa, uncle;\n      if (after) {\n        node.P = after;\n        node.N = after.N;\n        if (after.N) after.N.P = node;\n        after.N = node;\n        if (after.R) {\n          after = after.R;\n          while (after.L) after = after.L;\n          after.L = node;\n        } else {\n          after.R = node;\n        }\n        parent = after;\n      } else if (this._) {\n        after = d3_geom_voronoiRedBlackFirst(this._);\n        node.P = null;\n        node.N = after;\n        after.P = after.L = node;\n        parent = after;\n      } else {\n        node.P = node.N = null;\n        this._ = node;\n        parent = null;\n      }\n      node.L = node.R = null;\n      node.U = parent;\n      node.C = true;\n      after = node;\n      while (parent && parent.C) {\n        grandpa = parent.U;\n        if (parent === grandpa.L) {\n          uncle = grandpa.R;\n          if (uncle && uncle.C) {\n            parent.C = uncle.C = false;\n            grandpa.C = true;\n            after = grandpa;\n          } else {\n            if (after === parent.R) {\n              d3_geom_voronoiRedBlackRotateLeft(this, parent);\n              after = parent;\n              parent = after.U;\n            }\n            parent.C = false;\n            grandpa.C = true;\n            d3_geom_voronoiRedBlackRotateRight(this, grandpa);\n          }\n        } else {\n          uncle = grandpa.L;\n          if (uncle && uncle.C) {\n            parent.C = uncle.C = false;\n            grandpa.C = true;\n            after = grandpa;\n          } else {\n            if (after === parent.L) {\n              d3_geom_voronoiRedBlackRotateRight(this, parent);\n              after = parent;\n              parent = after.U;\n            }\n            parent.C = false;\n            grandpa.C = true;\n            d3_geom_voronoiRedBlackRotateLeft(this, grandpa);\n          }\n        }\n        parent = after.U;\n      }\n      this._.C = false;\n    },\n    remove: function(node) {\n      if (node.N) node.N.P = node.P;\n      if (node.P) node.P.N = node.N;\n      node.N = node.P = null;\n      var parent = node.U, sibling, left = node.L, right = node.R, next, red;\n      if (!left) next = right; else if (!right) next = left; else next = d3_geom_voronoiRedBlackFirst(right);\n      if (parent) {\n        if (parent.L === node) parent.L = next; else parent.R = next;\n      } else {\n        this._ = next;\n      }\n      if (left && right) {\n        red = next.C;\n        next.C = node.C;\n        next.L = left;\n        left.U = next;\n        if (next !== right) {\n          parent = next.U;\n          next.U = node.U;\n          node = next.R;\n          parent.L = node;\n          next.R = right;\n          right.U = next;\n        } else {\n          next.U = parent;\n          parent = next;\n          node = next.R;\n        }\n      } else {\n        red = node.C;\n        node = next;\n      }\n      if (node) node.U = parent;\n      if (red) return;\n      if (node && node.C) {\n        node.C = false;\n        return;\n      }\n      do {\n        if (node === this._) break;\n        if (node === parent.L) {\n          sibling = parent.R;\n          if (sibling.C) {\n            sibling.C = false;\n            parent.C = true;\n            d3_geom_voronoiRedBlackRotateLeft(this, parent);\n            sibling = parent.R;\n          }\n          if (sibling.L && sibling.L.C || sibling.R && sibling.R.C) {\n            if (!sibling.R || !sibling.R.C) {\n              sibling.L.C = false;\n              sibling.C = true;\n              d3_geom_voronoiRedBlackRotateRight(this, sibling);\n              sibling = parent.R;\n            }\n            sibling.C = parent.C;\n            parent.C = sibling.R.C = false;\n            d3_geom_voronoiRedBlackRotateLeft(this, parent);\n            node = this._;\n            break;\n          }\n        } else {\n          sibling = parent.L;\n          if (sibling.C) {\n            sibling.C = false;\n            parent.C = true;\n            d3_geom_voronoiRedBlackRotateRight(this, parent);\n            sibling = parent.L;\n          }\n          if (sibling.L && sibling.L.C || sibling.R && sibling.R.C) {\n            if (!sibling.L || !sibling.L.C) {\n              sibling.R.C = false;\n              sibling.C = true;\n              d3_geom_voronoiRedBlackRotateLeft(this, sibling);\n              sibling = parent.L;\n            }\n            sibling.C = parent.C;\n            parent.C = sibling.L.C = false;\n            d3_geom_voronoiRedBlackRotateRight(this, parent);\n            node = this._;\n            break;\n          }\n        }\n        sibling.C = true;\n        node = parent;\n        parent = parent.U;\n      } while (!node.C);\n      if (node) node.C = false;\n    }\n  };\n  function d3_geom_voronoiRedBlackRotateLeft(tree, node) {\n    var p = node, q = node.R, parent = p.U;\n    if (parent) {\n      if (parent.L === p) parent.L = q; else parent.R = q;\n    } else {\n      tree._ = q;\n    }\n    q.U = parent;\n    p.U = q;\n    p.R = q.L;\n    if (p.R) p.R.U = p;\n    q.L = p;\n  }\n  function d3_geom_voronoiRedBlackRotateRight(tree, node) {\n    var p = node, q = node.L, parent = p.U;\n    if (parent) {\n      if (parent.L === p) parent.L = q; else parent.R = q;\n    } else {\n      tree._ = q;\n    }\n    q.U = parent;\n    p.U = q;\n    p.L = q.R;\n    if (p.L) p.L.U = p;\n    q.R = p;\n  }\n  function d3_geom_voronoiRedBlackFirst(node) {\n    while (node.L) node = node.L;\n    return node;\n  }\n  function d3_geom_voronoi(sites, bbox) {\n    var site = sites.sort(d3_geom_voronoiVertexOrder).pop(), x0, y0, circle;\n    d3_geom_voronoiEdges = [];\n    d3_geom_voronoiCells = new Array(sites.length);\n    d3_geom_voronoiBeaches = new d3_geom_voronoiRedBlackTree();\n    d3_geom_voronoiCircles = new d3_geom_voronoiRedBlackTree();\n    while (true) {\n      circle = d3_geom_voronoiFirstCircle;\n      if (site && (!circle || site.y < circle.y || site.y === circle.y && site.x < circle.x)) {\n        if (site.x !== x0 || site.y !== y0) {\n          d3_geom_voronoiCells[site.i] = new d3_geom_voronoiCell(site);\n          d3_geom_voronoiAddBeach(site);\n          x0 = site.x, y0 = site.y;\n        }\n        site = sites.pop();\n      } else if (circle) {\n        d3_geom_voronoiRemoveBeach(circle.arc);\n      } else {\n        break;\n      }\n    }\n    if (bbox) d3_geom_voronoiClipEdges(bbox), d3_geom_voronoiCloseCells(bbox);\n    var diagram = {\n      cells: d3_geom_voronoiCells,\n      edges: d3_geom_voronoiEdges\n    };\n    d3_geom_voronoiBeaches = d3_geom_voronoiCircles = d3_geom_voronoiEdges = d3_geom_voronoiCells = null;\n    return diagram;\n  }\n  function d3_geom_voronoiVertexOrder(a, b) {\n    return b.y - a.y || b.x - a.x;\n  }\n  d3.geom.voronoi = function(points) {\n    var x = d3_geom_pointX, y = d3_geom_pointY, fx = x, fy = y, clipExtent = d3_geom_voronoiClipExtent;\n    if (points) return voronoi(points);\n    function voronoi(data) {\n      var polygons = new Array(data.length), x0 = clipExtent[0][0], y0 = clipExtent[0][1], x1 = clipExtent[1][0], y1 = clipExtent[1][1];\n      d3_geom_voronoi(sites(data), clipExtent).cells.forEach(function(cell, i) {\n        var edges = cell.edges, site = cell.site, polygon = polygons[i] = edges.length ? edges.map(function(e) {\n          var s = e.start();\n          return [ s.x, s.y ];\n        }) : site.x >= x0 && site.x <= x1 && site.y >= y0 && site.y <= y1 ? [ [ x0, y1 ], [ x1, y1 ], [ x1, y0 ], [ x0, y0 ] ] : [];\n        polygon.point = data[i];\n      });\n      return polygons;\n    }\n    function sites(data) {\n      return data.map(function(d, i) {\n        return {\n          x: Math.round(fx(d, i) / ε) * ε,\n          y: Math.round(fy(d, i) / ε) * ε,\n          i: i\n        };\n      });\n    }\n    voronoi.links = function(data) {\n      return d3_geom_voronoi(sites(data)).edges.filter(function(edge) {\n        return edge.l && edge.r;\n      }).map(function(edge) {\n        return {\n          source: data[edge.l.i],\n          target: data[edge.r.i]\n        };\n      });\n    };\n    voronoi.triangles = function(data) {\n      var triangles = [];\n      d3_geom_voronoi(sites(data)).cells.forEach(function(cell, i) {\n        var site = cell.site, edges = cell.edges.sort(d3_geom_voronoiHalfEdgeOrder), j = -1, m = edges.length, e0, s0, e1 = edges[m - 1].edge, s1 = e1.l === site ? e1.r : e1.l;\n        while (++j < m) {\n          e0 = e1;\n          s0 = s1;\n          e1 = edges[j].edge;\n          s1 = e1.l === site ? e1.r : e1.l;\n          if (i < s0.i && i < s1.i && d3_geom_voronoiTriangleArea(site, s0, s1) < 0) {\n            triangles.push([ data[i], data[s0.i], data[s1.i] ]);\n          }\n        }\n      });\n      return triangles;\n    };\n    voronoi.x = function(_) {\n      return arguments.length ? (fx = d3_functor(x = _), voronoi) : x;\n    };\n    voronoi.y = function(_) {\n      return arguments.length ? (fy = d3_functor(y = _), voronoi) : y;\n    };\n    voronoi.clipExtent = function(_) {\n      if (!arguments.length) return clipExtent === d3_geom_voronoiClipExtent ? null : clipExtent;\n      clipExtent = _ == null ? d3_geom_voronoiClipExtent : _;\n      return voronoi;\n    };\n    voronoi.size = function(_) {\n      if (!arguments.length) return clipExtent === d3_geom_voronoiClipExtent ? null : clipExtent && clipExtent[1];\n      return voronoi.clipExtent(_ && [ [ 0, 0 ], _ ]);\n    };\n    return voronoi;\n  };\n  var d3_geom_voronoiClipExtent = [ [ -1e6, -1e6 ], [ 1e6, 1e6 ] ];\n  function d3_geom_voronoiTriangleArea(a, b, c) {\n    return (a.x - c.x) * (b.y - a.y) - (a.x - b.x) * (c.y - a.y);\n  }\n  d3.geom.delaunay = function(vertices) {\n    return d3.geom.voronoi().triangles(vertices);\n  };\n  d3.geom.quadtree = function(points, x1, y1, x2, y2) {\n    var x = d3_geom_pointX, y = d3_geom_pointY, compat;\n    if (compat = arguments.length) {\n      x = d3_geom_quadtreeCompatX;\n      y = d3_geom_quadtreeCompatY;\n      if (compat === 3) {\n        y2 = y1;\n        x2 = x1;\n        y1 = x1 = 0;\n      }\n      return quadtree(points);\n    }\n    function quadtree(data) {\n      var d, fx = d3_functor(x), fy = d3_functor(y), xs, ys, i, n, x1_, y1_, x2_, y2_;\n      if (x1 != null) {\n        x1_ = x1, y1_ = y1, x2_ = x2, y2_ = y2;\n      } else {\n        x2_ = y2_ = -(x1_ = y1_ = Infinity);\n        xs = [], ys = [];\n        n = data.length;\n        if (compat) for (i = 0; i < n; ++i) {\n          d = data[i];\n          if (d.x < x1_) x1_ = d.x;\n          if (d.y < y1_) y1_ = d.y;\n          if (d.x > x2_) x2_ = d.x;\n          if (d.y > y2_) y2_ = d.y;\n          xs.push(d.x);\n          ys.push(d.y);\n        } else for (i = 0; i < n; ++i) {\n          var x_ = +fx(d = data[i], i), y_ = +fy(d, i);\n          if (x_ < x1_) x1_ = x_;\n          if (y_ < y1_) y1_ = y_;\n          if (x_ > x2_) x2_ = x_;\n          if (y_ > y2_) y2_ = y_;\n          xs.push(x_);\n          ys.push(y_);\n        }\n      }\n      var dx = x2_ - x1_, dy = y2_ - y1_;\n      if (dx > dy) y2_ = y1_ + dx; else x2_ = x1_ + dy;\n      function insert(n, d, x, y, x1, y1, x2, y2) {\n        if (isNaN(x) || isNaN(y)) return;\n        if (n.leaf) {\n          var nx = n.x, ny = n.y;\n          if (nx != null) {\n            if (abs(nx - x) + abs(ny - y) < .01) {\n              insertChild(n, d, x, y, x1, y1, x2, y2);\n            } else {\n              var nPoint = n.point;\n              n.x = n.y = n.point = null;\n              insertChild(n, nPoint, nx, ny, x1, y1, x2, y2);\n              insertChild(n, d, x, y, x1, y1, x2, y2);\n            }\n          } else {\n            n.x = x, n.y = y, n.point = d;\n          }\n        } else {\n          insertChild(n, d, x, y, x1, y1, x2, y2);\n        }\n      }\n      function insertChild(n, d, x, y, x1, y1, x2, y2) {\n        var sx = (x1 + x2) * .5, sy = (y1 + y2) * .5, right = x >= sx, bottom = y >= sy, i = (bottom << 1) + right;\n        n.leaf = false;\n        n = n.nodes[i] || (n.nodes[i] = d3_geom_quadtreeNode());\n        if (right) x1 = sx; else x2 = sx;\n        if (bottom) y1 = sy; else y2 = sy;\n        insert(n, d, x, y, x1, y1, x2, y2);\n      }\n      var root = d3_geom_quadtreeNode();\n      root.add = function(d) {\n        insert(root, d, +fx(d, ++i), +fy(d, i), x1_, y1_, x2_, y2_);\n      };\n      root.visit = function(f) {\n        d3_geom_quadtreeVisit(f, root, x1_, y1_, x2_, y2_);\n      };\n      i = -1;\n      if (x1 == null) {\n        while (++i < n) {\n          insert(root, data[i], xs[i], ys[i], x1_, y1_, x2_, y2_);\n        }\n        --i;\n      } else data.forEach(root.add);\n      xs = ys = data = d = null;\n      return root;\n    }\n    quadtree.x = function(_) {\n      return arguments.length ? (x = _, quadtree) : x;\n    };\n    quadtree.y = function(_) {\n      return arguments.length ? (y = _, quadtree) : y;\n    };\n    quadtree.extent = function(_) {\n      if (!arguments.length) return x1 == null ? null : [ [ x1, y1 ], [ x2, y2 ] ];\n      if (_ == null) x1 = y1 = x2 = y2 = null; else x1 = +_[0][0], y1 = +_[0][1], x2 = +_[1][0], \n      y2 = +_[1][1];\n      return quadtree;\n    };\n    quadtree.size = function(_) {\n      if (!arguments.length) return x1 == null ? null : [ x2 - x1, y2 - y1 ];\n      if (_ == null) x1 = y1 = x2 = y2 = null; else x1 = y1 = 0, x2 = +_[0], y2 = +_[1];\n      return quadtree;\n    };\n    return quadtree;\n  };\n  function d3_geom_quadtreeCompatX(d) {\n    return d.x;\n  }\n  function d3_geom_quadtreeCompatY(d) {\n    return d.y;\n  }\n  function d3_geom_quadtreeNode() {\n    return {\n      leaf: true,\n      nodes: [],\n      point: null,\n      x: null,\n      y: null\n    };\n  }\n  function d3_geom_quadtreeVisit(f, node, x1, y1, x2, y2) {\n    if (!f(node, x1, y1, x2, y2)) {\n      var sx = (x1 + x2) * .5, sy = (y1 + y2) * .5, children = node.nodes;\n      if (children[0]) d3_geom_quadtreeVisit(f, children[0], x1, y1, sx, sy);\n      if (children[1]) d3_geom_quadtreeVisit(f, children[1], sx, y1, x2, sy);\n      if (children[2]) d3_geom_quadtreeVisit(f, children[2], x1, sy, sx, y2);\n      if (children[3]) d3_geom_quadtreeVisit(f, children[3], sx, sy, x2, y2);\n    }\n  }\n  d3.interpolateRgb = d3_interpolateRgb;\n  function d3_interpolateRgb(a, b) {\n    a = d3.rgb(a);\n    b = d3.rgb(b);\n    var ar = a.r, ag = a.g, ab = a.b, br = b.r - ar, bg = b.g - ag, bb = b.b - ab;\n    return function(t) {\n      return \"#\" + d3_rgb_hex(Math.round(ar + br * t)) + d3_rgb_hex(Math.round(ag + bg * t)) + d3_rgb_hex(Math.round(ab + bb * t));\n    };\n  }\n  d3.interpolateObject = d3_interpolateObject;\n  function d3_interpolateObject(a, b) {\n    var i = {}, c = {}, k;\n    for (k in a) {\n      if (k in b) {\n        i[k] = d3_interpolate(a[k], b[k]);\n      } else {\n        c[k] = a[k];\n      }\n    }\n    for (k in b) {\n      if (!(k in a)) {\n        c[k] = b[k];\n      }\n    }\n    return function(t) {\n      for (k in i) c[k] = i[k](t);\n      return c;\n    };\n  }\n  d3.interpolateNumber = d3_interpolateNumber;\n  function d3_interpolateNumber(a, b) {\n    b -= a = +a;\n    return function(t) {\n      return a + b * t;\n    };\n  }\n  d3.interpolateString = d3_interpolateString;\n  function d3_interpolateString(a, b) {\n    var bi = d3_interpolate_numberA.lastIndex = d3_interpolate_numberB.lastIndex = 0, am, bm, bs, i = -1, s = [], q = [];\n    a = a + \"\", b = b + \"\";\n    while ((am = d3_interpolate_numberA.exec(a)) && (bm = d3_interpolate_numberB.exec(b))) {\n      if ((bs = bm.index) > bi) {\n        bs = b.substring(bi, bs);\n        if (s[i]) s[i] += bs; else s[++i] = bs;\n      }\n      if ((am = am[0]) === (bm = bm[0])) {\n        if (s[i]) s[i] += bm; else s[++i] = bm;\n      } else {\n        s[++i] = null;\n        q.push({\n          i: i,\n          x: d3_interpolateNumber(am, bm)\n        });\n      }\n      bi = d3_interpolate_numberB.lastIndex;\n    }\n    if (bi < b.length) {\n      bs = b.substring(bi);\n      if (s[i]) s[i] += bs; else s[++i] = bs;\n    }\n    return s.length < 2 ? q[0] ? (b = q[0].x, function(t) {\n      return b(t) + \"\";\n    }) : function() {\n      return b;\n    } : (b = q.length, function(t) {\n      for (var i = 0, o; i < b; ++i) s[(o = q[i]).i] = o.x(t);\n      return s.join(\"\");\n    });\n  }\n  var d3_interpolate_numberA = /[-+]?(?:\\d+\\.?\\d*|\\.?\\d+)(?:[eE][-+]?\\d+)?/g, d3_interpolate_numberB = new RegExp(d3_interpolate_numberA.source, \"g\");\n  d3.interpolate = d3_interpolate;\n  function d3_interpolate(a, b) {\n    var i = d3.interpolators.length, f;\n    while (--i >= 0 && !(f = d3.interpolators[i](a, b))) ;\n    return f;\n  }\n  d3.interpolators = [ function(a, b) {\n    var t = typeof b;\n    return (t === \"string\" ? d3_rgb_names.has(b) || /^(#|rgb\\(|hsl\\()/.test(b) ? d3_interpolateRgb : d3_interpolateString : b instanceof d3_Color ? d3_interpolateRgb : Array.isArray(b) ? d3_interpolateArray : t === \"object\" && isNaN(b) ? d3_interpolateObject : d3_interpolateNumber)(a, b);\n  } ];\n  d3.interpolateArray = d3_interpolateArray;\n  function d3_interpolateArray(a, b) {\n    var x = [], c = [], na = a.length, nb = b.length, n0 = Math.min(a.length, b.length), i;\n    for (i = 0; i < n0; ++i) x.push(d3_interpolate(a[i], b[i]));\n    for (;i < na; ++i) c[i] = a[i];\n    for (;i < nb; ++i) c[i] = b[i];\n    return function(t) {\n      for (i = 0; i < n0; ++i) c[i] = x[i](t);\n      return c;\n    };\n  }\n  var d3_ease_default = function() {\n    return d3_identity;\n  };\n  var d3_ease = d3.map({\n    linear: d3_ease_default,\n    poly: d3_ease_poly,\n    quad: function() {\n      return d3_ease_quad;\n    },\n    cubic: function() {\n      return d3_ease_cubic;\n    },\n    sin: function() {\n      return d3_ease_sin;\n    },\n    exp: function() {\n      return d3_ease_exp;\n    },\n    circle: function() {\n      return d3_ease_circle;\n    },\n    elastic: d3_ease_elastic,\n    back: d3_ease_back,\n    bounce: function() {\n      return d3_ease_bounce;\n    }\n  });\n  var d3_ease_mode = d3.map({\n    \"in\": d3_identity,\n    out: d3_ease_reverse,\n    \"in-out\": d3_ease_reflect,\n    \"out-in\": function(f) {\n      return d3_ease_reflect(d3_ease_reverse(f));\n    }\n  });\n  d3.ease = function(name) {\n    var i = name.indexOf(\"-\"), t = i >= 0 ? name.substring(0, i) : name, m = i >= 0 ? name.substring(i + 1) : \"in\";\n    t = d3_ease.get(t) || d3_ease_default;\n    m = d3_ease_mode.get(m) || d3_identity;\n    return d3_ease_clamp(m(t.apply(null, d3_arraySlice.call(arguments, 1))));\n  };\n  function d3_ease_clamp(f) {\n    return function(t) {\n      return t <= 0 ? 0 : t >= 1 ? 1 : f(t);\n    };\n  }\n  function d3_ease_reverse(f) {\n    return function(t) {\n      return 1 - f(1 - t);\n    };\n  }\n  function d3_ease_reflect(f) {\n    return function(t) {\n      return .5 * (t < .5 ? f(2 * t) : 2 - f(2 - 2 * t));\n    };\n  }\n  function d3_ease_quad(t) {\n    return t * t;\n  }\n  function d3_ease_cubic(t) {\n    return t * t * t;\n  }\n  function d3_ease_cubicInOut(t) {\n    if (t <= 0) return 0;\n    if (t >= 1) return 1;\n    var t2 = t * t, t3 = t2 * t;\n    return 4 * (t < .5 ? t3 : 3 * (t - t2) + t3 - .75);\n  }\n  function d3_ease_poly(e) {\n    return function(t) {\n      return Math.pow(t, e);\n    };\n  }\n  function d3_ease_sin(t) {\n    return 1 - Math.cos(t * halfπ);\n  }\n  function d3_ease_exp(t) {\n    return Math.pow(2, 10 * (t - 1));\n  }\n  function d3_ease_circle(t) {\n    return 1 - Math.sqrt(1 - t * t);\n  }\n  function d3_ease_elastic(a, p) {\n    var s;\n    if (arguments.length < 2) p = .45;\n    if (arguments.length) s = p / τ * Math.asin(1 / a); else a = 1, s = p / 4;\n    return function(t) {\n      return 1 + a * Math.pow(2, -10 * t) * Math.sin((t - s) * τ / p);\n    };\n  }\n  function d3_ease_back(s) {\n    if (!s) s = 1.70158;\n    return function(t) {\n      return t * t * ((s + 1) * t - s);\n    };\n  }\n  function d3_ease_bounce(t) {\n    return t < 1 / 2.75 ? 7.5625 * t * t : t < 2 / 2.75 ? 7.5625 * (t -= 1.5 / 2.75) * t + .75 : t < 2.5 / 2.75 ? 7.5625 * (t -= 2.25 / 2.75) * t + .9375 : 7.5625 * (t -= 2.625 / 2.75) * t + .984375;\n  }\n  d3.interpolateHcl = d3_interpolateHcl;\n  function d3_interpolateHcl(a, b) {\n    a = d3.hcl(a);\n    b = d3.hcl(b);\n    var ah = a.h, ac = a.c, al = a.l, bh = b.h - ah, bc = b.c - ac, bl = b.l - al;\n    if (isNaN(bc)) bc = 0, ac = isNaN(ac) ? b.c : ac;\n    if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah; else if (bh > 180) bh -= 360; else if (bh < -180) bh += 360;\n    return function(t) {\n      return d3_hcl_lab(ah + bh * t, ac + bc * t, al + bl * t) + \"\";\n    };\n  }\n  d3.interpolateHsl = d3_interpolateHsl;\n  function d3_interpolateHsl(a, b) {\n    a = d3.hsl(a);\n    b = d3.hsl(b);\n    var ah = a.h, as = a.s, al = a.l, bh = b.h - ah, bs = b.s - as, bl = b.l - al;\n    if (isNaN(bs)) bs = 0, as = isNaN(as) ? b.s : as;\n    if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah; else if (bh > 180) bh -= 360; else if (bh < -180) bh += 360;\n    return function(t) {\n      return d3_hsl_rgb(ah + bh * t, as + bs * t, al + bl * t) + \"\";\n    };\n  }\n  d3.interpolateLab = d3_interpolateLab;\n  function d3_interpolateLab(a, b) {\n    a = d3.lab(a);\n    b = d3.lab(b);\n    var al = a.l, aa = a.a, ab = a.b, bl = b.l - al, ba = b.a - aa, bb = b.b - ab;\n    return function(t) {\n      return d3_lab_rgb(al + bl * t, aa + ba * t, ab + bb * t) + \"\";\n    };\n  }\n  d3.interpolateRound = d3_interpolateRound;\n  function d3_interpolateRound(a, b) {\n    b -= a;\n    return function(t) {\n      return Math.round(a + b * t);\n    };\n  }\n  d3.transform = function(string) {\n    var g = d3_document.createElementNS(d3.ns.prefix.svg, \"g\");\n    return (d3.transform = function(string) {\n      if (string != null) {\n        g.setAttribute(\"transform\", string);\n        var t = g.transform.baseVal.consolidate();\n      }\n      return new d3_transform(t ? t.matrix : d3_transformIdentity);\n    })(string);\n  };\n  function d3_transform(m) {\n    var r0 = [ m.a, m.b ], r1 = [ m.c, m.d ], kx = d3_transformNormalize(r0), kz = d3_transformDot(r0, r1), ky = d3_transformNormalize(d3_transformCombine(r1, r0, -kz)) || 0;\n    if (r0[0] * r1[1] < r1[0] * r0[1]) {\n      r0[0] *= -1;\n      r0[1] *= -1;\n      kx *= -1;\n      kz *= -1;\n    }\n    this.rotate = (kx ? Math.atan2(r0[1], r0[0]) : Math.atan2(-r1[0], r1[1])) * d3_degrees;\n    this.translate = [ m.e, m.f ];\n    this.scale = [ kx, ky ];\n    this.skew = ky ? Math.atan2(kz, ky) * d3_degrees : 0;\n  }\n  d3_transform.prototype.toString = function() {\n    return \"translate(\" + this.translate + \")rotate(\" + this.rotate + \")skewX(\" + this.skew + \")scale(\" + this.scale + \")\";\n  };\n  function d3_transformDot(a, b) {\n    return a[0] * b[0] + a[1] * b[1];\n  }\n  function d3_transformNormalize(a) {\n    var k = Math.sqrt(d3_transformDot(a, a));\n    if (k) {\n      a[0] /= k;\n      a[1] /= k;\n    }\n    return k;\n  }\n  function d3_transformCombine(a, b, k) {\n    a[0] += k * b[0];\n    a[1] += k * b[1];\n    return a;\n  }\n  var d3_transformIdentity = {\n    a: 1,\n    b: 0,\n    c: 0,\n    d: 1,\n    e: 0,\n    f: 0\n  };\n  d3.interpolateTransform = d3_interpolateTransform;\n  function d3_interpolateTransform(a, b) {\n    var s = [], q = [], n, A = d3.transform(a), B = d3.transform(b), ta = A.translate, tb = B.translate, ra = A.rotate, rb = B.rotate, wa = A.skew, wb = B.skew, ka = A.scale, kb = B.scale;\n    if (ta[0] != tb[0] || ta[1] != tb[1]) {\n      s.push(\"translate(\", null, \",\", null, \")\");\n      q.push({\n        i: 1,\n        x: d3_interpolateNumber(ta[0], tb[0])\n      }, {\n        i: 3,\n        x: d3_interpolateNumber(ta[1], tb[1])\n      });\n    } else if (tb[0] || tb[1]) {\n      s.push(\"translate(\" + tb + \")\");\n    } else {\n      s.push(\"\");\n    }\n    if (ra != rb) {\n      if (ra - rb > 180) rb += 360; else if (rb - ra > 180) ra += 360;\n      q.push({\n        i: s.push(s.pop() + \"rotate(\", null, \")\") - 2,\n        x: d3_interpolateNumber(ra, rb)\n      });\n    } else if (rb) {\n      s.push(s.pop() + \"rotate(\" + rb + \")\");\n    }\n    if (wa != wb) {\n      q.push({\n        i: s.push(s.pop() + \"skewX(\", null, \")\") - 2,\n        x: d3_interpolateNumber(wa, wb)\n      });\n    } else if (wb) {\n      s.push(s.pop() + \"skewX(\" + wb + \")\");\n    }\n    if (ka[0] != kb[0] || ka[1] != kb[1]) {\n      n = s.push(s.pop() + \"scale(\", null, \",\", null, \")\");\n      q.push({\n        i: n - 4,\n        x: d3_interpolateNumber(ka[0], kb[0])\n      }, {\n        i: n - 2,\n        x: d3_interpolateNumber(ka[1], kb[1])\n      });\n    } else if (kb[0] != 1 || kb[1] != 1) {\n      s.push(s.pop() + \"scale(\" + kb + \")\");\n    }\n    n = q.length;\n    return function(t) {\n      var i = -1, o;\n      while (++i < n) s[(o = q[i]).i] = o.x(t);\n      return s.join(\"\");\n    };\n  }\n  function d3_uninterpolateNumber(a, b) {\n    b = b - (a = +a) ? 1 / (b - a) : 0;\n    return function(x) {\n      return (x - a) * b;\n    };\n  }\n  function d3_uninterpolateClamp(a, b) {\n    b = b - (a = +a) ? 1 / (b - a) : 0;\n    return function(x) {\n      return Math.max(0, Math.min(1, (x - a) * b));\n    };\n  }\n  d3.layout = {};\n  d3.layout.bundle = function() {\n    return function(links) {\n      var paths = [], i = -1, n = links.length;\n      while (++i < n) paths.push(d3_layout_bundlePath(links[i]));\n      return paths;\n    };\n  };\n  function d3_layout_bundlePath(link) {\n    var start = link.source, end = link.target, lca = d3_layout_bundleLeastCommonAncestor(start, end), points = [ start ];\n    while (start !== lca) {\n      start = start.parent;\n      points.push(start);\n    }\n    var k = points.length;\n    while (end !== lca) {\n      points.splice(k, 0, end);\n      end = end.parent;\n    }\n    return points;\n  }\n  function d3_layout_bundleAncestors(node) {\n    var ancestors = [], parent = node.parent;\n    while (parent != null) {\n      ancestors.push(node);\n      node = parent;\n      parent = parent.parent;\n    }\n    ancestors.push(node);\n    return ancestors;\n  }\n  function d3_layout_bundleLeastCommonAncestor(a, b) {\n    if (a === b) return a;\n    var aNodes = d3_layout_bundleAncestors(a), bNodes = d3_layout_bundleAncestors(b), aNode = aNodes.pop(), bNode = bNodes.pop(), sharedNode = null;\n    while (aNode === bNode) {\n      sharedNode = aNode;\n      aNode = aNodes.pop();\n      bNode = bNodes.pop();\n    }\n    return sharedNode;\n  }\n  d3.layout.chord = function() {\n    var chord = {}, chords, groups, matrix, n, padding = 0, sortGroups, sortSubgroups, sortChords;\n    function relayout() {\n      var subgroups = {}, groupSums = [], groupIndex = d3.range(n), subgroupIndex = [], k, x, x0, i, j;\n      chords = [];\n      groups = [];\n      k = 0, i = -1;\n      while (++i < n) {\n        x = 0, j = -1;\n        while (++j < n) {\n          x += matrix[i][j];\n        }\n        groupSums.push(x);\n        subgroupIndex.push(d3.range(n));\n        k += x;\n      }\n      if (sortGroups) {\n        groupIndex.sort(function(a, b) {\n          return sortGroups(groupSums[a], groupSums[b]);\n        });\n      }\n      if (sortSubgroups) {\n        subgroupIndex.forEach(function(d, i) {\n          d.sort(function(a, b) {\n            return sortSubgroups(matrix[i][a], matrix[i][b]);\n          });\n        });\n      }\n      k = (τ - padding * n) / k;\n      x = 0, i = -1;\n      while (++i < n) {\n        x0 = x, j = -1;\n        while (++j < n) {\n          var di = groupIndex[i], dj = subgroupIndex[di][j], v = matrix[di][dj], a0 = x, a1 = x += v * k;\n          subgroups[di + \"-\" + dj] = {\n            index: di,\n            subindex: dj,\n            startAngle: a0,\n            endAngle: a1,\n            value: v\n          };\n        }\n        groups[di] = {\n          index: di,\n          startAngle: x0,\n          endAngle: x,\n          value: (x - x0) / k\n        };\n        x += padding;\n      }\n      i = -1;\n      while (++i < n) {\n        j = i - 1;\n        while (++j < n) {\n          var source = subgroups[i + \"-\" + j], target = subgroups[j + \"-\" + i];\n          if (source.value || target.value) {\n            chords.push(source.value < target.value ? {\n              source: target,\n              target: source\n            } : {\n              source: source,\n              target: target\n            });\n          }\n        }\n      }\n      if (sortChords) resort();\n    }\n    function resort() {\n      chords.sort(function(a, b) {\n        return sortChords((a.source.value + a.target.value) / 2, (b.source.value + b.target.value) / 2);\n      });\n    }\n    chord.matrix = function(x) {\n      if (!arguments.length) return matrix;\n      n = (matrix = x) && matrix.length;\n      chords = groups = null;\n      return chord;\n    };\n    chord.padding = function(x) {\n      if (!arguments.length) return padding;\n      padding = x;\n      chords = groups = null;\n      return chord;\n    };\n    chord.sortGroups = function(x) {\n      if (!arguments.length) return sortGroups;\n      sortGroups = x;\n      chords = groups = null;\n      return chord;\n    };\n    chord.sortSubgroups = function(x) {\n      if (!arguments.length) return sortSubgroups;\n      sortSubgroups = x;\n      chords = null;\n      return chord;\n    };\n    chord.sortChords = function(x) {\n      if (!arguments.length) return sortChords;\n      sortChords = x;\n      if (chords) resort();\n      return chord;\n    };\n    chord.chords = function() {\n      if (!chords) relayout();\n      return chords;\n    };\n    chord.groups = function() {\n      if (!groups) relayout();\n      return groups;\n    };\n    return chord;\n  };\n  d3.layout.force = function() {\n    var force = {}, event = d3.dispatch(\"start\", \"tick\", \"end\"), size = [ 1, 1 ], drag, alpha, friction = .9, linkDistance = d3_layout_forceLinkDistance, linkStrength = d3_layout_forceLinkStrength, charge = -30, chargeDistance2 = d3_layout_forceChargeDistance2, gravity = .1, theta2 = .64, nodes = [], links = [], distances, strengths, charges;\n    function repulse(node) {\n      return function(quad, x1, _, x2) {\n        if (quad.point !== node) {\n          var dx = quad.cx - node.x, dy = quad.cy - node.y, dw = x2 - x1, dn = dx * dx + dy * dy;\n          if (dw * dw / theta2 < dn) {\n            if (dn < chargeDistance2) {\n              var k = quad.charge / dn;\n              node.px -= dx * k;\n              node.py -= dy * k;\n            }\n            return true;\n          }\n          if (quad.point && dn && dn < chargeDistance2) {\n            var k = quad.pointCharge / dn;\n            node.px -= dx * k;\n            node.py -= dy * k;\n          }\n        }\n        return !quad.charge;\n      };\n    }\n    force.tick = function() {\n      if ((alpha *= .99) < .005) {\n        event.end({\n          type: \"end\",\n          alpha: alpha = 0\n        });\n        return true;\n      }\n      var n = nodes.length, m = links.length, q, i, o, s, t, l, k, x, y;\n      for (i = 0; i < m; ++i) {\n        o = links[i];\n        s = o.source;\n        t = o.target;\n        x = t.x - s.x;\n        y = t.y - s.y;\n        if (l = x * x + y * y) {\n          l = alpha * strengths[i] * ((l = Math.sqrt(l)) - distances[i]) / l;\n          x *= l;\n          y *= l;\n          t.x -= x * (k = s.weight / (t.weight + s.weight));\n          t.y -= y * k;\n          s.x += x * (k = 1 - k);\n          s.y += y * k;\n        }\n      }\n      if (k = alpha * gravity) {\n        x = size[0] / 2;\n        y = size[1] / 2;\n        i = -1;\n        if (k) while (++i < n) {\n          o = nodes[i];\n          o.x += (x - o.x) * k;\n          o.y += (y - o.y) * k;\n        }\n      }\n      if (charge) {\n        d3_layout_forceAccumulate(q = d3.geom.quadtree(nodes), alpha, charges);\n        i = -1;\n        while (++i < n) {\n          if (!(o = nodes[i]).fixed) {\n            q.visit(repulse(o));\n          }\n        }\n      }\n      i = -1;\n      while (++i < n) {\n        o = nodes[i];\n        if (o.fixed) {\n          o.x = o.px;\n          o.y = o.py;\n        } else {\n          o.x -= (o.px - (o.px = o.x)) * friction;\n          o.y -= (o.py - (o.py = o.y)) * friction;\n        }\n      }\n      event.tick({\n        type: \"tick\",\n        alpha: alpha\n      });\n    };\n    force.nodes = function(x) {\n      if (!arguments.length) return nodes;\n      nodes = x;\n      return force;\n    };\n    force.links = function(x) {\n      if (!arguments.length) return links;\n      links = x;\n      return force;\n    };\n    force.size = function(x) {\n      if (!arguments.length) return size;\n      size = x;\n      return force;\n    };\n    force.linkDistance = function(x) {\n      if (!arguments.length) return linkDistance;\n      linkDistance = typeof x === \"function\" ? x : +x;\n      return force;\n    };\n    force.distance = force.linkDistance;\n    force.linkStrength = function(x) {\n      if (!arguments.length) return linkStrength;\n      linkStrength = typeof x === \"function\" ? x : +x;\n      return force;\n    };\n    force.friction = function(x) {\n      if (!arguments.length) return friction;\n      friction = +x;\n      return force;\n    };\n    force.charge = function(x) {\n      if (!arguments.length) return charge;\n      charge = typeof x === \"function\" ? x : +x;\n      return force;\n    };\n    force.chargeDistance = function(x) {\n      if (!arguments.length) return Math.sqrt(chargeDistance2);\n      chargeDistance2 = x * x;\n      return force;\n    };\n    force.gravity = function(x) {\n      if (!arguments.length) return gravity;\n      gravity = +x;\n      return force;\n    };\n    force.theta = function(x) {\n      if (!arguments.length) return Math.sqrt(theta2);\n      theta2 = x * x;\n      return force;\n    };\n    force.alpha = function(x) {\n      if (!arguments.length) return alpha;\n      x = +x;\n      if (alpha) {\n        if (x > 0) alpha = x; else alpha = 0;\n      } else if (x > 0) {\n        event.start({\n          type: \"start\",\n          alpha: alpha = x\n        });\n        d3.timer(force.tick);\n      }\n      return force;\n    };\n    force.start = function() {\n      var i, n = nodes.length, m = links.length, w = size[0], h = size[1], neighbors, o;\n      for (i = 0; i < n; ++i) {\n        (o = nodes[i]).index = i;\n        o.weight = 0;\n      }\n      for (i = 0; i < m; ++i) {\n        o = links[i];\n        if (typeof o.source == \"number\") o.source = nodes[o.source];\n        if (typeof o.target == \"number\") o.target = nodes[o.target];\n        ++o.source.weight;\n        ++o.target.weight;\n      }\n      for (i = 0; i < n; ++i) {\n        o = nodes[i];\n        if (isNaN(o.x)) o.x = position(\"x\", w);\n        if (isNaN(o.y)) o.y = position(\"y\", h);\n        if (isNaN(o.px)) o.px = o.x;\n        if (isNaN(o.py)) o.py = o.y;\n      }\n      distances = [];\n      if (typeof linkDistance === \"function\") for (i = 0; i < m; ++i) distances[i] = +linkDistance.call(this, links[i], i); else for (i = 0; i < m; ++i) distances[i] = linkDistance;\n      strengths = [];\n      if (typeof linkStrength === \"function\") for (i = 0; i < m; ++i) strengths[i] = +linkStrength.call(this, links[i], i); else for (i = 0; i < m; ++i) strengths[i] = linkStrength;\n      charges = [];\n      if (typeof charge === \"function\") for (i = 0; i < n; ++i) charges[i] = +charge.call(this, nodes[i], i); else for (i = 0; i < n; ++i) charges[i] = charge;\n      function position(dimension, size) {\n        if (!neighbors) {\n          neighbors = new Array(n);\n          for (j = 0; j < n; ++j) {\n            neighbors[j] = [];\n          }\n          for (j = 0; j < m; ++j) {\n            var o = links[j];\n            neighbors[o.source.index].push(o.target);\n            neighbors[o.target.index].push(o.source);\n          }\n        }\n        var candidates = neighbors[i], j = -1, m = candidates.length, x;\n        while (++j < m) if (!isNaN(x = candidates[j][dimension])) return x;\n        return Math.random() * size;\n      }\n      return force.resume();\n    };\n    force.resume = function() {\n      return force.alpha(.1);\n    };\n    force.stop = function() {\n      return force.alpha(0);\n    };\n    force.drag = function() {\n      if (!drag) drag = d3.behavior.drag().origin(d3_identity).on(\"dragstart.force\", d3_layout_forceDragstart).on(\"drag.force\", dragmove).on(\"dragend.force\", d3_layout_forceDragend);\n      if (!arguments.length) return drag;\n      this.on(\"mouseover.force\", d3_layout_forceMouseover).on(\"mouseout.force\", d3_layout_forceMouseout).call(drag);\n    };\n    function dragmove(d) {\n      d.px = d3.event.x, d.py = d3.event.y;\n      force.resume();\n    }\n    return d3.rebind(force, event, \"on\");\n  };\n  function d3_layout_forceDragstart(d) {\n    d.fixed |= 2;\n  }\n  function d3_layout_forceDragend(d) {\n    d.fixed &= ~6;\n  }\n  function d3_layout_forceMouseover(d) {\n    d.fixed |= 4;\n    d.px = d.x, d.py = d.y;\n  }\n  function d3_layout_forceMouseout(d) {\n    d.fixed &= ~4;\n  }\n  function d3_layout_forceAccumulate(quad, alpha, charges) {\n    var cx = 0, cy = 0;\n    quad.charge = 0;\n    if (!quad.leaf) {\n      var nodes = quad.nodes, n = nodes.length, i = -1, c;\n      while (++i < n) {\n        c = nodes[i];\n        if (c == null) continue;\n        d3_layout_forceAccumulate(c, alpha, charges);\n        quad.charge += c.charge;\n        cx += c.charge * c.cx;\n        cy += c.charge * c.cy;\n      }\n    }\n    if (quad.point) {\n      if (!quad.leaf) {\n        quad.point.x += Math.random() - .5;\n        quad.point.y += Math.random() - .5;\n      }\n      var k = alpha * charges[quad.point.index];\n      quad.charge += quad.pointCharge = k;\n      cx += k * quad.point.x;\n      cy += k * quad.point.y;\n    }\n    quad.cx = cx / quad.charge;\n    quad.cy = cy / quad.charge;\n  }\n  var d3_layout_forceLinkDistance = 20, d3_layout_forceLinkStrength = 1, d3_layout_forceChargeDistance2 = Infinity;\n  d3.layout.hierarchy = function() {\n    var sort = d3_layout_hierarchySort, children = d3_layout_hierarchyChildren, value = d3_layout_hierarchyValue;\n    function recurse(node, depth, nodes) {\n      var childs = children.call(hierarchy, node, depth);\n      node.depth = depth;\n      nodes.push(node);\n      if (childs && (n = childs.length)) {\n        var i = -1, n, c = node.children = new Array(n), v = 0, j = depth + 1, d;\n        while (++i < n) {\n          d = c[i] = recurse(childs[i], j, nodes);\n          d.parent = node;\n          v += d.value;\n        }\n        if (sort) c.sort(sort);\n        if (value) node.value = v;\n      } else {\n        delete node.children;\n        if (value) {\n          node.value = +value.call(hierarchy, node, depth) || 0;\n        }\n      }\n      return node;\n    }\n    function revalue(node, depth) {\n      var children = node.children, v = 0;\n      if (children && (n = children.length)) {\n        var i = -1, n, j = depth + 1;\n        while (++i < n) v += revalue(children[i], j);\n      } else if (value) {\n        v = +value.call(hierarchy, node, depth) || 0;\n      }\n      if (value) node.value = v;\n      return v;\n    }\n    function hierarchy(d) {\n      var nodes = [];\n      recurse(d, 0, nodes);\n      return nodes;\n    }\n    hierarchy.sort = function(x) {\n      if (!arguments.length) return sort;\n      sort = x;\n      return hierarchy;\n    };\n    hierarchy.children = function(x) {\n      if (!arguments.length) return children;\n      children = x;\n      return hierarchy;\n    };\n    hierarchy.value = function(x) {\n      if (!arguments.length) return value;\n      value = x;\n      return hierarchy;\n    };\n    hierarchy.revalue = function(root) {\n      revalue(root, 0);\n      return root;\n    };\n    return hierarchy;\n  };\n  function d3_layout_hierarchyRebind(object, hierarchy) {\n    d3.rebind(object, hierarchy, \"sort\", \"children\", \"value\");\n    object.nodes = object;\n    object.links = d3_layout_hierarchyLinks;\n    return object;\n  }\n  function d3_layout_hierarchyChildren(d) {\n    return d.children;\n  }\n  function d3_layout_hierarchyValue(d) {\n    return d.value;\n  }\n  function d3_layout_hierarchySort(a, b) {\n    return b.value - a.value;\n  }\n  function d3_layout_hierarchyLinks(nodes) {\n    return d3.merge(nodes.map(function(parent) {\n      return (parent.children || []).map(function(child) {\n        return {\n          source: parent,\n          target: child\n        };\n      });\n    }));\n  }\n  d3.layout.partition = function() {\n    var hierarchy = d3.layout.hierarchy(), size = [ 1, 1 ];\n    function position(node, x, dx, dy) {\n      var children = node.children;\n      node.x = x;\n      node.y = node.depth * dy;\n      node.dx = dx;\n      node.dy = dy;\n      if (children && (n = children.length)) {\n        var i = -1, n, c, d;\n        dx = node.value ? dx / node.value : 0;\n        while (++i < n) {\n          position(c = children[i], x, d = c.value * dx, dy);\n          x += d;\n        }\n      }\n    }\n    function depth(node) {\n      var children = node.children, d = 0;\n      if (children && (n = children.length)) {\n        var i = -1, n;\n        while (++i < n) d = Math.max(d, depth(children[i]));\n      }\n      return 1 + d;\n    }\n    function partition(d, i) {\n      var nodes = hierarchy.call(this, d, i);\n      position(nodes[0], 0, size[0], size[1] / depth(nodes[0]));\n      return nodes;\n    }\n    partition.size = function(x) {\n      if (!arguments.length) return size;\n      size = x;\n      return partition;\n    };\n    return d3_layout_hierarchyRebind(partition, hierarchy);\n  };\n  d3.layout.pie = function() {\n    var value = Number, sort = d3_layout_pieSortByValue, startAngle = 0, endAngle = τ;\n    function pie(data) {\n      var values = data.map(function(d, i) {\n        return +value.call(pie, d, i);\n      });\n      var a = +(typeof startAngle === \"function\" ? startAngle.apply(this, arguments) : startAngle);\n      var k = ((typeof endAngle === \"function\" ? endAngle.apply(this, arguments) : endAngle) - a) / d3.sum(values);\n      var index = d3.range(data.length);\n      if (sort != null) index.sort(sort === d3_layout_pieSortByValue ? function(i, j) {\n        return values[j] - values[i];\n      } : function(i, j) {\n        return sort(data[i], data[j]);\n      });\n      var arcs = [];\n      index.forEach(function(i) {\n        var d;\n        arcs[i] = {\n          data: data[i],\n          value: d = values[i],\n          startAngle: a,\n          endAngle: a += d * k\n        };\n      });\n      return arcs;\n    }\n    pie.value = function(x) {\n      if (!arguments.length) return value;\n      value = x;\n      return pie;\n    };\n    pie.sort = function(x) {\n      if (!arguments.length) return sort;\n      sort = x;\n      return pie;\n    };\n    pie.startAngle = function(x) {\n      if (!arguments.length) return startAngle;\n      startAngle = x;\n      return pie;\n    };\n    pie.endAngle = function(x) {\n      if (!arguments.length) return endAngle;\n      endAngle = x;\n      return pie;\n    };\n    return pie;\n  };\n  var d3_layout_pieSortByValue = {};\n  d3.layout.stack = function() {\n    var values = d3_identity, order = d3_layout_stackOrderDefault, offset = d3_layout_stackOffsetZero, out = d3_layout_stackOut, x = d3_layout_stackX, y = d3_layout_stackY;\n    function stack(data, index) {\n      var series = data.map(function(d, i) {\n        return values.call(stack, d, i);\n      });\n      var points = series.map(function(d) {\n        return d.map(function(v, i) {\n          return [ x.call(stack, v, i), y.call(stack, v, i) ];\n        });\n      });\n      var orders = order.call(stack, points, index);\n      series = d3.permute(series, orders);\n      points = d3.permute(points, orders);\n      var offsets = offset.call(stack, points, index);\n      var n = series.length, m = series[0].length, i, j, o;\n      for (j = 0; j < m; ++j) {\n        out.call(stack, series[0][j], o = offsets[j], points[0][j][1]);\n        for (i = 1; i < n; ++i) {\n          out.call(stack, series[i][j], o += points[i - 1][j][1], points[i][j][1]);\n        }\n      }\n      return data;\n    }\n    stack.values = function(x) {\n      if (!arguments.length) return values;\n      values = x;\n      return stack;\n    };\n    stack.order = function(x) {\n      if (!arguments.length) return order;\n      order = typeof x === \"function\" ? x : d3_layout_stackOrders.get(x) || d3_layout_stackOrderDefault;\n      return stack;\n    };\n    stack.offset = function(x) {\n      if (!arguments.length) return offset;\n      offset = typeof x === \"function\" ? x : d3_layout_stackOffsets.get(x) || d3_layout_stackOffsetZero;\n      return stack;\n    };\n    stack.x = function(z) {\n      if (!arguments.length) return x;\n      x = z;\n      return stack;\n    };\n    stack.y = function(z) {\n      if (!arguments.length) return y;\n      y = z;\n      return stack;\n    };\n    stack.out = function(z) {\n      if (!arguments.length) return out;\n      out = z;\n      return stack;\n    };\n    return stack;\n  };\n  function d3_layout_stackX(d) {\n    return d.x;\n  }\n  function d3_layout_stackY(d) {\n    return d.y;\n  }\n  function d3_layout_stackOut(d, y0, y) {\n    d.y0 = y0;\n    d.y = y;\n  }\n  var d3_layout_stackOrders = d3.map({\n    \"inside-out\": function(data) {\n      var n = data.length, i, j, max = data.map(d3_layout_stackMaxIndex), sums = data.map(d3_layout_stackReduceSum), index = d3.range(n).sort(function(a, b) {\n        return max[a] - max[b];\n      }), top = 0, bottom = 0, tops = [], bottoms = [];\n      for (i = 0; i < n; ++i) {\n        j = index[i];\n        if (top < bottom) {\n          top += sums[j];\n          tops.push(j);\n        } else {\n          bottom += sums[j];\n          bottoms.push(j);\n        }\n      }\n      return bottoms.reverse().concat(tops);\n    },\n    reverse: function(data) {\n      return d3.range(data.length).reverse();\n    },\n    \"default\": d3_layout_stackOrderDefault\n  });\n  var d3_layout_stackOffsets = d3.map({\n    silhouette: function(data) {\n      var n = data.length, m = data[0].length, sums = [], max = 0, i, j, o, y0 = [];\n      for (j = 0; j < m; ++j) {\n        for (i = 0, o = 0; i < n; i++) o += data[i][j][1];\n        if (o > max) max = o;\n        sums.push(o);\n      }\n      for (j = 0; j < m; ++j) {\n        y0[j] = (max - sums[j]) / 2;\n      }\n      return y0;\n    },\n    wiggle: function(data) {\n      var n = data.length, x = data[0], m = x.length, i, j, k, s1, s2, s3, dx, o, o0, y0 = [];\n      y0[0] = o = o0 = 0;\n      for (j = 1; j < m; ++j) {\n        for (i = 0, s1 = 0; i < n; ++i) s1 += data[i][j][1];\n        for (i = 0, s2 = 0, dx = x[j][0] - x[j - 1][0]; i < n; ++i) {\n          for (k = 0, s3 = (data[i][j][1] - data[i][j - 1][1]) / (2 * dx); k < i; ++k) {\n            s3 += (data[k][j][1] - data[k][j - 1][1]) / dx;\n          }\n          s2 += s3 * data[i][j][1];\n        }\n        y0[j] = o -= s1 ? s2 / s1 * dx : 0;\n        if (o < o0) o0 = o;\n      }\n      for (j = 0; j < m; ++j) y0[j] -= o0;\n      return y0;\n    },\n    expand: function(data) {\n      var n = data.length, m = data[0].length, k = 1 / n, i, j, o, y0 = [];\n      for (j = 0; j < m; ++j) {\n        for (i = 0, o = 0; i < n; i++) o += data[i][j][1];\n        if (o) for (i = 0; i < n; i++) data[i][j][1] /= o; else for (i = 0; i < n; i++) data[i][j][1] = k;\n      }\n      for (j = 0; j < m; ++j) y0[j] = 0;\n      return y0;\n    },\n    zero: d3_layout_stackOffsetZero\n  });\n  function d3_layout_stackOrderDefault(data) {\n    return d3.range(data.length);\n  }\n  function d3_layout_stackOffsetZero(data) {\n    var j = -1, m = data[0].length, y0 = [];\n    while (++j < m) y0[j] = 0;\n    return y0;\n  }\n  function d3_layout_stackMaxIndex(array) {\n    var i = 1, j = 0, v = array[0][1], k, n = array.length;\n    for (;i < n; ++i) {\n      if ((k = array[i][1]) > v) {\n        j = i;\n        v = k;\n      }\n    }\n    return j;\n  }\n  function d3_layout_stackReduceSum(d) {\n    return d.reduce(d3_layout_stackSum, 0);\n  }\n  function d3_layout_stackSum(p, d) {\n    return p + d[1];\n  }\n  d3.layout.histogram = function() {\n    var frequency = true, valuer = Number, ranger = d3_layout_histogramRange, binner = d3_layout_histogramBinSturges;\n    function histogram(data, i) {\n      var bins = [], values = data.map(valuer, this), range = ranger.call(this, values, i), thresholds = binner.call(this, range, values, i), bin, i = -1, n = values.length, m = thresholds.length - 1, k = frequency ? 1 : 1 / n, x;\n      while (++i < m) {\n        bin = bins[i] = [];\n        bin.dx = thresholds[i + 1] - (bin.x = thresholds[i]);\n        bin.y = 0;\n      }\n      if (m > 0) {\n        i = -1;\n        while (++i < n) {\n          x = values[i];\n          if (x >= range[0] && x <= range[1]) {\n            bin = bins[d3.bisect(thresholds, x, 1, m) - 1];\n            bin.y += k;\n            bin.push(data[i]);\n          }\n        }\n      }\n      return bins;\n    }\n    histogram.value = function(x) {\n      if (!arguments.length) return valuer;\n      valuer = x;\n      return histogram;\n    };\n    histogram.range = function(x) {\n      if (!arguments.length) return ranger;\n      ranger = d3_functor(x);\n      return histogram;\n    };\n    histogram.bins = function(x) {\n      if (!arguments.length) return binner;\n      binner = typeof x === \"number\" ? function(range) {\n        return d3_layout_histogramBinFixed(range, x);\n      } : d3_functor(x);\n      return histogram;\n    };\n    histogram.frequency = function(x) {\n      if (!arguments.length) return frequency;\n      frequency = !!x;\n      return histogram;\n    };\n    return histogram;\n  };\n  function d3_layout_histogramBinSturges(range, values) {\n    return d3_layout_histogramBinFixed(range, Math.ceil(Math.log(values.length) / Math.LN2 + 1));\n  }\n  function d3_layout_histogramBinFixed(range, n) {\n    var x = -1, b = +range[0], m = (range[1] - b) / n, f = [];\n    while (++x <= n) f[x] = m * x + b;\n    return f;\n  }\n  function d3_layout_histogramRange(values) {\n    return [ d3.min(values), d3.max(values) ];\n  }\n  d3.layout.tree = function() {\n    var hierarchy = d3.layout.hierarchy().sort(null).value(null), separation = d3_layout_treeSeparation, size = [ 1, 1 ], nodeSize = false;\n    function tree(d, i) {\n      var nodes = hierarchy.call(this, d, i), root = nodes[0];\n      function firstWalk(node, previousSibling) {\n        var children = node.children, layout = node._tree;\n        if (children && (n = children.length)) {\n          var n, firstChild = children[0], previousChild, ancestor = firstChild, child, i = -1;\n          while (++i < n) {\n            child = children[i];\n            firstWalk(child, previousChild);\n            ancestor = apportion(child, previousChild, ancestor);\n            previousChild = child;\n          }\n          d3_layout_treeShift(node);\n          var midpoint = .5 * (firstChild._tree.prelim + child._tree.prelim);\n          if (previousSibling) {\n            layout.prelim = previousSibling._tree.prelim + separation(node, previousSibling);\n            layout.mod = layout.prelim - midpoint;\n          } else {\n            layout.prelim = midpoint;\n          }\n        } else {\n          if (previousSibling) {\n            layout.prelim = previousSibling._tree.prelim + separation(node, previousSibling);\n          }\n        }\n      }\n      function secondWalk(node, x) {\n        node.x = node._tree.prelim + x;\n        var children = node.children;\n        if (children && (n = children.length)) {\n          var i = -1, n;\n          x += node._tree.mod;\n          while (++i < n) {\n            secondWalk(children[i], x);\n          }\n        }\n      }\n      function apportion(node, previousSibling, ancestor) {\n        if (previousSibling) {\n          var vip = node, vop = node, vim = previousSibling, vom = node.parent.children[0], sip = vip._tree.mod, sop = vop._tree.mod, sim = vim._tree.mod, som = vom._tree.mod, shift;\n          while (vim = d3_layout_treeRight(vim), vip = d3_layout_treeLeft(vip), vim && vip) {\n            vom = d3_layout_treeLeft(vom);\n            vop = d3_layout_treeRight(vop);\n            vop._tree.ancestor = node;\n            shift = vim._tree.prelim + sim - vip._tree.prelim - sip + separation(vim, vip);\n            if (shift > 0) {\n              d3_layout_treeMove(d3_layout_treeAncestor(vim, node, ancestor), node, shift);\n              sip += shift;\n              sop += shift;\n            }\n            sim += vim._tree.mod;\n            sip += vip._tree.mod;\n            som += vom._tree.mod;\n            sop += vop._tree.mod;\n          }\n          if (vim && !d3_layout_treeRight(vop)) {\n            vop._tree.thread = vim;\n            vop._tree.mod += sim - sop;\n          }\n          if (vip && !d3_layout_treeLeft(vom)) {\n            vom._tree.thread = vip;\n            vom._tree.mod += sip - som;\n            ancestor = node;\n          }\n        }\n        return ancestor;\n      }\n      d3_layout_treeVisitAfter(root, function(node, previousSibling) {\n        node._tree = {\n          ancestor: node,\n          prelim: 0,\n          mod: 0,\n          change: 0,\n          shift: 0,\n          number: previousSibling ? previousSibling._tree.number + 1 : 0\n        };\n      });\n      firstWalk(root);\n      secondWalk(root, -root._tree.prelim);\n      var left = d3_layout_treeSearch(root, d3_layout_treeLeftmost), right = d3_layout_treeSearch(root, d3_layout_treeRightmost), deep = d3_layout_treeSearch(root, d3_layout_treeDeepest), x0 = left.x - separation(left, right) / 2, x1 = right.x + separation(right, left) / 2, y1 = deep.depth || 1;\n      d3_layout_treeVisitAfter(root, nodeSize ? function(node) {\n        node.x *= size[0];\n        node.y = node.depth * size[1];\n        delete node._tree;\n      } : function(node) {\n        node.x = (node.x - x0) / (x1 - x0) * size[0];\n        node.y = node.depth / y1 * size[1];\n        delete node._tree;\n      });\n      return nodes;\n    }\n    tree.separation = function(x) {\n      if (!arguments.length) return separation;\n      separation = x;\n      return tree;\n    };\n    tree.size = function(x) {\n      if (!arguments.length) return nodeSize ? null : size;\n      nodeSize = (size = x) == null;\n      return tree;\n    };\n    tree.nodeSize = function(x) {\n      if (!arguments.length) return nodeSize ? size : null;\n      nodeSize = (size = x) != null;\n      return tree;\n    };\n    return d3_layout_hierarchyRebind(tree, hierarchy);\n  };\n  function d3_layout_treeSeparation(a, b) {\n    return a.parent == b.parent ? 1 : 2;\n  }\n  function d3_layout_treeLeft(node) {\n    var children = node.children;\n    return children && children.length ? children[0] : node._tree.thread;\n  }\n  function d3_layout_treeRight(node) {\n    var children = node.children, n;\n    return children && (n = children.length) ? children[n - 1] : node._tree.thread;\n  }\n  function d3_layout_treeSearch(node, compare) {\n    var children = node.children;\n    if (children && (n = children.length)) {\n      var child, n, i = -1;\n      while (++i < n) {\n        if (compare(child = d3_layout_treeSearch(children[i], compare), node) > 0) {\n          node = child;\n        }\n      }\n    }\n    return node;\n  }\n  function d3_layout_treeRightmost(a, b) {\n    return a.x - b.x;\n  }\n  function d3_layout_treeLeftmost(a, b) {\n    return b.x - a.x;\n  }\n  function d3_layout_treeDeepest(a, b) {\n    return a.depth - b.depth;\n  }\n  function d3_layout_treeVisitAfter(node, callback) {\n    function visit(node, previousSibling) {\n      var children = node.children;\n      if (children && (n = children.length)) {\n        var child, previousChild = null, i = -1, n;\n        while (++i < n) {\n          child = children[i];\n          visit(child, previousChild);\n          previousChild = child;\n        }\n      }\n      callback(node, previousSibling);\n    }\n    visit(node, null);\n  }\n  function d3_layout_treeShift(node) {\n    var shift = 0, change = 0, children = node.children, i = children.length, child;\n    while (--i >= 0) {\n      child = children[i]._tree;\n      child.prelim += shift;\n      child.mod += shift;\n      shift += child.shift + (change += child.change);\n    }\n  }\n  function d3_layout_treeMove(ancestor, node, shift) {\n    ancestor = ancestor._tree;\n    node = node._tree;\n    var change = shift / (node.number - ancestor.number);\n    ancestor.change += change;\n    node.change -= change;\n    node.shift += shift;\n    node.prelim += shift;\n    node.mod += shift;\n  }\n  function d3_layout_treeAncestor(vim, node, ancestor) {\n    return vim._tree.ancestor.parent == node.parent ? vim._tree.ancestor : ancestor;\n  }\n  d3.layout.pack = function() {\n    var hierarchy = d3.layout.hierarchy().sort(d3_layout_packSort), padding = 0, size = [ 1, 1 ], radius;\n    function pack(d, i) {\n      var nodes = hierarchy.call(this, d, i), root = nodes[0], w = size[0], h = size[1], r = radius == null ? Math.sqrt : typeof radius === \"function\" ? radius : function() {\n        return radius;\n      };\n      root.x = root.y = 0;\n      d3_layout_treeVisitAfter(root, function(d) {\n        d.r = +r(d.value);\n      });\n      d3_layout_treeVisitAfter(root, d3_layout_packSiblings);\n      if (padding) {\n        var dr = padding * (radius ? 1 : Math.max(2 * root.r / w, 2 * root.r / h)) / 2;\n        d3_layout_treeVisitAfter(root, function(d) {\n          d.r += dr;\n        });\n        d3_layout_treeVisitAfter(root, d3_layout_packSiblings);\n        d3_layout_treeVisitAfter(root, function(d) {\n          d.r -= dr;\n        });\n      }\n      d3_layout_packTransform(root, w / 2, h / 2, radius ? 1 : 1 / Math.max(2 * root.r / w, 2 * root.r / h));\n      return nodes;\n    }\n    pack.size = function(_) {\n      if (!arguments.length) return size;\n      size = _;\n      return pack;\n    };\n    pack.radius = function(_) {\n      if (!arguments.length) return radius;\n      radius = _ == null || typeof _ === \"function\" ? _ : +_;\n      return pack;\n    };\n    pack.padding = function(_) {\n      if (!arguments.length) return padding;\n      padding = +_;\n      return pack;\n    };\n    return d3_layout_hierarchyRebind(pack, hierarchy);\n  };\n  function d3_layout_packSort(a, b) {\n    return a.value - b.value;\n  }\n  function d3_layout_packInsert(a, b) {\n    var c = a._pack_next;\n    a._pack_next = b;\n    b._pack_prev = a;\n    b._pack_next = c;\n    c._pack_prev = b;\n  }\n  function d3_layout_packSplice(a, b) {\n    a._pack_next = b;\n    b._pack_prev = a;\n  }\n  function d3_layout_packIntersects(a, b) {\n    var dx = b.x - a.x, dy = b.y - a.y, dr = a.r + b.r;\n    return .999 * dr * dr > dx * dx + dy * dy;\n  }\n  function d3_layout_packSiblings(node) {\n    if (!(nodes = node.children) || !(n = nodes.length)) return;\n    var nodes, xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity, a, b, c, i, j, k, n;\n    function bound(node) {\n      xMin = Math.min(node.x - node.r, xMin);\n      xMax = Math.max(node.x + node.r, xMax);\n      yMin = Math.min(node.y - node.r, yMin);\n      yMax = Math.max(node.y + node.r, yMax);\n    }\n    nodes.forEach(d3_layout_packLink);\n    a = nodes[0];\n    a.x = -a.r;\n    a.y = 0;\n    bound(a);\n    if (n > 1) {\n      b = nodes[1];\n      b.x = b.r;\n      b.y = 0;\n      bound(b);\n      if (n > 2) {\n        c = nodes[2];\n        d3_layout_packPlace(a, b, c);\n        bound(c);\n        d3_layout_packInsert(a, c);\n        a._pack_prev = c;\n        d3_layout_packInsert(c, b);\n        b = a._pack_next;\n        for (i = 3; i < n; i++) {\n          d3_layout_packPlace(a, b, c = nodes[i]);\n          var isect = 0, s1 = 1, s2 = 1;\n          for (j = b._pack_next; j !== b; j = j._pack_next, s1++) {\n            if (d3_layout_packIntersects(j, c)) {\n              isect = 1;\n              break;\n            }\n          }\n          if (isect == 1) {\n            for (k = a._pack_prev; k !== j._pack_prev; k = k._pack_prev, s2++) {\n              if (d3_layout_packIntersects(k, c)) {\n                break;\n              }\n            }\n          }\n          if (isect) {\n            if (s1 < s2 || s1 == s2 && b.r < a.r) d3_layout_packSplice(a, b = j); else d3_layout_packSplice(a = k, b);\n            i--;\n          } else {\n            d3_layout_packInsert(a, c);\n            b = c;\n            bound(c);\n          }\n        }\n      }\n    }\n    var cx = (xMin + xMax) / 2, cy = (yMin + yMax) / 2, cr = 0;\n    for (i = 0; i < n; i++) {\n      c = nodes[i];\n      c.x -= cx;\n      c.y -= cy;\n      cr = Math.max(cr, c.r + Math.sqrt(c.x * c.x + c.y * c.y));\n    }\n    node.r = cr;\n    nodes.forEach(d3_layout_packUnlink);\n  }\n  function d3_layout_packLink(node) {\n    node._pack_next = node._pack_prev = node;\n  }\n  function d3_layout_packUnlink(node) {\n    delete node._pack_next;\n    delete node._pack_prev;\n  }\n  function d3_layout_packTransform(node, x, y, k) {\n    var children = node.children;\n    node.x = x += k * node.x;\n    node.y = y += k * node.y;\n    node.r *= k;\n    if (children) {\n      var i = -1, n = children.length;\n      while (++i < n) d3_layout_packTransform(children[i], x, y, k);\n    }\n  }\n  function d3_layout_packPlace(a, b, c) {\n    var db = a.r + c.r, dx = b.x - a.x, dy = b.y - a.y;\n    if (db && (dx || dy)) {\n      var da = b.r + c.r, dc = dx * dx + dy * dy;\n      da *= da;\n      db *= db;\n      var x = .5 + (db - da) / (2 * dc), y = Math.sqrt(Math.max(0, 2 * da * (db + dc) - (db -= dc) * db - da * da)) / (2 * dc);\n      c.x = a.x + x * dx + y * dy;\n      c.y = a.y + x * dy - y * dx;\n    } else {\n      c.x = a.x + db;\n      c.y = a.y;\n    }\n  }\n  d3.layout.cluster = function() {\n    var hierarchy = d3.layout.hierarchy().sort(null).value(null), separation = d3_layout_treeSeparation, size = [ 1, 1 ], nodeSize = false;\n    function cluster(d, i) {\n      var nodes = hierarchy.call(this, d, i), root = nodes[0], previousNode, x = 0;\n      d3_layout_treeVisitAfter(root, function(node) {\n        var children = node.children;\n        if (children && children.length) {\n          node.x = d3_layout_clusterX(children);\n          node.y = d3_layout_clusterY(children);\n        } else {\n          node.x = previousNode ? x += separation(node, previousNode) : 0;\n          node.y = 0;\n          previousNode = node;\n        }\n      });\n      var left = d3_layout_clusterLeft(root), right = d3_layout_clusterRight(root), x0 = left.x - separation(left, right) / 2, x1 = right.x + separation(right, left) / 2;\n      d3_layout_treeVisitAfter(root, nodeSize ? function(node) {\n        node.x = (node.x - root.x) * size[0];\n        node.y = (root.y - node.y) * size[1];\n      } : function(node) {\n        node.x = (node.x - x0) / (x1 - x0) * size[0];\n        node.y = (1 - (root.y ? node.y / root.y : 1)) * size[1];\n      });\n      return nodes;\n    }\n    cluster.separation = function(x) {\n      if (!arguments.length) return separation;\n      separation = x;\n      return cluster;\n    };\n    cluster.size = function(x) {\n      if (!arguments.length) return nodeSize ? null : size;\n      nodeSize = (size = x) == null;\n      return cluster;\n    };\n    cluster.nodeSize = function(x) {\n      if (!arguments.length) return nodeSize ? size : null;\n      nodeSize = (size = x) != null;\n      return cluster;\n    };\n    return d3_layout_hierarchyRebind(cluster, hierarchy);\n  };\n  function d3_layout_clusterY(children) {\n    return 1 + d3.max(children, function(child) {\n      return child.y;\n    });\n  }\n  function d3_layout_clusterX(children) {\n    return children.reduce(function(x, child) {\n      return x + child.x;\n    }, 0) / children.length;\n  }\n  function d3_layout_clusterLeft(node) {\n    var children = node.children;\n    return children && children.length ? d3_layout_clusterLeft(children[0]) : node;\n  }\n  function d3_layout_clusterRight(node) {\n    var children = node.children, n;\n    return children && (n = children.length) ? d3_layout_clusterRight(children[n - 1]) : node;\n  }\n  d3.layout.treemap = function() {\n    var hierarchy = d3.layout.hierarchy(), round = Math.round, size = [ 1, 1 ], padding = null, pad = d3_layout_treemapPadNull, sticky = false, stickies, mode = \"squarify\", ratio = .5 * (1 + Math.sqrt(5));\n    function scale(children, k) {\n      var i = -1, n = children.length, child, area;\n      while (++i < n) {\n        area = (child = children[i]).value * (k < 0 ? 0 : k);\n        child.area = isNaN(area) || area <= 0 ? 0 : area;\n      }\n    }\n    function squarify(node) {\n      var children = node.children;\n      if (children && children.length) {\n        var rect = pad(node), row = [], remaining = children.slice(), child, best = Infinity, score, u = mode === \"slice\" ? rect.dx : mode === \"dice\" ? rect.dy : mode === \"slice-dice\" ? node.depth & 1 ? rect.dy : rect.dx : Math.min(rect.dx, rect.dy), n;\n        scale(remaining, rect.dx * rect.dy / node.value);\n        row.area = 0;\n        while ((n = remaining.length) > 0) {\n          row.push(child = remaining[n - 1]);\n          row.area += child.area;\n          if (mode !== \"squarify\" || (score = worst(row, u)) <= best) {\n            remaining.pop();\n            best = score;\n          } else {\n            row.area -= row.pop().area;\n            position(row, u, rect, false);\n            u = Math.min(rect.dx, rect.dy);\n            row.length = row.area = 0;\n            best = Infinity;\n          }\n        }\n        if (row.length) {\n          position(row, u, rect, true);\n          row.length = row.area = 0;\n        }\n        children.forEach(squarify);\n      }\n    }\n    function stickify(node) {\n      var children = node.children;\n      if (children && children.length) {\n        var rect = pad(node), remaining = children.slice(), child, row = [];\n        scale(remaining, rect.dx * rect.dy / node.value);\n        row.area = 0;\n        while (child = remaining.pop()) {\n          row.push(child);\n          row.area += child.area;\n          if (child.z != null) {\n            position(row, child.z ? rect.dx : rect.dy, rect, !remaining.length);\n            row.length = row.area = 0;\n          }\n        }\n        children.forEach(stickify);\n      }\n    }\n    function worst(row, u) {\n      var s = row.area, r, rmax = 0, rmin = Infinity, i = -1, n = row.length;\n      while (++i < n) {\n        if (!(r = row[i].area)) continue;\n        if (r < rmin) rmin = r;\n        if (r > rmax) rmax = r;\n      }\n      s *= s;\n      u *= u;\n      return s ? Math.max(u * rmax * ratio / s, s / (u * rmin * ratio)) : Infinity;\n    }\n    function position(row, u, rect, flush) {\n      var i = -1, n = row.length, x = rect.x, y = rect.y, v = u ? round(row.area / u) : 0, o;\n      if (u == rect.dx) {\n        if (flush || v > rect.dy) v = rect.dy;\n        while (++i < n) {\n          o = row[i];\n          o.x = x;\n          o.y = y;\n          o.dy = v;\n          x += o.dx = Math.min(rect.x + rect.dx - x, v ? round(o.area / v) : 0);\n        }\n        o.z = true;\n        o.dx += rect.x + rect.dx - x;\n        rect.y += v;\n        rect.dy -= v;\n      } else {\n        if (flush || v > rect.dx) v = rect.dx;\n        while (++i < n) {\n          o = row[i];\n          o.x = x;\n          o.y = y;\n          o.dx = v;\n          y += o.dy = Math.min(rect.y + rect.dy - y, v ? round(o.area / v) : 0);\n        }\n        o.z = false;\n        o.dy += rect.y + rect.dy - y;\n        rect.x += v;\n        rect.dx -= v;\n      }\n    }\n    function treemap(d) {\n      var nodes = stickies || hierarchy(d), root = nodes[0];\n      root.x = 0;\n      root.y = 0;\n      root.dx = size[0];\n      root.dy = size[1];\n      if (stickies) hierarchy.revalue(root);\n      scale([ root ], root.dx * root.dy / root.value);\n      (stickies ? stickify : squarify)(root);\n      if (sticky) stickies = nodes;\n      return nodes;\n    }\n    treemap.size = function(x) {\n      if (!arguments.length) return size;\n      size = x;\n      return treemap;\n    };\n    treemap.padding = function(x) {\n      if (!arguments.length) return padding;\n      function padFunction(node) {\n        var p = x.call(treemap, node, node.depth);\n        return p == null ? d3_layout_treemapPadNull(node) : d3_layout_treemapPad(node, typeof p === \"number\" ? [ p, p, p, p ] : p);\n      }\n      function padConstant(node) {\n        return d3_layout_treemapPad(node, x);\n      }\n      var type;\n      pad = (padding = x) == null ? d3_layout_treemapPadNull : (type = typeof x) === \"function\" ? padFunction : type === \"number\" ? (x = [ x, x, x, x ], \n      padConstant) : padConstant;\n      return treemap;\n    };\n    treemap.round = function(x) {\n      if (!arguments.length) return round != Number;\n      round = x ? Math.round : Number;\n      return treemap;\n    };\n    treemap.sticky = function(x) {\n      if (!arguments.length) return sticky;\n      sticky = x;\n      stickies = null;\n      return treemap;\n    };\n    treemap.ratio = function(x) {\n      if (!arguments.length) return ratio;\n      ratio = x;\n      return treemap;\n    };\n    treemap.mode = function(x) {\n      if (!arguments.length) return mode;\n      mode = x + \"\";\n      return treemap;\n    };\n    return d3_layout_hierarchyRebind(treemap, hierarchy);\n  };\n  function d3_layout_treemapPadNull(node) {\n    return {\n      x: node.x,\n      y: node.y,\n      dx: node.dx,\n      dy: node.dy\n    };\n  }\n  function d3_layout_treemapPad(node, padding) {\n    var x = node.x + padding[3], y = node.y + padding[0], dx = node.dx - padding[1] - padding[3], dy = node.dy - padding[0] - padding[2];\n    if (dx < 0) {\n      x += dx / 2;\n      dx = 0;\n    }\n    if (dy < 0) {\n      y += dy / 2;\n      dy = 0;\n    }\n    return {\n      x: x,\n      y: y,\n      dx: dx,\n      dy: dy\n    };\n  }\n  d3.random = {\n    normal: function(µ, σ) {\n      var n = arguments.length;\n      if (n < 2) σ = 1;\n      if (n < 1) µ = 0;\n      return function() {\n        var x, y, r;\n        do {\n          x = Math.random() * 2 - 1;\n          y = Math.random() * 2 - 1;\n          r = x * x + y * y;\n        } while (!r || r > 1);\n        return µ + σ * x * Math.sqrt(-2 * Math.log(r) / r);\n      };\n    },\n    logNormal: function() {\n      var random = d3.random.normal.apply(d3, arguments);\n      return function() {\n        return Math.exp(random());\n      };\n    },\n    bates: function(m) {\n      var random = d3.random.irwinHall(m);\n      return function() {\n        return random() / m;\n      };\n    },\n    irwinHall: function(m) {\n      return function() {\n        for (var s = 0, j = 0; j < m; j++) s += Math.random();\n        return s;\n      };\n    }\n  };\n  d3.scale = {};\n  function d3_scaleExtent(domain) {\n    var start = domain[0], stop = domain[domain.length - 1];\n    return start < stop ? [ start, stop ] : [ stop, start ];\n  }\n  function d3_scaleRange(scale) {\n    return scale.rangeExtent ? scale.rangeExtent() : d3_scaleExtent(scale.range());\n  }\n  function d3_scale_bilinear(domain, range, uninterpolate, interpolate) {\n    var u = uninterpolate(domain[0], domain[1]), i = interpolate(range[0], range[1]);\n    return function(x) {\n      return i(u(x));\n    };\n  }\n  function d3_scale_nice(domain, nice) {\n    var i0 = 0, i1 = domain.length - 1, x0 = domain[i0], x1 = domain[i1], dx;\n    if (x1 < x0) {\n      dx = i0, i0 = i1, i1 = dx;\n      dx = x0, x0 = x1, x1 = dx;\n    }\n    domain[i0] = nice.floor(x0);\n    domain[i1] = nice.ceil(x1);\n    return domain;\n  }\n  function d3_scale_niceStep(step) {\n    return step ? {\n      floor: function(x) {\n        return Math.floor(x / step) * step;\n      },\n      ceil: function(x) {\n        return Math.ceil(x / step) * step;\n      }\n    } : d3_scale_niceIdentity;\n  }\n  var d3_scale_niceIdentity = {\n    floor: d3_identity,\n    ceil: d3_identity\n  };\n  function d3_scale_polylinear(domain, range, uninterpolate, interpolate) {\n    var u = [], i = [], j = 0, k = Math.min(domain.length, range.length) - 1;\n    if (domain[k] < domain[0]) {\n      domain = domain.slice().reverse();\n      range = range.slice().reverse();\n    }\n    while (++j <= k) {\n      u.push(uninterpolate(domain[j - 1], domain[j]));\n      i.push(interpolate(range[j - 1], range[j]));\n    }\n    return function(x) {\n      var j = d3.bisect(domain, x, 1, k) - 1;\n      return i[j](u[j](x));\n    };\n  }\n  d3.scale.linear = function() {\n    return d3_scale_linear([ 0, 1 ], [ 0, 1 ], d3_interpolate, false);\n  };\n  function d3_scale_linear(domain, range, interpolate, clamp) {\n    var output, input;\n    function rescale() {\n      var linear = Math.min(domain.length, range.length) > 2 ? d3_scale_polylinear : d3_scale_bilinear, uninterpolate = clamp ? d3_uninterpolateClamp : d3_uninterpolateNumber;\n      output = linear(domain, range, uninterpolate, interpolate);\n      input = linear(range, domain, uninterpolate, d3_interpolate);\n      return scale;\n    }\n    function scale(x) {\n      return output(x);\n    }\n    scale.invert = function(y) {\n      return input(y);\n    };\n    scale.domain = function(x) {\n      if (!arguments.length) return domain;\n      domain = x.map(Number);\n      return rescale();\n    };\n    scale.range = function(x) {\n      if (!arguments.length) return range;\n      range = x;\n      return rescale();\n    };\n    scale.rangeRound = function(x) {\n      return scale.range(x).interpolate(d3_interpolateRound);\n    };\n    scale.clamp = function(x) {\n      if (!arguments.length) return clamp;\n      clamp = x;\n      return rescale();\n    };\n    scale.interpolate = function(x) {\n      if (!arguments.length) return interpolate;\n      interpolate = x;\n      return rescale();\n    };\n    scale.ticks = function(m) {\n      return d3_scale_linearTicks(domain, m);\n    };\n    scale.tickFormat = function(m, format) {\n      return d3_scale_linearTickFormat(domain, m, format);\n    };\n    scale.nice = function(m) {\n      d3_scale_linearNice(domain, m);\n      return rescale();\n    };\n    scale.copy = function() {\n      return d3_scale_linear(domain, range, interpolate, clamp);\n    };\n    return rescale();\n  }\n  function d3_scale_linearRebind(scale, linear) {\n    return d3.rebind(scale, linear, \"range\", \"rangeRound\", \"interpolate\", \"clamp\");\n  }\n  function d3_scale_linearNice(domain, m) {\n    return d3_scale_nice(domain, d3_scale_niceStep(d3_scale_linearTickRange(domain, m)[2]));\n  }\n  function d3_scale_linearTickRange(domain, m) {\n    if (m == null) m = 10;\n    var extent = d3_scaleExtent(domain), span = extent[1] - extent[0], step = Math.pow(10, Math.floor(Math.log(span / m) / Math.LN10)), err = m / span * step;\n    if (err <= .15) step *= 10; else if (err <= .35) step *= 5; else if (err <= .75) step *= 2;\n    extent[0] = Math.ceil(extent[0] / step) * step;\n    extent[1] = Math.floor(extent[1] / step) * step + step * .5;\n    extent[2] = step;\n    return extent;\n  }\n  function d3_scale_linearTicks(domain, m) {\n    return d3.range.apply(d3, d3_scale_linearTickRange(domain, m));\n  }\n  function d3_scale_linearTickFormat(domain, m, format) {\n    var range = d3_scale_linearTickRange(domain, m);\n    if (format) {\n      var match = d3_format_re.exec(format);\n      match.shift();\n      if (match[8] === \"s\") {\n        var prefix = d3.formatPrefix(Math.max(abs(range[0]), abs(range[1])));\n        if (!match[7]) match[7] = \".\" + d3_scale_linearPrecision(prefix.scale(range[2]));\n        match[8] = \"f\";\n        format = d3.format(match.join(\"\"));\n        return function(d) {\n          return format(prefix.scale(d)) + prefix.symbol;\n        };\n      }\n      if (!match[7]) match[7] = \".\" + d3_scale_linearFormatPrecision(match[8], range);\n      format = match.join(\"\");\n    } else {\n      format = \",.\" + d3_scale_linearPrecision(range[2]) + \"f\";\n    }\n    return d3.format(format);\n  }\n  var d3_scale_linearFormatSignificant = {\n    s: 1,\n    g: 1,\n    p: 1,\n    r: 1,\n    e: 1\n  };\n  function d3_scale_linearPrecision(value) {\n    return -Math.floor(Math.log(value) / Math.LN10 + .01);\n  }\n  function d3_scale_linearFormatPrecision(type, range) {\n    var p = d3_scale_linearPrecision(range[2]);\n    return type in d3_scale_linearFormatSignificant ? Math.abs(p - d3_scale_linearPrecision(Math.max(abs(range[0]), abs(range[1])))) + +(type !== \"e\") : p - (type === \"%\") * 2;\n  }\n  d3.scale.log = function() {\n    return d3_scale_log(d3.scale.linear().domain([ 0, 1 ]), 10, true, [ 1, 10 ]);\n  };\n  function d3_scale_log(linear, base, positive, domain) {\n    function log(x) {\n      return (positive ? Math.log(x < 0 ? 0 : x) : -Math.log(x > 0 ? 0 : -x)) / Math.log(base);\n    }\n    function pow(x) {\n      return positive ? Math.pow(base, x) : -Math.pow(base, -x);\n    }\n    function scale(x) {\n      return linear(log(x));\n    }\n    scale.invert = function(x) {\n      return pow(linear.invert(x));\n    };\n    scale.domain = function(x) {\n      if (!arguments.length) return domain;\n      positive = x[0] >= 0;\n      linear.domain((domain = x.map(Number)).map(log));\n      return scale;\n    };\n    scale.base = function(_) {\n      if (!arguments.length) return base;\n      base = +_;\n      linear.domain(domain.map(log));\n      return scale;\n    };\n    scale.nice = function() {\n      var niced = d3_scale_nice(domain.map(log), positive ? Math : d3_scale_logNiceNegative);\n      linear.domain(niced);\n      domain = niced.map(pow);\n      return scale;\n    };\n    scale.ticks = function() {\n      var extent = d3_scaleExtent(domain), ticks = [], u = extent[0], v = extent[1], i = Math.floor(log(u)), j = Math.ceil(log(v)), n = base % 1 ? 2 : base;\n      if (isFinite(j - i)) {\n        if (positive) {\n          for (;i < j; i++) for (var k = 1; k < n; k++) ticks.push(pow(i) * k);\n          ticks.push(pow(i));\n        } else {\n          ticks.push(pow(i));\n          for (;i++ < j; ) for (var k = n - 1; k > 0; k--) ticks.push(pow(i) * k);\n        }\n        for (i = 0; ticks[i] < u; i++) {}\n        for (j = ticks.length; ticks[j - 1] > v; j--) {}\n        ticks = ticks.slice(i, j);\n      }\n      return ticks;\n    };\n    scale.tickFormat = function(n, format) {\n      if (!arguments.length) return d3_scale_logFormat;\n      if (arguments.length < 2) format = d3_scale_logFormat; else if (typeof format !== \"function\") format = d3.format(format);\n      var k = Math.max(.1, n / scale.ticks().length), f = positive ? (e = 1e-12, Math.ceil) : (e = -1e-12, \n      Math.floor), e;\n      return function(d) {\n        return d / pow(f(log(d) + e)) <= k ? format(d) : \"\";\n      };\n    };\n    scale.copy = function() {\n      return d3_scale_log(linear.copy(), base, positive, domain);\n    };\n    return d3_scale_linearRebind(scale, linear);\n  }\n  var d3_scale_logFormat = d3.format(\".0e\"), d3_scale_logNiceNegative = {\n    floor: function(x) {\n      return -Math.ceil(-x);\n    },\n    ceil: function(x) {\n      return -Math.floor(-x);\n    }\n  };\n  d3.scale.pow = function() {\n    return d3_scale_pow(d3.scale.linear(), 1, [ 0, 1 ]);\n  };\n  function d3_scale_pow(linear, exponent, domain) {\n    var powp = d3_scale_powPow(exponent), powb = d3_scale_powPow(1 / exponent);\n    function scale(x) {\n      return linear(powp(x));\n    }\n    scale.invert = function(x) {\n      return powb(linear.invert(x));\n    };\n    scale.domain = function(x) {\n      if (!arguments.length) return domain;\n      linear.domain((domain = x.map(Number)).map(powp));\n      return scale;\n    };\n    scale.ticks = function(m) {\n      return d3_scale_linearTicks(domain, m);\n    };\n    scale.tickFormat = function(m, format) {\n      return d3_scale_linearTickFormat(domain, m, format);\n    };\n    scale.nice = function(m) {\n      return scale.domain(d3_scale_linearNice(domain, m));\n    };\n    scale.exponent = function(x) {\n      if (!arguments.length) return exponent;\n      powp = d3_scale_powPow(exponent = x);\n      powb = d3_scale_powPow(1 / exponent);\n      linear.domain(domain.map(powp));\n      return scale;\n    };\n    scale.copy = function() {\n      return d3_scale_pow(linear.copy(), exponent, domain);\n    };\n    return d3_scale_linearRebind(scale, linear);\n  }\n  function d3_scale_powPow(e) {\n    return function(x) {\n      return x < 0 ? -Math.pow(-x, e) : Math.pow(x, e);\n    };\n  }\n  d3.scale.sqrt = function() {\n    return d3.scale.pow().exponent(.5);\n  };\n  d3.scale.ordinal = function() {\n    return d3_scale_ordinal([], {\n      t: \"range\",\n      a: [ [] ]\n    });\n  };\n  function d3_scale_ordinal(domain, ranger) {\n    var index, range, rangeBand;\n    function scale(x) {\n      return range[((index.get(x) || (ranger.t === \"range\" ? index.set(x, domain.push(x)) : NaN)) - 1) % range.length];\n    }\n    function steps(start, step) {\n      return d3.range(domain.length).map(function(i) {\n        return start + step * i;\n      });\n    }\n    scale.domain = function(x) {\n      if (!arguments.length) return domain;\n      domain = [];\n      index = new d3_Map();\n      var i = -1, n = x.length, xi;\n      while (++i < n) if (!index.has(xi = x[i])) index.set(xi, domain.push(xi));\n      return scale[ranger.t].apply(scale, ranger.a);\n    };\n    scale.range = function(x) {\n      if (!arguments.length) return range;\n      range = x;\n      rangeBand = 0;\n      ranger = {\n        t: \"range\",\n        a: arguments\n      };\n      return scale;\n    };\n    scale.rangePoints = function(x, padding) {\n      if (arguments.length < 2) padding = 0;\n      var start = x[0], stop = x[1], step = (stop - start) / (Math.max(1, domain.length - 1) + padding);\n      range = steps(domain.length < 2 ? (start + stop) / 2 : start + step * padding / 2, step);\n      rangeBand = 0;\n      ranger = {\n        t: \"rangePoints\",\n        a: arguments\n      };\n      return scale;\n    };\n    scale.rangeBands = function(x, padding, outerPadding) {\n      if (arguments.length < 2) padding = 0;\n      if (arguments.length < 3) outerPadding = padding;\n      var reverse = x[1] < x[0], start = x[reverse - 0], stop = x[1 - reverse], step = (stop - start) / (domain.length - padding + 2 * outerPadding);\n      range = steps(start + step * outerPadding, step);\n      if (reverse) range.reverse();\n      rangeBand = step * (1 - padding);\n      ranger = {\n        t: \"rangeBands\",\n        a: arguments\n      };\n      return scale;\n    };\n    scale.rangeRoundBands = function(x, padding, outerPadding) {\n      if (arguments.length < 2) padding = 0;\n      if (arguments.length < 3) outerPadding = padding;\n      var reverse = x[1] < x[0], start = x[reverse - 0], stop = x[1 - reverse], step = Math.floor((stop - start) / (domain.length - padding + 2 * outerPadding)), error = stop - start - (domain.length - padding) * step;\n      range = steps(start + Math.round(error / 2), step);\n      if (reverse) range.reverse();\n      rangeBand = Math.round(step * (1 - padding));\n      ranger = {\n        t: \"rangeRoundBands\",\n        a: arguments\n      };\n      return scale;\n    };\n    scale.rangeBand = function() {\n      return rangeBand;\n    };\n    scale.rangeExtent = function() {\n      return d3_scaleExtent(ranger.a[0]);\n    };\n    scale.copy = function() {\n      return d3_scale_ordinal(domain, ranger);\n    };\n    return scale.domain(domain);\n  }\n  d3.scale.category10 = function() {\n    return d3.scale.ordinal().range(d3_category10);\n  };\n  d3.scale.category20 = function() {\n    return d3.scale.ordinal().range(d3_category20);\n  };\n  d3.scale.category20b = function() {\n    return d3.scale.ordinal().range(d3_category20b);\n  };\n  d3.scale.category20c = function() {\n    return d3.scale.ordinal().range(d3_category20c);\n  };\n  var d3_category10 = [ 2062260, 16744206, 2924588, 14034728, 9725885, 9197131, 14907330, 8355711, 12369186, 1556175 ].map(d3_rgbString);\n  var d3_category20 = [ 2062260, 11454440, 16744206, 16759672, 2924588, 10018698, 14034728, 16750742, 9725885, 12955861, 9197131, 12885140, 14907330, 16234194, 8355711, 13092807, 12369186, 14408589, 1556175, 10410725 ].map(d3_rgbString);\n  var d3_category20b = [ 3750777, 5395619, 7040719, 10264286, 6519097, 9216594, 11915115, 13556636, 9202993, 12426809, 15186514, 15190932, 8666169, 11356490, 14049643, 15177372, 8077683, 10834324, 13528509, 14589654 ].map(d3_rgbString);\n  var d3_category20c = [ 3244733, 7057110, 10406625, 13032431, 15095053, 16616764, 16625259, 16634018, 3253076, 7652470, 10607003, 13101504, 7695281, 10394312, 12369372, 14342891, 6513507, 9868950, 12434877, 14277081 ].map(d3_rgbString);\n  d3.scale.quantile = function() {\n    return d3_scale_quantile([], []);\n  };\n  function d3_scale_quantile(domain, range) {\n    var thresholds;\n    function rescale() {\n      var k = 0, q = range.length;\n      thresholds = [];\n      while (++k < q) thresholds[k - 1] = d3.quantile(domain, k / q);\n      return scale;\n    }\n    function scale(x) {\n      if (!isNaN(x = +x)) return range[d3.bisect(thresholds, x)];\n    }\n    scale.domain = function(x) {\n      if (!arguments.length) return domain;\n      domain = x.filter(d3_number).sort(d3_ascending);\n      return rescale();\n    };\n    scale.range = function(x) {\n      if (!arguments.length) return range;\n      range = x;\n      return rescale();\n    };\n    scale.quantiles = function() {\n      return thresholds;\n    };\n    scale.invertExtent = function(y) {\n      y = range.indexOf(y);\n      return y < 0 ? [ NaN, NaN ] : [ y > 0 ? thresholds[y - 1] : domain[0], y < thresholds.length ? thresholds[y] : domain[domain.length - 1] ];\n    };\n    scale.copy = function() {\n      return d3_scale_quantile(domain, range);\n    };\n    return rescale();\n  }\n  d3.scale.quantize = function() {\n    return d3_scale_quantize(0, 1, [ 0, 1 ]);\n  };\n  function d3_scale_quantize(x0, x1, range) {\n    var kx, i;\n    function scale(x) {\n      return range[Math.max(0, Math.min(i, Math.floor(kx * (x - x0))))];\n    }\n    function rescale() {\n      kx = range.length / (x1 - x0);\n      i = range.length - 1;\n      return scale;\n    }\n    scale.domain = function(x) {\n      if (!arguments.length) return [ x0, x1 ];\n      x0 = +x[0];\n      x1 = +x[x.length - 1];\n      return rescale();\n    };\n    scale.range = function(x) {\n      if (!arguments.length) return range;\n      range = x;\n      return rescale();\n    };\n    scale.invertExtent = function(y) {\n      y = range.indexOf(y);\n      y = y < 0 ? NaN : y / kx + x0;\n      return [ y, y + 1 / kx ];\n    };\n    scale.copy = function() {\n      return d3_scale_quantize(x0, x1, range);\n    };\n    return rescale();\n  }\n  d3.scale.threshold = function() {\n    return d3_scale_threshold([ .5 ], [ 0, 1 ]);\n  };\n  function d3_scale_threshold(domain, range) {\n    function scale(x) {\n      if (x <= x) return range[d3.bisect(domain, x)];\n    }\n    scale.domain = function(_) {\n      if (!arguments.length) return domain;\n      domain = _;\n      return scale;\n    };\n    scale.range = function(_) {\n      if (!arguments.length) return range;\n      range = _;\n      return scale;\n    };\n    scale.invertExtent = function(y) {\n      y = range.indexOf(y);\n      return [ domain[y - 1], domain[y] ];\n    };\n    scale.copy = function() {\n      return d3_scale_threshold(domain, range);\n    };\n    return scale;\n  }\n  d3.scale.identity = function() {\n    return d3_scale_identity([ 0, 1 ]);\n  };\n  function d3_scale_identity(domain) {\n    function identity(x) {\n      return +x;\n    }\n    identity.invert = identity;\n    identity.domain = identity.range = function(x) {\n      if (!arguments.length) return domain;\n      domain = x.map(identity);\n      return identity;\n    };\n    identity.ticks = function(m) {\n      return d3_scale_linearTicks(domain, m);\n    };\n    identity.tickFormat = function(m, format) {\n      return d3_scale_linearTickFormat(domain, m, format);\n    };\n    identity.copy = function() {\n      return d3_scale_identity(domain);\n    };\n    return identity;\n  }\n  d3.svg = {};\n  d3.svg.arc = function() {\n    var innerRadius = d3_svg_arcInnerRadius, outerRadius = d3_svg_arcOuterRadius, startAngle = d3_svg_arcStartAngle, endAngle = d3_svg_arcEndAngle;\n    function arc() {\n      var r0 = innerRadius.apply(this, arguments), r1 = outerRadius.apply(this, arguments), a0 = startAngle.apply(this, arguments) + d3_svg_arcOffset, a1 = endAngle.apply(this, arguments) + d3_svg_arcOffset, da = (a1 < a0 && (da = a0, \n      a0 = a1, a1 = da), a1 - a0), df = da < π ? \"0\" : \"1\", c0 = Math.cos(a0), s0 = Math.sin(a0), c1 = Math.cos(a1), s1 = Math.sin(a1);\n      return da >= d3_svg_arcMax ? r0 ? \"M0,\" + r1 + \"A\" + r1 + \",\" + r1 + \" 0 1,1 0,\" + -r1 + \"A\" + r1 + \",\" + r1 + \" 0 1,1 0,\" + r1 + \"M0,\" + r0 + \"A\" + r0 + \",\" + r0 + \" 0 1,0 0,\" + -r0 + \"A\" + r0 + \",\" + r0 + \" 0 1,0 0,\" + r0 + \"Z\" : \"M0,\" + r1 + \"A\" + r1 + \",\" + r1 + \" 0 1,1 0,\" + -r1 + \"A\" + r1 + \",\" + r1 + \" 0 1,1 0,\" + r1 + \"Z\" : r0 ? \"M\" + r1 * c0 + \",\" + r1 * s0 + \"A\" + r1 + \",\" + r1 + \" 0 \" + df + \",1 \" + r1 * c1 + \",\" + r1 * s1 + \"L\" + r0 * c1 + \",\" + r0 * s1 + \"A\" + r0 + \",\" + r0 + \" 0 \" + df + \",0 \" + r0 * c0 + \",\" + r0 * s0 + \"Z\" : \"M\" + r1 * c0 + \",\" + r1 * s0 + \"A\" + r1 + \",\" + r1 + \" 0 \" + df + \",1 \" + r1 * c1 + \",\" + r1 * s1 + \"L0,0\" + \"Z\";\n    }\n    arc.innerRadius = function(v) {\n      if (!arguments.length) return innerRadius;\n      innerRadius = d3_functor(v);\n      return arc;\n    };\n    arc.outerRadius = function(v) {\n      if (!arguments.length) return outerRadius;\n      outerRadius = d3_functor(v);\n      return arc;\n    };\n    arc.startAngle = function(v) {\n      if (!arguments.length) return startAngle;\n      startAngle = d3_functor(v);\n      return arc;\n    };\n    arc.endAngle = function(v) {\n      if (!arguments.length) return endAngle;\n      endAngle = d3_functor(v);\n      return arc;\n    };\n    arc.centroid = function() {\n      var r = (innerRadius.apply(this, arguments) + outerRadius.apply(this, arguments)) / 2, a = (startAngle.apply(this, arguments) + endAngle.apply(this, arguments)) / 2 + d3_svg_arcOffset;\n      return [ Math.cos(a) * r, Math.sin(a) * r ];\n    };\n    return arc;\n  };\n  var d3_svg_arcOffset = -halfπ, d3_svg_arcMax = τ - ε;\n  function d3_svg_arcInnerRadius(d) {\n    return d.innerRadius;\n  }\n  function d3_svg_arcOuterRadius(d) {\n    return d.outerRadius;\n  }\n  function d3_svg_arcStartAngle(d) {\n    return d.startAngle;\n  }\n  function d3_svg_arcEndAngle(d) {\n    return d.endAngle;\n  }\n  function d3_svg_line(projection) {\n    var x = d3_geom_pointX, y = d3_geom_pointY, defined = d3_true, interpolate = d3_svg_lineLinear, interpolateKey = interpolate.key, tension = .7;\n    function line(data) {\n      var segments = [], points = [], i = -1, n = data.length, d, fx = d3_functor(x), fy = d3_functor(y);\n      function segment() {\n        segments.push(\"M\", interpolate(projection(points), tension));\n      }\n      while (++i < n) {\n        if (defined.call(this, d = data[i], i)) {\n          points.push([ +fx.call(this, d, i), +fy.call(this, d, i) ]);\n        } else if (points.length) {\n          segment();\n          points = [];\n        }\n      }\n      if (points.length) segment();\n      return segments.length ? segments.join(\"\") : null;\n    }\n    line.x = function(_) {\n      if (!arguments.length) return x;\n      x = _;\n      return line;\n    };\n    line.y = function(_) {\n      if (!arguments.length) return y;\n      y = _;\n      return line;\n    };\n    line.defined = function(_) {\n      if (!arguments.length) return defined;\n      defined = _;\n      return line;\n    };\n    line.interpolate = function(_) {\n      if (!arguments.length) return interpolateKey;\n      if (typeof _ === \"function\") interpolateKey = interpolate = _; else interpolateKey = (interpolate = d3_svg_lineInterpolators.get(_) || d3_svg_lineLinear).key;\n      return line;\n    };\n    line.tension = function(_) {\n      if (!arguments.length) return tension;\n      tension = _;\n      return line;\n    };\n    return line;\n  }\n  d3.svg.line = function() {\n    return d3_svg_line(d3_identity);\n  };\n  var d3_svg_lineInterpolators = d3.map({\n    linear: d3_svg_lineLinear,\n    \"linear-closed\": d3_svg_lineLinearClosed,\n    step: d3_svg_lineStep,\n    \"step-before\": d3_svg_lineStepBefore,\n    \"step-after\": d3_svg_lineStepAfter,\n    basis: d3_svg_lineBasis,\n    \"basis-open\": d3_svg_lineBasisOpen,\n    \"basis-closed\": d3_svg_lineBasisClosed,\n    bundle: d3_svg_lineBundle,\n    cardinal: d3_svg_lineCardinal,\n    \"cardinal-open\": d3_svg_lineCardinalOpen,\n    \"cardinal-closed\": d3_svg_lineCardinalClosed,\n    monotone: d3_svg_lineMonotone\n  });\n  d3_svg_lineInterpolators.forEach(function(key, value) {\n    value.key = key;\n    value.closed = /-closed$/.test(key);\n  });\n  function d3_svg_lineLinear(points) {\n    return points.join(\"L\");\n  }\n  function d3_svg_lineLinearClosed(points) {\n    return d3_svg_lineLinear(points) + \"Z\";\n  }\n  function d3_svg_lineStep(points) {\n    var i = 0, n = points.length, p = points[0], path = [ p[0], \",\", p[1] ];\n    while (++i < n) path.push(\"H\", (p[0] + (p = points[i])[0]) / 2, \"V\", p[1]);\n    if (n > 1) path.push(\"H\", p[0]);\n    return path.join(\"\");\n  }\n  function d3_svg_lineStepBefore(points) {\n    var i = 0, n = points.length, p = points[0], path = [ p[0], \",\", p[1] ];\n    while (++i < n) path.push(\"V\", (p = points[i])[1], \"H\", p[0]);\n    return path.join(\"\");\n  }\n  function d3_svg_lineStepAfter(points) {\n    var i = 0, n = points.length, p = points[0], path = [ p[0], \",\", p[1] ];\n    while (++i < n) path.push(\"H\", (p = points[i])[0], \"V\", p[1]);\n    return path.join(\"\");\n  }\n  function d3_svg_lineCardinalOpen(points, tension) {\n    return points.length < 4 ? d3_svg_lineLinear(points) : points[1] + d3_svg_lineHermite(points.slice(1, points.length - 1), d3_svg_lineCardinalTangents(points, tension));\n  }\n  function d3_svg_lineCardinalClosed(points, tension) {\n    return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite((points.push(points[0]), \n    points), d3_svg_lineCardinalTangents([ points[points.length - 2] ].concat(points, [ points[1] ]), tension));\n  }\n  function d3_svg_lineCardinal(points, tension) {\n    return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite(points, d3_svg_lineCardinalTangents(points, tension));\n  }\n  function d3_svg_lineHermite(points, tangents) {\n    if (tangents.length < 1 || points.length != tangents.length && points.length != tangents.length + 2) {\n      return d3_svg_lineLinear(points);\n    }\n    var quad = points.length != tangents.length, path = \"\", p0 = points[0], p = points[1], t0 = tangents[0], t = t0, pi = 1;\n    if (quad) {\n      path += \"Q\" + (p[0] - t0[0] * 2 / 3) + \",\" + (p[1] - t0[1] * 2 / 3) + \",\" + p[0] + \",\" + p[1];\n      p0 = points[1];\n      pi = 2;\n    }\n    if (tangents.length > 1) {\n      t = tangents[1];\n      p = points[pi];\n      pi++;\n      path += \"C\" + (p0[0] + t0[0]) + \",\" + (p0[1] + t0[1]) + \",\" + (p[0] - t[0]) + \",\" + (p[1] - t[1]) + \",\" + p[0] + \",\" + p[1];\n      for (var i = 2; i < tangents.length; i++, pi++) {\n        p = points[pi];\n        t = tangents[i];\n        path += \"S\" + (p[0] - t[0]) + \",\" + (p[1] - t[1]) + \",\" + p[0] + \",\" + p[1];\n      }\n    }\n    if (quad) {\n      var lp = points[pi];\n      path += \"Q\" + (p[0] + t[0] * 2 / 3) + \",\" + (p[1] + t[1] * 2 / 3) + \",\" + lp[0] + \",\" + lp[1];\n    }\n    return path;\n  }\n  function d3_svg_lineCardinalTangents(points, tension) {\n    var tangents = [], a = (1 - tension) / 2, p0, p1 = points[0], p2 = points[1], i = 1, n = points.length;\n    while (++i < n) {\n      p0 = p1;\n      p1 = p2;\n      p2 = points[i];\n      tangents.push([ a * (p2[0] - p0[0]), a * (p2[1] - p0[1]) ]);\n    }\n    return tangents;\n  }\n  function d3_svg_lineBasis(points) {\n    if (points.length < 3) return d3_svg_lineLinear(points);\n    var i = 1, n = points.length, pi = points[0], x0 = pi[0], y0 = pi[1], px = [ x0, x0, x0, (pi = points[1])[0] ], py = [ y0, y0, y0, pi[1] ], path = [ x0, \",\", y0, \"L\", d3_svg_lineDot4(d3_svg_lineBasisBezier3, px), \",\", d3_svg_lineDot4(d3_svg_lineBasisBezier3, py) ];\n    points.push(points[n - 1]);\n    while (++i <= n) {\n      pi = points[i];\n      px.shift();\n      px.push(pi[0]);\n      py.shift();\n      py.push(pi[1]);\n      d3_svg_lineBasisBezier(path, px, py);\n    }\n    points.pop();\n    path.push(\"L\", pi);\n    return path.join(\"\");\n  }\n  function d3_svg_lineBasisOpen(points) {\n    if (points.length < 4) return d3_svg_lineLinear(points);\n    var path = [], i = -1, n = points.length, pi, px = [ 0 ], py = [ 0 ];\n    while (++i < 3) {\n      pi = points[i];\n      px.push(pi[0]);\n      py.push(pi[1]);\n    }\n    path.push(d3_svg_lineDot4(d3_svg_lineBasisBezier3, px) + \",\" + d3_svg_lineDot4(d3_svg_lineBasisBezier3, py));\n    --i;\n    while (++i < n) {\n      pi = points[i];\n      px.shift();\n      px.push(pi[0]);\n      py.shift();\n      py.push(pi[1]);\n      d3_svg_lineBasisBezier(path, px, py);\n    }\n    return path.join(\"\");\n  }\n  function d3_svg_lineBasisClosed(points) {\n    var path, i = -1, n = points.length, m = n + 4, pi, px = [], py = [];\n    while (++i < 4) {\n      pi = points[i % n];\n      px.push(pi[0]);\n      py.push(pi[1]);\n    }\n    path = [ d3_svg_lineDot4(d3_svg_lineBasisBezier3, px), \",\", d3_svg_lineDot4(d3_svg_lineBasisBezier3, py) ];\n    --i;\n    while (++i < m) {\n      pi = points[i % n];\n      px.shift();\n      px.push(pi[0]);\n      py.shift();\n      py.push(pi[1]);\n      d3_svg_lineBasisBezier(path, px, py);\n    }\n    return path.join(\"\");\n  }\n  function d3_svg_lineBundle(points, tension) {\n    var n = points.length - 1;\n    if (n) {\n      var x0 = points[0][0], y0 = points[0][1], dx = points[n][0] - x0, dy = points[n][1] - y0, i = -1, p, t;\n      while (++i <= n) {\n        p = points[i];\n        t = i / n;\n        p[0] = tension * p[0] + (1 - tension) * (x0 + t * dx);\n        p[1] = tension * p[1] + (1 - tension) * (y0 + t * dy);\n      }\n    }\n    return d3_svg_lineBasis(points);\n  }\n  function d3_svg_lineDot4(a, b) {\n    return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];\n  }\n  var d3_svg_lineBasisBezier1 = [ 0, 2 / 3, 1 / 3, 0 ], d3_svg_lineBasisBezier2 = [ 0, 1 / 3, 2 / 3, 0 ], d3_svg_lineBasisBezier3 = [ 0, 1 / 6, 2 / 3, 1 / 6 ];\n  function d3_svg_lineBasisBezier(path, x, y) {\n    path.push(\"C\", d3_svg_lineDot4(d3_svg_lineBasisBezier1, x), \",\", d3_svg_lineDot4(d3_svg_lineBasisBezier1, y), \",\", d3_svg_lineDot4(d3_svg_lineBasisBezier2, x), \",\", d3_svg_lineDot4(d3_svg_lineBasisBezier2, y), \",\", d3_svg_lineDot4(d3_svg_lineBasisBezier3, x), \",\", d3_svg_lineDot4(d3_svg_lineBasisBezier3, y));\n  }\n  function d3_svg_lineSlope(p0, p1) {\n    return (p1[1] - p0[1]) / (p1[0] - p0[0]);\n  }\n  function d3_svg_lineFiniteDifferences(points) {\n    var i = 0, j = points.length - 1, m = [], p0 = points[0], p1 = points[1], d = m[0] = d3_svg_lineSlope(p0, p1);\n    while (++i < j) {\n      m[i] = (d + (d = d3_svg_lineSlope(p0 = p1, p1 = points[i + 1]))) / 2;\n    }\n    m[i] = d;\n    return m;\n  }\n  function d3_svg_lineMonotoneTangents(points) {\n    var tangents = [], d, a, b, s, m = d3_svg_lineFiniteDifferences(points), i = -1, j = points.length - 1;\n    while (++i < j) {\n      d = d3_svg_lineSlope(points[i], points[i + 1]);\n      if (abs(d) < ε) {\n        m[i] = m[i + 1] = 0;\n      } else {\n        a = m[i] / d;\n        b = m[i + 1] / d;\n        s = a * a + b * b;\n        if (s > 9) {\n          s = d * 3 / Math.sqrt(s);\n          m[i] = s * a;\n          m[i + 1] = s * b;\n        }\n      }\n    }\n    i = -1;\n    while (++i <= j) {\n      s = (points[Math.min(j, i + 1)][0] - points[Math.max(0, i - 1)][0]) / (6 * (1 + m[i] * m[i]));\n      tangents.push([ s || 0, m[i] * s || 0 ]);\n    }\n    return tangents;\n  }\n  function d3_svg_lineMonotone(points) {\n    return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite(points, d3_svg_lineMonotoneTangents(points));\n  }\n  d3.svg.line.radial = function() {\n    var line = d3_svg_line(d3_svg_lineRadial);\n    line.radius = line.x, delete line.x;\n    line.angle = line.y, delete line.y;\n    return line;\n  };\n  function d3_svg_lineRadial(points) {\n    var point, i = -1, n = points.length, r, a;\n    while (++i < n) {\n      point = points[i];\n      r = point[0];\n      a = point[1] + d3_svg_arcOffset;\n      point[0] = r * Math.cos(a);\n      point[1] = r * Math.sin(a);\n    }\n    return points;\n  }\n  function d3_svg_area(projection) {\n    var x0 = d3_geom_pointX, x1 = d3_geom_pointX, y0 = 0, y1 = d3_geom_pointY, defined = d3_true, interpolate = d3_svg_lineLinear, interpolateKey = interpolate.key, interpolateReverse = interpolate, L = \"L\", tension = .7;\n    function area(data) {\n      var segments = [], points0 = [], points1 = [], i = -1, n = data.length, d, fx0 = d3_functor(x0), fy0 = d3_functor(y0), fx1 = x0 === x1 ? function() {\n        return x;\n      } : d3_functor(x1), fy1 = y0 === y1 ? function() {\n        return y;\n      } : d3_functor(y1), x, y;\n      function segment() {\n        segments.push(\"M\", interpolate(projection(points1), tension), L, interpolateReverse(projection(points0.reverse()), tension), \"Z\");\n      }\n      while (++i < n) {\n        if (defined.call(this, d = data[i], i)) {\n          points0.push([ x = +fx0.call(this, d, i), y = +fy0.call(this, d, i) ]);\n          points1.push([ +fx1.call(this, d, i), +fy1.call(this, d, i) ]);\n        } else if (points0.length) {\n          segment();\n          points0 = [];\n          points1 = [];\n        }\n      }\n      if (points0.length) segment();\n      return segments.length ? segments.join(\"\") : null;\n    }\n    area.x = function(_) {\n      if (!arguments.length) return x1;\n      x0 = x1 = _;\n      return area;\n    };\n    area.x0 = function(_) {\n      if (!arguments.length) return x0;\n      x0 = _;\n      return area;\n    };\n    area.x1 = function(_) {\n      if (!arguments.length) return x1;\n      x1 = _;\n      return area;\n    };\n    area.y = function(_) {\n      if (!arguments.length) return y1;\n      y0 = y1 = _;\n      return area;\n    };\n    area.y0 = function(_) {\n      if (!arguments.length) return y0;\n      y0 = _;\n      return area;\n    };\n    area.y1 = function(_) {\n      if (!arguments.length) return y1;\n      y1 = _;\n      return area;\n    };\n    area.defined = function(_) {\n      if (!arguments.length) return defined;\n      defined = _;\n      return area;\n    };\n    area.interpolate = function(_) {\n      if (!arguments.length) return interpolateKey;\n      if (typeof _ === \"function\") interpolateKey = interpolate = _; else interpolateKey = (interpolate = d3_svg_lineInterpolators.get(_) || d3_svg_lineLinear).key;\n      interpolateReverse = interpolate.reverse || interpolate;\n      L = interpolate.closed ? \"M\" : \"L\";\n      return area;\n    };\n    area.tension = function(_) {\n      if (!arguments.length) return tension;\n      tension = _;\n      return area;\n    };\n    return area;\n  }\n  d3_svg_lineStepBefore.reverse = d3_svg_lineStepAfter;\n  d3_svg_lineStepAfter.reverse = d3_svg_lineStepBefore;\n  d3.svg.area = function() {\n    return d3_svg_area(d3_identity);\n  };\n  d3.svg.area.radial = function() {\n    var area = d3_svg_area(d3_svg_lineRadial);\n    area.radius = area.x, delete area.x;\n    area.innerRadius = area.x0, delete area.x0;\n    area.outerRadius = area.x1, delete area.x1;\n    area.angle = area.y, delete area.y;\n    area.startAngle = area.y0, delete area.y0;\n    area.endAngle = area.y1, delete area.y1;\n    return area;\n  };\n  d3.svg.chord = function() {\n    var source = d3_source, target = d3_target, radius = d3_svg_chordRadius, startAngle = d3_svg_arcStartAngle, endAngle = d3_svg_arcEndAngle;\n    function chord(d, i) {\n      var s = subgroup(this, source, d, i), t = subgroup(this, target, d, i);\n      return \"M\" + s.p0 + arc(s.r, s.p1, s.a1 - s.a0) + (equals(s, t) ? curve(s.r, s.p1, s.r, s.p0) : curve(s.r, s.p1, t.r, t.p0) + arc(t.r, t.p1, t.a1 - t.a0) + curve(t.r, t.p1, s.r, s.p0)) + \"Z\";\n    }\n    function subgroup(self, f, d, i) {\n      var subgroup = f.call(self, d, i), r = radius.call(self, subgroup, i), a0 = startAngle.call(self, subgroup, i) + d3_svg_arcOffset, a1 = endAngle.call(self, subgroup, i) + d3_svg_arcOffset;\n      return {\n        r: r,\n        a0: a0,\n        a1: a1,\n        p0: [ r * Math.cos(a0), r * Math.sin(a0) ],\n        p1: [ r * Math.cos(a1), r * Math.sin(a1) ]\n      };\n    }\n    function equals(a, b) {\n      return a.a0 == b.a0 && a.a1 == b.a1;\n    }\n    function arc(r, p, a) {\n      return \"A\" + r + \",\" + r + \" 0 \" + +(a > π) + \",1 \" + p;\n    }\n    function curve(r0, p0, r1, p1) {\n      return \"Q 0,0 \" + p1;\n    }\n    chord.radius = function(v) {\n      if (!arguments.length) return radius;\n      radius = d3_functor(v);\n      return chord;\n    };\n    chord.source = function(v) {\n      if (!arguments.length) return source;\n      source = d3_functor(v);\n      return chord;\n    };\n    chord.target = function(v) {\n      if (!arguments.length) return target;\n      target = d3_functor(v);\n      return chord;\n    };\n    chord.startAngle = function(v) {\n      if (!arguments.length) return startAngle;\n      startAngle = d3_functor(v);\n      return chord;\n    };\n    chord.endAngle = function(v) {\n      if (!arguments.length) return endAngle;\n      endAngle = d3_functor(v);\n      return chord;\n    };\n    return chord;\n  };\n  function d3_svg_chordRadius(d) {\n    return d.radius;\n  }\n  d3.svg.diagonal = function() {\n    var source = d3_source, target = d3_target, projection = d3_svg_diagonalProjection;\n    function diagonal(d, i) {\n      var p0 = source.call(this, d, i), p3 = target.call(this, d, i), m = (p0.y + p3.y) / 2, p = [ p0, {\n        x: p0.x,\n        y: m\n      }, {\n        x: p3.x,\n        y: m\n      }, p3 ];\n      p = p.map(projection);\n      return \"M\" + p[0] + \"C\" + p[1] + \" \" + p[2] + \" \" + p[3];\n    }\n    diagonal.source = function(x) {\n      if (!arguments.length) return source;\n      source = d3_functor(x);\n      return diagonal;\n    };\n    diagonal.target = function(x) {\n      if (!arguments.length) return target;\n      target = d3_functor(x);\n      return diagonal;\n    };\n    diagonal.projection = function(x) {\n      if (!arguments.length) return projection;\n      projection = x;\n      return diagonal;\n    };\n    return diagonal;\n  };\n  function d3_svg_diagonalProjection(d) {\n    return [ d.x, d.y ];\n  }\n  d3.svg.diagonal.radial = function() {\n    var diagonal = d3.svg.diagonal(), projection = d3_svg_diagonalProjection, projection_ = diagonal.projection;\n    diagonal.projection = function(x) {\n      return arguments.length ? projection_(d3_svg_diagonalRadialProjection(projection = x)) : projection;\n    };\n    return diagonal;\n  };\n  function d3_svg_diagonalRadialProjection(projection) {\n    return function() {\n      var d = projection.apply(this, arguments), r = d[0], a = d[1] + d3_svg_arcOffset;\n      return [ r * Math.cos(a), r * Math.sin(a) ];\n    };\n  }\n  d3.svg.symbol = function() {\n    var type = d3_svg_symbolType, size = d3_svg_symbolSize;\n    function symbol(d, i) {\n      return (d3_svg_symbols.get(type.call(this, d, i)) || d3_svg_symbolCircle)(size.call(this, d, i));\n    }\n    symbol.type = function(x) {\n      if (!arguments.length) return type;\n      type = d3_functor(x);\n      return symbol;\n    };\n    symbol.size = function(x) {\n      if (!arguments.length) return size;\n      size = d3_functor(x);\n      return symbol;\n    };\n    return symbol;\n  };\n  function d3_svg_symbolSize() {\n    return 64;\n  }\n  function d3_svg_symbolType() {\n    return \"circle\";\n  }\n  function d3_svg_symbolCircle(size) {\n    var r = Math.sqrt(size / π);\n    return \"M0,\" + r + \"A\" + r + \",\" + r + \" 0 1,1 0,\" + -r + \"A\" + r + \",\" + r + \" 0 1,1 0,\" + r + \"Z\";\n  }\n  var d3_svg_symbols = d3.map({\n    circle: d3_svg_symbolCircle,\n    cross: function(size) {\n      var r = Math.sqrt(size / 5) / 2;\n      return \"M\" + -3 * r + \",\" + -r + \"H\" + -r + \"V\" + -3 * r + \"H\" + r + \"V\" + -r + \"H\" + 3 * r + \"V\" + r + \"H\" + r + \"V\" + 3 * r + \"H\" + -r + \"V\" + r + \"H\" + -3 * r + \"Z\";\n    },\n    diamond: function(size) {\n      var ry = Math.sqrt(size / (2 * d3_svg_symbolTan30)), rx = ry * d3_svg_symbolTan30;\n      return \"M0,\" + -ry + \"L\" + rx + \",0\" + \" 0,\" + ry + \" \" + -rx + \",0\" + \"Z\";\n    },\n    square: function(size) {\n      var r = Math.sqrt(size) / 2;\n      return \"M\" + -r + \",\" + -r + \"L\" + r + \",\" + -r + \" \" + r + \",\" + r + \" \" + -r + \",\" + r + \"Z\";\n    },\n    \"triangle-down\": function(size) {\n      var rx = Math.sqrt(size / d3_svg_symbolSqrt3), ry = rx * d3_svg_symbolSqrt3 / 2;\n      return \"M0,\" + ry + \"L\" + rx + \",\" + -ry + \" \" + -rx + \",\" + -ry + \"Z\";\n    },\n    \"triangle-up\": function(size) {\n      var rx = Math.sqrt(size / d3_svg_symbolSqrt3), ry = rx * d3_svg_symbolSqrt3 / 2;\n      return \"M0,\" + -ry + \"L\" + rx + \",\" + ry + \" \" + -rx + \",\" + ry + \"Z\";\n    }\n  });\n  d3.svg.symbolTypes = d3_svg_symbols.keys();\n  var d3_svg_symbolSqrt3 = Math.sqrt(3), d3_svg_symbolTan30 = Math.tan(30 * d3_radians);\n  function d3_transition(groups, id) {\n    d3_subclass(groups, d3_transitionPrototype);\n    groups.id = id;\n    return groups;\n  }\n  var d3_transitionPrototype = [], d3_transitionId = 0, d3_transitionInheritId, d3_transitionInherit;\n  d3_transitionPrototype.call = d3_selectionPrototype.call;\n  d3_transitionPrototype.empty = d3_selectionPrototype.empty;\n  d3_transitionPrototype.node = d3_selectionPrototype.node;\n  d3_transitionPrototype.size = d3_selectionPrototype.size;\n  d3.transition = function(selection) {\n    return arguments.length ? d3_transitionInheritId ? selection.transition() : selection : d3_selectionRoot.transition();\n  };\n  d3.transition.prototype = d3_transitionPrototype;\n  d3_transitionPrototype.select = function(selector) {\n    var id = this.id, subgroups = [], subgroup, subnode, node;\n    selector = d3_selection_selector(selector);\n    for (var j = -1, m = this.length; ++j < m; ) {\n      subgroups.push(subgroup = []);\n      for (var group = this[j], i = -1, n = group.length; ++i < n; ) {\n        if ((node = group[i]) && (subnode = selector.call(node, node.__data__, i, j))) {\n          if (\"__data__\" in node) subnode.__data__ = node.__data__;\n          d3_transitionNode(subnode, i, id, node.__transition__[id]);\n          subgroup.push(subnode);\n        } else {\n          subgroup.push(null);\n        }\n      }\n    }\n    return d3_transition(subgroups, id);\n  };\n  d3_transitionPrototype.selectAll = function(selector) {\n    var id = this.id, subgroups = [], subgroup, subnodes, node, subnode, transition;\n    selector = d3_selection_selectorAll(selector);\n    for (var j = -1, m = this.length; ++j < m; ) {\n      for (var group = this[j], i = -1, n = group.length; ++i < n; ) {\n        if (node = group[i]) {\n          transition = node.__transition__[id];\n          subnodes = selector.call(node, node.__data__, i, j);\n          subgroups.push(subgroup = []);\n          for (var k = -1, o = subnodes.length; ++k < o; ) {\n            if (subnode = subnodes[k]) d3_transitionNode(subnode, k, id, transition);\n            subgroup.push(subnode);\n          }\n        }\n      }\n    }\n    return d3_transition(subgroups, id);\n  };\n  d3_transitionPrototype.filter = function(filter) {\n    var subgroups = [], subgroup, group, node;\n    if (typeof filter !== \"function\") filter = d3_selection_filter(filter);\n    for (var j = 0, m = this.length; j < m; j++) {\n      subgroups.push(subgroup = []);\n      for (var group = this[j], i = 0, n = group.length; i < n; i++) {\n        if ((node = group[i]) && filter.call(node, node.__data__, i, j)) {\n          subgroup.push(node);\n        }\n      }\n    }\n    return d3_transition(subgroups, this.id);\n  };\n  d3_transitionPrototype.tween = function(name, tween) {\n    var id = this.id;\n    if (arguments.length < 2) return this.node().__transition__[id].tween.get(name);\n    return d3_selection_each(this, tween == null ? function(node) {\n      node.__transition__[id].tween.remove(name);\n    } : function(node) {\n      node.__transition__[id].tween.set(name, tween);\n    });\n  };\n  function d3_transition_tween(groups, name, value, tween) {\n    var id = groups.id;\n    return d3_selection_each(groups, typeof value === \"function\" ? function(node, i, j) {\n      node.__transition__[id].tween.set(name, tween(value.call(node, node.__data__, i, j)));\n    } : (value = tween(value), function(node) {\n      node.__transition__[id].tween.set(name, value);\n    }));\n  }\n  d3_transitionPrototype.attr = function(nameNS, value) {\n    if (arguments.length < 2) {\n      for (value in nameNS) this.attr(value, nameNS[value]);\n      return this;\n    }\n    var interpolate = nameNS == \"transform\" ? d3_interpolateTransform : d3_interpolate, name = d3.ns.qualify(nameNS);\n    function attrNull() {\n      this.removeAttribute(name);\n    }\n    function attrNullNS() {\n      this.removeAttributeNS(name.space, name.local);\n    }\n    function attrTween(b) {\n      return b == null ? attrNull : (b += \"\", function() {\n        var a = this.getAttribute(name), i;\n        return a !== b && (i = interpolate(a, b), function(t) {\n          this.setAttribute(name, i(t));\n        });\n      });\n    }\n    function attrTweenNS(b) {\n      return b == null ? attrNullNS : (b += \"\", function() {\n        var a = this.getAttributeNS(name.space, name.local), i;\n        return a !== b && (i = interpolate(a, b), function(t) {\n          this.setAttributeNS(name.space, name.local, i(t));\n        });\n      });\n    }\n    return d3_transition_tween(this, \"attr.\" + nameNS, value, name.local ? attrTweenNS : attrTween);\n  };\n  d3_transitionPrototype.attrTween = function(nameNS, tween) {\n    var name = d3.ns.qualify(nameNS);\n    function attrTween(d, i) {\n      var f = tween.call(this, d, i, this.getAttribute(name));\n      return f && function(t) {\n        this.setAttribute(name, f(t));\n      };\n    }\n    function attrTweenNS(d, i) {\n      var f = tween.call(this, d, i, this.getAttributeNS(name.space, name.local));\n      return f && function(t) {\n        this.setAttributeNS(name.space, name.local, f(t));\n      };\n    }\n    return this.tween(\"attr.\" + nameNS, name.local ? attrTweenNS : attrTween);\n  };\n  d3_transitionPrototype.style = function(name, value, priority) {\n    var n = arguments.length;\n    if (n < 3) {\n      if (typeof name !== \"string\") {\n        if (n < 2) value = \"\";\n        for (priority in name) this.style(priority, name[priority], value);\n        return this;\n      }\n      priority = \"\";\n    }\n    function styleNull() {\n      this.style.removeProperty(name);\n    }\n    function styleString(b) {\n      return b == null ? styleNull : (b += \"\", function() {\n        var a = d3_window.getComputedStyle(this, null).getPropertyValue(name), i;\n        return a !== b && (i = d3_interpolate(a, b), function(t) {\n          this.style.setProperty(name, i(t), priority);\n        });\n      });\n    }\n    return d3_transition_tween(this, \"style.\" + name, value, styleString);\n  };\n  d3_transitionPrototype.styleTween = function(name, tween, priority) {\n    if (arguments.length < 3) priority = \"\";\n    function styleTween(d, i) {\n      var f = tween.call(this, d, i, d3_window.getComputedStyle(this, null).getPropertyValue(name));\n      return f && function(t) {\n        this.style.setProperty(name, f(t), priority);\n      };\n    }\n    return this.tween(\"style.\" + name, styleTween);\n  };\n  d3_transitionPrototype.text = function(value) {\n    return d3_transition_tween(this, \"text\", value, d3_transition_text);\n  };\n  function d3_transition_text(b) {\n    if (b == null) b = \"\";\n    return function() {\n      this.textContent = b;\n    };\n  }\n  d3_transitionPrototype.remove = function() {\n    return this.each(\"end.transition\", function() {\n      var p;\n      if (this.__transition__.count < 2 && (p = this.parentNode)) p.removeChild(this);\n    });\n  };\n  d3_transitionPrototype.ease = function(value) {\n    var id = this.id;\n    if (arguments.length < 1) return this.node().__transition__[id].ease;\n    if (typeof value !== \"function\") value = d3.ease.apply(d3, arguments);\n    return d3_selection_each(this, function(node) {\n      node.__transition__[id].ease = value;\n    });\n  };\n  d3_transitionPrototype.delay = function(value) {\n    var id = this.id;\n    if (arguments.length < 1) return this.node().__transition__[id].delay;\n    return d3_selection_each(this, typeof value === \"function\" ? function(node, i, j) {\n      node.__transition__[id].delay = +value.call(node, node.__data__, i, j);\n    } : (value = +value, function(node) {\n      node.__transition__[id].delay = value;\n    }));\n  };\n  d3_transitionPrototype.duration = function(value) {\n    var id = this.id;\n    if (arguments.length < 1) return this.node().__transition__[id].duration;\n    return d3_selection_each(this, typeof value === \"function\" ? function(node, i, j) {\n      node.__transition__[id].duration = Math.max(1, value.call(node, node.__data__, i, j));\n    } : (value = Math.max(1, value), function(node) {\n      node.__transition__[id].duration = value;\n    }));\n  };\n  d3_transitionPrototype.each = function(type, listener) {\n    var id = this.id;\n    if (arguments.length < 2) {\n      var inherit = d3_transitionInherit, inheritId = d3_transitionInheritId;\n      d3_transitionInheritId = id;\n      d3_selection_each(this, function(node, i, j) {\n        d3_transitionInherit = node.__transition__[id];\n        type.call(node, node.__data__, i, j);\n      });\n      d3_transitionInherit = inherit;\n      d3_transitionInheritId = inheritId;\n    } else {\n      d3_selection_each(this, function(node) {\n        var transition = node.__transition__[id];\n        (transition.event || (transition.event = d3.dispatch(\"start\", \"end\"))).on(type, listener);\n      });\n    }\n    return this;\n  };\n  d3_transitionPrototype.transition = function() {\n    var id0 = this.id, id1 = ++d3_transitionId, subgroups = [], subgroup, group, node, transition;\n    for (var j = 0, m = this.length; j < m; j++) {\n      subgroups.push(subgroup = []);\n      for (var group = this[j], i = 0, n = group.length; i < n; i++) {\n        if (node = group[i]) {\n          transition = Object.create(node.__transition__[id0]);\n          transition.delay += transition.duration;\n          d3_transitionNode(node, i, id1, transition);\n        }\n        subgroup.push(node);\n      }\n    }\n    return d3_transition(subgroups, id1);\n  };\n  function d3_transitionNode(node, i, id, inherit) {\n    var lock = node.__transition__ || (node.__transition__ = {\n      active: 0,\n      count: 0\n    }), transition = lock[id];\n    if (!transition) {\n      var time = inherit.time;\n      transition = lock[id] = {\n        tween: new d3_Map(),\n        time: time,\n        ease: inherit.ease,\n        delay: inherit.delay,\n        duration: inherit.duration\n      };\n      ++lock.count;\n      d3.timer(function(elapsed) {\n        var d = node.__data__, ease = transition.ease, delay = transition.delay, duration = transition.duration, timer = d3_timer_active, tweened = [];\n        timer.t = delay + time;\n        if (delay <= elapsed) return start(elapsed - delay);\n        timer.c = start;\n        function start(elapsed) {\n          if (lock.active > id) return stop();\n          lock.active = id;\n          transition.event && transition.event.start.call(node, d, i);\n          transition.tween.forEach(function(key, value) {\n            if (value = value.call(node, d, i)) {\n              tweened.push(value);\n            }\n          });\n          d3.timer(function() {\n            timer.c = tick(elapsed || 1) ? d3_true : tick;\n            return 1;\n          }, 0, time);\n        }\n        function tick(elapsed) {\n          if (lock.active !== id) return stop();\n          var t = elapsed / duration, e = ease(t), n = tweened.length;\n          while (n > 0) {\n            tweened[--n].call(node, e);\n          }\n          if (t >= 1) {\n            transition.event && transition.event.end.call(node, d, i);\n            return stop();\n          }\n        }\n        function stop() {\n          if (--lock.count) delete lock[id]; else delete node.__transition__;\n          return 1;\n        }\n      }, 0, time);\n    }\n  }\n  d3.svg.axis = function() {\n    var scale = d3.scale.linear(), orient = d3_svg_axisDefaultOrient, innerTickSize = 6, outerTickSize = 6, tickPadding = 3, tickArguments_ = [ 10 ], tickValues = null, tickFormat_;\n    function axis(g) {\n      g.each(function() {\n        var g = d3.select(this);\n        var scale0 = this.__chart__ || scale, scale1 = this.__chart__ = scale.copy();\n        var ticks = tickValues == null ? scale1.ticks ? scale1.ticks.apply(scale1, tickArguments_) : scale1.domain() : tickValues, tickFormat = tickFormat_ == null ? scale1.tickFormat ? scale1.tickFormat.apply(scale1, tickArguments_) : d3_identity : tickFormat_, tick = g.selectAll(\".tick\").data(ticks, scale1), tickEnter = tick.enter().insert(\"g\", \".domain\").attr(\"class\", \"tick\").style(\"opacity\", ε), tickExit = d3.transition(tick.exit()).style(\"opacity\", ε).remove(), tickUpdate = d3.transition(tick.order()).style(\"opacity\", 1), tickTransform;\n        var range = d3_scaleRange(scale1), path = g.selectAll(\".domain\").data([ 0 ]), pathUpdate = (path.enter().append(\"path\").attr(\"class\", \"domain\"), \n        d3.transition(path));\n        tickEnter.append(\"line\");\n        tickEnter.append(\"text\");\n        var lineEnter = tickEnter.select(\"line\"), lineUpdate = tickUpdate.select(\"line\"), text = tick.select(\"text\").text(tickFormat), textEnter = tickEnter.select(\"text\"), textUpdate = tickUpdate.select(\"text\");\n        switch (orient) {\n         case \"bottom\":\n          {\n            tickTransform = d3_svg_axisX;\n            lineEnter.attr(\"y2\", innerTickSize);\n            textEnter.attr(\"y\", Math.max(innerTickSize, 0) + tickPadding);\n            lineUpdate.attr(\"x2\", 0).attr(\"y2\", innerTickSize);\n            textUpdate.attr(\"x\", 0).attr(\"y\", Math.max(innerTickSize, 0) + tickPadding);\n            text.attr(\"dy\", \".71em\").style(\"text-anchor\", \"middle\");\n            pathUpdate.attr(\"d\", \"M\" + range[0] + \",\" + outerTickSize + \"V0H\" + range[1] + \"V\" + outerTickSize);\n            break;\n          }\n\n         case \"top\":\n          {\n            tickTransform = d3_svg_axisX;\n            lineEnter.attr(\"y2\", -innerTickSize);\n            textEnter.attr(\"y\", -(Math.max(innerTickSize, 0) + tickPadding));\n            lineUpdate.attr(\"x2\", 0).attr(\"y2\", -innerTickSize);\n            textUpdate.attr(\"x\", 0).attr(\"y\", -(Math.max(innerTickSize, 0) + tickPadding));\n            text.attr(\"dy\", \"0em\").style(\"text-anchor\", \"middle\");\n            pathUpdate.attr(\"d\", \"M\" + range[0] + \",\" + -outerTickSize + \"V0H\" + range[1] + \"V\" + -outerTickSize);\n            break;\n          }\n\n         case \"left\":\n          {\n            tickTransform = d3_svg_axisY;\n            lineEnter.attr(\"x2\", -innerTickSize);\n            textEnter.attr(\"x\", -(Math.max(innerTickSize, 0) + tickPadding));\n            lineUpdate.attr(\"x2\", -innerTickSize).attr(\"y2\", 0);\n            textUpdate.attr(\"x\", -(Math.max(innerTickSize, 0) + tickPadding)).attr(\"y\", 0);\n            text.attr(\"dy\", \".32em\").style(\"text-anchor\", \"end\");\n            pathUpdate.attr(\"d\", \"M\" + -outerTickSize + \",\" + range[0] + \"H0V\" + range[1] + \"H\" + -outerTickSize);\n            break;\n          }\n\n         case \"right\":\n          {\n            tickTransform = d3_svg_axisY;\n            lineEnter.attr(\"x2\", innerTickSize);\n            textEnter.attr(\"x\", Math.max(innerTickSize, 0) + tickPadding);\n            lineUpdate.attr(\"x2\", innerTickSize).attr(\"y2\", 0);\n            textUpdate.attr(\"x\", Math.max(innerTickSize, 0) + tickPadding).attr(\"y\", 0);\n            text.attr(\"dy\", \".32em\").style(\"text-anchor\", \"start\");\n            pathUpdate.attr(\"d\", \"M\" + outerTickSize + \",\" + range[0] + \"H0V\" + range[1] + \"H\" + outerTickSize);\n            break;\n          }\n        }\n        if (scale1.rangeBand) {\n          var x = scale1, dx = x.rangeBand() / 2;\n          scale0 = scale1 = function(d) {\n            return x(d) + dx;\n          };\n        } else if (scale0.rangeBand) {\n          scale0 = scale1;\n        } else {\n          tickExit.call(tickTransform, scale1);\n        }\n        tickEnter.call(tickTransform, scale0);\n        tickUpdate.call(tickTransform, scale1);\n      });\n    }\n    axis.scale = function(x) {\n      if (!arguments.length) return scale;\n      scale = x;\n      return axis;\n    };\n    axis.orient = function(x) {\n      if (!arguments.length) return orient;\n      orient = x in d3_svg_axisOrients ? x + \"\" : d3_svg_axisDefaultOrient;\n      return axis;\n    };\n    axis.ticks = function() {\n      if (!arguments.length) return tickArguments_;\n      tickArguments_ = arguments;\n      return axis;\n    };\n    axis.tickValues = function(x) {\n      if (!arguments.length) return tickValues;\n      tickValues = x;\n      return axis;\n    };\n    axis.tickFormat = function(x) {\n      if (!arguments.length) return tickFormat_;\n      tickFormat_ = x;\n      return axis;\n    };\n    axis.tickSize = function(x) {\n      var n = arguments.length;\n      if (!n) return innerTickSize;\n      innerTickSize = +x;\n      outerTickSize = +arguments[n - 1];\n      return axis;\n    };\n    axis.innerTickSize = function(x) {\n      if (!arguments.length) return innerTickSize;\n      innerTickSize = +x;\n      return axis;\n    };\n    axis.outerTickSize = function(x) {\n      if (!arguments.length) return outerTickSize;\n      outerTickSize = +x;\n      return axis;\n    };\n    axis.tickPadding = function(x) {\n      if (!arguments.length) return tickPadding;\n      tickPadding = +x;\n      return axis;\n    };\n    axis.tickSubdivide = function() {\n      return arguments.length && axis;\n    };\n    return axis;\n  };\n  var d3_svg_axisDefaultOrient = \"bottom\", d3_svg_axisOrients = {\n    top: 1,\n    right: 1,\n    bottom: 1,\n    left: 1\n  };\n  function d3_svg_axisX(selection, x) {\n    selection.attr(\"transform\", function(d) {\n      return \"translate(\" + x(d) + \",0)\";\n    });\n  }\n  function d3_svg_axisY(selection, y) {\n    selection.attr(\"transform\", function(d) {\n      return \"translate(0,\" + y(d) + \")\";\n    });\n  }\n  d3.svg.brush = function() {\n    var event = d3_eventDispatch(brush, \"brushstart\", \"brush\", \"brushend\"), x = null, y = null, xExtent = [ 0, 0 ], yExtent = [ 0, 0 ], xExtentDomain, yExtentDomain, xClamp = true, yClamp = true, resizes = d3_svg_brushResizes[0];\n    function brush(g) {\n      g.each(function() {\n        var g = d3.select(this).style(\"pointer-events\", \"all\").style(\"-webkit-tap-highlight-color\", \"rgba(0,0,0,0)\").on(\"mousedown.brush\", brushstart).on(\"touchstart.brush\", brushstart);\n        var background = g.selectAll(\".background\").data([ 0 ]);\n        background.enter().append(\"rect\").attr(\"class\", \"background\").style(\"visibility\", \"hidden\").style(\"cursor\", \"crosshair\");\n        g.selectAll(\".extent\").data([ 0 ]).enter().append(\"rect\").attr(\"class\", \"extent\").style(\"cursor\", \"move\");\n        var resize = g.selectAll(\".resize\").data(resizes, d3_identity);\n        resize.exit().remove();\n        resize.enter().append(\"g\").attr(\"class\", function(d) {\n          return \"resize \" + d;\n        }).style(\"cursor\", function(d) {\n          return d3_svg_brushCursor[d];\n        }).append(\"rect\").attr(\"x\", function(d) {\n          return /[ew]$/.test(d) ? -3 : null;\n        }).attr(\"y\", function(d) {\n          return /^[ns]/.test(d) ? -3 : null;\n        }).attr(\"width\", 6).attr(\"height\", 6).style(\"visibility\", \"hidden\");\n        resize.style(\"display\", brush.empty() ? \"none\" : null);\n        var gUpdate = d3.transition(g), backgroundUpdate = d3.transition(background), range;\n        if (x) {\n          range = d3_scaleRange(x);\n          backgroundUpdate.attr(\"x\", range[0]).attr(\"width\", range[1] - range[0]);\n          redrawX(gUpdate);\n        }\n        if (y) {\n          range = d3_scaleRange(y);\n          backgroundUpdate.attr(\"y\", range[0]).attr(\"height\", range[1] - range[0]);\n          redrawY(gUpdate);\n        }\n        redraw(gUpdate);\n      });\n    }\n    brush.event = function(g) {\n      g.each(function() {\n        var event_ = event.of(this, arguments), extent1 = {\n          x: xExtent,\n          y: yExtent,\n          i: xExtentDomain,\n          j: yExtentDomain\n        }, extent0 = this.__chart__ || extent1;\n        this.__chart__ = extent1;\n        if (d3_transitionInheritId) {\n          d3.select(this).transition().each(\"start.brush\", function() {\n            xExtentDomain = extent0.i;\n            yExtentDomain = extent0.j;\n            xExtent = extent0.x;\n            yExtent = extent0.y;\n            event_({\n              type: \"brushstart\"\n            });\n          }).tween(\"brush:brush\", function() {\n            var xi = d3_interpolateArray(xExtent, extent1.x), yi = d3_interpolateArray(yExtent, extent1.y);\n            xExtentDomain = yExtentDomain = null;\n            return function(t) {\n              xExtent = extent1.x = xi(t);\n              yExtent = extent1.y = yi(t);\n              event_({\n                type: \"brush\",\n                mode: \"resize\"\n              });\n            };\n          }).each(\"end.brush\", function() {\n            xExtentDomain = extent1.i;\n            yExtentDomain = extent1.j;\n            event_({\n              type: \"brush\",\n              mode: \"resize\"\n            });\n            event_({\n              type: \"brushend\"\n            });\n          });\n        } else {\n          event_({\n            type: \"brushstart\"\n          });\n          event_({\n            type: \"brush\",\n            mode: \"resize\"\n          });\n          event_({\n            type: \"brushend\"\n          });\n        }\n      });\n    };\n    function redraw(g) {\n      g.selectAll(\".resize\").attr(\"transform\", function(d) {\n        return \"translate(\" + xExtent[+/e$/.test(d)] + \",\" + yExtent[+/^s/.test(d)] + \")\";\n      });\n    }\n    function redrawX(g) {\n      g.select(\".extent\").attr(\"x\", xExtent[0]);\n      g.selectAll(\".extent,.n>rect,.s>rect\").attr(\"width\", xExtent[1] - xExtent[0]);\n    }\n    function redrawY(g) {\n      g.select(\".extent\").attr(\"y\", yExtent[0]);\n      g.selectAll(\".extent,.e>rect,.w>rect\").attr(\"height\", yExtent[1] - yExtent[0]);\n    }\n    function brushstart() {\n      var target = this, eventTarget = d3.select(d3.event.target), event_ = event.of(target, arguments), g = d3.select(target), resizing = eventTarget.datum(), resizingX = !/^(n|s)$/.test(resizing) && x, resizingY = !/^(e|w)$/.test(resizing) && y, dragging = eventTarget.classed(\"extent\"), dragRestore = d3_event_dragSuppress(), center, origin = d3.mouse(target), offset;\n      var w = d3.select(d3_window).on(\"keydown.brush\", keydown).on(\"keyup.brush\", keyup);\n      if (d3.event.changedTouches) {\n        w.on(\"touchmove.brush\", brushmove).on(\"touchend.brush\", brushend);\n      } else {\n        w.on(\"mousemove.brush\", brushmove).on(\"mouseup.brush\", brushend);\n      }\n      g.interrupt().selectAll(\"*\").interrupt();\n      if (dragging) {\n        origin[0] = xExtent[0] - origin[0];\n        origin[1] = yExtent[0] - origin[1];\n      } else if (resizing) {\n        var ex = +/w$/.test(resizing), ey = +/^n/.test(resizing);\n        offset = [ xExtent[1 - ex] - origin[0], yExtent[1 - ey] - origin[1] ];\n        origin[0] = xExtent[ex];\n        origin[1] = yExtent[ey];\n      } else if (d3.event.altKey) center = origin.slice();\n      g.style(\"pointer-events\", \"none\").selectAll(\".resize\").style(\"display\", null);\n      d3.select(\"body\").style(\"cursor\", eventTarget.style(\"cursor\"));\n      event_({\n        type: \"brushstart\"\n      });\n      brushmove();\n      function keydown() {\n        if (d3.event.keyCode == 32) {\n          if (!dragging) {\n            center = null;\n            origin[0] -= xExtent[1];\n            origin[1] -= yExtent[1];\n            dragging = 2;\n          }\n          d3_eventPreventDefault();\n        }\n      }\n      function keyup() {\n        if (d3.event.keyCode == 32 && dragging == 2) {\n          origin[0] += xExtent[1];\n          origin[1] += yExtent[1];\n          dragging = 0;\n          d3_eventPreventDefault();\n        }\n      }\n      function brushmove() {\n        var point = d3.mouse(target), moved = false;\n        if (offset) {\n          point[0] += offset[0];\n          point[1] += offset[1];\n        }\n        if (!dragging) {\n          if (d3.event.altKey) {\n            if (!center) center = [ (xExtent[0] + xExtent[1]) / 2, (yExtent[0] + yExtent[1]) / 2 ];\n            origin[0] = xExtent[+(point[0] < center[0])];\n            origin[1] = yExtent[+(point[1] < center[1])];\n          } else center = null;\n        }\n        if (resizingX && move1(point, x, 0)) {\n          redrawX(g);\n          moved = true;\n        }\n        if (resizingY && move1(point, y, 1)) {\n          redrawY(g);\n          moved = true;\n        }\n        if (moved) {\n          redraw(g);\n          event_({\n            type: \"brush\",\n            mode: dragging ? \"move\" : \"resize\"\n          });\n        }\n      }\n      function move1(point, scale, i) {\n        var range = d3_scaleRange(scale), r0 = range[0], r1 = range[1], position = origin[i], extent = i ? yExtent : xExtent, size = extent[1] - extent[0], min, max;\n        if (dragging) {\n          r0 -= position;\n          r1 -= size + position;\n        }\n        min = (i ? yClamp : xClamp) ? Math.max(r0, Math.min(r1, point[i])) : point[i];\n        if (dragging) {\n          max = (min += position) + size;\n        } else {\n          if (center) position = Math.max(r0, Math.min(r1, 2 * center[i] - min));\n          if (position < min) {\n            max = min;\n            min = position;\n          } else {\n            max = position;\n          }\n        }\n        if (extent[0] != min || extent[1] != max) {\n          if (i) yExtentDomain = null; else xExtentDomain = null;\n          extent[0] = min;\n          extent[1] = max;\n          return true;\n        }\n      }\n      function brushend() {\n        brushmove();\n        g.style(\"pointer-events\", \"all\").selectAll(\".resize\").style(\"display\", brush.empty() ? \"none\" : null);\n        d3.select(\"body\").style(\"cursor\", null);\n        w.on(\"mousemove.brush\", null).on(\"mouseup.brush\", null).on(\"touchmove.brush\", null).on(\"touchend.brush\", null).on(\"keydown.brush\", null).on(\"keyup.brush\", null);\n        dragRestore();\n        event_({\n          type: \"brushend\"\n        });\n      }\n    }\n    brush.x = function(z) {\n      if (!arguments.length) return x;\n      x = z;\n      resizes = d3_svg_brushResizes[!x << 1 | !y];\n      return brush;\n    };\n    brush.y = function(z) {\n      if (!arguments.length) return y;\n      y = z;\n      resizes = d3_svg_brushResizes[!x << 1 | !y];\n      return brush;\n    };\n    brush.clamp = function(z) {\n      if (!arguments.length) return x && y ? [ xClamp, yClamp ] : x ? xClamp : y ? yClamp : null;\n      if (x && y) xClamp = !!z[0], yClamp = !!z[1]; else if (x) xClamp = !!z; else if (y) yClamp = !!z;\n      return brush;\n    };\n    brush.extent = function(z) {\n      var x0, x1, y0, y1, t;\n      if (!arguments.length) {\n        if (x) {\n          if (xExtentDomain) {\n            x0 = xExtentDomain[0], x1 = xExtentDomain[1];\n          } else {\n            x0 = xExtent[0], x1 = xExtent[1];\n            if (x.invert) x0 = x.invert(x0), x1 = x.invert(x1);\n            if (x1 < x0) t = x0, x0 = x1, x1 = t;\n          }\n        }\n        if (y) {\n          if (yExtentDomain) {\n            y0 = yExtentDomain[0], y1 = yExtentDomain[1];\n          } else {\n            y0 = yExtent[0], y1 = yExtent[1];\n            if (y.invert) y0 = y.invert(y0), y1 = y.invert(y1);\n            if (y1 < y0) t = y0, y0 = y1, y1 = t;\n          }\n        }\n        return x && y ? [ [ x0, y0 ], [ x1, y1 ] ] : x ? [ x0, x1 ] : y && [ y0, y1 ];\n      }\n      if (x) {\n        x0 = z[0], x1 = z[1];\n        if (y) x0 = x0[0], x1 = x1[0];\n        xExtentDomain = [ x0, x1 ];\n        if (x.invert) x0 = x(x0), x1 = x(x1);\n        if (x1 < x0) t = x0, x0 = x1, x1 = t;\n        if (x0 != xExtent[0] || x1 != xExtent[1]) xExtent = [ x0, x1 ];\n      }\n      if (y) {\n        y0 = z[0], y1 = z[1];\n        if (x) y0 = y0[1], y1 = y1[1];\n        yExtentDomain = [ y0, y1 ];\n        if (y.invert) y0 = y(y0), y1 = y(y1);\n        if (y1 < y0) t = y0, y0 = y1, y1 = t;\n        if (y0 != yExtent[0] || y1 != yExtent[1]) yExtent = [ y0, y1 ];\n      }\n      return brush;\n    };\n    brush.clear = function() {\n      if (!brush.empty()) {\n        xExtent = [ 0, 0 ], yExtent = [ 0, 0 ];\n        xExtentDomain = yExtentDomain = null;\n      }\n      return brush;\n    };\n    brush.empty = function() {\n      return !!x && xExtent[0] == xExtent[1] || !!y && yExtent[0] == yExtent[1];\n    };\n    return d3.rebind(brush, event, \"on\");\n  };\n  var d3_svg_brushCursor = {\n    n: \"ns-resize\",\n    e: \"ew-resize\",\n    s: \"ns-resize\",\n    w: \"ew-resize\",\n    nw: \"nwse-resize\",\n    ne: \"nesw-resize\",\n    se: \"nwse-resize\",\n    sw: \"nesw-resize\"\n  };\n  var d3_svg_brushResizes = [ [ \"n\", \"e\", \"s\", \"w\", \"nw\", \"ne\", \"se\", \"sw\" ], [ \"e\", \"w\" ], [ \"n\", \"s\" ], [] ];\n  var d3_time_format = d3_time.format = d3_locale_enUS.timeFormat;\n  var d3_time_formatUtc = d3_time_format.utc;\n  var d3_time_formatIso = d3_time_formatUtc(\"%Y-%m-%dT%H:%M:%S.%LZ\");\n  d3_time_format.iso = Date.prototype.toISOString && +new Date(\"2000-01-01T00:00:00.000Z\") ? d3_time_formatIsoNative : d3_time_formatIso;\n  function d3_time_formatIsoNative(date) {\n    return date.toISOString();\n  }\n  d3_time_formatIsoNative.parse = function(string) {\n    var date = new Date(string);\n    return isNaN(date) ? null : date;\n  };\n  d3_time_formatIsoNative.toString = d3_time_formatIso.toString;\n  d3_time.second = d3_time_interval(function(date) {\n    return new d3_date(Math.floor(date / 1e3) * 1e3);\n  }, function(date, offset) {\n    date.setTime(date.getTime() + Math.floor(offset) * 1e3);\n  }, function(date) {\n    return date.getSeconds();\n  });\n  d3_time.seconds = d3_time.second.range;\n  d3_time.seconds.utc = d3_time.second.utc.range;\n  d3_time.minute = d3_time_interval(function(date) {\n    return new d3_date(Math.floor(date / 6e4) * 6e4);\n  }, function(date, offset) {\n    date.setTime(date.getTime() + Math.floor(offset) * 6e4);\n  }, function(date) {\n    return date.getMinutes();\n  });\n  d3_time.minutes = d3_time.minute.range;\n  d3_time.minutes.utc = d3_time.minute.utc.range;\n  d3_time.hour = d3_time_interval(function(date) {\n    var timezone = date.getTimezoneOffset() / 60;\n    return new d3_date((Math.floor(date / 36e5 - timezone) + timezone) * 36e5);\n  }, function(date, offset) {\n    date.setTime(date.getTime() + Math.floor(offset) * 36e5);\n  }, function(date) {\n    return date.getHours();\n  });\n  d3_time.hours = d3_time.hour.range;\n  d3_time.hours.utc = d3_time.hour.utc.range;\n  d3_time.month = d3_time_interval(function(date) {\n    date = d3_time.day(date);\n    date.setDate(1);\n    return date;\n  }, function(date, offset) {\n    date.setMonth(date.getMonth() + offset);\n  }, function(date) {\n    return date.getMonth();\n  });\n  d3_time.months = d3_time.month.range;\n  d3_time.months.utc = d3_time.month.utc.range;\n  function d3_time_scale(linear, methods, format) {\n    function scale(x) {\n      return linear(x);\n    }\n    scale.invert = function(x) {\n      return d3_time_scaleDate(linear.invert(x));\n    };\n    scale.domain = function(x) {\n      if (!arguments.length) return linear.domain().map(d3_time_scaleDate);\n      linear.domain(x);\n      return scale;\n    };\n    function tickMethod(extent, count) {\n      var span = extent[1] - extent[0], target = span / count, i = d3.bisect(d3_time_scaleSteps, target);\n      return i == d3_time_scaleSteps.length ? [ methods.year, d3_scale_linearTickRange(extent.map(function(d) {\n        return d / 31536e6;\n      }), count)[2] ] : !i ? [ d3_time_scaleMilliseconds, d3_scale_linearTickRange(extent, count)[2] ] : methods[target / d3_time_scaleSteps[i - 1] < d3_time_scaleSteps[i] / target ? i - 1 : i];\n    }\n    scale.nice = function(interval, skip) {\n      var domain = scale.domain(), extent = d3_scaleExtent(domain), method = interval == null ? tickMethod(extent, 10) : typeof interval === \"number\" && tickMethod(extent, interval);\n      if (method) interval = method[0], skip = method[1];\n      function skipped(date) {\n        return !isNaN(date) && !interval.range(date, d3_time_scaleDate(+date + 1), skip).length;\n      }\n      return scale.domain(d3_scale_nice(domain, skip > 1 ? {\n        floor: function(date) {\n          while (skipped(date = interval.floor(date))) date = d3_time_scaleDate(date - 1);\n          return date;\n        },\n        ceil: function(date) {\n          while (skipped(date = interval.ceil(date))) date = d3_time_scaleDate(+date + 1);\n          return date;\n        }\n      } : interval));\n    };\n    scale.ticks = function(interval, skip) {\n      var extent = d3_scaleExtent(scale.domain()), method = interval == null ? tickMethod(extent, 10) : typeof interval === \"number\" ? tickMethod(extent, interval) : !interval.range && [ {\n        range: interval\n      }, skip ];\n      if (method) interval = method[0], skip = method[1];\n      return interval.range(extent[0], d3_time_scaleDate(+extent[1] + 1), skip < 1 ? 1 : skip);\n    };\n    scale.tickFormat = function() {\n      return format;\n    };\n    scale.copy = function() {\n      return d3_time_scale(linear.copy(), methods, format);\n    };\n    return d3_scale_linearRebind(scale, linear);\n  }\n  function d3_time_scaleDate(t) {\n    return new Date(t);\n  }\n  var d3_time_scaleSteps = [ 1e3, 5e3, 15e3, 3e4, 6e4, 3e5, 9e5, 18e5, 36e5, 108e5, 216e5, 432e5, 864e5, 1728e5, 6048e5, 2592e6, 7776e6, 31536e6 ];\n  var d3_time_scaleLocalMethods = [ [ d3_time.second, 1 ], [ d3_time.second, 5 ], [ d3_time.second, 15 ], [ d3_time.second, 30 ], [ d3_time.minute, 1 ], [ d3_time.minute, 5 ], [ d3_time.minute, 15 ], [ d3_time.minute, 30 ], [ d3_time.hour, 1 ], [ d3_time.hour, 3 ], [ d3_time.hour, 6 ], [ d3_time.hour, 12 ], [ d3_time.day, 1 ], [ d3_time.day, 2 ], [ d3_time.week, 1 ], [ d3_time.month, 1 ], [ d3_time.month, 3 ], [ d3_time.year, 1 ] ];\n  var d3_time_scaleLocalFormat = d3_time_format.multi([ [ \".%L\", function(d) {\n    return d.getMilliseconds();\n  } ], [ \":%S\", function(d) {\n    return d.getSeconds();\n  } ], [ \"%I:%M\", function(d) {\n    return d.getMinutes();\n  } ], [ \"%I %p\", function(d) {\n    return d.getHours();\n  } ], [ \"%a %d\", function(d) {\n    return d.getDay() && d.getDate() != 1;\n  } ], [ \"%b %d\", function(d) {\n    return d.getDate() != 1;\n  } ], [ \"%B\", function(d) {\n    return d.getMonth();\n  } ], [ \"%Y\", d3_true ] ]);\n  var d3_time_scaleMilliseconds = {\n    range: function(start, stop, step) {\n      return d3.range(Math.ceil(start / step) * step, +stop, step).map(d3_time_scaleDate);\n    },\n    floor: d3_identity,\n    ceil: d3_identity\n  };\n  d3_time_scaleLocalMethods.year = d3_time.year;\n  d3_time.scale = function() {\n    return d3_time_scale(d3.scale.linear(), d3_time_scaleLocalMethods, d3_time_scaleLocalFormat);\n  };\n  var d3_time_scaleUtcMethods = d3_time_scaleLocalMethods.map(function(m) {\n    return [ m[0].utc, m[1] ];\n  });\n  var d3_time_scaleUtcFormat = d3_time_formatUtc.multi([ [ \".%L\", function(d) {\n    return d.getUTCMilliseconds();\n  } ], [ \":%S\", function(d) {\n    return d.getUTCSeconds();\n  } ], [ \"%I:%M\", function(d) {\n    return d.getUTCMinutes();\n  } ], [ \"%I %p\", function(d) {\n    return d.getUTCHours();\n  } ], [ \"%a %d\", function(d) {\n    return d.getUTCDay() && d.getUTCDate() != 1;\n  } ], [ \"%b %d\", function(d) {\n    return d.getUTCDate() != 1;\n  } ], [ \"%B\", function(d) {\n    return d.getUTCMonth();\n  } ], [ \"%Y\", d3_true ] ]);\n  d3_time_scaleUtcMethods.year = d3_time.year.utc;\n  d3_time.scale.utc = function() {\n    return d3_time_scale(d3.scale.linear(), d3_time_scaleUtcMethods, d3_time_scaleUtcFormat);\n  };\n  d3.text = d3_xhrType(function(request) {\n    return request.responseText;\n  });\n  d3.json = function(url, callback) {\n    return d3_xhr(url, \"application/json\", d3_json, callback);\n  };\n  function d3_json(request) {\n    return JSON.parse(request.responseText);\n  }\n  d3.html = function(url, callback) {\n    return d3_xhr(url, \"text/html\", d3_html, callback);\n  };\n  function d3_html(request) {\n    var range = d3_document.createRange();\n    range.selectNode(d3_document.body);\n    return range.createContextualFragment(request.responseText);\n  }\n  d3.xml = d3_xhrType(function(request) {\n    return request.responseXML;\n  });\n  if (typeof define === \"function\" && define.amd) {\n    define(d3);\n  } else if (typeof module === \"object\" && module.exports) {\n    module.exports = d3;\n  } else {\n    this.d3 = d3;\n  }\n}();"
  },
  {
    "path": "shared-data/contrib/forcegrapher/d3.js",
    "content": "!function(){function n(n,t){return t>n?-1:n>t?1:n>=t?0:0/0}function t(n){return null!=n&&!isNaN(n)}function e(n){return{left:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n(t[i],e)<0?r=i+1:u=i}return r},right:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n(t[i],e)>0?u=i:r=i+1}return r}}}function r(n){return n.length}function u(n){for(var t=1;n*t%1;)t*=10;return t}function i(n,t){try{for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}catch(r){n.prototype=t}}function o(){}function a(n){return ha+n in this}function c(n){return n=ha+n,n in this&&delete this[n]}function s(){var n=[];return this.forEach(function(t){n.push(t)}),n}function l(){var n=0;for(var t in this)t.charCodeAt(0)===ga&&++n;return n}function f(){for(var n in this)if(n.charCodeAt(0)===ga)return!1;return!0}function h(){}function g(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function p(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.substring(1);for(var e=0,r=pa.length;r>e;++e){var u=pa[e]+t;if(u in n)return u}}function v(){}function d(){}function m(n){function t(){for(var t,r=e,u=-1,i=r.length;++u<i;)(t=r[u].on)&&t.apply(this,arguments);return n}var e=[],r=new o;return t.on=function(t,u){var i,o=r.get(t);return arguments.length<2?o&&o.on:(o&&(o.on=null,e=e.slice(0,i=e.indexOf(o)).concat(e.slice(i+1)),r.remove(t)),u&&e.push(r.set(t,{on:u})),n)},t}function y(){Go.event.preventDefault()}function x(){for(var n,t=Go.event;n=t.sourceEvent;)t=n;return t}function M(n){for(var t=new d,e=0,r=arguments.length;++e<r;)t[arguments[e]]=m(t);return t.of=function(e,r){return function(u){try{var i=u.sourceEvent=Go.event;u.target=n,Go.event=u,t[u.type].apply(e,r)}finally{Go.event=i}}},t}function _(n){return da(n,_a),n}function b(n){return\"function\"==typeof n?n:function(){return ma(n,this)}}function w(n){return\"function\"==typeof n?n:function(){return ya(n,this)}}function S(n,t){function e(){this.removeAttribute(n)}function r(){this.removeAttributeNS(n.space,n.local)}function u(){this.setAttribute(n,t)}function i(){this.setAttributeNS(n.space,n.local,t)}function o(){var e=t.apply(this,arguments);null==e?this.removeAttribute(n):this.setAttribute(n,e)}function a(){var e=t.apply(this,arguments);null==e?this.removeAttributeNS(n.space,n.local):this.setAttributeNS(n.space,n.local,e)}return n=Go.ns.qualify(n),null==t?n.local?r:e:\"function\"==typeof t?n.local?a:o:n.local?i:u}function k(n){return n.trim().replace(/\\s+/g,\" \")}function E(n){return new RegExp(\"(?:^|\\\\s+)\"+Go.requote(n)+\"(?:\\\\s+|$)\",\"g\")}function A(n){return n.trim().split(/^|\\s+/)}function C(n,t){function e(){for(var e=-1;++e<u;)n[e](this,t)}function r(){for(var e=-1,r=t.apply(this,arguments);++e<u;)n[e](this,r)}n=A(n).map(N);var u=n.length;return\"function\"==typeof t?r:e}function N(n){var t=E(n);return function(e,r){if(u=e.classList)return r?u.add(n):u.remove(n);var u=e.getAttribute(\"class\")||\"\";r?(t.lastIndex=0,t.test(u)||e.setAttribute(\"class\",k(u+\" \"+n))):e.setAttribute(\"class\",k(u.replace(t,\" \")))}}function L(n,t,e){function r(){this.style.removeProperty(n)}function u(){this.style.setProperty(n,t,e)}function i(){var r=t.apply(this,arguments);null==r?this.style.removeProperty(n):this.style.setProperty(n,r,e)}return null==t?r:\"function\"==typeof t?i:u}function T(n,t){function e(){delete this[n]}function r(){this[n]=t}function u(){var e=t.apply(this,arguments);null==e?delete this[n]:this[n]=e}return null==t?e:\"function\"==typeof t?u:r}function q(n){return\"function\"==typeof n?n:(n=Go.ns.qualify(n)).local?function(){return this.ownerDocument.createElementNS(n.space,n.local)}:function(){return this.ownerDocument.createElementNS(this.namespaceURI,n)}}function z(n){return{__data__:n}}function R(n){return function(){return Ma(this,n)}}function D(t){return arguments.length||(t=n),function(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}}function P(n,t){for(var e=0,r=n.length;r>e;e++)for(var u,i=n[e],o=0,a=i.length;a>o;o++)(u=i[o])&&t(u,o,e);return n}function U(n){return da(n,wa),n}function j(n){var t,e;return function(r,u,i){var o,a=n[i].update,c=a.length;for(i!=e&&(e=i,t=0),u>=t&&(t=u+1);!(o=a[t])&&++t<c;);return o}}function H(){var n=this.__transition__;n&&++n.active}function F(n,t,e){function r(){var t=this[o];t&&(this.removeEventListener(n,t,t.$),delete this[o])}function u(){var u=c(t,Qo(arguments));r.call(this),this.addEventListener(n,this[o]=u,u.$=e),u._=t}function i(){var t,e=new RegExp(\"^__on([^.]+)\"+Go.requote(n)+\"$\");for(var r in this)if(t=r.match(e)){var u=this[r];this.removeEventListener(t[1],u,u.$),delete this[r]}}var o=\"__on\"+n,a=n.indexOf(\".\"),c=O;a>0&&(n=n.substring(0,a));var s=ka.get(n);return s&&(n=s,c=I),a?t?u:r:t?v:i}function O(n,t){return function(e){var r=Go.event;Go.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{Go.event=r}}}function I(n,t){var e=O(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function Y(){var n=\".dragsuppress-\"+ ++Aa,t=\"click\"+n,e=Go.select(ea).on(\"touchmove\"+n,y).on(\"dragstart\"+n,y).on(\"selectstart\"+n,y);if(Ea){var r=ta.style,u=r[Ea];r[Ea]=\"none\"}return function(i){function o(){e.on(t,null)}e.on(n,null),Ea&&(r[Ea]=u),i&&(e.on(t,function(){y(),o()},!0),setTimeout(o,0))}}function Z(n,t){t.changedTouches&&(t=t.changedTouches[0]);var e=n.ownerSVGElement||n;if(e.createSVGPoint){var r=e.createSVGPoint();return r.x=t.clientX,r.y=t.clientY,r=r.matrixTransform(n.getScreenCTM().inverse()),[r.x,r.y]}var u=n.getBoundingClientRect();return[t.clientX-u.left-n.clientLeft,t.clientY-u.top-n.clientTop]}function V(){return Go.event.changedTouches[0].identifier}function $(){return Go.event.target}function X(){return ea}function B(n){return n>0?1:0>n?-1:0}function J(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function W(n){return n>1?0:-1>n?Ca:Math.acos(n)}function G(n){return n>1?La:-1>n?-La:Math.asin(n)}function K(n){return((n=Math.exp(n))-1/n)/2}function Q(n){return((n=Math.exp(n))+1/n)/2}function nt(n){return((n=Math.exp(2*n))-1)/(n+1)}function tt(n){return(n=Math.sin(n/2))*n}function et(){}function rt(n,t,e){return new ut(n,t,e)}function ut(n,t,e){this.h=n,this.s=t,this.l=e}function it(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?i+(o-i)*n/60:180>n?o:240>n?i+(o-i)*(240-n)/60:i}function u(n){return Math.round(255*r(n))}var i,o;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,o=.5>=e?e*(1+t):e+t-e*t,i=2*e-o,yt(u(n+120),u(n),u(n-120))}function ot(n,t,e){return new at(n,t,e)}function at(n,t,e){this.h=n,this.c=t,this.l=e}function ct(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),st(e,Math.cos(n*=za)*t,Math.sin(n)*t)}function st(n,t,e){return new lt(n,t,e)}function lt(n,t,e){this.l=n,this.a=t,this.b=e}function ft(n,t,e){var r=(n+16)/116,u=r+t/500,i=r-e/200;return u=gt(u)*Za,r=gt(r)*Va,i=gt(i)*$a,yt(vt(3.2404542*u-1.5371385*r-.4985314*i),vt(-.969266*u+1.8760108*r+.041556*i),vt(.0556434*u-.2040259*r+1.0572252*i))}function ht(n,t,e){return n>0?ot(Math.atan2(e,t)*Ra,Math.sqrt(t*t+e*e),n):ot(0/0,0/0,n)}function gt(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function pt(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function vt(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function dt(n){return yt(n>>16,255&n>>8,255&n)}function mt(n){return dt(n)+\"\"}function yt(n,t,e){return new xt(n,t,e)}function xt(n,t,e){this.r=n,this.g=t,this.b=e}function Mt(n){return 16>n?\"0\"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function _t(n,t,e){var r,u,i,o=0,a=0,c=0;if(r=/([a-z]+)\\((.*)\\)/i.exec(n))switch(u=r[2].split(\",\"),r[1]){case\"hsl\":return e(parseFloat(u[0]),parseFloat(u[1])/100,parseFloat(u[2])/100);case\"rgb\":return t(kt(u[0]),kt(u[1]),kt(u[2]))}return(i=Ja.get(n))?t(i.r,i.g,i.b):(null==n||\"#\"!==n.charAt(0)||isNaN(i=parseInt(n.substring(1),16))||(4===n.length?(o=(3840&i)>>4,o=o>>4|o,a=240&i,a=a>>4|a,c=15&i,c=c<<4|c):7===n.length&&(o=(16711680&i)>>16,a=(65280&i)>>8,c=255&i)),t(o,a,c))}function bt(n,t,e){var r,u,i=Math.min(n/=255,t/=255,e/=255),o=Math.max(n,t,e),a=o-i,c=(o+i)/2;return a?(u=.5>c?a/(o+i):a/(2-o-i),r=n==o?(t-e)/a+(e>t?6:0):t==o?(e-n)/a+2:(n-t)/a+4,r*=60):(r=0/0,u=c>0&&1>c?0:r),rt(r,u,c)}function wt(n,t,e){n=St(n),t=St(t),e=St(e);var r=pt((.4124564*n+.3575761*t+.1804375*e)/Za),u=pt((.2126729*n+.7151522*t+.072175*e)/Va),i=pt((.0193339*n+.119192*t+.9503041*e)/$a);return st(116*u-16,500*(r-u),200*(u-i))}function St(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function kt(n){var t=parseFloat(n);return\"%\"===n.charAt(n.length-1)?Math.round(2.55*t):t}function Et(n){return\"function\"==typeof n?n:function(){return n}}function At(n){return n}function Ct(n){return function(t,e,r){return 2===arguments.length&&\"function\"==typeof e&&(r=e,e=null),Nt(t,e,n,r)}}function Nt(n,t,e,r){function u(){var n,t=c.status;if(!t&&c.responseText||t>=200&&300>t||304===t){try{n=e.call(i,c)}catch(r){return o.error.call(i,r),void 0}o.load.call(i,n)}else o.error.call(i,c)}var i={},o=Go.dispatch(\"beforesend\",\"progress\",\"load\",\"error\"),a={},c=new XMLHttpRequest,s=null;return!ea.XDomainRequest||\"withCredentials\"in c||!/^(http(s)?:)?\\/\\//.test(n)||(c=new XDomainRequest),\"onload\"in c?c.onload=c.onerror=u:c.onreadystatechange=function(){c.readyState>3&&u()},c.onprogress=function(n){var t=Go.event;Go.event=n;try{o.progress.call(i,c)}finally{Go.event=t}},i.header=function(n,t){return n=(n+\"\").toLowerCase(),arguments.length<2?a[n]:(null==t?delete a[n]:a[n]=t+\"\",i)},i.mimeType=function(n){return arguments.length?(t=null==n?null:n+\"\",i):t},i.responseType=function(n){return arguments.length?(s=n,i):s},i.response=function(n){return e=n,i},[\"get\",\"post\"].forEach(function(n){i[n]=function(){return i.send.apply(i,[n].concat(Qo(arguments)))}}),i.send=function(e,r,u){if(2===arguments.length&&\"function\"==typeof r&&(u=r,r=null),c.open(e,n,!0),null==t||\"accept\"in a||(a.accept=t+\",*/*\"),c.setRequestHeader)for(var l in a)c.setRequestHeader(l,a[l]);return null!=t&&c.overrideMimeType&&c.overrideMimeType(t),null!=s&&(c.responseType=s),null!=u&&i.on(\"error\",u).on(\"load\",function(n){u(null,n)}),o.beforesend.call(i,c),c.send(null==r?null:r),i},i.abort=function(){return c.abort(),i},Go.rebind(i,o,\"on\"),null==r?i:i.get(Lt(r))}function Lt(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function Tt(){var n=qt(),t=zt()-n;t>24?(isFinite(t)&&(clearTimeout(Qa),Qa=setTimeout(Tt,t)),Ka=0):(Ka=1,tc(Tt))}function qt(){var n=Date.now();for(nc=Wa;nc;)n>=nc.t&&(nc.f=nc.c(n-nc.t)),nc=nc.n;return n}function zt(){for(var n,t=Wa,e=1/0;t;)t.f?t=n?n.n=t.n:Wa=t.n:(t.t<e&&(e=t.t),t=(n=t).n);return Ga=n,e}function Rt(n,t){return t-(n?Math.ceil(Math.log(n)/Math.LN10):1)}function Dt(n,t){var e=Math.pow(10,3*fa(8-t));return{scale:t>8?function(n){return n/e}:function(n){return n*e},symbol:n}}function Pt(n){var t=n.decimal,e=n.thousands,r=n.grouping,u=n.currency,i=r?function(n){for(var t=n.length,u=[],i=0,o=r[0];t>0&&o>0;)u.push(n.substring(t-=o,t+o)),o=r[i=(i+1)%r.length];return u.reverse().join(e)}:At;return function(n){var e=rc.exec(n),r=e[1]||\" \",o=e[2]||\">\",a=e[3]||\"\",c=e[4]||\"\",s=e[5],l=+e[6],f=e[7],h=e[8],g=e[9],p=1,v=\"\",d=\"\",m=!1;switch(h&&(h=+h.substring(1)),(s||\"0\"===r&&\"=\"===o)&&(s=r=\"0\",o=\"=\",f&&(l-=Math.floor((l-1)/4))),g){case\"n\":f=!0,g=\"g\";break;case\"%\":p=100,d=\"%\",g=\"f\";break;case\"p\":p=100,d=\"%\",g=\"r\";break;case\"b\":case\"o\":case\"x\":case\"X\":\"#\"===c&&(v=\"0\"+g.toLowerCase());case\"c\":case\"d\":m=!0,h=0;break;case\"s\":p=-1,g=\"r\"}\"$\"===c&&(v=u[0],d=u[1]),\"r\"!=g||h||(g=\"g\"),null!=h&&(\"g\"==g?h=Math.max(1,Math.min(21,h)):(\"e\"==g||\"f\"==g)&&(h=Math.max(0,Math.min(20,h)))),g=uc.get(g)||Ut;var y=s&&f;return function(n){var e=d;if(m&&n%1)return\"\";var u=0>n||0===n&&0>1/n?(n=-n,\"-\"):a;if(0>p){var c=Go.formatPrefix(n,h);n=c.scale(n),e=c.symbol+d}else n*=p;n=g(n,h);var x=n.lastIndexOf(\".\"),M=0>x?n:n.substring(0,x),_=0>x?\"\":t+n.substring(x+1);!s&&f&&(M=i(M));var b=v.length+M.length+_.length+(y?0:u.length),w=l>b?new Array(b=l-b+1).join(r):\"\";return y&&(M=i(w+M)),u+=v,n=M+_,(\"<\"===o?u+n+w:\">\"===o?w+u+n:\"^\"===o?w.substring(0,b>>=1)+u+n+w.substring(b):u+(y?n:w+n))+e}}}function Ut(n){return n+\"\"}function jt(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function Ht(n,t,e){function r(t){var e=n(t),r=i(e,1);return r-t>t-e?e:r}function u(e){return t(e=n(new oc(e-1)),1),e}function i(n,e){return t(n=new oc(+n),e),n}function o(n,r,i){var o=u(n),a=[];if(i>1)for(;r>o;)e(o)%i||a.push(new Date(+o)),t(o,1);else for(;r>o;)a.push(new Date(+o)),t(o,1);return a}function a(n,t,e){try{oc=jt;var r=new jt;return r._=n,o(r,t,e)}finally{oc=Date}}n.floor=n,n.round=r,n.ceil=u,n.offset=i,n.range=o;var c=n.utc=Ft(n);return c.floor=c,c.round=Ft(r),c.ceil=Ft(u),c.offset=Ft(i),c.range=a,n}function Ft(n){return function(t,e){try{oc=jt;var r=new jt;return r._=t,n(r,e)._}finally{oc=Date}}}function Ot(n){function t(n){function t(t){for(var e,u,i,o=[],a=-1,c=0;++a<r;)37===n.charCodeAt(a)&&(o.push(n.substring(c,a)),null!=(u=cc[e=n.charAt(++a)])&&(e=n.charAt(++a)),(i=C[e])&&(e=i(t,null==u?\"e\"===e?\" \":\"0\":u)),o.push(e),c=a+1);return o.push(n.substring(c,a)),o.join(\"\")}var r=n.length;return t.parse=function(t){var r={y:1900,m:0,d:1,H:0,M:0,S:0,L:0,Z:null},u=e(r,n,t,0);if(u!=t.length)return null;\"p\"in r&&(r.H=r.H%12+12*r.p);var i=null!=r.Z&&oc!==jt,o=new(i?jt:oc);return\"j\"in r?o.setFullYear(r.y,0,r.j):\"w\"in r&&(\"W\"in r||\"U\"in r)?(o.setFullYear(r.y,0,1),o.setFullYear(r.y,0,\"W\"in r?(r.w+6)%7+7*r.W-(o.getDay()+5)%7:r.w+7*r.U-(o.getDay()+6)%7)):o.setFullYear(r.y,r.m,r.d),o.setHours(r.H+Math.floor(r.Z/100),r.M+r.Z%100,r.S,r.L),i?o._:o},t.toString=function(){return n},t}function e(n,t,e,r){for(var u,i,o,a=0,c=t.length,s=e.length;c>a;){if(r>=s)return-1;if(u=t.charCodeAt(a++),37===u){if(o=t.charAt(a++),i=N[o in cc?t.charAt(a++):o],!i||(r=i(n,e,r))<0)return-1}else if(u!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){b.lastIndex=0;var r=b.exec(t.substring(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){M.lastIndex=0;var r=M.exec(t.substring(e));return r?(n.w=_.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){E.lastIndex=0;var r=E.exec(t.substring(e));return r?(n.m=A.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,e){S.lastIndex=0;var r=S.exec(t.substring(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,r){return e(n,C.c.toString(),t,r)}function c(n,t,r){return e(n,C.x.toString(),t,r)}function s(n,t,r){return e(n,C.X.toString(),t,r)}function l(n,t,e){var r=x.get(t.substring(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var f=n.dateTime,h=n.date,g=n.time,p=n.periods,v=n.days,d=n.shortDays,m=n.months,y=n.shortMonths;t.utc=function(n){function e(n){try{oc=jt;var t=new oc;return t._=n,r(t)}finally{oc=Date}}var r=t(n);return e.parse=function(n){try{oc=jt;var t=r.parse(n);return t&&t._}finally{oc=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ae;var x=Go.map(),M=Yt(v),_=Zt(v),b=Yt(d),w=Zt(d),S=Yt(m),k=Zt(m),E=Yt(y),A=Zt(y);p.forEach(function(n,t){x.set(n.toLowerCase(),t)});var C={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return y[n.getMonth()]},B:function(n){return m[n.getMonth()]},c:t(f),d:function(n,t){return It(n.getDate(),t,2)},e:function(n,t){return It(n.getDate(),t,2)},H:function(n,t){return It(n.getHours(),t,2)},I:function(n,t){return It(n.getHours()%12||12,t,2)},j:function(n,t){return It(1+ic.dayOfYear(n),t,3)},L:function(n,t){return It(n.getMilliseconds(),t,3)},m:function(n,t){return It(n.getMonth()+1,t,2)},M:function(n,t){return It(n.getMinutes(),t,2)},p:function(n){return p[+(n.getHours()>=12)]},S:function(n,t){return It(n.getSeconds(),t,2)},U:function(n,t){return It(ic.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return It(ic.mondayOfYear(n),t,2)},x:t(h),X:t(g),y:function(n,t){return It(n.getFullYear()%100,t,2)},Y:function(n,t){return It(n.getFullYear()%1e4,t,4)},Z:ie,\"%\":function(){return\"%\"}},N={a:r,A:u,b:i,B:o,c:a,d:Qt,e:Qt,H:te,I:te,j:ne,L:ue,m:Kt,M:ee,p:l,S:re,U:$t,w:Vt,W:Xt,x:c,X:s,y:Jt,Y:Bt,Z:Wt,\"%\":oe};return t}function It(n,t,e){var r=0>n?\"-\":\"\",u=(r?-n:n)+\"\",i=u.length;return r+(e>i?new Array(e-i+1).join(t)+u:u)}function Yt(n){return new RegExp(\"^(?:\"+n.map(Go.requote).join(\"|\")+\")\",\"i\")}function Zt(n){for(var t=new o,e=-1,r=n.length;++e<r;)t.set(n[e].toLowerCase(),e);return t}function Vt(n,t,e){sc.lastIndex=0;var r=sc.exec(t.substring(e,e+1));return r?(n.w=+r[0],e+r[0].length):-1}function $t(n,t,e){sc.lastIndex=0;var r=sc.exec(t.substring(e));return r?(n.U=+r[0],e+r[0].length):-1}function Xt(n,t,e){sc.lastIndex=0;var r=sc.exec(t.substring(e));return r?(n.W=+r[0],e+r[0].length):-1}function Bt(n,t,e){sc.lastIndex=0;var r=sc.exec(t.substring(e,e+4));return r?(n.y=+r[0],e+r[0].length):-1}function Jt(n,t,e){sc.lastIndex=0;var r=sc.exec(t.substring(e,e+2));return r?(n.y=Gt(+r[0]),e+r[0].length):-1}function Wt(n,t,e){return/^[+-]\\d{4}$/.test(t=t.substring(e,e+5))?(n.Z=-t,e+5):-1}function Gt(n){return n+(n>68?1900:2e3)}function Kt(n,t,e){sc.lastIndex=0;var r=sc.exec(t.substring(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function Qt(n,t,e){sc.lastIndex=0;var r=sc.exec(t.substring(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function ne(n,t,e){sc.lastIndex=0;var r=sc.exec(t.substring(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function te(n,t,e){sc.lastIndex=0;var r=sc.exec(t.substring(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function ee(n,t,e){sc.lastIndex=0;var r=sc.exec(t.substring(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function re(n,t,e){sc.lastIndex=0;var r=sc.exec(t.substring(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function ue(n,t,e){sc.lastIndex=0;var r=sc.exec(t.substring(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function ie(n){var t=n.getTimezoneOffset(),e=t>0?\"-\":\"+\",r=~~(fa(t)/60),u=fa(t)%60;return e+It(r,\"0\",2)+It(u,\"0\",2)}function oe(n,t,e){lc.lastIndex=0;var r=lc.exec(t.substring(e,e+1));return r?e+r[0].length:-1}function ae(n){for(var t=n.length,e=-1;++e<t;)n[e][0]=this(n[e][0]);return function(t){for(var e=0,r=n[e];!r[1](t);)r=n[++e];return r[0](t)}}function ce(){}function se(n,t,e){var r=e.s=n+t,u=r-n,i=r-u;e.t=n-i+(t-u)}function le(n,t){n&&pc.hasOwnProperty(n.type)&&pc[n.type](n,t)}function fe(n,t,e){var r,u=-1,i=n.length-e;for(t.lineStart();++u<i;)r=n[u],t.point(r[0],r[1],r[2]);t.lineEnd()}function he(n,t){var e=-1,r=n.length;for(t.polygonStart();++e<r;)fe(n[e],t,1);t.polygonEnd()}function ge(){function n(n,t){n*=za,t=t*za/2+Ca/4;var e=n-r,o=e>=0?1:-1,a=o*e,c=Math.cos(t),s=Math.sin(t),l=i*s,f=u*c+l*Math.cos(a),h=l*o*Math.sin(a);dc.add(Math.atan2(h,f)),r=n,u=c,i=s}var t,e,r,u,i;mc.point=function(o,a){mc.point=n,r=(t=o)*za,u=Math.cos(a=(e=a)*za/2+Ca/4),i=Math.sin(a)},mc.lineEnd=function(){n(t,e)}}function pe(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function ve(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function de(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function me(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function ye(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function xe(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function Me(n){return[Math.atan2(n[1],n[0]),G(n[2])]}function _e(n,t){return fa(n[0]-t[0])<Ta&&fa(n[1]-t[1])<Ta}function be(n,t){n*=za;var e=Math.cos(t*=za);we(e*Math.cos(n),e*Math.sin(n),Math.sin(t))}function we(n,t,e){++yc,Mc+=(n-Mc)/yc,_c+=(t-_c)/yc,bc+=(e-bc)/yc}function Se(){function n(n,u){n*=za;var i=Math.cos(u*=za),o=i*Math.cos(n),a=i*Math.sin(n),c=Math.sin(u),s=Math.atan2(Math.sqrt((s=e*c-r*a)*s+(s=r*o-t*c)*s+(s=t*a-e*o)*s),t*o+e*a+r*c);xc+=s,wc+=s*(t+(t=o)),Sc+=s*(e+(e=a)),kc+=s*(r+(r=c)),we(t,e,r)}var t,e,r;Nc.point=function(u,i){u*=za;var o=Math.cos(i*=za);t=o*Math.cos(u),e=o*Math.sin(u),r=Math.sin(i),Nc.point=n,we(t,e,r)}}function ke(){Nc.point=be}function Ee(){function n(n,t){n*=za;var e=Math.cos(t*=za),o=e*Math.cos(n),a=e*Math.sin(n),c=Math.sin(t),s=u*c-i*a,l=i*o-r*c,f=r*a-u*o,h=Math.sqrt(s*s+l*l+f*f),g=r*o+u*a+i*c,p=h&&-W(g)/h,v=Math.atan2(h,g);Ec+=p*s,Ac+=p*l,Cc+=p*f,xc+=v,wc+=v*(r+(r=o)),Sc+=v*(u+(u=a)),kc+=v*(i+(i=c)),we(r,u,i)}var t,e,r,u,i;Nc.point=function(o,a){t=o,e=a,Nc.point=n,o*=za;var c=Math.cos(a*=za);r=c*Math.cos(o),u=c*Math.sin(o),i=Math.sin(a),we(r,u,i)},Nc.lineEnd=function(){n(t,e),Nc.lineEnd=ke,Nc.point=be}}function Ae(){return!0}function Ce(n,t,e,r,u){var i=[],o=[];if(n.forEach(function(n){if(!((t=n.length-1)<=0)){var t,e=n[0],r=n[t];if(_e(e,r)){u.lineStart();for(var a=0;t>a;++a)u.point((e=n[a])[0],e[1]);return u.lineEnd(),void 0}var c=new Le(e,n,null,!0),s=new Le(e,null,c,!1);c.o=s,i.push(c),o.push(s),c=new Le(r,n,null,!1),s=new Le(r,null,c,!0),c.o=s,i.push(c),o.push(s)}}),o.sort(t),Ne(i),Ne(o),i.length){for(var a=0,c=e,s=o.length;s>a;++a)o[a].e=c=!c;for(var l,f,h=i[0];;){for(var g=h,p=!0;g.v;)if((g=g.n)===h)return;l=g.z,u.lineStart();do{if(g.v=g.o.v=!0,g.e){if(p)for(var a=0,s=l.length;s>a;++a)u.point((f=l[a])[0],f[1]);else r(g.x,g.n.x,1,u);g=g.n}else{if(p){l=g.p.z;for(var a=l.length-1;a>=0;--a)u.point((f=l[a])[0],f[1])}else r(g.x,g.p.x,-1,u);g=g.p}g=g.o,l=g.z,p=!p}while(!g.v);u.lineEnd()}}}function Ne(n){if(t=n.length){for(var t,e,r=0,u=n[0];++r<t;)u.n=e=n[r],e.p=u,u=e;u.n=e=n[0],e.p=u}}function Le(n,t,e,r){this.x=n,this.z=t,this.o=e,this.e=r,this.v=!1,this.n=this.p=null}function Te(n,t,e,r){return function(u,i){function o(t,e){var r=u(t,e);n(t=r[0],e=r[1])&&i.point(t,e)}function a(n,t){var e=u(n,t);d.point(e[0],e[1])}function c(){y.point=a,d.lineStart()}function s(){y.point=o,d.lineEnd()}function l(n,t){v.push([n,t]);var e=u(n,t);M.point(e[0],e[1])}function f(){M.lineStart(),v=[]}function h(){l(v[0][0],v[0][1]),M.lineEnd();var n,t=M.clean(),e=x.buffer(),r=e.length;if(v.pop(),p.push(v),v=null,r)if(1&t){n=e[0];var u,r=n.length-1,o=-1;if(r>0){for(_||(i.polygonStart(),_=!0),i.lineStart();++o<r;)i.point((u=n[o])[0],u[1]);i.lineEnd()}}else r>1&&2&t&&e.push(e.pop().concat(e.shift())),g.push(e.filter(qe))}var g,p,v,d=t(i),m=u.invert(r[0],r[1]),y={point:o,lineStart:c,lineEnd:s,polygonStart:function(){y.point=l,y.lineStart=f,y.lineEnd=h,g=[],p=[]},polygonEnd:function(){y.point=o,y.lineStart=c,y.lineEnd=s,g=Go.merge(g);var n=De(m,p);g.length?(_||(i.polygonStart(),_=!0),Ce(g,Re,n,e,i)):n&&(_||(i.polygonStart(),_=!0),i.lineStart(),e(null,null,1,i),i.lineEnd()),_&&(i.polygonEnd(),_=!1),g=p=null},sphere:function(){i.polygonStart(),i.lineStart(),e(null,null,1,i),i.lineEnd(),i.polygonEnd()}},x=ze(),M=t(x),_=!1;return y}}function qe(n){return n.length>1}function ze(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:v,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Re(n,t){return((n=n.x)[0]<0?n[1]-La-Ta:La-n[1])-((t=t.x)[0]<0?t[1]-La-Ta:La-t[1])}function De(n,t){var e=n[0],r=n[1],u=[Math.sin(e),-Math.cos(e),0],i=0,o=0;dc.reset();for(var a=0,c=t.length;c>a;++a){var s=t[a],l=s.length;if(l)for(var f=s[0],h=f[0],g=f[1]/2+Ca/4,p=Math.sin(g),v=Math.cos(g),d=1;;){d===l&&(d=0),n=s[d];var m=n[0],y=n[1]/2+Ca/4,x=Math.sin(y),M=Math.cos(y),_=m-h,b=_>=0?1:-1,w=b*_,S=w>Ca,k=p*x;if(dc.add(Math.atan2(k*b*Math.sin(w),v*M+k*Math.cos(w))),i+=S?_+b*Na:_,S^h>=e^m>=e){var E=de(pe(f),pe(n));xe(E);var A=de(u,E);xe(A);var C=(S^_>=0?-1:1)*G(A[2]);(r>C||r===C&&(E[0]||E[1]))&&(o+=S^_>=0?1:-1)}if(!d++)break;h=m,p=x,v=M,f=n}}return(-Ta>i||Ta>i&&0>dc)^1&o}function Pe(n){var t,e=0/0,r=0/0,u=0/0;return{lineStart:function(){n.lineStart(),t=1},point:function(i,o){var a=i>0?Ca:-Ca,c=fa(i-e);fa(c-Ca)<Ta?(n.point(e,r=(r+o)/2>0?La:-La),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(a,r),n.point(i,r),t=0):u!==a&&c>=Ca&&(fa(e-u)<Ta&&(e-=u*Ta),fa(i-a)<Ta&&(i-=a*Ta),r=Ue(e,r,i,o),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(a,r),t=0),n.point(e=i,r=o),u=a},lineEnd:function(){n.lineEnd(),e=r=0/0},clean:function(){return 2-t}}}function Ue(n,t,e,r){var u,i,o=Math.sin(n-e);return fa(o)>Ta?Math.atan((Math.sin(t)*(i=Math.cos(r))*Math.sin(e)-Math.sin(r)*(u=Math.cos(t))*Math.sin(n))/(u*i*o)):(t+r)/2}function je(n,t,e,r){var u;if(null==n)u=e*La,r.point(-Ca,u),r.point(0,u),r.point(Ca,u),r.point(Ca,0),r.point(Ca,-u),r.point(0,-u),r.point(-Ca,-u),r.point(-Ca,0),r.point(-Ca,u);else if(fa(n[0]-t[0])>Ta){var i=n[0]<t[0]?Ca:-Ca;u=e*i/2,r.point(-i,u),r.point(0,u),r.point(i,u)}else r.point(t[0],t[1])}function He(n){function t(n,t){return Math.cos(n)*Math.cos(t)>i}function e(n){var e,i,c,s,l;return{lineStart:function(){s=c=!1,l=1},point:function(f,h){var g,p=[f,h],v=t(f,h),d=o?v?0:u(f,h):v?u(f+(0>f?Ca:-Ca),h):0;if(!e&&(s=c=v)&&n.lineStart(),v!==c&&(g=r(e,p),(_e(e,g)||_e(p,g))&&(p[0]+=Ta,p[1]+=Ta,v=t(p[0],p[1]))),v!==c)l=0,v?(n.lineStart(),g=r(p,e),n.point(g[0],g[1])):(g=r(e,p),n.point(g[0],g[1]),n.lineEnd()),e=g;else if(a&&e&&o^v){var m;d&i||!(m=r(p,e,!0))||(l=0,o?(n.lineStart(),n.point(m[0][0],m[0][1]),n.point(m[1][0],m[1][1]),n.lineEnd()):(n.point(m[1][0],m[1][1]),n.lineEnd(),n.lineStart(),n.point(m[0][0],m[0][1])))}!v||e&&_e(e,p)||n.point(p[0],p[1]),e=p,c=v,i=d},lineEnd:function(){c&&n.lineEnd(),e=null},clean:function(){return l|(s&&c)<<1}}}function r(n,t,e){var r=pe(n),u=pe(t),o=[1,0,0],a=de(r,u),c=ve(a,a),s=a[0],l=c-s*s;if(!l)return!e&&n;var f=i*c/l,h=-i*s/l,g=de(o,a),p=ye(o,f),v=ye(a,h);me(p,v);var d=g,m=ve(p,d),y=ve(d,d),x=m*m-y*(ve(p,p)-1);if(!(0>x)){var M=Math.sqrt(x),_=ye(d,(-m-M)/y);if(me(_,p),_=Me(_),!e)return _;var b,w=n[0],S=t[0],k=n[1],E=t[1];w>S&&(b=w,w=S,S=b);var A=S-w,C=fa(A-Ca)<Ta,N=C||Ta>A;if(!C&&k>E&&(b=k,k=E,E=b),N?C?k+E>0^_[1]<(fa(_[0]-w)<Ta?k:E):k<=_[1]&&_[1]<=E:A>Ca^(w<=_[0]&&_[0]<=S)){var L=ye(d,(-m+M)/y);return me(L,p),[_,Me(L)]}}}function u(t,e){var r=o?n:Ca-n,u=0;return-r>t?u|=1:t>r&&(u|=2),-r>e?u|=4:e>r&&(u|=8),u}var i=Math.cos(n),o=i>0,a=fa(i)>Ta,c=gr(n,6*za);return Te(t,e,c,o?[0,-n]:[-Ca,n-Ca])}function Fe(n,t,e,r){return function(u){var i,o=u.a,a=u.b,c=o.x,s=o.y,l=a.x,f=a.y,h=0,g=1,p=l-c,v=f-s;if(i=n-c,p||!(i>0)){if(i/=p,0>p){if(h>i)return;g>i&&(g=i)}else if(p>0){if(i>g)return;i>h&&(h=i)}if(i=e-c,p||!(0>i)){if(i/=p,0>p){if(i>g)return;i>h&&(h=i)}else if(p>0){if(h>i)return;g>i&&(g=i)}if(i=t-s,v||!(i>0)){if(i/=v,0>v){if(h>i)return;g>i&&(g=i)}else if(v>0){if(i>g)return;i>h&&(h=i)}if(i=r-s,v||!(0>i)){if(i/=v,0>v){if(i>g)return;i>h&&(h=i)}else if(v>0){if(h>i)return;g>i&&(g=i)}return h>0&&(u.a={x:c+h*p,y:s+h*v}),1>g&&(u.b={x:c+g*p,y:s+g*v}),u}}}}}}function Oe(n,t,e,r){function u(r,u){return fa(r[0]-n)<Ta?u>0?0:3:fa(r[0]-e)<Ta?u>0?2:1:fa(r[1]-t)<Ta?u>0?1:0:u>0?3:2}function i(n,t){return o(n.x,t.x)}function o(n,t){var e=u(n,1),r=u(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(a){function c(n){for(var t=0,e=d.length,r=n[1],u=0;e>u;++u)for(var i,o=1,a=d[u],c=a.length,s=a[0];c>o;++o)i=a[o],s[1]<=r?i[1]>r&&J(s,i,n)>0&&++t:i[1]<=r&&J(s,i,n)<0&&--t,s=i;return 0!==t}function s(i,a,c,s){var l=0,f=0;if(null==i||(l=u(i,c))!==(f=u(a,c))||o(i,a)<0^c>0){do s.point(0===l||3===l?n:e,l>1?r:t);while((l=(l+c+4)%4)!==f)}else s.point(a[0],a[1])}function l(u,i){return u>=n&&e>=u&&i>=t&&r>=i}function f(n,t){l(n,t)&&a.point(n,t)}function h(){N.point=p,d&&d.push(m=[]),S=!0,w=!1,_=b=0/0}function g(){v&&(p(y,x),M&&w&&A.rejoin(),v.push(A.buffer())),N.point=f,w&&a.lineEnd()}function p(n,t){n=Math.max(-Tc,Math.min(Tc,n)),t=Math.max(-Tc,Math.min(Tc,t));var e=l(n,t);if(d&&m.push([n,t]),S)y=n,x=t,M=e,S=!1,e&&(a.lineStart(),a.point(n,t));else if(e&&w)a.point(n,t);else{var r={a:{x:_,y:b},b:{x:n,y:t}};C(r)?(w||(a.lineStart(),a.point(r.a.x,r.a.y)),a.point(r.b.x,r.b.y),e||a.lineEnd(),k=!1):e&&(a.lineStart(),a.point(n,t),k=!1)}_=n,b=t,w=e}var v,d,m,y,x,M,_,b,w,S,k,E=a,A=ze(),C=Fe(n,t,e,r),N={point:f,lineStart:h,lineEnd:g,polygonStart:function(){a=A,v=[],d=[],k=!0},polygonEnd:function(){a=E,v=Go.merge(v);var t=c([n,r]),e=k&&t,u=v.length;(e||u)&&(a.polygonStart(),e&&(a.lineStart(),s(null,null,1,a),a.lineEnd()),u&&Ce(v,i,t,s,a),a.polygonEnd()),v=d=m=null}};return N}}function Ie(n,t){function e(e,r){return e=n(e,r),t(e[0],e[1])}return n.invert&&t.invert&&(e.invert=function(e,r){return e=t.invert(e,r),e&&n.invert(e[0],e[1])}),e}function Ye(n){var t=0,e=Ca/3,r=ir(n),u=r(t,e);return u.parallels=function(n){return arguments.length?r(t=n[0]*Ca/180,e=n[1]*Ca/180):[180*(t/Ca),180*(e/Ca)]},u}function Ze(n,t){function e(n,t){var e=Math.sqrt(i-2*u*Math.sin(t))/u;return[e*Math.sin(n*=u),o-e*Math.cos(n)]}var r=Math.sin(n),u=(r+Math.sin(t))/2,i=1+r*(2*u-r),o=Math.sqrt(i)/u;return e.invert=function(n,t){var e=o-t;return[Math.atan2(n,e)/u,G((i-(n*n+e*e)*u*u)/(2*u))]},e}function Ve(){function n(n,t){zc+=u*n-r*t,r=n,u=t}var t,e,r,u;jc.point=function(i,o){jc.point=n,t=r=i,e=u=o},jc.lineEnd=function(){n(t,e)}}function $e(n,t){Rc>n&&(Rc=n),n>Pc&&(Pc=n),Dc>t&&(Dc=t),t>Uc&&(Uc=t)}function Xe(){function n(n,t){o.push(\"M\",n,\",\",t,i)}function t(n,t){o.push(\"M\",n,\",\",t),a.point=e}function e(n,t){o.push(\"L\",n,\",\",t)}function r(){a.point=n}function u(){o.push(\"Z\")}var i=Be(4.5),o=[],a={point:n,lineStart:function(){a.point=t},lineEnd:r,polygonStart:function(){a.lineEnd=u},polygonEnd:function(){a.lineEnd=r,a.point=n},pointRadius:function(n){return i=Be(n),a},result:function(){if(o.length){var n=o.join(\"\");return o=[],n}}};return a}function Be(n){return\"m0,\"+n+\"a\"+n+\",\"+n+\" 0 1,1 0,\"+-2*n+\"a\"+n+\",\"+n+\" 0 1,1 0,\"+2*n+\"z\"}function Je(n,t){Mc+=n,_c+=t,++bc}function We(){function n(n,r){var u=n-t,i=r-e,o=Math.sqrt(u*u+i*i);wc+=o*(t+n)/2,Sc+=o*(e+r)/2,kc+=o,Je(t=n,e=r)}var t,e;Fc.point=function(r,u){Fc.point=n,Je(t=r,e=u)}}function Ge(){Fc.point=Je}function Ke(){function n(n,t){var e=n-r,i=t-u,o=Math.sqrt(e*e+i*i);wc+=o*(r+n)/2,Sc+=o*(u+t)/2,kc+=o,o=u*n-r*t,Ec+=o*(r+n),Ac+=o*(u+t),Cc+=3*o,Je(r=n,u=t)}var t,e,r,u;Fc.point=function(i,o){Fc.point=n,Je(t=r=i,e=u=o)},Fc.lineEnd=function(){n(t,e)}}function Qe(n){function t(t,e){n.moveTo(t,e),n.arc(t,e,o,0,Na)}function e(t,e){n.moveTo(t,e),a.point=r}function r(t,e){n.lineTo(t,e)}function u(){a.point=t}function i(){n.closePath()}var o=4.5,a={point:t,lineStart:function(){a.point=e},lineEnd:u,polygonStart:function(){a.lineEnd=i},polygonEnd:function(){a.lineEnd=u,a.point=t},pointRadius:function(n){return o=n,a},result:v};return a}function nr(n){function t(n){return(a?r:e)(n)}function e(t){return rr(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){x=0/0,S.point=i,t.lineStart()}function i(e,r){var i=pe([e,r]),o=n(e,r);u(x,M,y,_,b,w,x=o[0],M=o[1],y=e,_=i[0],b=i[1],w=i[2],a,t),t.point(x,M)}function o(){S.point=e,t.lineEnd()}function c(){r(),S.point=s,S.lineEnd=l}function s(n,t){i(f=n,h=t),g=x,p=M,v=_,d=b,m=w,S.point=i}function l(){u(x,M,y,_,b,w,g,p,f,v,d,m,a,t),S.lineEnd=o,o()}var f,h,g,p,v,d,m,y,x,M,_,b,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=c},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function u(t,e,r,a,c,s,l,f,h,g,p,v,d,m){var y=l-t,x=f-e,M=y*y+x*x;if(M>4*i&&d--){var _=a+g,b=c+p,w=s+v,S=Math.sqrt(_*_+b*b+w*w),k=Math.asin(w/=S),E=fa(fa(w)-1)<Ta||fa(r-h)<Ta?(r+h)/2:Math.atan2(b,_),A=n(E,k),C=A[0],N=A[1],L=C-t,T=N-e,q=x*L-y*T;(q*q/M>i||fa((y*L+x*T)/M-.5)>.3||o>a*g+c*p+s*v)&&(u(t,e,r,a,c,s,C,N,E,_/=S,b/=S,w,d,m),m.point(C,N),u(C,N,E,_,b,w,l,f,h,g,p,v,d,m))}}var i=.5,o=Math.cos(30*za),a=16;return t.precision=function(n){return arguments.length?(a=(i=n*n)>0&&16,t):Math.sqrt(i)},t}function tr(n){var t=nr(function(t,e){return n([t*Ra,e*Ra])});return function(n){return or(t(n))}}function er(n){this.stream=n}function rr(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function ur(n){return ir(function(){return n})()}function ir(n){function t(n){return n=a(n[0]*za,n[1]*za),[n[0]*h+c,s-n[1]*h]}function e(n){return n=a.invert((n[0]-c)/h,(s-n[1])/h),n&&[n[0]*Ra,n[1]*Ra]}function r(){a=Ie(o=sr(m,y,x),i);var n=i(v,d);return c=g-n[0]*h,s=p+n[1]*h,u()\n}function u(){return l&&(l.valid=!1,l=null),t}var i,o,a,c,s,l,f=nr(function(n,t){return n=i(n,t),[n[0]*h+c,s-n[1]*h]}),h=150,g=480,p=250,v=0,d=0,m=0,y=0,x=0,M=Lc,_=At,b=null,w=null;return t.stream=function(n){return l&&(l.valid=!1),l=or(M(o,f(_(n)))),l.valid=!0,l},t.clipAngle=function(n){return arguments.length?(M=null==n?(b=n,Lc):He((b=+n)*za),u()):b},t.clipExtent=function(n){return arguments.length?(w=n,_=n?Oe(n[0][0],n[0][1],n[1][0],n[1][1]):At,u()):w},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(g=+n[0],p=+n[1],r()):[g,p]},t.center=function(n){return arguments.length?(v=n[0]%360*za,d=n[1]%360*za,r()):[v*Ra,d*Ra]},t.rotate=function(n){return arguments.length?(m=n[0]%360*za,y=n[1]%360*za,x=n.length>2?n[2]%360*za:0,r()):[m*Ra,y*Ra,x*Ra]},Go.rebind(t,f,\"precision\"),function(){return i=n.apply(this,arguments),t.invert=i.invert&&e,r()}}function or(n){return rr(n,function(t,e){n.point(t*za,e*za)})}function ar(n,t){return[n,t]}function cr(n,t){return[n>Ca?n-Na:-Ca>n?n+Na:n,t]}function sr(n,t,e){return n?t||e?Ie(fr(n),hr(t,e)):fr(n):t||e?hr(t,e):cr}function lr(n){return function(t,e){return t+=n,[t>Ca?t-Na:-Ca>t?t+Na:t,e]}}function fr(n){var t=lr(n);return t.invert=lr(-n),t}function hr(n,t){function e(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,s=Math.sin(t),l=s*r+a*u;return[Math.atan2(c*i-l*o,a*r-s*u),G(l*i+c*o)]}var r=Math.cos(n),u=Math.sin(n),i=Math.cos(t),o=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,s=Math.sin(t),l=s*i-c*o;return[Math.atan2(c*i+s*o,a*r+l*u),G(l*r-a*u)]},e}function gr(n,t){var e=Math.cos(n),r=Math.sin(n);return function(u,i,o,a){var c=o*t;null!=u?(u=pr(e,u),i=pr(e,i),(o>0?i>u:u>i)&&(u+=o*Na)):(u=n+o*Na,i=n-.5*c);for(var s,l=u;o>0?l>i:i>l;l-=c)a.point((s=Me([e,-r*Math.cos(l),-r*Math.sin(l)]))[0],s[1])}}function pr(n,t){var e=pe(t);e[0]-=n,xe(e);var r=W(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Ta)%(2*Math.PI)}function vr(n,t,e){var r=Go.range(n,t-Ta,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function dr(n,t,e){var r=Go.range(n,t-Ta,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function mr(n){return n.source}function yr(n){return n.target}function xr(n,t,e,r){var u=Math.cos(t),i=Math.sin(t),o=Math.cos(r),a=Math.sin(r),c=u*Math.cos(n),s=u*Math.sin(n),l=o*Math.cos(e),f=o*Math.sin(e),h=2*Math.asin(Math.sqrt(tt(r-t)+u*o*tt(e-n))),g=1/Math.sin(h),p=h?function(n){var t=Math.sin(n*=h)*g,e=Math.sin(h-n)*g,r=e*c+t*l,u=e*s+t*f,o=e*i+t*a;return[Math.atan2(u,r)*Ra,Math.atan2(o,Math.sqrt(r*r+u*u))*Ra]}:function(){return[n*Ra,t*Ra]};return p.distance=h,p}function Mr(){function n(n,u){var i=Math.sin(u*=za),o=Math.cos(u),a=fa((n*=za)-t),c=Math.cos(a);Oc+=Math.atan2(Math.sqrt((a=o*Math.sin(a))*a+(a=r*i-e*o*c)*a),e*i+r*o*c),t=n,e=i,r=o}var t,e,r;Ic.point=function(u,i){t=u*za,e=Math.sin(i*=za),r=Math.cos(i),Ic.point=n},Ic.lineEnd=function(){Ic.point=Ic.lineEnd=v}}function _r(n,t){function e(t,e){var r=Math.cos(t),u=Math.cos(e),i=n(r*u);return[i*u*Math.sin(t),i*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),u=t(r),i=Math.sin(u),o=Math.cos(u);return[Math.atan2(n*i,r*o),Math.asin(r&&e*i/r)]},e}function br(n,t){function e(n,t){o>0?-La+Ta>t&&(t=-La+Ta):t>La-Ta&&(t=La-Ta);var e=o/Math.pow(u(t),i);return[e*Math.sin(i*n),o-e*Math.cos(i*n)]}var r=Math.cos(n),u=function(n){return Math.tan(Ca/4+n/2)},i=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(u(t)/u(n)),o=r*Math.pow(u(n),i)/i;return i?(e.invert=function(n,t){var e=o-t,r=B(i)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/i,2*Math.atan(Math.pow(o/r,1/i))-La]},e):Sr}function wr(n,t){function e(n,t){var e=i-t;return[e*Math.sin(u*n),i-e*Math.cos(u*n)]}var r=Math.cos(n),u=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),i=r/u+n;return fa(u)<Ta?ar:(e.invert=function(n,t){var e=i-t;return[Math.atan2(n,e)/u,i-B(u)*Math.sqrt(n*n+e*e)]},e)}function Sr(n,t){return[n,Math.log(Math.tan(Ca/4+t/2))]}function kr(n){var t,e=ur(n),r=e.scale,u=e.translate,i=e.clipExtent;return e.scale=function(){var n=r.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.translate=function(){var n=u.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.clipExtent=function(n){var o=i.apply(e,arguments);if(o===e){if(t=null==n){var a=Ca*r(),c=u();i([[c[0]-a,c[1]-a],[c[0]+a,c[1]+a]])}}else t&&(o=null);return o},e.clipExtent(null)}function Er(n,t){return[Math.log(Math.tan(Ca/4+t/2)),-n]}function Ar(n){return n[0]}function Cr(n){return n[1]}function Nr(n){for(var t=n.length,e=[0,1],r=2,u=2;t>u;u++){for(;r>1&&J(n[e[r-2]],n[e[r-1]],n[u])<=0;)--r;e[r++]=u}return e.slice(0,r)}function Lr(n,t){return n[0]-t[0]||n[1]-t[1]}function Tr(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function qr(n,t,e,r){var u=n[0],i=e[0],o=t[0]-u,a=r[0]-i,c=n[1],s=e[1],l=t[1]-c,f=r[1]-s,h=(a*(c-s)-f*(u-i))/(f*o-a*l);return[u+h*o,c+h*l]}function zr(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Rr(){tu(this),this.edge=this.site=this.circle=null}function Dr(n){var t=ns.pop()||new Rr;return t.site=n,t}function Pr(n){$r(n),Gc.remove(n),ns.push(n),tu(n)}function Ur(n){var t=n.circle,e=t.x,r=t.cy,u={x:e,y:r},i=n.P,o=n.N,a=[n];Pr(n);for(var c=i;c.circle&&fa(e-c.circle.x)<Ta&&fa(r-c.circle.cy)<Ta;)i=c.P,a.unshift(c),Pr(c),c=i;a.unshift(c),$r(c);for(var s=o;s.circle&&fa(e-s.circle.x)<Ta&&fa(r-s.circle.cy)<Ta;)o=s.N,a.push(s),Pr(s),s=o;a.push(s),$r(s);var l,f=a.length;for(l=1;f>l;++l)s=a[l],c=a[l-1],Kr(s.edge,c.site,s.site,u);c=a[0],s=a[f-1],s.edge=Wr(c.site,s.site,null,u),Vr(c),Vr(s)}function jr(n){for(var t,e,r,u,i=n.x,o=n.y,a=Gc._;a;)if(r=Hr(a,o)-i,r>Ta)a=a.L;else{if(u=i-Fr(a,o),!(u>Ta)){r>-Ta?(t=a.P,e=a):u>-Ta?(t=a,e=a.N):t=e=a;break}if(!a.R){t=a;break}a=a.R}var c=Dr(n);if(Gc.insert(t,c),t||e){if(t===e)return $r(t),e=Dr(t.site),Gc.insert(c,e),c.edge=e.edge=Wr(t.site,c.site),Vr(t),Vr(e),void 0;if(!e)return c.edge=Wr(t.site,c.site),void 0;$r(t),$r(e);var s=t.site,l=s.x,f=s.y,h=n.x-l,g=n.y-f,p=e.site,v=p.x-l,d=p.y-f,m=2*(h*d-g*v),y=h*h+g*g,x=v*v+d*d,M={x:(d*y-g*x)/m+l,y:(h*x-v*y)/m+f};Kr(e.edge,s,p,M),c.edge=Wr(s,n,null,M),e.edge=Wr(n,p,null,M),Vr(t),Vr(e)}}function Hr(n,t){var e=n.site,r=e.x,u=e.y,i=u-t;if(!i)return r;var o=n.P;if(!o)return-1/0;e=o.site;var a=e.x,c=e.y,s=c-t;if(!s)return a;var l=a-r,f=1/i-1/s,h=l/s;return f?(-h+Math.sqrt(h*h-2*f*(l*l/(-2*s)-c+s/2+u-i/2)))/f+r:(r+a)/2}function Fr(n,t){var e=n.N;if(e)return Hr(e,t);var r=n.site;return r.y===t?r.x:1/0}function Or(n){this.site=n,this.edges=[]}function Ir(n){for(var t,e,r,u,i,o,a,c,s,l,f=n[0][0],h=n[1][0],g=n[0][1],p=n[1][1],v=Wc,d=v.length;d--;)if(i=v[d],i&&i.prepare())for(a=i.edges,c=a.length,o=0;c>o;)l=a[o].end(),r=l.x,u=l.y,s=a[++o%c].start(),t=s.x,e=s.y,(fa(r-t)>Ta||fa(u-e)>Ta)&&(a.splice(o,0,new Qr(Gr(i.site,l,fa(r-f)<Ta&&p-u>Ta?{x:f,y:fa(t-f)<Ta?e:p}:fa(u-p)<Ta&&h-r>Ta?{x:fa(e-p)<Ta?t:h,y:p}:fa(r-h)<Ta&&u-g>Ta?{x:h,y:fa(t-h)<Ta?e:g}:fa(u-g)<Ta&&r-f>Ta?{x:fa(e-g)<Ta?t:f,y:g}:null),i.site,null)),++c)}function Yr(n,t){return t.angle-n.angle}function Zr(){tu(this),this.x=this.y=this.arc=this.site=this.cy=null}function Vr(n){var t=n.P,e=n.N;if(t&&e){var r=t.site,u=n.site,i=e.site;if(r!==i){var o=u.x,a=u.y,c=r.x-o,s=r.y-a,l=i.x-o,f=i.y-a,h=2*(c*f-s*l);if(!(h>=-qa)){var g=c*c+s*s,p=l*l+f*f,v=(f*g-s*p)/h,d=(c*p-l*g)/h,f=d+a,m=ts.pop()||new Zr;m.arc=n,m.site=u,m.x=v+o,m.y=f+Math.sqrt(v*v+d*d),m.cy=f,n.circle=m;for(var y=null,x=Qc._;x;)if(m.y<x.y||m.y===x.y&&m.x<=x.x){if(!x.L){y=x.P;break}x=x.L}else{if(!x.R){y=x;break}x=x.R}Qc.insert(y,m),y||(Kc=m)}}}}function $r(n){var t=n.circle;t&&(t.P||(Kc=t.N),Qc.remove(t),ts.push(t),tu(t),n.circle=null)}function Xr(n){for(var t,e=Jc,r=Fe(n[0][0],n[0][1],n[1][0],n[1][1]),u=e.length;u--;)t=e[u],(!Br(t,n)||!r(t)||fa(t.a.x-t.b.x)<Ta&&fa(t.a.y-t.b.y)<Ta)&&(t.a=t.b=null,e.splice(u,1))}function Br(n,t){var e=n.b;if(e)return!0;var r,u,i=n.a,o=t[0][0],a=t[1][0],c=t[0][1],s=t[1][1],l=n.l,f=n.r,h=l.x,g=l.y,p=f.x,v=f.y,d=(h+p)/2,m=(g+v)/2;if(v===g){if(o>d||d>=a)return;if(h>p){if(i){if(i.y>=s)return}else i={x:d,y:c};e={x:d,y:s}}else{if(i){if(i.y<c)return}else i={x:d,y:s};e={x:d,y:c}}}else if(r=(h-p)/(v-g),u=m-r*d,-1>r||r>1)if(h>p){if(i){if(i.y>=s)return}else i={x:(c-u)/r,y:c};e={x:(s-u)/r,y:s}}else{if(i){if(i.y<c)return}else i={x:(s-u)/r,y:s};e={x:(c-u)/r,y:c}}else if(v>g){if(i){if(i.x>=a)return}else i={x:o,y:r*o+u};e={x:a,y:r*a+u}}else{if(i){if(i.x<o)return}else i={x:a,y:r*a+u};e={x:o,y:r*o+u}}return n.a=i,n.b=e,!0}function Jr(n,t){this.l=n,this.r=t,this.a=this.b=null}function Wr(n,t,e,r){var u=new Jr(n,t);return Jc.push(u),e&&Kr(u,n,t,e),r&&Kr(u,t,n,r),Wc[n.i].edges.push(new Qr(u,n,t)),Wc[t.i].edges.push(new Qr(u,t,n)),u}function Gr(n,t,e){var r=new Jr(n,null);return r.a=t,r.b=e,Jc.push(r),r}function Kr(n,t,e,r){n.a||n.b?n.l===e?n.b=r:n.a=r:(n.a=r,n.l=t,n.r=e)}function Qr(n,t,e){var r=n.a,u=n.b;this.edge=n,this.site=t,this.angle=e?Math.atan2(e.y-t.y,e.x-t.x):n.l===t?Math.atan2(u.x-r.x,r.y-u.y):Math.atan2(r.x-u.x,u.y-r.y)}function nu(){this._=null}function tu(n){n.U=n.C=n.L=n.R=n.P=n.N=null}function eu(n,t){var e=t,r=t.R,u=e.U;u?u.L===e?u.L=r:u.R=r:n._=r,r.U=u,e.U=r,e.R=r.L,e.R&&(e.R.U=e),r.L=e}function ru(n,t){var e=t,r=t.L,u=e.U;u?u.L===e?u.L=r:u.R=r:n._=r,r.U=u,e.U=r,e.L=r.R,e.L&&(e.L.U=e),r.R=e}function uu(n){for(;n.L;)n=n.L;return n}function iu(n,t){var e,r,u,i=n.sort(ou).pop();for(Jc=[],Wc=new Array(n.length),Gc=new nu,Qc=new nu;;)if(u=Kc,i&&(!u||i.y<u.y||i.y===u.y&&i.x<u.x))(i.x!==e||i.y!==r)&&(Wc[i.i]=new Or(i),jr(i),e=i.x,r=i.y),i=n.pop();else{if(!u)break;Ur(u.arc)}t&&(Xr(t),Ir(t));var o={cells:Wc,edges:Jc};return Gc=Qc=Jc=Wc=null,o}function ou(n,t){return t.y-n.y||t.x-n.x}function au(n,t,e){return(n.x-e.x)*(t.y-n.y)-(n.x-t.x)*(e.y-n.y)}function cu(n){return n.x}function su(n){return n.y}function lu(){return{leaf:!0,nodes:[],point:null,x:null,y:null}}function fu(n,t,e,r,u,i){if(!n(t,e,r,u,i)){var o=.5*(e+u),a=.5*(r+i),c=t.nodes;c[0]&&fu(n,c[0],e,r,o,a),c[1]&&fu(n,c[1],o,r,u,a),c[2]&&fu(n,c[2],e,a,o,i),c[3]&&fu(n,c[3],o,a,u,i)}}function hu(n,t){n=Go.rgb(n),t=Go.rgb(t);var e=n.r,r=n.g,u=n.b,i=t.r-e,o=t.g-r,a=t.b-u;return function(n){return\"#\"+Mt(Math.round(e+i*n))+Mt(Math.round(r+o*n))+Mt(Math.round(u+a*n))}}function gu(n,t){var e,r={},u={};for(e in n)e in t?r[e]=du(n[e],t[e]):u[e]=n[e];for(e in t)e in n||(u[e]=t[e]);return function(n){for(e in r)u[e]=r[e](n);return u}}function pu(n,t){return t-=n=+n,function(e){return n+t*e}}function vu(n,t){var e,r,u,i=rs.lastIndex=us.lastIndex=0,o=-1,a=[],c=[];for(n+=\"\",t+=\"\";(e=rs.exec(n))&&(r=us.exec(t));)(u=r.index)>i&&(u=t.substring(i,u),a[o]?a[o]+=u:a[++o]=u),(e=e[0])===(r=r[0])?a[o]?a[o]+=r:a[++o]=r:(a[++o]=null,c.push({i:o,x:pu(e,r)})),i=us.lastIndex;return i<t.length&&(u=t.substring(i),a[o]?a[o]+=u:a[++o]=u),a.length<2?c[0]?(t=c[0].x,function(n){return t(n)+\"\"}):function(){return t}:(t=c.length,function(n){for(var e,r=0;t>r;++r)a[(e=c[r]).i]=e.x(n);return a.join(\"\")})}function du(n,t){for(var e,r=Go.interpolators.length;--r>=0&&!(e=Go.interpolators[r](n,t)););return e}function mu(n,t){var e,r=[],u=[],i=n.length,o=t.length,a=Math.min(n.length,t.length);for(e=0;a>e;++e)r.push(du(n[e],t[e]));for(;i>e;++e)u[e]=n[e];for(;o>e;++e)u[e]=t[e];return function(n){for(e=0;a>e;++e)u[e]=r[e](n);return u}}function yu(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function xu(n){return function(t){return 1-n(1-t)}}function Mu(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function _u(n){return n*n}function bu(n){return n*n*n}function wu(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function Su(n){return function(t){return Math.pow(t,n)}}function ku(n){return 1-Math.cos(n*La)}function Eu(n){return Math.pow(2,10*(n-1))}function Au(n){return 1-Math.sqrt(1-n*n)}function Cu(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/Na*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*Na/t)}}function Nu(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function Lu(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Tu(n,t){n=Go.hcl(n),t=Go.hcl(t);var e=n.h,r=n.c,u=n.l,i=t.h-e,o=t.c-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.c:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return ct(e+i*n,r+o*n,u+a*n)+\"\"}}function qu(n,t){n=Go.hsl(n),t=Go.hsl(t);var e=n.h,r=n.s,u=n.l,i=t.h-e,o=t.s-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.s:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return it(e+i*n,r+o*n,u+a*n)+\"\"}}function zu(n,t){n=Go.lab(n),t=Go.lab(t);var e=n.l,r=n.a,u=n.b,i=t.l-e,o=t.a-r,a=t.b-u;return function(n){return ft(e+i*n,r+o*n,u+a*n)+\"\"}}function Ru(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function Du(n){var t=[n.a,n.b],e=[n.c,n.d],r=Uu(t),u=Pu(t,e),i=Uu(ju(e,t,-u))||0;t[0]*e[1]<e[0]*t[1]&&(t[0]*=-1,t[1]*=-1,r*=-1,u*=-1),this.rotate=(r?Math.atan2(t[1],t[0]):Math.atan2(-e[0],e[1]))*Ra,this.translate=[n.e,n.f],this.scale=[r,i],this.skew=i?Math.atan2(u,i)*Ra:0}function Pu(n,t){return n[0]*t[0]+n[1]*t[1]}function Uu(n){var t=Math.sqrt(Pu(n,n));return t&&(n[0]/=t,n[1]/=t),t}function ju(n,t,e){return n[0]+=e*t[0],n[1]+=e*t[1],n}function Hu(n,t){var e,r=[],u=[],i=Go.transform(n),o=Go.transform(t),a=i.translate,c=o.translate,s=i.rotate,l=o.rotate,f=i.skew,h=o.skew,g=i.scale,p=o.scale;return a[0]!=c[0]||a[1]!=c[1]?(r.push(\"translate(\",null,\",\",null,\")\"),u.push({i:1,x:pu(a[0],c[0])},{i:3,x:pu(a[1],c[1])})):c[0]||c[1]?r.push(\"translate(\"+c+\")\"):r.push(\"\"),s!=l?(s-l>180?l+=360:l-s>180&&(s+=360),u.push({i:r.push(r.pop()+\"rotate(\",null,\")\")-2,x:pu(s,l)})):l&&r.push(r.pop()+\"rotate(\"+l+\")\"),f!=h?u.push({i:r.push(r.pop()+\"skewX(\",null,\")\")-2,x:pu(f,h)}):h&&r.push(r.pop()+\"skewX(\"+h+\")\"),g[0]!=p[0]||g[1]!=p[1]?(e=r.push(r.pop()+\"scale(\",null,\",\",null,\")\"),u.push({i:e-4,x:pu(g[0],p[0])},{i:e-2,x:pu(g[1],p[1])})):(1!=p[0]||1!=p[1])&&r.push(r.pop()+\"scale(\"+p+\")\"),e=u.length,function(n){for(var t,i=-1;++i<e;)r[(t=u[i]).i]=t.x(n);return r.join(\"\")}}function Fu(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return(e-n)*t}}function Ou(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return Math.max(0,Math.min(1,(e-n)*t))}}function Iu(n){for(var t=n.source,e=n.target,r=Zu(t,e),u=[t];t!==r;)t=t.parent,u.push(t);for(var i=u.length;e!==r;)u.splice(i,0,e),e=e.parent;return u}function Yu(n){for(var t=[],e=n.parent;null!=e;)t.push(n),n=e,e=e.parent;return t.push(n),t}function Zu(n,t){if(n===t)return n;for(var e=Yu(n),r=Yu(t),u=e.pop(),i=r.pop(),o=null;u===i;)o=u,u=e.pop(),i=r.pop();return o}function Vu(n){n.fixed|=2}function $u(n){n.fixed&=-7}function Xu(n){n.fixed|=4,n.px=n.x,n.py=n.y}function Bu(n){n.fixed&=-5}function Ju(n,t,e){var r=0,u=0;if(n.charge=0,!n.leaf)for(var i,o=n.nodes,a=o.length,c=-1;++c<a;)i=o[c],null!=i&&(Ju(i,t,e),n.charge+=i.charge,r+=i.charge*i.cx,u+=i.charge*i.cy);if(n.point){n.leaf||(n.point.x+=Math.random()-.5,n.point.y+=Math.random()-.5);var s=t*e[n.point.index];n.charge+=n.pointCharge=s,r+=s*n.point.x,u+=s*n.point.y}n.cx=r/n.charge,n.cy=u/n.charge}function Wu(n,t){return Go.rebind(n,t,\"sort\",\"children\",\"value\"),n.nodes=n,n.links=ni,n}function Gu(n){return n.children}function Ku(n){return n.value}function Qu(n,t){return t.value-n.value}function ni(n){return Go.merge(n.map(function(n){return(n.children||[]).map(function(t){return{source:n,target:t}})}))}function ti(n){return n.x}function ei(n){return n.y}function ri(n,t,e){n.y0=t,n.y=e}function ui(n){return Go.range(n.length)}function ii(n){for(var t=-1,e=n[0].length,r=[];++t<e;)r[t]=0;return r}function oi(n){for(var t,e=1,r=0,u=n[0][1],i=n.length;i>e;++e)(t=n[e][1])>u&&(r=e,u=t);return r}function ai(n){return n.reduce(ci,0)}function ci(n,t){return n+t[1]}function si(n,t){return li(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function li(n,t){for(var e=-1,r=+n[0],u=(n[1]-r)/t,i=[];++e<=t;)i[e]=u*e+r;return i}function fi(n){return[Go.min(n),Go.max(n)]}function hi(n,t){return n.parent==t.parent?1:2}function gi(n){var t=n.children;return t&&t.length?t[0]:n._tree.thread}function pi(n){var t,e=n.children;return e&&(t=e.length)?e[t-1]:n._tree.thread}function vi(n,t){var e=n.children;if(e&&(u=e.length))for(var r,u,i=-1;++i<u;)t(r=vi(e[i],t),n)>0&&(n=r);return n}function di(n,t){return n.x-t.x}function mi(n,t){return t.x-n.x}function yi(n,t){return n.depth-t.depth}function xi(n,t){function e(n,r){var u=n.children;if(u&&(o=u.length))for(var i,o,a=null,c=-1;++c<o;)i=u[c],e(i,a),a=i;t(n,r)}e(n,null)}function Mi(n){for(var t,e=0,r=0,u=n.children,i=u.length;--i>=0;)t=u[i]._tree,t.prelim+=e,t.mod+=e,e+=t.shift+(r+=t.change)}function _i(n,t,e){n=n._tree,t=t._tree;var r=e/(t.number-n.number);n.change+=r,t.change-=r,t.shift+=e,t.prelim+=e,t.mod+=e}function bi(n,t,e){return n._tree.ancestor.parent==t.parent?n._tree.ancestor:e}function wi(n,t){return n.value-t.value}function Si(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function ki(n,t){n._pack_next=t,t._pack_prev=n}function Ei(n,t){var e=t.x-n.x,r=t.y-n.y,u=n.r+t.r;return.999*u*u>e*e+r*r}function Ai(n){function t(n){l=Math.min(n.x-n.r,l),f=Math.max(n.x+n.r,f),h=Math.min(n.y-n.r,h),g=Math.max(n.y+n.r,g)}if((e=n.children)&&(s=e.length)){var e,r,u,i,o,a,c,s,l=1/0,f=-1/0,h=1/0,g=-1/0;if(e.forEach(Ci),r=e[0],r.x=-r.r,r.y=0,t(r),s>1&&(u=e[1],u.x=u.r,u.y=0,t(u),s>2))for(i=e[2],Ti(r,u,i),t(i),Si(r,i),r._pack_prev=i,Si(i,u),u=r._pack_next,o=3;s>o;o++){Ti(r,u,i=e[o]);var p=0,v=1,d=1;for(a=u._pack_next;a!==u;a=a._pack_next,v++)if(Ei(a,i)){p=1;break}if(1==p)for(c=r._pack_prev;c!==a._pack_prev&&!Ei(c,i);c=c._pack_prev,d++);p?(d>v||v==d&&u.r<r.r?ki(r,u=a):ki(r=c,u),o--):(Si(r,i),u=i,t(i))}var m=(l+f)/2,y=(h+g)/2,x=0;for(o=0;s>o;o++)i=e[o],i.x-=m,i.y-=y,x=Math.max(x,i.r+Math.sqrt(i.x*i.x+i.y*i.y));n.r=x,e.forEach(Ni)}}function Ci(n){n._pack_next=n._pack_prev=n}function Ni(n){delete n._pack_next,delete n._pack_prev}function Li(n,t,e,r){var u=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,u)for(var i=-1,o=u.length;++i<o;)Li(u[i],t,e,r)}function Ti(n,t,e){var r=n.r+e.r,u=t.x-n.x,i=t.y-n.y;if(r&&(u||i)){var o=t.r+e.r,a=u*u+i*i;o*=o,r*=r;var c=.5+(r-o)/(2*a),s=Math.sqrt(Math.max(0,2*o*(r+a)-(r-=a)*r-o*o))/(2*a);e.x=n.x+c*u+s*i,e.y=n.y+c*i-s*u}else e.x=n.x+r,e.y=n.y}function qi(n){return 1+Go.max(n,function(n){return n.y})}function zi(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Ri(n){var t=n.children;return t&&t.length?Ri(t[0]):n}function Di(n){var t,e=n.children;return e&&(t=e.length)?Di(e[t-1]):n}function Pi(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function Ui(n,t){var e=n.x+t[3],r=n.y+t[0],u=n.dx-t[1]-t[3],i=n.dy-t[0]-t[2];return 0>u&&(e+=u/2,u=0),0>i&&(r+=i/2,i=0),{x:e,y:r,dx:u,dy:i}}function ji(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Hi(n){return n.rangeExtent?n.rangeExtent():ji(n.range())}function Fi(n,t,e,r){var u=e(n[0],n[1]),i=r(t[0],t[1]);return function(n){return i(u(n))}}function Oi(n,t){var e,r=0,u=n.length-1,i=n[r],o=n[u];return i>o&&(e=r,r=u,u=e,e=i,i=o,o=e),n[r]=t.floor(i),n[u]=t.ceil(o),n}function Ii(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:vs}function Yi(n,t,e,r){var u=[],i=[],o=0,a=Math.min(n.length,t.length)-1;for(n[a]<n[0]&&(n=n.slice().reverse(),t=t.slice().reverse());++o<=a;)u.push(e(n[o-1],n[o])),i.push(r(t[o-1],t[o]));return function(t){var e=Go.bisect(n,t,1,a)-1;return i[e](u[e](t))}}function Zi(n,t,e,r){function u(){var u=Math.min(n.length,t.length)>2?Yi:Fi,c=r?Ou:Fu;return o=u(n,t,c,e),a=u(t,n,c,du),i}function i(n){return o(n)}var o,a;return i.invert=function(n){return a(n)},i.domain=function(t){return arguments.length?(n=t.map(Number),u()):n},i.range=function(n){return arguments.length?(t=n,u()):t},i.rangeRound=function(n){return i.range(n).interpolate(Ru)},i.clamp=function(n){return arguments.length?(r=n,u()):r},i.interpolate=function(n){return arguments.length?(e=n,u()):e},i.ticks=function(t){return Bi(n,t)},i.tickFormat=function(t,e){return Ji(n,t,e)},i.nice=function(t){return $i(n,t),u()},i.copy=function(){return Zi(n,t,e,r)},u()}function Vi(n,t){return Go.rebind(n,t,\"range\",\"rangeRound\",\"interpolate\",\"clamp\")}function $i(n,t){return Oi(n,Ii(Xi(n,t)[2]))}function Xi(n,t){null==t&&(t=10);var e=ji(n),r=e[1]-e[0],u=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),i=t/r*u;return.15>=i?u*=10:.35>=i?u*=5:.75>=i&&(u*=2),e[0]=Math.ceil(e[0]/u)*u,e[1]=Math.floor(e[1]/u)*u+.5*u,e[2]=u,e}function Bi(n,t){return Go.range.apply(Go,Xi(n,t))}function Ji(n,t,e){var r=Xi(n,t);if(e){var u=rc.exec(e);if(u.shift(),\"s\"===u[8]){var i=Go.formatPrefix(Math.max(fa(r[0]),fa(r[1])));return u[7]||(u[7]=\".\"+Wi(i.scale(r[2]))),u[8]=\"f\",e=Go.format(u.join(\"\")),function(n){return e(i.scale(n))+i.symbol}}u[7]||(u[7]=\".\"+Gi(u[8],r)),e=u.join(\"\")}else e=\",.\"+Wi(r[2])+\"f\";return Go.format(e)}function Wi(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function Gi(n,t){var e=Wi(t[2]);return n in ds?Math.abs(e-Wi(Math.max(fa(t[0]),fa(t[1]))))+ +(\"e\"!==n):e-2*(\"%\"===n)}function Ki(n,t,e,r){function u(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function i(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function o(t){return n(u(t))}return o.invert=function(t){return i(n.invert(t))},o.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(u)),o):r},o.base=function(e){return arguments.length?(t=+e,n.domain(r.map(u)),o):t},o.nice=function(){var t=Oi(r.map(u),e?Math:ys);return n.domain(t),r=t.map(i),o},o.ticks=function(){var n=ji(r),o=[],a=n[0],c=n[1],s=Math.floor(u(a)),l=Math.ceil(u(c)),f=t%1?2:t;if(isFinite(l-s)){if(e){for(;l>s;s++)for(var h=1;f>h;h++)o.push(i(s)*h);o.push(i(s))}else for(o.push(i(s));s++<l;)for(var h=f-1;h>0;h--)o.push(i(s)*h);for(s=0;o[s]<a;s++);for(l=o.length;o[l-1]>c;l--);o=o.slice(s,l)}return o},o.tickFormat=function(n,t){if(!arguments.length)return ms;arguments.length<2?t=ms:\"function\"!=typeof t&&(t=Go.format(t));var r,a=Math.max(.1,n/o.ticks().length),c=e?(r=1e-12,Math.ceil):(r=-1e-12,Math.floor);return function(n){return n/i(c(u(n)+r))<=a?t(n):\"\"}},o.copy=function(){return Ki(n.copy(),t,e,r)},Vi(o,n)}function Qi(n,t,e){function r(t){return n(u(t))}var u=no(t),i=no(1/t);return r.invert=function(t){return i(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(u)),r):e},r.ticks=function(n){return Bi(e,n)},r.tickFormat=function(n,t){return Ji(e,n,t)},r.nice=function(n){return r.domain($i(e,n))},r.exponent=function(o){return arguments.length?(u=no(t=o),i=no(1/t),n.domain(e.map(u)),r):t},r.copy=function(){return Qi(n.copy(),t,e)},Vi(r,n)}function no(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function to(n,t){function e(e){return i[((u.get(e)||(\"range\"===t.t?u.set(e,n.push(e)):0/0))-1)%i.length]}function r(t,e){return Go.range(n.length).map(function(n){return t+e*n})}var u,i,a;return e.domain=function(r){if(!arguments.length)return n;n=[],u=new o;for(var i,a=-1,c=r.length;++a<c;)u.has(i=r[a])||u.set(i,n.push(i));return e[t.t].apply(e,t.a)},e.range=function(n){return arguments.length?(i=n,a=0,t={t:\"range\",a:arguments},e):i},e.rangePoints=function(u,o){arguments.length<2&&(o=0);var c=u[0],s=u[1],l=(s-c)/(Math.max(1,n.length-1)+o);return i=r(n.length<2?(c+s)/2:c+l*o/2,l),a=0,t={t:\"rangePoints\",a:arguments},e},e.rangeBands=function(u,o,c){arguments.length<2&&(o=0),arguments.length<3&&(c=o);var s=u[1]<u[0],l=u[s-0],f=u[1-s],h=(f-l)/(n.length-o+2*c);return i=r(l+h*c,h),s&&i.reverse(),a=h*(1-o),t={t:\"rangeBands\",a:arguments},e},e.rangeRoundBands=function(u,o,c){arguments.length<2&&(o=0),arguments.length<3&&(c=o);var s=u[1]<u[0],l=u[s-0],f=u[1-s],h=Math.floor((f-l)/(n.length-o+2*c)),g=f-l-(n.length-o)*h;return i=r(l+Math.round(g/2),h),s&&i.reverse(),a=Math.round(h*(1-o)),t={t:\"rangeRoundBands\",a:arguments},e},e.rangeBand=function(){return a},e.rangeExtent=function(){return ji(t.a[0])},e.copy=function(){return to(n,t)},e.domain(n)}function eo(e,r){function u(){var n=0,t=r.length;for(o=[];++n<t;)o[n-1]=Go.quantile(e,n/t);return i}function i(n){return isNaN(n=+n)?void 0:r[Go.bisect(o,n)]}var o;return i.domain=function(r){return arguments.length?(e=r.filter(t).sort(n),u()):e},i.range=function(n){return arguments.length?(r=n,u()):r},i.quantiles=function(){return o},i.invertExtent=function(n){return n=r.indexOf(n),0>n?[0/0,0/0]:[n>0?o[n-1]:e[0],n<o.length?o[n]:e[e.length-1]]},i.copy=function(){return eo(e,r)},u()}function ro(n,t,e){function r(t){return e[Math.max(0,Math.min(o,Math.floor(i*(t-n))))]}function u(){return i=e.length/(t-n),o=e.length-1,r}var i,o;return r.domain=function(e){return arguments.length?(n=+e[0],t=+e[e.length-1],u()):[n,t]},r.range=function(n){return arguments.length?(e=n,u()):e},r.invertExtent=function(t){return t=e.indexOf(t),t=0>t?0/0:t/i+n,[t,t+1/i]},r.copy=function(){return ro(n,t,e)},u()}function uo(n,t){function e(e){return e>=e?t[Go.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return uo(n,t)},e}function io(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Bi(n,t)},t.tickFormat=function(t,e){return Ji(n,t,e)},t.copy=function(){return io(n)},t}function oo(n){return n.innerRadius}function ao(n){return n.outerRadius}function co(n){return n.startAngle}function so(n){return n.endAngle}function lo(n){function t(t){function o(){s.push(\"M\",i(n(l),a))}for(var c,s=[],l=[],f=-1,h=t.length,g=Et(e),p=Et(r);++f<h;)u.call(this,c=t[f],f)?l.push([+g.call(this,c,f),+p.call(this,c,f)]):l.length&&(o(),l=[]);return l.length&&o(),s.length?s.join(\"\"):null}var e=Ar,r=Cr,u=Ae,i=fo,o=i.key,a=.7;return t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t.defined=function(n){return arguments.length?(u=n,t):u},t.interpolate=function(n){return arguments.length?(o=\"function\"==typeof n?i=n:(i=ks.get(n)||fo).key,t):o},t.tension=function(n){return arguments.length?(a=n,t):a},t}function fo(n){return n.join(\"L\")}function ho(n){return fo(n)+\"Z\"}function go(n){for(var t=0,e=n.length,r=n[0],u=[r[0],\",\",r[1]];++t<e;)u.push(\"H\",(r[0]+(r=n[t])[0])/2,\"V\",r[1]);return e>1&&u.push(\"H\",r[0]),u.join(\"\")}function po(n){for(var t=0,e=n.length,r=n[0],u=[r[0],\",\",r[1]];++t<e;)u.push(\"V\",(r=n[t])[1],\"H\",r[0]);return u.join(\"\")}function vo(n){for(var t=0,e=n.length,r=n[0],u=[r[0],\",\",r[1]];++t<e;)u.push(\"H\",(r=n[t])[0],\"V\",r[1]);return u.join(\"\")}function mo(n,t){return n.length<4?fo(n):n[1]+Mo(n.slice(1,n.length-1),_o(n,t))}function yo(n,t){return n.length<3?fo(n):n[0]+Mo((n.push(n[0]),n),_o([n[n.length-2]].concat(n,[n[1]]),t))}function xo(n,t){return n.length<3?fo(n):n[0]+Mo(n,_o(n,t))}function Mo(n,t){if(t.length<1||n.length!=t.length&&n.length!=t.length+2)return fo(n);var e=n.length!=t.length,r=\"\",u=n[0],i=n[1],o=t[0],a=o,c=1;if(e&&(r+=\"Q\"+(i[0]-2*o[0]/3)+\",\"+(i[1]-2*o[1]/3)+\",\"+i[0]+\",\"+i[1],u=n[1],c=2),t.length>1){a=t[1],i=n[c],c++,r+=\"C\"+(u[0]+o[0])+\",\"+(u[1]+o[1])+\",\"+(i[0]-a[0])+\",\"+(i[1]-a[1])+\",\"+i[0]+\",\"+i[1];for(var s=2;s<t.length;s++,c++)i=n[c],a=t[s],r+=\"S\"+(i[0]-a[0])+\",\"+(i[1]-a[1])+\",\"+i[0]+\",\"+i[1]}if(e){var l=n[c];r+=\"Q\"+(i[0]+2*a[0]/3)+\",\"+(i[1]+2*a[1]/3)+\",\"+l[0]+\",\"+l[1]}return r}function _o(n,t){for(var e,r=[],u=(1-t)/2,i=n[0],o=n[1],a=1,c=n.length;++a<c;)e=i,i=o,o=n[a],r.push([u*(o[0]-e[0]),u*(o[1]-e[1])]);return r}function bo(n){if(n.length<3)return fo(n);var t=1,e=n.length,r=n[0],u=r[0],i=r[1],o=[u,u,u,(r=n[1])[0]],a=[i,i,i,r[1]],c=[u,\",\",i,\"L\",Eo(Cs,o),\",\",Eo(Cs,a)];for(n.push(n[e-1]);++t<=e;)r=n[t],o.shift(),o.push(r[0]),a.shift(),a.push(r[1]),Ao(c,o,a);return n.pop(),c.push(\"L\",r),c.join(\"\")}function wo(n){if(n.length<4)return fo(n);for(var t,e=[],r=-1,u=n.length,i=[0],o=[0];++r<3;)t=n[r],i.push(t[0]),o.push(t[1]);for(e.push(Eo(Cs,i)+\",\"+Eo(Cs,o)),--r;++r<u;)t=n[r],i.shift(),i.push(t[0]),o.shift(),o.push(t[1]),Ao(e,i,o);return e.join(\"\")}function So(n){for(var t,e,r=-1,u=n.length,i=u+4,o=[],a=[];++r<4;)e=n[r%u],o.push(e[0]),a.push(e[1]);for(t=[Eo(Cs,o),\",\",Eo(Cs,a)],--r;++r<i;)e=n[r%u],o.shift(),o.push(e[0]),a.shift(),a.push(e[1]),Ao(t,o,a);return t.join(\"\")}function ko(n,t){var e=n.length-1;if(e)for(var r,u,i=n[0][0],o=n[0][1],a=n[e][0]-i,c=n[e][1]-o,s=-1;++s<=e;)r=n[s],u=s/e,r[0]=t*r[0]+(1-t)*(i+u*a),r[1]=t*r[1]+(1-t)*(o+u*c);return bo(n)}function Eo(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]+n[3]*t[3]}function Ao(n,t,e){n.push(\"C\",Eo(Es,t),\",\",Eo(Es,e),\",\",Eo(As,t),\",\",Eo(As,e),\",\",Eo(Cs,t),\",\",Eo(Cs,e))}function Co(n,t){return(t[1]-n[1])/(t[0]-n[0])}function No(n){for(var t=0,e=n.length-1,r=[],u=n[0],i=n[1],o=r[0]=Co(u,i);++t<e;)r[t]=(o+(o=Co(u=i,i=n[t+1])))/2;return r[t]=o,r}function Lo(n){for(var t,e,r,u,i=[],o=No(n),a=-1,c=n.length-1;++a<c;)t=Co(n[a],n[a+1]),fa(t)<Ta?o[a]=o[a+1]=0:(e=o[a]/t,r=o[a+1]/t,u=e*e+r*r,u>9&&(u=3*t/Math.sqrt(u),o[a]=u*e,o[a+1]=u*r));for(a=-1;++a<=c;)u=(n[Math.min(c,a+1)][0]-n[Math.max(0,a-1)][0])/(6*(1+o[a]*o[a])),i.push([u||0,o[a]*u||0]);return i}function To(n){return n.length<3?fo(n):n[0]+Mo(n,Lo(n))}function qo(n){for(var t,e,r,u=-1,i=n.length;++u<i;)t=n[u],e=t[0],r=t[1]+ws,t[0]=e*Math.cos(r),t[1]=e*Math.sin(r);return n}function zo(n){function t(t){function c(){v.push(\"M\",a(n(m),f),l,s(n(d.reverse()),f),\"Z\")}for(var h,g,p,v=[],d=[],m=[],y=-1,x=t.length,M=Et(e),_=Et(u),b=e===r?function(){return g}:Et(r),w=u===i?function(){return p}:Et(i);++y<x;)o.call(this,h=t[y],y)?(d.push([g=+M.call(this,h,y),p=+_.call(this,h,y)]),m.push([+b.call(this,h,y),+w.call(this,h,y)])):d.length&&(c(),d=[],m=[]);return d.length&&c(),v.length?v.join(\"\"):null}var e=Ar,r=Ar,u=0,i=Cr,o=Ae,a=fo,c=a.key,s=a,l=\"L\",f=.7;return t.x=function(n){return arguments.length?(e=r=n,t):r},t.x0=function(n){return arguments.length?(e=n,t):e},t.x1=function(n){return arguments.length?(r=n,t):r},t.y=function(n){return arguments.length?(u=i=n,t):i},t.y0=function(n){return arguments.length?(u=n,t):u},t.y1=function(n){return arguments.length?(i=n,t):i},t.defined=function(n){return arguments.length?(o=n,t):o},t.interpolate=function(n){return arguments.length?(c=\"function\"==typeof n?a=n:(a=ks.get(n)||fo).key,s=a.reverse||a,l=a.closed?\"M\":\"L\",t):c},t.tension=function(n){return arguments.length?(f=n,t):f},t}function Ro(n){return n.radius}function Do(n){return[n.x,n.y]}function Po(n){return function(){var t=n.apply(this,arguments),e=t[0],r=t[1]+ws;return[e*Math.cos(r),e*Math.sin(r)]}}function Uo(){return 64}function jo(){return\"circle\"}function Ho(n){var t=Math.sqrt(n/Ca);return\"M0,\"+t+\"A\"+t+\",\"+t+\" 0 1,1 0,\"+-t+\"A\"+t+\",\"+t+\" 0 1,1 0,\"+t+\"Z\"}function Fo(n,t){return da(n,Rs),n.id=t,n}function Oo(n,t,e,r){var u=n.id;return P(n,\"function\"==typeof e?function(n,i,o){n.__transition__[u].tween.set(t,r(e.call(n,n.__data__,i,o)))}:(e=r(e),function(n){n.__transition__[u].tween.set(t,e)}))}function Io(n){return null==n&&(n=\"\"),function(){this.textContent=n}}function Yo(n,t,e,r){var u=n.__transition__||(n.__transition__={active:0,count:0}),i=u[e];if(!i){var a=r.time;i=u[e]={tween:new o,time:a,ease:r.ease,delay:r.delay,duration:r.duration},++u.count,Go.timer(function(r){function o(r){return u.active>e?s():(u.active=e,i.event&&i.event.start.call(n,l,t),i.tween.forEach(function(e,r){(r=r.call(n,l,t))&&v.push(r)}),Go.timer(function(){return p.c=c(r||1)?Ae:c,1},0,a),void 0)}function c(r){if(u.active!==e)return s();for(var o=r/g,a=f(o),c=v.length;c>0;)v[--c].call(n,a);return o>=1?(i.event&&i.event.end.call(n,l,t),s()):void 0}function s(){return--u.count?delete u[e]:delete n.__transition__,1}var l=n.__data__,f=i.ease,h=i.delay,g=i.duration,p=nc,v=[];return p.t=h+a,r>=h?o(r-h):(p.c=o,void 0)},0,a)}}function Zo(n,t){n.attr(\"transform\",function(n){return\"translate(\"+t(n)+\",0)\"})}function Vo(n,t){n.attr(\"transform\",function(n){return\"translate(0,\"+t(n)+\")\"})}function $o(n){return n.toISOString()}function Xo(n,t,e){function r(t){return n(t)\n}function u(n,e){var r=n[1]-n[0],u=r/e,i=Go.bisect(Ys,u);return i==Ys.length?[t.year,Xi(n.map(function(n){return n/31536e6}),e)[2]]:i?t[u/Ys[i-1]<Ys[i]/u?i-1:i]:[$s,Xi(n,e)[2]]}return r.invert=function(t){return Bo(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain(t),r):n.domain().map(Bo)},r.nice=function(n,t){function e(e){return!isNaN(e)&&!n.range(e,Bo(+e+1),t).length}var i=r.domain(),o=ji(i),a=null==n?u(o,10):\"number\"==typeof n&&u(o,n);return a&&(n=a[0],t=a[1]),r.domain(Oi(i,t>1?{floor:function(t){for(;e(t=n.floor(t));)t=Bo(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=Bo(+t+1);return t}}:n))},r.ticks=function(n,t){var e=ji(r.domain()),i=null==n?u(e,10):\"number\"==typeof n?u(e,n):!n.range&&[{range:n},t];return i&&(n=i[0],t=i[1]),n.range(e[0],Bo(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return Xo(n.copy(),t,e)},Vi(r,n)}function Bo(n){return new Date(n)}function Jo(n){return JSON.parse(n.responseText)}function Wo(n){var t=na.createRange();return t.selectNode(na.body),t.createContextualFragment(n.responseText)}var Go={version:\"3.4.6\"};Date.now||(Date.now=function(){return+new Date});var Ko=[].slice,Qo=function(n){return Ko.call(n)},na=document,ta=na.documentElement,ea=window;try{Qo(ta.childNodes)[0].nodeType}catch(ra){Qo=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}try{na.createElement(\"div\").style.setProperty(\"opacity\",0,\"\")}catch(ua){var ia=ea.Element.prototype,oa=ia.setAttribute,aa=ia.setAttributeNS,ca=ea.CSSStyleDeclaration.prototype,sa=ca.setProperty;ia.setAttribute=function(n,t){oa.call(this,n,t+\"\")},ia.setAttributeNS=function(n,t,e){aa.call(this,n,t,e+\"\")},ca.setProperty=function(n,t,e){sa.call(this,n,t+\"\",e)}}Go.ascending=n,Go.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:0/0},Go.min=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u<i&&!(null!=(e=n[u])&&e>=e);)e=void 0;for(;++u<i;)null!=(r=n[u])&&e>r&&(e=r)}else{for(;++u<i&&!(null!=(e=t.call(n,n[u],u))&&e>=e);)e=void 0;for(;++u<i;)null!=(r=t.call(n,n[u],u))&&e>r&&(e=r)}return e},Go.max=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u<i&&!(null!=(e=n[u])&&e>=e);)e=void 0;for(;++u<i;)null!=(r=n[u])&&r>e&&(e=r)}else{for(;++u<i&&!(null!=(e=t.call(n,n[u],u))&&e>=e);)e=void 0;for(;++u<i;)null!=(r=t.call(n,n[u],u))&&r>e&&(e=r)}return e},Go.extent=function(n,t){var e,r,u,i=-1,o=n.length;if(1===arguments.length){for(;++i<o&&!(null!=(e=u=n[i])&&e>=e);)e=u=void 0;for(;++i<o;)null!=(r=n[i])&&(e>r&&(e=r),r>u&&(u=r))}else{for(;++i<o&&!(null!=(e=u=t.call(n,n[i],i))&&e>=e);)e=void 0;for(;++i<o;)null!=(r=t.call(n,n[i],i))&&(e>r&&(e=r),r>u&&(u=r))}return[e,u]},Go.sum=function(n,t){var e,r=0,u=n.length,i=-1;if(1===arguments.length)for(;++i<u;)isNaN(e=+n[i])||(r+=e);else for(;++i<u;)isNaN(e=+t.call(n,n[i],i))||(r+=e);return r},Go.mean=function(n,e){var r,u=0,i=n.length,o=-1,a=i;if(1===arguments.length)for(;++o<i;)t(r=n[o])?u+=r:--a;else for(;++o<i;)t(r=e.call(n,n[o],o))?u+=r:--a;return a?u/a:void 0},Go.quantile=function(n,t){var e=(n.length-1)*t+1,r=Math.floor(e),u=+n[r-1],i=e-r;return i?u+i*(n[r]-u):u},Go.median=function(e,r){return arguments.length>1&&(e=e.map(r)),e=e.filter(t),e.length?Go.quantile(e.sort(n),.5):void 0};var la=e(n);Go.bisectLeft=la.left,Go.bisect=Go.bisectRight=la.right,Go.bisector=function(t){return e(1===t.length?function(e,r){return n(t(e),r)}:t)},Go.shuffle=function(n){for(var t,e,r=n.length;r;)e=0|Math.random()*r--,t=n[r],n[r]=n[e],n[e]=t;return n},Go.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},Go.pairs=function(n){for(var t,e=0,r=n.length-1,u=n[0],i=new Array(0>r?0:r);r>e;)i[e]=[t=u,u=n[++e]];return i},Go.zip=function(){if(!(u=arguments.length))return[];for(var n=-1,t=Go.min(arguments,r),e=new Array(t);++n<t;)for(var u,i=-1,o=e[n]=new Array(u);++i<u;)o[i]=arguments[i][n];return e},Go.transpose=function(n){return Go.zip.apply(Go,n)},Go.keys=function(n){var t=[];for(var e in n)t.push(e);return t},Go.values=function(n){var t=[];for(var e in n)t.push(n[e]);return t},Go.entries=function(n){var t=[];for(var e in n)t.push({key:e,value:n[e]});return t},Go.merge=function(n){for(var t,e,r,u=n.length,i=-1,o=0;++i<u;)o+=n[i].length;for(e=new Array(o);--u>=0;)for(r=n[u],t=r.length;--t>=0;)e[--o]=r[t];return e};var fa=Math.abs;Go.range=function(n,t,e){if(arguments.length<3&&(e=1,arguments.length<2&&(t=n,n=0)),1/0===(t-n)/e)throw new Error(\"infinite range\");var r,i=[],o=u(fa(e)),a=-1;if(n*=o,t*=o,e*=o,0>e)for(;(r=n+e*++a)>t;)i.push(r/o);else for(;(r=n+e*++a)<t;)i.push(r/o);return i},Go.map=function(n){var t=new o;if(n instanceof o)n.forEach(function(n,e){t.set(n,e)});else for(var e in n)t.set(e,n[e]);return t},i(o,{has:a,get:function(n){return this[ha+n]},set:function(n,t){return this[ha+n]=t},remove:c,keys:s,values:function(){var n=[];return this.forEach(function(t,e){n.push(e)}),n},entries:function(){var n=[];return this.forEach(function(t,e){n.push({key:t,value:e})}),n},size:l,empty:f,forEach:function(n){for(var t in this)t.charCodeAt(0)===ga&&n.call(this,t.substring(1),this[t])}});var ha=\"\\x00\",ga=ha.charCodeAt(0);Go.nest=function(){function n(t,a,c){if(c>=i.length)return r?r.call(u,a):e?a.sort(e):a;for(var s,l,f,h,g=-1,p=a.length,v=i[c++],d=new o;++g<p;)(h=d.get(s=v(l=a[g])))?h.push(l):d.set(s,[l]);return t?(l=t(),f=function(e,r){l.set(e,n(t,r,c))}):(l={},f=function(e,r){l[e]=n(t,r,c)}),d.forEach(f),l}function t(n,e){if(e>=i.length)return n;var r=[],u=a[e++];return n.forEach(function(n,u){r.push({key:n,values:t(u,e)})}),u?r.sort(function(n,t){return u(n.key,t.key)}):r}var e,r,u={},i=[],a=[];return u.map=function(t,e){return n(e,t,0)},u.entries=function(e){return t(n(Go.map,e,0),0)},u.key=function(n){return i.push(n),u},u.sortKeys=function(n){return a[i.length-1]=n,u},u.sortValues=function(n){return e=n,u},u.rollup=function(n){return r=n,u},u},Go.set=function(n){var t=new h;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},i(h,{has:a,add:function(n){return this[ha+n]=!0,n},remove:function(n){return n=ha+n,n in this&&delete this[n]},values:s,size:l,empty:f,forEach:function(n){for(var t in this)t.charCodeAt(0)===ga&&n.call(this,t.substring(1))}}),Go.behavior={},Go.rebind=function(n,t){for(var e,r=1,u=arguments.length;++r<u;)n[e=arguments[r]]=g(n,t,t[e]);return n};var pa=[\"webkit\",\"ms\",\"moz\",\"Moz\",\"o\",\"O\"];Go.dispatch=function(){for(var n=new d,t=-1,e=arguments.length;++t<e;)n[arguments[t]]=m(n);return n},d.prototype.on=function(n,t){var e=n.indexOf(\".\"),r=\"\";if(e>=0&&(r=n.substring(e+1),n=n.substring(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},Go.event=null,Go.requote=function(n){return n.replace(va,\"\\\\$&\")};var va=/[\\\\\\^\\$\\*\\+\\?\\|\\[\\]\\(\\)\\.\\{\\}]/g,da={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},ma=function(n,t){return t.querySelector(n)},ya=function(n,t){return t.querySelectorAll(n)},xa=ta[p(ta,\"matchesSelector\")],Ma=function(n,t){return xa.call(n,t)};\"function\"==typeof Sizzle&&(ma=function(n,t){return Sizzle(n,t)[0]||null},ya=Sizzle,Ma=Sizzle.matchesSelector),Go.selection=function(){return Sa};var _a=Go.selection.prototype=[];_a.select=function(n){var t,e,r,u,i=[];n=b(n);for(var o=-1,a=this.length;++o<a;){i.push(t=[]),t.parentNode=(r=this[o]).parentNode;for(var c=-1,s=r.length;++c<s;)(u=r[c])?(t.push(e=n.call(u,u.__data__,c,o)),e&&\"__data__\"in u&&(e.__data__=u.__data__)):t.push(null)}return _(i)},_a.selectAll=function(n){var t,e,r=[];n=w(n);for(var u=-1,i=this.length;++u<i;)for(var o=this[u],a=-1,c=o.length;++a<c;)(e=o[a])&&(r.push(t=Qo(n.call(e,e.__data__,a,u))),t.parentNode=e);return _(r)};var ba={svg:\"http://www.w3.org/2000/svg\",xhtml:\"http://www.w3.org/1999/xhtml\",xlink:\"http://www.w3.org/1999/xlink\",xml:\"http://www.w3.org/XML/1998/namespace\",xmlns:\"http://www.w3.org/2000/xmlns/\"};Go.ns={prefix:ba,qualify:function(n){var t=n.indexOf(\":\"),e=n;return t>=0&&(e=n.substring(0,t),n=n.substring(t+1)),ba.hasOwnProperty(e)?{space:ba[e],local:n}:n}},_a.attr=function(n,t){if(arguments.length<2){if(\"string\"==typeof n){var e=this.node();return n=Go.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(S(t,n[t]));return this}return this.each(S(n,t))},_a.classed=function(n,t){if(arguments.length<2){if(\"string\"==typeof n){var e=this.node(),r=(n=A(n)).length,u=-1;if(t=e.classList){for(;++u<r;)if(!t.contains(n[u]))return!1}else for(t=e.getAttribute(\"class\");++u<r;)if(!E(n[u]).test(t))return!1;return!0}for(t in n)this.each(C(t,n[t]));return this}return this.each(C(n,t))},_a.style=function(n,t,e){var r=arguments.length;if(3>r){if(\"string\"!=typeof n){2>r&&(t=\"\");for(e in n)this.each(L(e,n[e],t));return this}if(2>r)return ea.getComputedStyle(this.node(),null).getPropertyValue(n);e=\"\"}return this.each(L(n,t,e))},_a.property=function(n,t){if(arguments.length<2){if(\"string\"==typeof n)return this.node()[n];for(t in n)this.each(T(t,n[t]));return this}return this.each(T(n,t))},_a.text=function(n){return arguments.length?this.each(\"function\"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?\"\":t}:null==n?function(){this.textContent=\"\"}:function(){this.textContent=n}):this.node().textContent},_a.html=function(n){return arguments.length?this.each(\"function\"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?\"\":t}:null==n?function(){this.innerHTML=\"\"}:function(){this.innerHTML=n}):this.node().innerHTML},_a.append=function(n){return n=q(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},_a.insert=function(n,t){return n=q(n),t=b(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},_a.remove=function(){return this.each(function(){var n=this.parentNode;n&&n.removeChild(this)})},_a.data=function(n,t){function e(n,e){var r,u,i,a=n.length,f=e.length,h=Math.min(a,f),g=new Array(f),p=new Array(f),v=new Array(a);if(t){var d,m=new o,y=new o,x=[];for(r=-1;++r<a;)d=t.call(u=n[r],u.__data__,r),m.has(d)?v[r]=u:m.set(d,u),x.push(d);for(r=-1;++r<f;)d=t.call(e,i=e[r],r),(u=m.get(d))?(g[r]=u,u.__data__=i):y.has(d)||(p[r]=z(i)),y.set(d,i),m.remove(d);for(r=-1;++r<a;)m.has(x[r])&&(v[r]=n[r])}else{for(r=-1;++r<h;)u=n[r],i=e[r],u?(u.__data__=i,g[r]=u):p[r]=z(i);for(;f>r;++r)p[r]=z(e[r]);for(;a>r;++r)v[r]=n[r]}p.update=g,p.parentNode=g.parentNode=v.parentNode=n.parentNode,c.push(p),s.push(g),l.push(v)}var r,u,i=-1,a=this.length;if(!arguments.length){for(n=new Array(a=(r=this[0]).length);++i<a;)(u=r[i])&&(n[i]=u.__data__);return n}var c=U([]),s=_([]),l=_([]);if(\"function\"==typeof n)for(;++i<a;)e(r=this[i],n.call(r,r.parentNode.__data__,i));else for(;++i<a;)e(r=this[i],n);return s.enter=function(){return c},s.exit=function(){return l},s},_a.datum=function(n){return arguments.length?this.property(\"__data__\",n):this.property(\"__data__\")},_a.filter=function(n){var t,e,r,u=[];\"function\"!=typeof n&&(n=R(n));for(var i=0,o=this.length;o>i;i++){u.push(t=[]),t.parentNode=(e=this[i]).parentNode;for(var a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return _(u)},_a.order=function(){for(var n=-1,t=this.length;++n<t;)for(var e,r=this[n],u=r.length-1,i=r[u];--u>=0;)(e=r[u])&&(i&&i!==e.nextSibling&&i.parentNode.insertBefore(e,i),i=e);return this},_a.sort=function(n){n=D.apply(this,arguments);for(var t=-1,e=this.length;++t<e;)this[t].sort(n);return this.order()},_a.each=function(n){return P(this,function(t,e,r){n.call(t,t.__data__,e,r)})},_a.call=function(n){var t=Qo(arguments);return n.apply(t[0]=this,t),this},_a.empty=function(){return!this.node()},_a.node=function(){for(var n=0,t=this.length;t>n;n++)for(var e=this[n],r=0,u=e.length;u>r;r++){var i=e[r];if(i)return i}return null},_a.size=function(){var n=0;return this.each(function(){++n}),n};var wa=[];Go.selection.enter=U,Go.selection.enter.prototype=wa,wa.append=_a.append,wa.empty=_a.empty,wa.node=_a.node,wa.call=_a.call,wa.size=_a.size,wa.select=function(n){for(var t,e,r,u,i,o=[],a=-1,c=this.length;++a<c;){r=(u=this[a]).update,o.push(t=[]),t.parentNode=u.parentNode;for(var s=-1,l=u.length;++s<l;)(i=u[s])?(t.push(r[s]=e=n.call(u.parentNode,i.__data__,s,a)),e.__data__=i.__data__):t.push(null)}return _(o)},wa.insert=function(n,t){return arguments.length<2&&(t=j(this)),_a.insert.call(this,n,t)},_a.transition=function(){for(var n,t,e=Ls||++Ds,r=[],u=Ts||{time:Date.now(),ease:wu,delay:0,duration:250},i=-1,o=this.length;++i<o;){r.push(n=[]);for(var a=this[i],c=-1,s=a.length;++c<s;)(t=a[c])&&Yo(t,c,e,u),n.push(t)}return Fo(r,e)},_a.interrupt=function(){return this.each(H)},Go.select=function(n){var t=[\"string\"==typeof n?ma(n,na):n];return t.parentNode=ta,_([t])},Go.selectAll=function(n){var t=Qo(\"string\"==typeof n?ya(n,na):n);return t.parentNode=ta,_([t])};var Sa=Go.select(ta);_a.on=function(n,t,e){var r=arguments.length;if(3>r){if(\"string\"!=typeof n){2>r&&(t=!1);for(e in n)this.each(F(e,n[e],t));return this}if(2>r)return(r=this.node()[\"__on\"+n])&&r._;e=!1}return this.each(F(n,t,e))};var ka=Go.map({mouseenter:\"mouseover\",mouseleave:\"mouseout\"});ka.forEach(function(n){\"on\"+n in na&&ka.remove(n)});var Ea=\"onselectstart\"in na?null:p(ta.style,\"userSelect\"),Aa=0;Go.mouse=function(n){return Z(n,x())},Go.touches=function(n,t){return arguments.length<2&&(t=x().touches),t?Qo(t).map(function(t){var e=Z(n,t);return e.identifier=t.identifier,e}):[]},Go.behavior.drag=function(){function n(){this.on(\"mousedown.drag\",u).on(\"touchstart.drag\",i)}function t(n,t,u,i,o){return function(){function a(){var n,e,r=t(h,v);r&&(n=r[0]-x[0],e=r[1]-x[1],p|=n|e,x=r,g({type:\"drag\",x:r[0]+s[0],y:r[1]+s[1],dx:n,dy:e}))}function c(){t(h,v)&&(m.on(i+d,null).on(o+d,null),y(p&&Go.event.target===f),g({type:\"dragend\"}))}var s,l=this,f=Go.event.target,h=l.parentNode,g=e.of(l,arguments),p=0,v=n(),d=\".drag\"+(null==v?\"\":\"-\"+v),m=Go.select(u()).on(i+d,a).on(o+d,c),y=Y(),x=t(h,v);r?(s=r.apply(l,arguments),s=[s.x-x[0],s.y-x[1]]):s=[0,0],g({type:\"dragstart\"})}}var e=M(n,\"drag\",\"dragstart\",\"dragend\"),r=null,u=t(v,Go.mouse,X,\"mousemove\",\"mouseup\"),i=t(V,Go.touch,$,\"touchmove\",\"touchend\");return n.origin=function(t){return arguments.length?(r=t,n):r},Go.rebind(n,e,\"on\")};var Ca=Math.PI,Na=2*Ca,La=Ca/2,Ta=1e-6,qa=Ta*Ta,za=Ca/180,Ra=180/Ca,Da=Math.SQRT2,Pa=2,Ua=4;Go.interpolateZoom=function(n,t){function e(n){var t=n*y;if(m){var e=Q(v),o=i/(Pa*h)*(e*nt(Da*t+v)-K(v));return[r+o*s,u+o*l,i*e/Q(Da*t+v)]}return[r+n*s,u+n*l,i*Math.exp(Da*t)]}var r=n[0],u=n[1],i=n[2],o=t[0],a=t[1],c=t[2],s=o-r,l=a-u,f=s*s+l*l,h=Math.sqrt(f),g=(c*c-i*i+Ua*f)/(2*i*Pa*h),p=(c*c-i*i-Ua*f)/(2*c*Pa*h),v=Math.log(Math.sqrt(g*g+1)-g),d=Math.log(Math.sqrt(p*p+1)-p),m=d-v,y=(m||Math.log(c/i))/Da;return e.duration=1e3*y,e},Go.behavior.zoom=function(){function n(n){n.on(A,s).on(Fa+\".zoom\",f).on(C,h).on(\"dblclick.zoom\",g).on(L,l)}function t(n){return[(n[0]-S.x)/S.k,(n[1]-S.y)/S.k]}function e(n){return[n[0]*S.k+S.x,n[1]*S.k+S.y]}function r(n){S.k=Math.max(E[0],Math.min(E[1],n))}function u(n,t){t=e(t),S.x+=n[0]-t[0],S.y+=n[1]-t[1]}function i(){_&&_.domain(x.range().map(function(n){return(n-S.x)/S.k}).map(x.invert)),w&&w.domain(b.range().map(function(n){return(n-S.y)/S.k}).map(b.invert))}function o(n){n({type:\"zoomstart\"})}function a(n){i(),n({type:\"zoom\",scale:S.k,translate:[S.x,S.y]})}function c(n){n({type:\"zoomend\"})}function s(){function n(){l=1,u(Go.mouse(r),g),a(s)}function e(){f.on(C,ea===r?h:null).on(N,null),p(l&&Go.event.target===i),c(s)}var r=this,i=Go.event.target,s=T.of(r,arguments),l=0,f=Go.select(ea).on(C,n).on(N,e),g=t(Go.mouse(r)),p=Y();H.call(r),o(s)}function l(){function n(){var n=Go.touches(g);return h=S.k,n.forEach(function(n){n.identifier in v&&(v[n.identifier]=t(n))}),n}function e(){for(var t=Go.event.changedTouches,e=0,i=t.length;i>e;++e)v[t[e].identifier]=null;var o=n(),c=Date.now();if(1===o.length){if(500>c-m){var s=o[0],l=v[s.identifier];r(2*S.k),u(s,l),y(),a(p)}m=c}else if(o.length>1){var s=o[0],f=o[1],h=s[0]-f[0],g=s[1]-f[1];d=h*h+g*g}}function i(){for(var n,t,e,i,o=Go.touches(g),c=0,s=o.length;s>c;++c,i=null)if(e=o[c],i=v[e.identifier]){if(t)break;n=e,t=i}if(i){var l=(l=e[0]-n[0])*l+(l=e[1]-n[1])*l,f=d&&Math.sqrt(l/d);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+i[0])/2,(t[1]+i[1])/2],r(f*h)}m=null,u(n,t),a(p)}function f(){if(Go.event.touches.length){for(var t=Go.event.changedTouches,e=0,r=t.length;r>e;++e)delete v[t[e].identifier];for(var u in v)return void n()}b.on(x,null),w.on(A,s).on(L,l),k(),c(p)}var h,g=this,p=T.of(g,arguments),v={},d=0,x=\".zoom-\"+Go.event.changedTouches[0].identifier,M=\"touchmove\"+x,_=\"touchend\"+x,b=Go.select(Go.event.target).on(M,i).on(_,f),w=Go.select(g).on(A,null).on(L,e),k=Y();H.call(g),e(),o(p)}function f(){var n=T.of(this,arguments);d?clearTimeout(d):(H.call(this),o(n)),d=setTimeout(function(){d=null,c(n)},50),y();var e=v||Go.mouse(this);p||(p=t(e)),r(Math.pow(2,.002*ja())*S.k),u(e,p),a(n)}function h(){p=null}function g(){var n=T.of(this,arguments),e=Go.mouse(this),i=t(e),s=Math.log(S.k)/Math.LN2;o(n),r(Math.pow(2,Go.event.shiftKey?Math.ceil(s)-1:Math.floor(s)+1)),u(e,i),a(n),c(n)}var p,v,d,m,x,_,b,w,S={x:0,y:0,k:1},k=[960,500],E=Ha,A=\"mousedown.zoom\",C=\"mousemove.zoom\",N=\"mouseup.zoom\",L=\"touchstart.zoom\",T=M(n,\"zoomstart\",\"zoom\",\"zoomend\");return n.event=function(n){n.each(function(){var n=T.of(this,arguments),t=S;Ls?Go.select(this).transition().each(\"start.zoom\",function(){S=this.__chart__||{x:0,y:0,k:1},o(n)}).tween(\"zoom:zoom\",function(){var e=k[0],r=k[1],u=e/2,i=r/2,o=Go.interpolateZoom([(u-S.x)/S.k,(i-S.y)/S.k,e/S.k],[(u-t.x)/t.k,(i-t.y)/t.k,e/t.k]);return function(t){var r=o(t),c=e/r[2];this.__chart__=S={x:u-r[0]*c,y:i-r[1]*c,k:c},a(n)}}).each(\"end.zoom\",function(){c(n)}):(this.__chart__=S,o(n),a(n),c(n))})},n.translate=function(t){return arguments.length?(S={x:+t[0],y:+t[1],k:S.k},i(),n):[S.x,S.y]},n.scale=function(t){return arguments.length?(S={x:S.x,y:S.y,k:+t},i(),n):S.k},n.scaleExtent=function(t){return arguments.length?(E=null==t?Ha:[+t[0],+t[1]],n):E},n.center=function(t){return arguments.length?(v=t&&[+t[0],+t[1]],n):v},n.size=function(t){return arguments.length?(k=t&&[+t[0],+t[1]],n):k},n.x=function(t){return arguments.length?(_=t,x=t.copy(),S={x:0,y:0,k:1},n):_},n.y=function(t){return arguments.length?(w=t,b=t.copy(),S={x:0,y:0,k:1},n):w},Go.rebind(n,T,\"on\")};var ja,Ha=[0,1/0],Fa=\"onwheel\"in na?(ja=function(){return-Go.event.deltaY*(Go.event.deltaMode?120:1)},\"wheel\"):\"onmousewheel\"in na?(ja=function(){return Go.event.wheelDelta},\"mousewheel\"):(ja=function(){return-Go.event.detail},\"MozMousePixelScroll\");et.prototype.toString=function(){return this.rgb()+\"\"},Go.hsl=function(n,t,e){return 1===arguments.length?n instanceof ut?rt(n.h,n.s,n.l):_t(\"\"+n,bt,rt):rt(+n,+t,+e)};var Oa=ut.prototype=new et;Oa.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),rt(this.h,this.s,this.l/n)},Oa.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),rt(this.h,this.s,n*this.l)},Oa.rgb=function(){return it(this.h,this.s,this.l)},Go.hcl=function(n,t,e){return 1===arguments.length?n instanceof at?ot(n.h,n.c,n.l):n instanceof lt?ht(n.l,n.a,n.b):ht((n=wt((n=Go.rgb(n)).r,n.g,n.b)).l,n.a,n.b):ot(+n,+t,+e)};var Ia=at.prototype=new et;Ia.brighter=function(n){return ot(this.h,this.c,Math.min(100,this.l+Ya*(arguments.length?n:1)))},Ia.darker=function(n){return ot(this.h,this.c,Math.max(0,this.l-Ya*(arguments.length?n:1)))},Ia.rgb=function(){return ct(this.h,this.c,this.l).rgb()},Go.lab=function(n,t,e){return 1===arguments.length?n instanceof lt?st(n.l,n.a,n.b):n instanceof at?ct(n.l,n.c,n.h):wt((n=Go.rgb(n)).r,n.g,n.b):st(+n,+t,+e)};var Ya=18,Za=.95047,Va=1,$a=1.08883,Xa=lt.prototype=new et;Xa.brighter=function(n){return st(Math.min(100,this.l+Ya*(arguments.length?n:1)),this.a,this.b)},Xa.darker=function(n){return st(Math.max(0,this.l-Ya*(arguments.length?n:1)),this.a,this.b)},Xa.rgb=function(){return ft(this.l,this.a,this.b)},Go.rgb=function(n,t,e){return 1===arguments.length?n instanceof xt?yt(n.r,n.g,n.b):_t(\"\"+n,yt,it):yt(~~n,~~t,~~e)};var Ba=xt.prototype=new et;Ba.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,u=30;return t||e||r?(t&&u>t&&(t=u),e&&u>e&&(e=u),r&&u>r&&(r=u),yt(Math.min(255,~~(t/n)),Math.min(255,~~(e/n)),Math.min(255,~~(r/n)))):yt(u,u,u)},Ba.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),yt(~~(n*this.r),~~(n*this.g),~~(n*this.b))},Ba.hsl=function(){return bt(this.r,this.g,this.b)},Ba.toString=function(){return\"#\"+Mt(this.r)+Mt(this.g)+Mt(this.b)};var Ja=Go.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});Ja.forEach(function(n,t){Ja.set(n,dt(t))}),Go.functor=Et,Go.xhr=Ct(At),Go.dsv=function(n,t){function e(n,e,i){arguments.length<3&&(i=e,e=null);var o=Nt(n,t,null==e?r:u(e),i);return o.row=function(n){return arguments.length?o.response(null==(e=n)?r:u(n)):e},o}function r(n){return e.parse(n.responseText)}function u(n){return function(t){return e.parse(t.responseText,n)}}function i(t){return t.map(o).join(n)}function o(n){return a.test(n)?'\"'+n.replace(/\\\"/g,'\"\"')+'\"':n}var a=new RegExp('[\"'+n+\"\\n]\"),c=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var u=new Function(\"d\",\"return {\"+n.map(function(n,t){return JSON.stringify(n)+\": d[\"+t+\"]\"}).join(\",\")+\"}\");r=t?function(n,e){return t(u(n),e)}:u})},e.parseRows=function(n,t){function e(){if(l>=s)return o;if(u)return u=!1,i;var t=l;if(34===n.charCodeAt(t)){for(var e=t;e++<s;)if(34===n.charCodeAt(e)){if(34!==n.charCodeAt(e+1))break;++e}l=e+2;var r=n.charCodeAt(e+1);return 13===r?(u=!0,10===n.charCodeAt(e+2)&&++l):10===r&&(u=!0),n.substring(t+1,e).replace(/\"\"/g,'\"')}for(;s>l;){var r=n.charCodeAt(l++),a=1;if(10===r)u=!0;else if(13===r)u=!0,10===n.charCodeAt(l)&&(++l,++a);else if(r!==c)continue;return n.substring(t,l-a)}return n.substring(t)}for(var r,u,i={},o={},a=[],s=n.length,l=0,f=0;(r=e())!==o;){for(var h=[];r!==i&&r!==o;)h.push(r),r=e();(!t||(h=t(h,f++)))&&a.push(h)}return a},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new h,u=[];return t.forEach(function(n){for(var t in n)r.has(t)||u.push(r.add(t))}),[u.map(o).join(n)].concat(t.map(function(t){return u.map(function(n){return o(t[n])}).join(n)})).join(\"\\n\")},e.formatRows=function(n){return n.map(i).join(\"\\n\")},e},Go.csv=Go.dsv(\",\",\"text/csv\"),Go.tsv=Go.dsv(\"\t\",\"text/tab-separated-values\"),Go.touch=function(n,t,e){if(arguments.length<3&&(e=t,t=x().changedTouches),t)for(var r,u=0,i=t.length;i>u;++u)if((r=t[u]).identifier===e)return Z(n,r)};var Wa,Ga,Ka,Qa,nc,tc=ea[p(ea,\"requestAnimationFrame\")]||function(n){setTimeout(n,17)};Go.timer=function(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var u=e+t,i={c:n,t:u,f:!1,n:null};Ga?Ga.n=i:Wa=i,Ga=i,Ka||(Qa=clearTimeout(Qa),Ka=1,tc(Tt))},Go.timer.flush=function(){qt(),zt()},Go.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var ec=[\"y\",\"z\",\"a\",\"f\",\"p\",\"n\",\"\\xb5\",\"m\",\"\",\"k\",\"M\",\"G\",\"T\",\"P\",\"E\",\"Z\",\"Y\"].map(Dt);Go.formatPrefix=function(n,t){var e=0;return n&&(0>n&&(n*=-1),t&&(n=Go.round(n,Rt(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((e-1)/3)))),ec[8+e/3]};var rc=/(?:([^{])?([<>=^]))?([+\\- ])?([$#])?(0)?(\\d+)?(,)?(\\.-?\\d+)?([a-z%])?/i,uc=Go.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=Go.round(n,Rt(n,t))).toFixed(Math.max(0,Math.min(20,Rt(n*(1+1e-15),t))))}}),ic=Go.time={},oc=Date;jt.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){ac.setUTCDate.apply(this._,arguments)},setDay:function(){ac.setUTCDay.apply(this._,arguments)},setFullYear:function(){ac.setUTCFullYear.apply(this._,arguments)},setHours:function(){ac.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){ac.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){ac.setUTCMinutes.apply(this._,arguments)},setMonth:function(){ac.setUTCMonth.apply(this._,arguments)},setSeconds:function(){ac.setUTCSeconds.apply(this._,arguments)},setTime:function(){ac.setTime.apply(this._,arguments)}};var ac=Date.prototype;ic.year=Ht(function(n){return n=ic.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),ic.years=ic.year.range,ic.years.utc=ic.year.utc.range,ic.day=Ht(function(n){var t=new oc(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),ic.days=ic.day.range,ic.days.utc=ic.day.utc.range,ic.dayOfYear=function(n){var t=ic.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},[\"sunday\",\"monday\",\"tuesday\",\"wednesday\",\"thursday\",\"friday\",\"saturday\"].forEach(function(n,t){t=7-t;var e=ic[n]=Ht(function(n){return(n=ic.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=ic.year(n).getDay();return Math.floor((ic.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});ic[n+\"s\"]=e.range,ic[n+\"s\"].utc=e.utc.range,ic[n+\"OfYear\"]=function(n){var e=ic.year(n).getDay();return Math.floor((ic.dayOfYear(n)+(e+t)%7)/7)}}),ic.week=ic.sunday,ic.weeks=ic.sunday.range,ic.weeks.utc=ic.sunday.utc.range,ic.weekOfYear=ic.sundayOfYear;var cc={\"-\":\"\",_:\" \",0:\"0\"},sc=/^\\s*\\d+/,lc=/^%/;Go.locale=function(n){return{numberFormat:Pt(n),timeFormat:Ot(n)}};var fc=Go.locale({decimal:\".\",thousands:\",\",grouping:[3],currency:[\"$\",\"\"],dateTime:\"%a %b %e %X %Y\",date:\"%m/%d/%Y\",time:\"%H:%M:%S\",periods:[\"AM\",\"PM\"],days:[\"Sunday\",\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\",\"Saturday\"],shortDays:[\"Sun\",\"Mon\",\"Tue\",\"Wed\",\"Thu\",\"Fri\",\"Sat\"],months:[\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"],shortMonths:[\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"]});Go.format=fc.numberFormat,Go.geo={},ce.prototype={s:0,t:0,add:function(n){se(n,this.t,hc),se(hc.s,this.s,this),this.s?this.t+=hc.t:this.s=hc.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var hc=new ce;Go.geo.stream=function(n,t){n&&gc.hasOwnProperty(n.type)?gc[n.type](n,t):le(n,t)};var gc={Feature:function(n,t){le(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,u=e.length;++r<u;)le(e[r].geometry,t)}},pc={Sphere:function(n,t){t.sphere()},Point:function(n,t){n=n.coordinates,t.point(n[0],n[1],n[2])},MultiPoint:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)n=e[r],t.point(n[0],n[1],n[2])},LineString:function(n,t){fe(n.coordinates,t,0)},MultiLineString:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)fe(e[r],t,0)},Polygon:function(n,t){he(n.coordinates,t)},MultiPolygon:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)he(e[r],t)},GeometryCollection:function(n,t){for(var e=n.geometries,r=-1,u=e.length;++r<u;)le(e[r],t)}};Go.geo.area=function(n){return vc=0,Go.geo.stream(n,mc),vc};var vc,dc=new ce,mc={sphere:function(){vc+=4*Ca},point:v,lineStart:v,lineEnd:v,polygonStart:function(){dc.reset(),mc.lineStart=ge},polygonEnd:function(){var n=2*dc;vc+=0>n?4*Ca+n:n,mc.lineStart=mc.lineEnd=mc.point=v}};Go.geo.bounds=function(){function n(n,t){x.push(M=[l=n,h=n]),f>t&&(f=t),t>g&&(g=t)}function t(t,e){var r=pe([t*za,e*za]);if(m){var u=de(m,r),i=[u[1],-u[0],0],o=de(i,u);xe(o),o=Me(o);var c=t-p,s=c>0?1:-1,v=o[0]*Ra*s,d=fa(c)>180;if(d^(v>s*p&&s*t>v)){var y=o[1]*Ra;y>g&&(g=y)}else if(v=(v+360)%360-180,d^(v>s*p&&s*t>v)){var y=-o[1]*Ra;f>y&&(f=y)}else f>e&&(f=e),e>g&&(g=e);d?p>t?a(l,t)>a(l,h)&&(h=t):a(t,h)>a(l,h)&&(l=t):h>=l?(l>t&&(l=t),t>h&&(h=t)):t>p?a(l,t)>a(l,h)&&(h=t):a(t,h)>a(l,h)&&(l=t)}else n(t,e);m=r,p=t}function e(){_.point=t}function r(){M[0]=l,M[1]=h,_.point=n,m=null}function u(n,e){if(m){var r=n-p;y+=fa(r)>180?r+(r>0?360:-360):r}else v=n,d=e;mc.point(n,e),t(n,e)}function i(){mc.lineStart()}function o(){u(v,d),mc.lineEnd(),fa(y)>Ta&&(l=-(h=180)),M[0]=l,M[1]=h,m=null}function a(n,t){return(t-=n)<0?t+360:t}function c(n,t){return n[0]-t[0]}function s(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:n<t[0]||t[1]<n}var l,f,h,g,p,v,d,m,y,x,M,_={point:n,lineStart:e,lineEnd:r,polygonStart:function(){_.point=u,_.lineStart=i,_.lineEnd=o,y=0,mc.polygonStart()},polygonEnd:function(){mc.polygonEnd(),_.point=n,_.lineStart=e,_.lineEnd=r,0>dc?(l=-(h=180),f=-(g=90)):y>Ta?g=90:-Ta>y&&(f=-90),M[0]=l,M[1]=h}};return function(n){g=h=-(l=f=1/0),x=[],Go.geo.stream(n,_);var t=x.length;if(t){x.sort(c);for(var e,r=1,u=x[0],i=[u];t>r;++r)e=x[r],s(e[0],u)||s(e[1],u)?(a(u[0],e[1])>a(u[0],u[1])&&(u[1]=e[1]),a(e[0],u[1])>a(u[0],u[1])&&(u[0]=e[0])):i.push(u=e);\nfor(var o,e,p=-1/0,t=i.length-1,r=0,u=i[t];t>=r;u=e,++r)e=i[r],(o=a(u[1],e[0]))>p&&(p=o,l=e[0],h=u[1])}return x=M=null,1/0===l||1/0===f?[[0/0,0/0],[0/0,0/0]]:[[l,f],[h,g]]}}(),Go.geo.centroid=function(n){yc=xc=Mc=_c=bc=wc=Sc=kc=Ec=Ac=Cc=0,Go.geo.stream(n,Nc);var t=Ec,e=Ac,r=Cc,u=t*t+e*e+r*r;return qa>u&&(t=wc,e=Sc,r=kc,Ta>xc&&(t=Mc,e=_c,r=bc),u=t*t+e*e+r*r,qa>u)?[0/0,0/0]:[Math.atan2(e,t)*Ra,G(r/Math.sqrt(u))*Ra]};var yc,xc,Mc,_c,bc,wc,Sc,kc,Ec,Ac,Cc,Nc={sphere:v,point:be,lineStart:Se,lineEnd:ke,polygonStart:function(){Nc.lineStart=Ee},polygonEnd:function(){Nc.lineStart=Se}},Lc=Te(Ae,Pe,je,[-Ca,-Ca/2]),Tc=1e9;Go.geo.clipExtent=function(){var n,t,e,r,u,i,o={stream:function(n){return u&&(u.valid=!1),u=i(n),u.valid=!0,u},extent:function(a){return arguments.length?(i=Oe(n=+a[0][0],t=+a[0][1],e=+a[1][0],r=+a[1][1]),u&&(u.valid=!1,u=null),o):[[n,t],[e,r]]}};return o.extent([[0,0],[960,500]])},(Go.geo.conicEqualArea=function(){return Ye(Ze)}).raw=Ze,Go.geo.albers=function(){return Go.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},Go.geo.albersUsa=function(){function n(n){var i=n[0],o=n[1];return t=null,e(i,o),t||(r(i,o),t)||u(i,o),t}var t,e,r,u,i=Go.geo.albers(),o=Go.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),a=Go.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),c={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=i.scale(),e=i.translate(),r=(n[0]-e[0])/t,u=(n[1]-e[1])/t;return(u>=.12&&.234>u&&r>=-.425&&-.214>r?o:u>=.166&&.234>u&&r>=-.214&&-.115>r?a:i).invert(n)},n.stream=function(n){var t=i.stream(n),e=o.stream(n),r=a.stream(n);return{point:function(n,u){t.point(n,u),e.point(n,u),r.point(n,u)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(i.precision(t),o.precision(t),a.precision(t),n):i.precision()},n.scale=function(t){return arguments.length?(i.scale(t),o.scale(.35*t),a.scale(t),n.translate(i.translate())):i.scale()},n.translate=function(t){if(!arguments.length)return i.translate();var s=i.scale(),l=+t[0],f=+t[1];return e=i.translate(t).clipExtent([[l-.455*s,f-.238*s],[l+.455*s,f+.238*s]]).stream(c).point,r=o.translate([l-.307*s,f+.201*s]).clipExtent([[l-.425*s+Ta,f+.12*s+Ta],[l-.214*s-Ta,f+.234*s-Ta]]).stream(c).point,u=a.translate([l-.205*s,f+.212*s]).clipExtent([[l-.214*s+Ta,f+.166*s+Ta],[l-.115*s-Ta,f+.234*s-Ta]]).stream(c).point,n},n.scale(1070)};var qc,zc,Rc,Dc,Pc,Uc,jc={point:v,lineStart:v,lineEnd:v,polygonStart:function(){zc=0,jc.lineStart=Ve},polygonEnd:function(){jc.lineStart=jc.lineEnd=jc.point=v,qc+=fa(zc/2)}},Hc={point:$e,lineStart:v,lineEnd:v,polygonStart:v,polygonEnd:v},Fc={point:Je,lineStart:We,lineEnd:Ge,polygonStart:function(){Fc.lineStart=Ke},polygonEnd:function(){Fc.point=Je,Fc.lineStart=We,Fc.lineEnd=Ge}};Go.geo.path=function(){function n(n){return n&&(\"function\"==typeof a&&i.pointRadius(+a.apply(this,arguments)),o&&o.valid||(o=u(i)),Go.geo.stream(n,o)),i.result()}function t(){return o=null,n}var e,r,u,i,o,a=4.5;return n.area=function(n){return qc=0,Go.geo.stream(n,u(jc)),qc},n.centroid=function(n){return Mc=_c=bc=wc=Sc=kc=Ec=Ac=Cc=0,Go.geo.stream(n,u(Fc)),Cc?[Ec/Cc,Ac/Cc]:kc?[wc/kc,Sc/kc]:bc?[Mc/bc,_c/bc]:[0/0,0/0]},n.bounds=function(n){return Pc=Uc=-(Rc=Dc=1/0),Go.geo.stream(n,u(Hc)),[[Rc,Dc],[Pc,Uc]]},n.projection=function(n){return arguments.length?(u=(e=n)?n.stream||tr(n):At,t()):e},n.context=function(n){return arguments.length?(i=null==(r=n)?new Xe:new Qe(n),\"function\"!=typeof a&&i.pointRadius(a),t()):r},n.pointRadius=function(t){return arguments.length?(a=\"function\"==typeof t?t:(i.pointRadius(+t),+t),n):a},n.projection(Go.geo.albersUsa()).context(null)},Go.geo.transform=function(n){return{stream:function(t){var e=new er(t);for(var r in n)e[r]=n[r];return e}}},er.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},Go.geo.projection=ur,Go.geo.projectionMutator=ir,(Go.geo.equirectangular=function(){return ur(ar)}).raw=ar.invert=ar,Go.geo.rotation=function(n){function t(t){return t=n(t[0]*za,t[1]*za),t[0]*=Ra,t[1]*=Ra,t}return n=sr(n[0]%360*za,n[1]*za,n.length>2?n[2]*za:0),t.invert=function(t){return t=n.invert(t[0]*za,t[1]*za),t[0]*=Ra,t[1]*=Ra,t},t},cr.invert=ar,Go.geo.circle=function(){function n(){var n=\"function\"==typeof r?r.apply(this,arguments):r,t=sr(-n[0]*za,-n[1]*za,0).invert,u=[];return e(null,null,1,{point:function(n,e){u.push(n=t(n,e)),n[0]*=Ra,n[1]*=Ra}}),{type:\"Polygon\",coordinates:[u]}}var t,e,r=[0,0],u=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=gr((t=+r)*za,u*za),n):t},n.precision=function(r){return arguments.length?(e=gr(t*za,(u=+r)*za),n):u},n.angle(90)},Go.geo.distance=function(n,t){var e,r=(t[0]-n[0])*za,u=n[1]*za,i=t[1]*za,o=Math.sin(r),a=Math.cos(r),c=Math.sin(u),s=Math.cos(u),l=Math.sin(i),f=Math.cos(i);return Math.atan2(Math.sqrt((e=f*o)*e+(e=s*l-c*f*a)*e),c*l+s*f*a)},Go.geo.graticule=function(){function n(){return{type:\"MultiLineString\",coordinates:t()}}function t(){return Go.range(Math.ceil(i/d)*d,u,d).map(h).concat(Go.range(Math.ceil(s/m)*m,c,m).map(g)).concat(Go.range(Math.ceil(r/p)*p,e,p).filter(function(n){return fa(n%d)>Ta}).map(l)).concat(Go.range(Math.ceil(a/v)*v,o,v).filter(function(n){return fa(n%m)>Ta}).map(f))}var e,r,u,i,o,a,c,s,l,f,h,g,p=10,v=p,d=90,m=360,y=2.5;return n.lines=function(){return t().map(function(n){return{type:\"LineString\",coordinates:n}})},n.outline=function(){return{type:\"Polygon\",coordinates:[h(i).concat(g(c).slice(1),h(u).reverse().slice(1),g(s).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(i=+t[0][0],u=+t[1][0],s=+t[0][1],c=+t[1][1],i>u&&(t=i,i=u,u=t),s>c&&(t=s,s=c,c=t),n.precision(y)):[[i,s],[u,c]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],a=+t[0][1],o=+t[1][1],r>e&&(t=r,r=e,e=t),a>o&&(t=a,a=o,o=t),n.precision(y)):[[r,a],[e,o]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],m=+t[1],n):[d,m]},n.minorStep=function(t){return arguments.length?(p=+t[0],v=+t[1],n):[p,v]},n.precision=function(t){return arguments.length?(y=+t,l=vr(a,o,90),f=dr(r,e,y),h=vr(s,c,90),g=dr(i,u,y),n):y},n.majorExtent([[-180,-90+Ta],[180,90-Ta]]).minorExtent([[-180,-80-Ta],[180,80+Ta]])},Go.geo.greatArc=function(){function n(){return{type:\"LineString\",coordinates:[t||r.apply(this,arguments),e||u.apply(this,arguments)]}}var t,e,r=mr,u=yr;return n.distance=function(){return Go.geo.distance(t||r.apply(this,arguments),e||u.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t=\"function\"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(u=t,e=\"function\"==typeof t?null:t,n):u},n.precision=function(){return arguments.length?n:0},n},Go.geo.interpolate=function(n,t){return xr(n[0]*za,n[1]*za,t[0]*za,t[1]*za)},Go.geo.length=function(n){return Oc=0,Go.geo.stream(n,Ic),Oc};var Oc,Ic={sphere:v,point:v,lineStart:Mr,lineEnd:v,polygonStart:v,polygonEnd:v},Yc=_r(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(Go.geo.azimuthalEqualArea=function(){return ur(Yc)}).raw=Yc;var Zc=_r(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},At);(Go.geo.azimuthalEquidistant=function(){return ur(Zc)}).raw=Zc,(Go.geo.conicConformal=function(){return Ye(br)}).raw=br,(Go.geo.conicEquidistant=function(){return Ye(wr)}).raw=wr;var Vc=_r(function(n){return 1/n},Math.atan);(Go.geo.gnomonic=function(){return ur(Vc)}).raw=Vc,Sr.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-La]},(Go.geo.mercator=function(){return kr(Sr)}).raw=Sr;var $c=_r(function(){return 1},Math.asin);(Go.geo.orthographic=function(){return ur($c)}).raw=$c;var Xc=_r(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(Go.geo.stereographic=function(){return ur(Xc)}).raw=Xc,Er.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-La]},(Go.geo.transverseMercator=function(){var n=kr(Er),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[-n[1],n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},n.rotate([0,0])}).raw=Er,Go.geom={},Go.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,u=Et(e),i=Et(r),o=n.length,a=[],c=[];for(t=0;o>t;t++)a.push([+u.call(this,n[t],t),+i.call(this,n[t],t),t]);for(a.sort(Lr),t=0;o>t;t++)c.push([a[t][0],-a[t][1]]);var s=Nr(a),l=Nr(c),f=l[0]===s[0],h=l[l.length-1]===s[s.length-1],g=[];for(t=s.length-1;t>=0;--t)g.push(n[a[s[t]][2]]);for(t=+f;t<l.length-h;++t)g.push(n[a[l[t]][2]]);return g}var e=Ar,r=Cr;return arguments.length?t(n):(t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t)},Go.geom.polygon=function(n){return da(n,Bc),n};var Bc=Go.geom.polygon.prototype=[];Bc.area=function(){for(var n,t=-1,e=this.length,r=this[e-1],u=0;++t<e;)n=r,r=this[t],u+=n[1]*r[0]-n[0]*r[1];return.5*u},Bc.centroid=function(n){var t,e,r=-1,u=this.length,i=0,o=0,a=this[u-1];for(arguments.length||(n=-1/(6*this.area()));++r<u;)t=a,a=this[r],e=t[0]*a[1]-a[0]*t[1],i+=(t[0]+a[0])*e,o+=(t[1]+a[1])*e;return[i*n,o*n]},Bc.clip=function(n){for(var t,e,r,u,i,o,a=zr(n),c=-1,s=this.length-zr(this),l=this[s-1];++c<s;){for(t=n.slice(),n.length=0,u=this[c],i=t[(r=t.length-a)-1],e=-1;++e<r;)o=t[e],Tr(o,l,u)?(Tr(i,l,u)||n.push(qr(i,o,l,u)),n.push(o)):Tr(i,l,u)&&n.push(qr(i,o,l,u)),i=o;a&&n.push(n[0]),l=u}return n};var Jc,Wc,Gc,Kc,Qc,ns=[],ts=[];Or.prototype.prepare=function(){for(var n,t=this.edges,e=t.length;e--;)n=t[e].edge,n.b&&n.a||t.splice(e,1);return t.sort(Yr),t.length},Qr.prototype={start:function(){return this.edge.l===this.site?this.edge.a:this.edge.b},end:function(){return this.edge.l===this.site?this.edge.b:this.edge.a}},nu.prototype={insert:function(n,t){var e,r,u;if(n){if(t.P=n,t.N=n.N,n.N&&(n.N.P=t),n.N=t,n.R){for(n=n.R;n.L;)n=n.L;n.L=t}else n.R=t;e=n}else this._?(n=uu(this._),t.P=null,t.N=n,n.P=n.L=t,e=n):(t.P=t.N=null,this._=t,e=null);for(t.L=t.R=null,t.U=e,t.C=!0,n=t;e&&e.C;)r=e.U,e===r.L?(u=r.R,u&&u.C?(e.C=u.C=!1,r.C=!0,n=r):(n===e.R&&(eu(this,e),n=e,e=n.U),e.C=!1,r.C=!0,ru(this,r))):(u=r.L,u&&u.C?(e.C=u.C=!1,r.C=!0,n=r):(n===e.L&&(ru(this,e),n=e,e=n.U),e.C=!1,r.C=!0,eu(this,r))),e=n.U;this._.C=!1},remove:function(n){n.N&&(n.N.P=n.P),n.P&&(n.P.N=n.N),n.N=n.P=null;var t,e,r,u=n.U,i=n.L,o=n.R;if(e=i?o?uu(o):i:o,u?u.L===n?u.L=e:u.R=e:this._=e,i&&o?(r=e.C,e.C=n.C,e.L=i,i.U=e,e!==o?(u=e.U,e.U=n.U,n=e.R,u.L=n,e.R=o,o.U=e):(e.U=u,u=e,n=e.R)):(r=n.C,n=e),n&&(n.U=u),!r){if(n&&n.C)return n.C=!1,void 0;do{if(n===this._)break;if(n===u.L){if(t=u.R,t.C&&(t.C=!1,u.C=!0,eu(this,u),t=u.R),t.L&&t.L.C||t.R&&t.R.C){t.R&&t.R.C||(t.L.C=!1,t.C=!0,ru(this,t),t=u.R),t.C=u.C,u.C=t.R.C=!1,eu(this,u),n=this._;break}}else if(t=u.L,t.C&&(t.C=!1,u.C=!0,ru(this,u),t=u.L),t.L&&t.L.C||t.R&&t.R.C){t.L&&t.L.C||(t.R.C=!1,t.C=!0,eu(this,t),t=u.L),t.C=u.C,u.C=t.L.C=!1,ru(this,u),n=this._;break}t.C=!0,n=u,u=u.U}while(!n.C);n&&(n.C=!1)}}},Go.geom.voronoi=function(n){function t(n){var t=new Array(n.length),r=a[0][0],u=a[0][1],i=a[1][0],o=a[1][1];return iu(e(n),a).cells.forEach(function(e,a){var c=e.edges,s=e.site,l=t[a]=c.length?c.map(function(n){var t=n.start();return[t.x,t.y]}):s.x>=r&&s.x<=i&&s.y>=u&&s.y<=o?[[r,o],[i,o],[i,u],[r,u]]:[];l.point=n[a]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(i(n,t)/Ta)*Ta,y:Math.round(o(n,t)/Ta)*Ta,i:t}})}var r=Ar,u=Cr,i=r,o=u,a=es;return n?t(n):(t.links=function(n){return iu(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return iu(e(n)).cells.forEach(function(e,r){for(var u,i,o=e.site,a=e.edges.sort(Yr),c=-1,s=a.length,l=a[s-1].edge,f=l.l===o?l.r:l.l;++c<s;)u=l,i=f,l=a[c].edge,f=l.l===o?l.r:l.l,r<i.i&&r<f.i&&au(o,i,f)<0&&t.push([n[r],n[i.i],n[f.i]])}),t},t.x=function(n){return arguments.length?(i=Et(r=n),t):r},t.y=function(n){return arguments.length?(o=Et(u=n),t):u},t.clipExtent=function(n){return arguments.length?(a=null==n?es:n,t):a===es?null:a},t.size=function(n){return arguments.length?t.clipExtent(n&&[[0,0],n]):a===es?null:a&&a[1]},t)};var es=[[-1e6,-1e6],[1e6,1e6]];Go.geom.delaunay=function(n){return Go.geom.voronoi().triangles(n)},Go.geom.quadtree=function(n,t,e,r,u){function i(n){function i(n,t,e,r,u,i,o,a){if(!isNaN(e)&&!isNaN(r))if(n.leaf){var c=n.x,l=n.y;if(null!=c)if(fa(c-e)+fa(l-r)<.01)s(n,t,e,r,u,i,o,a);else{var f=n.point;n.x=n.y=n.point=null,s(n,f,c,l,u,i,o,a),s(n,t,e,r,u,i,o,a)}else n.x=e,n.y=r,n.point=t}else s(n,t,e,r,u,i,o,a)}function s(n,t,e,r,u,o,a,c){var s=.5*(u+a),l=.5*(o+c),f=e>=s,h=r>=l,g=(h<<1)+f;n.leaf=!1,n=n.nodes[g]||(n.nodes[g]=lu()),f?u=s:a=s,h?o=l:c=l,i(n,t,e,r,u,o,a,c)}var l,f,h,g,p,v,d,m,y,x=Et(a),M=Et(c);if(null!=t)v=t,d=e,m=r,y=u;else if(m=y=-(v=d=1/0),f=[],h=[],p=n.length,o)for(g=0;p>g;++g)l=n[g],l.x<v&&(v=l.x),l.y<d&&(d=l.y),l.x>m&&(m=l.x),l.y>y&&(y=l.y),f.push(l.x),h.push(l.y);else for(g=0;p>g;++g){var _=+x(l=n[g],g),b=+M(l,g);v>_&&(v=_),d>b&&(d=b),_>m&&(m=_),b>y&&(y=b),f.push(_),h.push(b)}var w=m-v,S=y-d;w>S?y=d+w:m=v+S;var k=lu();if(k.add=function(n){i(k,n,+x(n,++g),+M(n,g),v,d,m,y)},k.visit=function(n){fu(n,k,v,d,m,y)},g=-1,null==t){for(;++g<p;)i(k,n[g],f[g],h[g],v,d,m,y);--g}else n.forEach(k.add);return f=h=n=l=null,k}var o,a=Ar,c=Cr;return(o=arguments.length)?(a=cu,c=su,3===o&&(u=e,r=t,e=t=0),i(n)):(i.x=function(n){return arguments.length?(a=n,i):a},i.y=function(n){return arguments.length?(c=n,i):c},i.extent=function(n){return arguments.length?(null==n?t=e=r=u=null:(t=+n[0][0],e=+n[0][1],r=+n[1][0],u=+n[1][1]),i):null==t?null:[[t,e],[r,u]]},i.size=function(n){return arguments.length?(null==n?t=e=r=u=null:(t=e=0,r=+n[0],u=+n[1]),i):null==t?null:[r-t,u-e]},i)},Go.interpolateRgb=hu,Go.interpolateObject=gu,Go.interpolateNumber=pu,Go.interpolateString=vu;var rs=/[-+]?(?:\\d+\\.?\\d*|\\.?\\d+)(?:[eE][-+]?\\d+)?/g,us=new RegExp(rs.source,\"g\");Go.interpolate=du,Go.interpolators=[function(n,t){var e=typeof t;return(\"string\"===e?Ja.has(t)||/^(#|rgb\\(|hsl\\()/.test(t)?hu:vu:t instanceof et?hu:Array.isArray(t)?mu:\"object\"===e&&isNaN(t)?gu:pu)(n,t)}],Go.interpolateArray=mu;var is=function(){return At},os=Go.map({linear:is,poly:Su,quad:function(){return _u},cubic:function(){return bu},sin:function(){return ku},exp:function(){return Eu},circle:function(){return Au},elastic:Cu,back:Nu,bounce:function(){return Lu}}),as=Go.map({\"in\":At,out:xu,\"in-out\":Mu,\"out-in\":function(n){return Mu(xu(n))}});Go.ease=function(n){var t=n.indexOf(\"-\"),e=t>=0?n.substring(0,t):n,r=t>=0?n.substring(t+1):\"in\";return e=os.get(e)||is,r=as.get(r)||At,yu(r(e.apply(null,Ko.call(arguments,1))))},Go.interpolateHcl=Tu,Go.interpolateHsl=qu,Go.interpolateLab=zu,Go.interpolateRound=Ru,Go.transform=function(n){var t=na.createElementNS(Go.ns.prefix.svg,\"g\");return(Go.transform=function(n){if(null!=n){t.setAttribute(\"transform\",n);var e=t.transform.baseVal.consolidate()}return new Du(e?e.matrix:cs)})(n)},Du.prototype.toString=function(){return\"translate(\"+this.translate+\")rotate(\"+this.rotate+\")skewX(\"+this.skew+\")scale(\"+this.scale+\")\"};var cs={a:1,b:0,c:0,d:1,e:0,f:0};Go.interpolateTransform=Hu,Go.layout={},Go.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++e<r;)t.push(Iu(n[e]));return t}},Go.layout.chord=function(){function n(){var n,s,f,h,g,p={},v=[],d=Go.range(i),m=[];for(e=[],r=[],n=0,h=-1;++h<i;){for(s=0,g=-1;++g<i;)s+=u[h][g];v.push(s),m.push(Go.range(i)),n+=s}for(o&&d.sort(function(n,t){return o(v[n],v[t])}),a&&m.forEach(function(n,t){n.sort(function(n,e){return a(u[t][n],u[t][e])})}),n=(Na-l*i)/n,s=0,h=-1;++h<i;){for(f=s,g=-1;++g<i;){var y=d[h],x=m[y][g],M=u[y][x],_=s,b=s+=M*n;p[y+\"-\"+x]={index:y,subindex:x,startAngle:_,endAngle:b,value:M}}r[y]={index:y,startAngle:f,endAngle:s,value:(s-f)/n},s+=l}for(h=-1;++h<i;)for(g=h-1;++g<i;){var w=p[h+\"-\"+g],S=p[g+\"-\"+h];(w.value||S.value)&&e.push(w.value<S.value?{source:S,target:w}:{source:w,target:S})}c&&t()}function t(){e.sort(function(n,t){return c((n.source.value+n.target.value)/2,(t.source.value+t.target.value)/2)})}var e,r,u,i,o,a,c,s={},l=0;return s.matrix=function(n){return arguments.length?(i=(u=n)&&u.length,e=r=null,s):u},s.padding=function(n){return arguments.length?(l=n,e=r=null,s):l},s.sortGroups=function(n){return arguments.length?(o=n,e=r=null,s):o},s.sortSubgroups=function(n){return arguments.length?(a=n,e=null,s):a},s.sortChords=function(n){return arguments.length?(c=n,e&&t(),s):c},s.chords=function(){return e||n(),e},s.groups=function(){return r||n(),r},s},Go.layout.force=function(){function n(n){return function(t,e,r,u){if(t.point!==n){var i=t.cx-n.x,o=t.cy-n.y,a=u-e,c=i*i+o*o;if(c>a*a/d){if(p>c){var s=t.charge/c;n.px-=i*s,n.py-=o*s}return!0}if(t.point&&c&&p>c){var s=t.pointCharge/c;n.px-=i*s,n.py-=o*s}}return!t.charge}}function t(n){n.px=Go.event.x,n.py=Go.event.y,a.resume()}var e,r,u,i,o,a={},c=Go.dispatch(\"start\",\"tick\",\"end\"),s=[1,1],l=.9,f=ss,h=ls,g=-30,p=fs,v=.1,d=.64,m=[],y=[];return a.tick=function(){if((r*=.99)<.005)return c.end({type:\"end\",alpha:r=0}),!0;var t,e,a,f,h,p,d,x,M,_=m.length,b=y.length;for(e=0;b>e;++e)a=y[e],f=a.source,h=a.target,x=h.x-f.x,M=h.y-f.y,(p=x*x+M*M)&&(p=r*i[e]*((p=Math.sqrt(p))-u[e])/p,x*=p,M*=p,h.x-=x*(d=f.weight/(h.weight+f.weight)),h.y-=M*d,f.x+=x*(d=1-d),f.y+=M*d);if((d=r*v)&&(x=s[0]/2,M=s[1]/2,e=-1,d))for(;++e<_;)a=m[e],a.x+=(x-a.x)*d,a.y+=(M-a.y)*d;if(g)for(Ju(t=Go.geom.quadtree(m),r,o),e=-1;++e<_;)(a=m[e]).fixed||t.visit(n(a));for(e=-1;++e<_;)a=m[e],a.fixed?(a.x=a.px,a.y=a.py):(a.x-=(a.px-(a.px=a.x))*l,a.y-=(a.py-(a.py=a.y))*l);c.tick({type:\"tick\",alpha:r})},a.nodes=function(n){return arguments.length?(m=n,a):m},a.links=function(n){return arguments.length?(y=n,a):y},a.size=function(n){return arguments.length?(s=n,a):s},a.linkDistance=function(n){return arguments.length?(f=\"function\"==typeof n?n:+n,a):f},a.distance=a.linkDistance,a.linkStrength=function(n){return arguments.length?(h=\"function\"==typeof n?n:+n,a):h},a.friction=function(n){return arguments.length?(l=+n,a):l},a.charge=function(n){return arguments.length?(g=\"function\"==typeof n?n:+n,a):g},a.chargeDistance=function(n){return arguments.length?(p=n*n,a):Math.sqrt(p)},a.gravity=function(n){return arguments.length?(v=+n,a):v},a.theta=function(n){return arguments.length?(d=n*n,a):Math.sqrt(d)},a.alpha=function(n){return arguments.length?(n=+n,r?r=n>0?n:0:n>0&&(c.start({type:\"start\",alpha:r=n}),Go.timer(a.tick)),a):r},a.start=function(){function n(n,r){if(!e){for(e=new Array(c),a=0;c>a;++a)e[a]=[];for(a=0;s>a;++a){var u=y[a];e[u.source.index].push(u.target),e[u.target.index].push(u.source)}}for(var i,o=e[t],a=-1,s=o.length;++a<s;)if(!isNaN(i=o[a][n]))return i;return Math.random()*r}var t,e,r,c=m.length,l=y.length,p=s[0],v=s[1];for(t=0;c>t;++t)(r=m[t]).index=t,r.weight=0;for(t=0;l>t;++t)r=y[t],\"number\"==typeof r.source&&(r.source=m[r.source]),\"number\"==typeof r.target&&(r.target=m[r.target]),++r.source.weight,++r.target.weight;for(t=0;c>t;++t)r=m[t],isNaN(r.x)&&(r.x=n(\"x\",p)),isNaN(r.y)&&(r.y=n(\"y\",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(u=[],\"function\"==typeof f)for(t=0;l>t;++t)u[t]=+f.call(this,y[t],t);else for(t=0;l>t;++t)u[t]=f;if(i=[],\"function\"==typeof h)for(t=0;l>t;++t)i[t]=+h.call(this,y[t],t);else for(t=0;l>t;++t)i[t]=h;if(o=[],\"function\"==typeof g)for(t=0;c>t;++t)o[t]=+g.call(this,m[t],t);else for(t=0;c>t;++t)o[t]=g;return a.resume()},a.resume=function(){return a.alpha(.1)},a.stop=function(){return a.alpha(0)},a.drag=function(){return e||(e=Go.behavior.drag().origin(At).on(\"dragstart.force\",Vu).on(\"drag.force\",t).on(\"dragend.force\",$u)),arguments.length?(this.on(\"mouseover.force\",Xu).on(\"mouseout.force\",Bu).call(e),void 0):e},Go.rebind(a,c,\"on\")};var ss=20,ls=1,fs=1/0;Go.layout.hierarchy=function(){function n(t,o,a){var c=u.call(e,t,o);if(t.depth=o,a.push(t),c&&(s=c.length)){for(var s,l,f=-1,h=t.children=new Array(s),g=0,p=o+1;++f<s;)l=h[f]=n(c[f],p,a),l.parent=t,g+=l.value;r&&h.sort(r),i&&(t.value=g)}else delete t.children,i&&(t.value=+i.call(e,t,o)||0);return t}function t(n,r){var u=n.children,o=0;if(u&&(a=u.length))for(var a,c=-1,s=r+1;++c<a;)o+=t(u[c],s);else i&&(o=+i.call(e,n,r)||0);return i&&(n.value=o),o}function e(t){var e=[];return n(t,0,e),e}var r=Qu,u=Gu,i=Ku;return e.sort=function(n){return arguments.length?(r=n,e):r},e.children=function(n){return arguments.length?(u=n,e):u},e.value=function(n){return arguments.length?(i=n,e):i},e.revalue=function(n){return t(n,0),n},e},Go.layout.partition=function(){function n(t,e,r,u){var i=t.children;if(t.x=e,t.y=t.depth*u,t.dx=r,t.dy=u,i&&(o=i.length)){var o,a,c,s=-1;for(r=t.value?r/t.value:0;++s<o;)n(a=i[s],e,c=a.value*r,u),e+=c}}function t(n){var e=n.children,r=0;if(e&&(u=e.length))for(var u,i=-1;++i<u;)r=Math.max(r,t(e[i]));return 1+r}function e(e,i){var o=r.call(this,e,i);return n(o[0],0,u[0],u[1]/t(o[0])),o}var r=Go.layout.hierarchy(),u=[1,1];return e.size=function(n){return arguments.length?(u=n,e):u},Wu(e,r)},Go.layout.pie=function(){function n(i){var o=i.map(function(e,r){return+t.call(n,e,r)}),a=+(\"function\"==typeof r?r.apply(this,arguments):r),c=((\"function\"==typeof u?u.apply(this,arguments):u)-a)/Go.sum(o),s=Go.range(i.length);null!=e&&s.sort(e===hs?function(n,t){return o[t]-o[n]}:function(n,t){return e(i[n],i[t])});var l=[];return s.forEach(function(n){var t;l[n]={data:i[n],value:t=o[n],startAngle:a,endAngle:a+=t*c}}),l}var t=Number,e=hs,r=0,u=Na;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(u=t,n):u},n};var hs={};Go.layout.stack=function(){function n(a,c){var s=a.map(function(e,r){return t.call(n,e,r)}),l=s.map(function(t){return t.map(function(t,e){return[i.call(n,t,e),o.call(n,t,e)]})}),f=e.call(n,l,c);s=Go.permute(s,f),l=Go.permute(l,f);var h,g,p,v=r.call(n,l,c),d=s.length,m=s[0].length;for(g=0;m>g;++g)for(u.call(n,s[0][g],p=v[g],l[0][g][1]),h=1;d>h;++h)u.call(n,s[h][g],p+=l[h-1][g][1],l[h][g][1]);return a}var t=At,e=ui,r=ii,u=ri,i=ti,o=ei;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e=\"function\"==typeof t?t:gs.get(t)||ui,n):e},n.offset=function(t){return arguments.length?(r=\"function\"==typeof t?t:ps.get(t)||ii,n):r},n.x=function(t){return arguments.length?(i=t,n):i},n.y=function(t){return arguments.length?(o=t,n):o},n.out=function(t){return arguments.length?(u=t,n):u},n};var gs=Go.map({\"inside-out\":function(n){var t,e,r=n.length,u=n.map(oi),i=n.map(ai),o=Go.range(r).sort(function(n,t){return u[n]-u[t]}),a=0,c=0,s=[],l=[];for(t=0;r>t;++t)e=o[t],c>a?(a+=i[e],s.push(e)):(c+=i[e],l.push(e));return l.reverse().concat(s)},reverse:function(n){return Go.range(n.length).reverse()},\"default\":ui}),ps=Go.map({silhouette:function(n){var t,e,r,u=n.length,i=n[0].length,o=[],a=0,c=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];r>a&&(a=r),o.push(r)}for(e=0;i>e;++e)c[e]=(a-o[e])/2;return c},wiggle:function(n){var t,e,r,u,i,o,a,c,s,l=n.length,f=n[0],h=f.length,g=[];for(g[0]=c=s=0,e=1;h>e;++e){for(t=0,u=0;l>t;++t)u+=n[t][e][1];for(t=0,i=0,a=f[e][0]-f[e-1][0];l>t;++t){for(r=0,o=(n[t][e][1]-n[t][e-1][1])/(2*a);t>r;++r)o+=(n[r][e][1]-n[r][e-1][1])/a;i+=o*n[t][e][1]}g[e]=c-=u?i/u*a:0,s>c&&(s=c)}for(e=0;h>e;++e)g[e]-=s;return g},expand:function(n){var t,e,r,u=n.length,i=n[0].length,o=1/u,a=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];if(r)for(t=0;u>t;t++)n[t][e][1]/=r;else for(t=0;u>t;t++)n[t][e][1]=o}for(e=0;i>e;++e)a[e]=0;return a},zero:ii});Go.layout.histogram=function(){function n(n,i){for(var o,a,c=[],s=n.map(e,this),l=r.call(this,s,i),f=u.call(this,l,s,i),i=-1,h=s.length,g=f.length-1,p=t?1:1/h;++i<g;)o=c[i]=[],o.dx=f[i+1]-(o.x=f[i]),o.y=0;if(g>0)for(i=-1;++i<h;)a=s[i],a>=l[0]&&a<=l[1]&&(o=c[Go.bisect(f,a,1,g)-1],o.y+=p,o.push(n[i]));return c}var t=!0,e=Number,r=fi,u=si;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=Et(t),n):r},n.bins=function(t){return arguments.length?(u=\"number\"==typeof t?function(n){return li(n,t)}:Et(t),n):u},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},Go.layout.tree=function(){function n(n,i){function o(n,t){var r=n.children,u=n._tree;if(r&&(i=r.length)){for(var i,a,s,l=r[0],f=l,h=-1;++h<i;)s=r[h],o(s,a),f=c(s,a,f),a=s;Mi(n);var g=.5*(l._tree.prelim+s._tree.prelim);t?(u.prelim=t._tree.prelim+e(n,t),u.mod=u.prelim-g):u.prelim=g}else t&&(u.prelim=t._tree.prelim+e(n,t))}function a(n,t){n.x=n._tree.prelim+t;var e=n.children;if(e&&(r=e.length)){var r,u=-1;for(t+=n._tree.mod;++u<r;)a(e[u],t)}}function c(n,t,r){if(t){for(var u,i=n,o=n,a=t,c=n.parent.children[0],s=i._tree.mod,l=o._tree.mod,f=a._tree.mod,h=c._tree.mod;a=pi(a),i=gi(i),a&&i;)c=gi(c),o=pi(o),o._tree.ancestor=n,u=a._tree.prelim+f-i._tree.prelim-s+e(a,i),u>0&&(_i(bi(a,n,r),n,u),s+=u,l+=u),f+=a._tree.mod,s+=i._tree.mod,h+=c._tree.mod,l+=o._tree.mod;a&&!pi(o)&&(o._tree.thread=a,o._tree.mod+=f-l),i&&!gi(c)&&(c._tree.thread=i,c._tree.mod+=s-h,r=n)}return r}var s=t.call(this,n,i),l=s[0];xi(l,function(n,t){n._tree={ancestor:n,prelim:0,mod:0,change:0,shift:0,number:t?t._tree.number+1:0}}),o(l),a(l,-l._tree.prelim);var f=vi(l,mi),h=vi(l,di),g=vi(l,yi),p=f.x-e(f,h)/2,v=h.x+e(h,f)/2,d=g.depth||1;return xi(l,u?function(n){n.x*=r[0],n.y=n.depth*r[1],delete n._tree}:function(n){n.x=(n.x-p)/(v-p)*r[0],n.y=n.depth/d*r[1],delete n._tree}),s}var t=Go.layout.hierarchy().sort(null).value(null),e=hi,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},Wu(n,t)},Go.layout.pack=function(){function n(n,i){var o=e.call(this,n,i),a=o[0],c=u[0],s=u[1],l=null==t?Math.sqrt:\"function\"==typeof t?t:function(){return t};if(a.x=a.y=0,xi(a,function(n){n.r=+l(n.value)}),xi(a,Ai),r){var f=r*(t?1:Math.max(2*a.r/c,2*a.r/s))/2;xi(a,function(n){n.r+=f}),xi(a,Ai),xi(a,function(n){n.r-=f})}return Li(a,c/2,s/2,t?1:1/Math.max(2*a.r/c,2*a.r/s)),o}var t,e=Go.layout.hierarchy().sort(wi),r=0,u=[1,1];return n.size=function(t){return arguments.length?(u=t,n):u},n.radius=function(e){return arguments.length?(t=null==e||\"function\"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},Wu(n,e)},Go.layout.cluster=function(){function n(n,i){var o,a=t.call(this,n,i),c=a[0],s=0;xi(c,function(n){var t=n.children;t&&t.length?(n.x=zi(t),n.y=qi(t)):(n.x=o?s+=e(n,o):0,n.y=0,o=n)});var l=Ri(c),f=Di(c),h=l.x-e(l,f)/2,g=f.x+e(f,l)/2;return xi(c,u?function(n){n.x=(n.x-c.x)*r[0],n.y=(c.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(g-h)*r[0],n.y=(1-(c.y?n.y/c.y:1))*r[1]}),a}var t=Go.layout.hierarchy().sort(null).value(null),e=hi,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},Wu(n,t)},Go.layout.treemap=function(){function n(n,t){for(var e,r,u=-1,i=n.length;++u<i;)r=(e=n[u]).value*(0>t?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var i=e.children;if(i&&i.length){var o,a,c,s=f(e),l=[],h=i.slice(),p=1/0,v=\"slice\"===g?s.dx:\"dice\"===g?s.dy:\"slice-dice\"===g?1&e.depth?s.dy:s.dx:Math.min(s.dx,s.dy);for(n(h,s.dx*s.dy/e.value),l.area=0;(c=h.length)>0;)l.push(o=h[c-1]),l.area+=o.area,\"squarify\"!==g||(a=r(l,v))<=p?(h.pop(),p=a):(l.area-=l.pop().area,u(l,v,s,!1),v=Math.min(s.dx,s.dy),l.length=l.area=0,p=1/0);l.length&&(u(l,v,s,!0),l.length=l.area=0),i.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var i,o=f(t),a=r.slice(),c=[];for(n(a,o.dx*o.dy/t.value),c.area=0;i=a.pop();)c.push(i),c.area+=i.area,null!=i.z&&(u(c,i.z?o.dx:o.dy,o,!a.length),c.length=c.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,u=0,i=1/0,o=-1,a=n.length;++o<a;)(e=n[o].area)&&(i>e&&(i=e),e>u&&(u=e));return r*=r,t*=t,r?Math.max(t*u*p/r,r/(t*i*p)):1/0}function u(n,t,e,r){var u,i=-1,o=n.length,a=e.x,s=e.y,l=t?c(n.area/t):0;if(t==e.dx){for((r||l>e.dy)&&(l=e.dy);++i<o;)u=n[i],u.x=a,u.y=s,u.dy=l,a+=u.dx=Math.min(e.x+e.dx-a,l?c(u.area/l):0);u.z=!0,u.dx+=e.x+e.dx-a,e.y+=l,e.dy-=l}else{for((r||l>e.dx)&&(l=e.dx);++i<o;)u=n[i],u.x=a,u.y=s,u.dx=l,s+=u.dy=Math.min(e.y+e.dy-s,l?c(u.area/l):0);u.z=!1,u.dy+=e.y+e.dy-s,e.x+=l,e.dx-=l}}function i(r){var u=o||a(r),i=u[0];return i.x=0,i.y=0,i.dx=s[0],i.dy=s[1],o&&a.revalue(i),n([i],i.dx*i.dy/i.value),(o?e:t)(i),h&&(o=u),u}var o,a=Go.layout.hierarchy(),c=Math.round,s=[1,1],l=null,f=Pi,h=!1,g=\"squarify\",p=.5*(1+Math.sqrt(5));return i.size=function(n){return arguments.length?(s=n,i):s},i.padding=function(n){function t(t){var e=n.call(i,t,t.depth);return null==e?Pi(t):Ui(t,\"number\"==typeof e?[e,e,e,e]:e)}function e(t){return Ui(t,n)}if(!arguments.length)return l;var r;return f=null==(l=n)?Pi:\"function\"==(r=typeof n)?t:\"number\"===r?(n=[n,n,n,n],e):e,i},i.round=function(n){return arguments.length?(c=n?Math.round:Number,i):c!=Number},i.sticky=function(n){return arguments.length?(h=n,o=null,i):h},i.ratio=function(n){return arguments.length?(p=n,i):p},i.mode=function(n){return arguments.length?(g=n+\"\",i):g},Wu(i,a)},Go.random={normal:function(n,t){var e=arguments.length;return 2>e&&(t=1),1>e&&(n=0),function(){var e,r,u;do e=2*Math.random()-1,r=2*Math.random()-1,u=e*e+r*r;while(!u||u>1);return n+t*e*Math.sqrt(-2*Math.log(u)/u)}},logNormal:function(){var n=Go.random.normal.apply(Go,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=Go.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},Go.scale={};var vs={floor:At,ceil:At};Go.scale.linear=function(){return Zi([0,1],[0,1],du,!1)};var ds={s:1,g:1,p:1,r:1,e:1};Go.scale.log=function(){return Ki(Go.scale.linear().domain([0,1]),10,!0,[1,10])};var ms=Go.format(\".0e\"),ys={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};Go.scale.pow=function(){return Qi(Go.scale.linear(),1,[0,1])},Go.scale.sqrt=function(){return Go.scale.pow().exponent(.5)},Go.scale.ordinal=function(){return to([],{t:\"range\",a:[[]]})},Go.scale.category10=function(){return Go.scale.ordinal().range(xs)},Go.scale.category20=function(){return Go.scale.ordinal().range(Ms)},Go.scale.category20b=function(){return Go.scale.ordinal().range(_s)},Go.scale.category20c=function(){return Go.scale.ordinal().range(bs)};var xs=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(mt),Ms=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(mt),_s=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(mt),bs=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(mt);Go.scale.quantile=function(){return eo([],[])},Go.scale.quantize=function(){return ro(0,1,[0,1])},Go.scale.threshold=function(){return uo([.5],[0,1])},Go.scale.identity=function(){return io([0,1])},Go.svg={},Go.svg.arc=function(){function n(){var n=t.apply(this,arguments),i=e.apply(this,arguments),o=r.apply(this,arguments)+ws,a=u.apply(this,arguments)+ws,c=(o>a&&(c=o,o=a,a=c),a-o),s=Ca>c?\"0\":\"1\",l=Math.cos(o),f=Math.sin(o),h=Math.cos(a),g=Math.sin(a);\nreturn c>=Ss?n?\"M0,\"+i+\"A\"+i+\",\"+i+\" 0 1,1 0,\"+-i+\"A\"+i+\",\"+i+\" 0 1,1 0,\"+i+\"M0,\"+n+\"A\"+n+\",\"+n+\" 0 1,0 0,\"+-n+\"A\"+n+\",\"+n+\" 0 1,0 0,\"+n+\"Z\":\"M0,\"+i+\"A\"+i+\",\"+i+\" 0 1,1 0,\"+-i+\"A\"+i+\",\"+i+\" 0 1,1 0,\"+i+\"Z\":n?\"M\"+i*l+\",\"+i*f+\"A\"+i+\",\"+i+\" 0 \"+s+\",1 \"+i*h+\",\"+i*g+\"L\"+n*h+\",\"+n*g+\"A\"+n+\",\"+n+\" 0 \"+s+\",0 \"+n*l+\",\"+n*f+\"Z\":\"M\"+i*l+\",\"+i*f+\"A\"+i+\",\"+i+\" 0 \"+s+\",1 \"+i*h+\",\"+i*g+\"L0,0\"+\"Z\"}var t=oo,e=ao,r=co,u=so;return n.innerRadius=function(e){return arguments.length?(t=Et(e),n):t},n.outerRadius=function(t){return arguments.length?(e=Et(t),n):e},n.startAngle=function(t){return arguments.length?(r=Et(t),n):r},n.endAngle=function(t){return arguments.length?(u=Et(t),n):u},n.centroid=function(){var n=(t.apply(this,arguments)+e.apply(this,arguments))/2,i=(r.apply(this,arguments)+u.apply(this,arguments))/2+ws;return[Math.cos(i)*n,Math.sin(i)*n]},n};var ws=-La,Ss=Na-Ta;Go.svg.line=function(){return lo(At)};var ks=Go.map({linear:fo,\"linear-closed\":ho,step:go,\"step-before\":po,\"step-after\":vo,basis:bo,\"basis-open\":wo,\"basis-closed\":So,bundle:ko,cardinal:xo,\"cardinal-open\":mo,\"cardinal-closed\":yo,monotone:To});ks.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var Es=[0,2/3,1/3,0],As=[0,1/3,2/3,0],Cs=[0,1/6,2/3,1/6];Go.svg.line.radial=function(){var n=lo(qo);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},po.reverse=vo,vo.reverse=po,Go.svg.area=function(){return zo(At)},Go.svg.area.radial=function(){var n=zo(qo);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},Go.svg.chord=function(){function n(n,a){var c=t(this,i,n,a),s=t(this,o,n,a);return\"M\"+c.p0+r(c.r,c.p1,c.a1-c.a0)+(e(c,s)?u(c.r,c.p1,c.r,c.p0):u(c.r,c.p1,s.r,s.p0)+r(s.r,s.p1,s.a1-s.a0)+u(s.r,s.p1,c.r,c.p0))+\"Z\"}function t(n,t,e,r){var u=t.call(n,e,r),i=a.call(n,u,r),o=c.call(n,u,r)+ws,l=s.call(n,u,r)+ws;return{r:i,a0:o,a1:l,p0:[i*Math.cos(o),i*Math.sin(o)],p1:[i*Math.cos(l),i*Math.sin(l)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return\"A\"+n+\",\"+n+\" 0 \"+ +(e>Ca)+\",1 \"+t}function u(n,t,e,r){return\"Q 0,0 \"+r}var i=mr,o=yr,a=Ro,c=co,s=so;return n.radius=function(t){return arguments.length?(a=Et(t),n):a},n.source=function(t){return arguments.length?(i=Et(t),n):i},n.target=function(t){return arguments.length?(o=Et(t),n):o},n.startAngle=function(t){return arguments.length?(c=Et(t),n):c},n.endAngle=function(t){return arguments.length?(s=Et(t),n):s},n},Go.svg.diagonal=function(){function n(n,u){var i=t.call(this,n,u),o=e.call(this,n,u),a=(i.y+o.y)/2,c=[i,{x:i.x,y:a},{x:o.x,y:a},o];return c=c.map(r),\"M\"+c[0]+\"C\"+c[1]+\" \"+c[2]+\" \"+c[3]}var t=mr,e=yr,r=Do;return n.source=function(e){return arguments.length?(t=Et(e),n):t},n.target=function(t){return arguments.length?(e=Et(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},Go.svg.diagonal.radial=function(){var n=Go.svg.diagonal(),t=Do,e=n.projection;return n.projection=function(n){return arguments.length?e(Po(t=n)):t},n},Go.svg.symbol=function(){function n(n,r){return(Ns.get(t.call(this,n,r))||Ho)(e.call(this,n,r))}var t=jo,e=Uo;return n.type=function(e){return arguments.length?(t=Et(e),n):t},n.size=function(t){return arguments.length?(e=Et(t),n):e},n};var Ns=Go.map({circle:Ho,cross:function(n){var t=Math.sqrt(n/5)/2;return\"M\"+-3*t+\",\"+-t+\"H\"+-t+\"V\"+-3*t+\"H\"+t+\"V\"+-t+\"H\"+3*t+\"V\"+t+\"H\"+t+\"V\"+3*t+\"H\"+-t+\"V\"+t+\"H\"+-3*t+\"Z\"},diamond:function(n){var t=Math.sqrt(n/(2*zs)),e=t*zs;return\"M0,\"+-t+\"L\"+e+\",0\"+\" 0,\"+t+\" \"+-e+\",0\"+\"Z\"},square:function(n){var t=Math.sqrt(n)/2;return\"M\"+-t+\",\"+-t+\"L\"+t+\",\"+-t+\" \"+t+\",\"+t+\" \"+-t+\",\"+t+\"Z\"},\"triangle-down\":function(n){var t=Math.sqrt(n/qs),e=t*qs/2;return\"M0,\"+e+\"L\"+t+\",\"+-e+\" \"+-t+\",\"+-e+\"Z\"},\"triangle-up\":function(n){var t=Math.sqrt(n/qs),e=t*qs/2;return\"M0,\"+-e+\"L\"+t+\",\"+e+\" \"+-t+\",\"+e+\"Z\"}});Go.svg.symbolTypes=Ns.keys();var Ls,Ts,qs=Math.sqrt(3),zs=Math.tan(30*za),Rs=[],Ds=0;Rs.call=_a.call,Rs.empty=_a.empty,Rs.node=_a.node,Rs.size=_a.size,Go.transition=function(n){return arguments.length?Ls?n.transition():n:Sa.transition()},Go.transition.prototype=Rs,Rs.select=function(n){var t,e,r,u=this.id,i=[];n=b(n);for(var o=-1,a=this.length;++o<a;){i.push(t=[]);for(var c=this[o],s=-1,l=c.length;++s<l;)(r=c[s])&&(e=n.call(r,r.__data__,s,o))?(\"__data__\"in r&&(e.__data__=r.__data__),Yo(e,s,u,r.__transition__[u]),t.push(e)):t.push(null)}return Fo(i,u)},Rs.selectAll=function(n){var t,e,r,u,i,o=this.id,a=[];n=w(n);for(var c=-1,s=this.length;++c<s;)for(var l=this[c],f=-1,h=l.length;++f<h;)if(r=l[f]){i=r.__transition__[o],e=n.call(r,r.__data__,f,c),a.push(t=[]);for(var g=-1,p=e.length;++g<p;)(u=e[g])&&Yo(u,g,o,i),t.push(u)}return Fo(a,o)},Rs.filter=function(n){var t,e,r,u=[];\"function\"!=typeof n&&(n=R(n));for(var i=0,o=this.length;o>i;i++){u.push(t=[]);for(var e=this[i],a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return Fo(u,this.id)},Rs.tween=function(n,t){var e=this.id;return arguments.length<2?this.node().__transition__[e].tween.get(n):P(this,null==t?function(t){t.__transition__[e].tween.remove(n)}:function(r){r.__transition__[e].tween.set(n,t)})},Rs.attr=function(n,t){function e(){this.removeAttribute(a)}function r(){this.removeAttributeNS(a.space,a.local)}function u(n){return null==n?e:(n+=\"\",function(){var t,e=this.getAttribute(a);return e!==n&&(t=o(e,n),function(n){this.setAttribute(a,t(n))})})}function i(n){return null==n?r:(n+=\"\",function(){var t,e=this.getAttributeNS(a.space,a.local);return e!==n&&(t=o(e,n),function(n){this.setAttributeNS(a.space,a.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var o=\"transform\"==n?Hu:du,a=Go.ns.qualify(n);return Oo(this,\"attr.\"+n,t,a.local?i:u)},Rs.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(u));return r&&function(n){this.setAttribute(u,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(u.space,u.local));return r&&function(n){this.setAttributeNS(u.space,u.local,r(n))}}var u=Go.ns.qualify(n);return this.tween(\"attr.\"+n,u.local?r:e)},Rs.style=function(n,t,e){function r(){this.style.removeProperty(n)}function u(t){return null==t?r:(t+=\"\",function(){var r,u=ea.getComputedStyle(this,null).getPropertyValue(n);return u!==t&&(r=du(u,t),function(t){this.style.setProperty(n,r(t),e)})})}var i=arguments.length;if(3>i){if(\"string\"!=typeof n){2>i&&(t=\"\");for(e in n)this.style(e,n[e],t);return this}e=\"\"}return Oo(this,\"style.\"+n,t,u)},Rs.styleTween=function(n,t,e){function r(r,u){var i=t.call(this,r,u,ea.getComputedStyle(this,null).getPropertyValue(n));return i&&function(t){this.style.setProperty(n,i(t),e)}}return arguments.length<3&&(e=\"\"),this.tween(\"style.\"+n,r)},Rs.text=function(n){return Oo(this,\"text\",n,Io)},Rs.remove=function(){return this.each(\"end.transition\",function(){var n;this.__transition__.count<2&&(n=this.parentNode)&&n.removeChild(this)})},Rs.ease=function(n){var t=this.id;return arguments.length<1?this.node().__transition__[t].ease:(\"function\"!=typeof n&&(n=Go.ease.apply(Go,arguments)),P(this,function(e){e.__transition__[t].ease=n}))},Rs.delay=function(n){var t=this.id;return arguments.length<1?this.node().__transition__[t].delay:P(this,\"function\"==typeof n?function(e,r,u){e.__transition__[t].delay=+n.call(e,e.__data__,r,u)}:(n=+n,function(e){e.__transition__[t].delay=n}))},Rs.duration=function(n){var t=this.id;return arguments.length<1?this.node().__transition__[t].duration:P(this,\"function\"==typeof n?function(e,r,u){e.__transition__[t].duration=Math.max(1,n.call(e,e.__data__,r,u))}:(n=Math.max(1,n),function(e){e.__transition__[t].duration=n}))},Rs.each=function(n,t){var e=this.id;if(arguments.length<2){var r=Ts,u=Ls;Ls=e,P(this,function(t,r,u){Ts=t.__transition__[e],n.call(t,t.__data__,r,u)}),Ts=r,Ls=u}else P(this,function(r){var u=r.__transition__[e];(u.event||(u.event=Go.dispatch(\"start\",\"end\"))).on(n,t)});return this},Rs.transition=function(){for(var n,t,e,r,u=this.id,i=++Ds,o=[],a=0,c=this.length;c>a;a++){o.push(n=[]);for(var t=this[a],s=0,l=t.length;l>s;s++)(e=t[s])&&(r=Object.create(e.__transition__[u]),r.delay+=r.duration,Yo(e,s,i,r)),n.push(e)}return Fo(o,i)},Go.svg.axis=function(){function n(n){n.each(function(){var n,s=Go.select(this),l=this.__chart__||e,f=this.__chart__=e.copy(),h=null==c?f.ticks?f.ticks.apply(f,a):f.domain():c,g=null==t?f.tickFormat?f.tickFormat.apply(f,a):At:t,p=s.selectAll(\".tick\").data(h,f),v=p.enter().insert(\"g\",\".domain\").attr(\"class\",\"tick\").style(\"opacity\",Ta),d=Go.transition(p.exit()).style(\"opacity\",Ta).remove(),m=Go.transition(p.order()).style(\"opacity\",1),y=Hi(f),x=s.selectAll(\".domain\").data([0]),M=(x.enter().append(\"path\").attr(\"class\",\"domain\"),Go.transition(x));v.append(\"line\"),v.append(\"text\");var _=v.select(\"line\"),b=m.select(\"line\"),w=p.select(\"text\").text(g),S=v.select(\"text\"),k=m.select(\"text\");switch(r){case\"bottom\":n=Zo,_.attr(\"y2\",u),S.attr(\"y\",Math.max(u,0)+o),b.attr(\"x2\",0).attr(\"y2\",u),k.attr(\"x\",0).attr(\"y\",Math.max(u,0)+o),w.attr(\"dy\",\".71em\").style(\"text-anchor\",\"middle\"),M.attr(\"d\",\"M\"+y[0]+\",\"+i+\"V0H\"+y[1]+\"V\"+i);break;case\"top\":n=Zo,_.attr(\"y2\",-u),S.attr(\"y\",-(Math.max(u,0)+o)),b.attr(\"x2\",0).attr(\"y2\",-u),k.attr(\"x\",0).attr(\"y\",-(Math.max(u,0)+o)),w.attr(\"dy\",\"0em\").style(\"text-anchor\",\"middle\"),M.attr(\"d\",\"M\"+y[0]+\",\"+-i+\"V0H\"+y[1]+\"V\"+-i);break;case\"left\":n=Vo,_.attr(\"x2\",-u),S.attr(\"x\",-(Math.max(u,0)+o)),b.attr(\"x2\",-u).attr(\"y2\",0),k.attr(\"x\",-(Math.max(u,0)+o)).attr(\"y\",0),w.attr(\"dy\",\".32em\").style(\"text-anchor\",\"end\"),M.attr(\"d\",\"M\"+-i+\",\"+y[0]+\"H0V\"+y[1]+\"H\"+-i);break;case\"right\":n=Vo,_.attr(\"x2\",u),S.attr(\"x\",Math.max(u,0)+o),b.attr(\"x2\",u).attr(\"y2\",0),k.attr(\"x\",Math.max(u,0)+o).attr(\"y\",0),w.attr(\"dy\",\".32em\").style(\"text-anchor\",\"start\"),M.attr(\"d\",\"M\"+i+\",\"+y[0]+\"H0V\"+y[1]+\"H\"+i)}if(f.rangeBand){var E=f,A=E.rangeBand()/2;l=f=function(n){return E(n)+A}}else l.rangeBand?l=f:d.call(n,f);v.call(n,l),m.call(n,f)})}var t,e=Go.scale.linear(),r=Ps,u=6,i=6,o=3,a=[10],c=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in Us?t+\"\":Ps,n):r},n.ticks=function(){return arguments.length?(a=arguments,n):a},n.tickValues=function(t){return arguments.length?(c=t,n):c},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(u=+t,i=+arguments[e-1],n):u},n.innerTickSize=function(t){return arguments.length?(u=+t,n):u},n.outerTickSize=function(t){return arguments.length?(i=+t,n):i},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(){return arguments.length&&n},n};var Ps=\"bottom\",Us={top:1,right:1,bottom:1,left:1};Go.svg.brush=function(){function n(i){i.each(function(){var i=Go.select(this).style(\"pointer-events\",\"all\").style(\"-webkit-tap-highlight-color\",\"rgba(0,0,0,0)\").on(\"mousedown.brush\",u).on(\"touchstart.brush\",u),o=i.selectAll(\".background\").data([0]);o.enter().append(\"rect\").attr(\"class\",\"background\").style(\"visibility\",\"hidden\").style(\"cursor\",\"crosshair\"),i.selectAll(\".extent\").data([0]).enter().append(\"rect\").attr(\"class\",\"extent\").style(\"cursor\",\"move\");var a=i.selectAll(\".resize\").data(p,At);a.exit().remove(),a.enter().append(\"g\").attr(\"class\",function(n){return\"resize \"+n}).style(\"cursor\",function(n){return js[n]}).append(\"rect\").attr(\"x\",function(n){return/[ew]$/.test(n)?-3:null}).attr(\"y\",function(n){return/^[ns]/.test(n)?-3:null}).attr(\"width\",6).attr(\"height\",6).style(\"visibility\",\"hidden\"),a.style(\"display\",n.empty()?\"none\":null);var l,f=Go.transition(i),h=Go.transition(o);c&&(l=Hi(c),h.attr(\"x\",l[0]).attr(\"width\",l[1]-l[0]),e(f)),s&&(l=Hi(s),h.attr(\"y\",l[0]).attr(\"height\",l[1]-l[0]),r(f)),t(f)})}function t(n){n.selectAll(\".resize\").attr(\"transform\",function(n){return\"translate(\"+l[+/e$/.test(n)]+\",\"+f[+/^s/.test(n)]+\")\"})}function e(n){n.select(\".extent\").attr(\"x\",l[0]),n.selectAll(\".extent,.n>rect,.s>rect\").attr(\"width\",l[1]-l[0])}function r(n){n.select(\".extent\").attr(\"y\",f[0]),n.selectAll(\".extent,.e>rect,.w>rect\").attr(\"height\",f[1]-f[0])}function u(){function u(){32==Go.event.keyCode&&(C||(x=null,L[0]-=l[1],L[1]-=f[1],C=2),y())}function p(){32==Go.event.keyCode&&2==C&&(L[0]+=l[1],L[1]+=f[1],C=0,y())}function v(){var n=Go.mouse(_),u=!1;M&&(n[0]+=M[0],n[1]+=M[1]),C||(Go.event.altKey?(x||(x=[(l[0]+l[1])/2,(f[0]+f[1])/2]),L[0]=l[+(n[0]<x[0])],L[1]=f[+(n[1]<x[1])]):x=null),E&&d(n,c,0)&&(e(S),u=!0),A&&d(n,s,1)&&(r(S),u=!0),u&&(t(S),w({type:\"brush\",mode:C?\"move\":\"resize\"}))}function d(n,t,e){var r,u,a=Hi(t),c=a[0],s=a[1],p=L[e],v=e?f:l,d=v[1]-v[0];return C&&(c-=p,s-=d+p),r=(e?g:h)?Math.max(c,Math.min(s,n[e])):n[e],C?u=(r+=p)+d:(x&&(p=Math.max(c,Math.min(s,2*x[e]-r))),r>p?(u=r,r=p):u=p),v[0]!=r||v[1]!=u?(e?o=null:i=null,v[0]=r,v[1]=u,!0):void 0}function m(){v(),S.style(\"pointer-events\",\"all\").selectAll(\".resize\").style(\"display\",n.empty()?\"none\":null),Go.select(\"body\").style(\"cursor\",null),T.on(\"mousemove.brush\",null).on(\"mouseup.brush\",null).on(\"touchmove.brush\",null).on(\"touchend.brush\",null).on(\"keydown.brush\",null).on(\"keyup.brush\",null),N(),w({type:\"brushend\"})}var x,M,_=this,b=Go.select(Go.event.target),w=a.of(_,arguments),S=Go.select(_),k=b.datum(),E=!/^(n|s)$/.test(k)&&c,A=!/^(e|w)$/.test(k)&&s,C=b.classed(\"extent\"),N=Y(),L=Go.mouse(_),T=Go.select(ea).on(\"keydown.brush\",u).on(\"keyup.brush\",p);if(Go.event.changedTouches?T.on(\"touchmove.brush\",v).on(\"touchend.brush\",m):T.on(\"mousemove.brush\",v).on(\"mouseup.brush\",m),S.interrupt().selectAll(\"*\").interrupt(),C)L[0]=l[0]-L[0],L[1]=f[0]-L[1];else if(k){var q=+/w$/.test(k),z=+/^n/.test(k);M=[l[1-q]-L[0],f[1-z]-L[1]],L[0]=l[q],L[1]=f[z]}else Go.event.altKey&&(x=L.slice());S.style(\"pointer-events\",\"none\").selectAll(\".resize\").style(\"display\",null),Go.select(\"body\").style(\"cursor\",b.style(\"cursor\")),w({type:\"brushstart\"}),v()}var i,o,a=M(n,\"brushstart\",\"brush\",\"brushend\"),c=null,s=null,l=[0,0],f=[0,0],h=!0,g=!0,p=Hs[0];return n.event=function(n){n.each(function(){var n=a.of(this,arguments),t={x:l,y:f,i:i,j:o},e=this.__chart__||t;this.__chart__=t,Ls?Go.select(this).transition().each(\"start.brush\",function(){i=e.i,o=e.j,l=e.x,f=e.y,n({type:\"brushstart\"})}).tween(\"brush:brush\",function(){var e=mu(l,t.x),r=mu(f,t.y);return i=o=null,function(u){l=t.x=e(u),f=t.y=r(u),n({type:\"brush\",mode:\"resize\"})}}).each(\"end.brush\",function(){i=t.i,o=t.j,n({type:\"brush\",mode:\"resize\"}),n({type:\"brushend\"})}):(n({type:\"brushstart\"}),n({type:\"brush\",mode:\"resize\"}),n({type:\"brushend\"}))})},n.x=function(t){return arguments.length?(c=t,p=Hs[!c<<1|!s],n):c},n.y=function(t){return arguments.length?(s=t,p=Hs[!c<<1|!s],n):s},n.clamp=function(t){return arguments.length?(c&&s?(h=!!t[0],g=!!t[1]):c?h=!!t:s&&(g=!!t),n):c&&s?[h,g]:c?h:s?g:null},n.extent=function(t){var e,r,u,a,h;return arguments.length?(c&&(e=t[0],r=t[1],s&&(e=e[0],r=r[0]),i=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(h=e,e=r,r=h),(e!=l[0]||r!=l[1])&&(l=[e,r])),s&&(u=t[0],a=t[1],c&&(u=u[1],a=a[1]),o=[u,a],s.invert&&(u=s(u),a=s(a)),u>a&&(h=u,u=a,a=h),(u!=f[0]||a!=f[1])&&(f=[u,a])),n):(c&&(i?(e=i[0],r=i[1]):(e=l[0],r=l[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(h=e,e=r,r=h))),s&&(o?(u=o[0],a=o[1]):(u=f[0],a=f[1],s.invert&&(u=s.invert(u),a=s.invert(a)),u>a&&(h=u,u=a,a=h))),c&&s?[[e,u],[r,a]]:c?[e,r]:s&&[u,a])},n.clear=function(){return n.empty()||(l=[0,0],f=[0,0],i=o=null),n},n.empty=function(){return!!c&&l[0]==l[1]||!!s&&f[0]==f[1]},Go.rebind(n,a,\"on\")};var js={n:\"ns-resize\",e:\"ew-resize\",s:\"ns-resize\",w:\"ew-resize\",nw:\"nwse-resize\",ne:\"nesw-resize\",se:\"nwse-resize\",sw:\"nesw-resize\"},Hs=[[\"n\",\"e\",\"s\",\"w\",\"nw\",\"ne\",\"se\",\"sw\"],[\"e\",\"w\"],[\"n\",\"s\"],[]],Fs=ic.format=fc.timeFormat,Os=Fs.utc,Is=Os(\"%Y-%m-%dT%H:%M:%S.%LZ\");Fs.iso=Date.prototype.toISOString&&+new Date(\"2000-01-01T00:00:00.000Z\")?$o:Is,$o.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},$o.toString=Is.toString,ic.second=Ht(function(n){return new oc(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),ic.seconds=ic.second.range,ic.seconds.utc=ic.second.utc.range,ic.minute=Ht(function(n){return new oc(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),ic.minutes=ic.minute.range,ic.minutes.utc=ic.minute.utc.range,ic.hour=Ht(function(n){var t=n.getTimezoneOffset()/60;return new oc(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),ic.hours=ic.hour.range,ic.hours.utc=ic.hour.utc.range,ic.month=Ht(function(n){return n=ic.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),ic.months=ic.month.range,ic.months.utc=ic.month.utc.range;var Ys=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Zs=[[ic.second,1],[ic.second,5],[ic.second,15],[ic.second,30],[ic.minute,1],[ic.minute,5],[ic.minute,15],[ic.minute,30],[ic.hour,1],[ic.hour,3],[ic.hour,6],[ic.hour,12],[ic.day,1],[ic.day,2],[ic.week,1],[ic.month,1],[ic.month,3],[ic.year,1]],Vs=Fs.multi([[\".%L\",function(n){return n.getMilliseconds()}],[\":%S\",function(n){return n.getSeconds()}],[\"%I:%M\",function(n){return n.getMinutes()}],[\"%I %p\",function(n){return n.getHours()}],[\"%a %d\",function(n){return n.getDay()&&1!=n.getDate()}],[\"%b %d\",function(n){return 1!=n.getDate()}],[\"%B\",function(n){return n.getMonth()}],[\"%Y\",Ae]]),$s={range:function(n,t,e){return Go.range(Math.ceil(n/e)*e,+t,e).map(Bo)},floor:At,ceil:At};Zs.year=ic.year,ic.scale=function(){return Xo(Go.scale.linear(),Zs,Vs)};var Xs=Zs.map(function(n){return[n[0].utc,n[1]]}),Bs=Os.multi([[\".%L\",function(n){return n.getUTCMilliseconds()}],[\":%S\",function(n){return n.getUTCSeconds()}],[\"%I:%M\",function(n){return n.getUTCMinutes()}],[\"%I %p\",function(n){return n.getUTCHours()}],[\"%a %d\",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],[\"%b %d\",function(n){return 1!=n.getUTCDate()}],[\"%B\",function(n){return n.getUTCMonth()}],[\"%Y\",Ae]]);Xs.year=ic.year.utc,ic.scale.utc=function(){return Xo(Go.scale.linear(),Xs,Bs)},Go.text=Ct(function(n){return n.responseText}),Go.json=function(n,t){return Nt(n,\"application/json\",Jo,t)},Go.html=function(n,t){return Nt(n,\"text/html\",Wo,t)},Go.xml=Ct(function(n){return n.responseXML}),\"function\"==typeof define&&define.amd?define(Go):\"object\"==typeof module&&module.exports?module.exports=Go:this.d3=Go}();\nreturn d3;\n"
  },
  {
    "path": "shared-data/contrib/forcegrapher/forcegrapher.css",
    "content": "#pile-graph {}"
  },
  {
    "path": "shared-data/contrib/forcegrapher/forcegrapher.html",
    "content": "{% extends \"layouts/\" + render_mode + \"-wide.html\" %}\n{% block title %}Use The Force, Grapher{% endblock %}\n{% block content %}\n{% if result %}\n\n<div id=\"content-tools\">\n  {% include(\"partials/tools_search.html\") ignore missing %}\n</div>\n<div id=\"content-view\">\n  <div id=\"pile-graph\" style=\"background: #FFFFFF; display: block; position: relative;\">\n    <div id=\"graph-actions\" style=\"position: absolute; top: 10px; right: 20px;\" class=\"clearfix\">\n      <a style=\"display: none;\" id=\"btn-compose-message\" class=\"bulk-action\" href=\"#add-to-group\"><span class=\"icon-compose\"></span> Compose Message to Selected</a>\n      <a style=\"display: none;\" id=\"btn-found-group\" class=\"bulk-action\" href=\"#add-to-group\"><span class=\"icon-groups\"></span> Add Selected to Group</a>\n    </div>\n    <div id=\"pile-graph-canvas\" style=\"width: 100%;\">\n      <svg id=\"pile-graph-canvas-svg\" width=\"100%\" height=\"500\"></svg>\n    </div>\n  </div>\n</div>\n\n<script id=\"modal-grapher-node-detail\" type=\"text/template\">\n  <div class=\"modal-dialog\">\n    <div class=\"modal-content\">\n      <div class=\"modal-header\">\n        <button type=\"button\" class=\"close button-info\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n        <h4 class=\"modal-title\">\n          <span class=\"icon-user\"></span> \n          <% if (name) { %>\n          <%= name %>\n          <% } else { %>\n          <%= email %>\n          <% } %>\n        </h4>\n      </div>\n      <div class=\"modal-body clearfix\">\n        <% if (search.metadata) { %>\n        <p style=\"padding: 10px 15px; background: #d9d9d9;\"><strong><%= search.metadata.length %> Recent Tags & Messages from: <%= email %></strong></p>\n\n        <% var shown_tags = [] %>\n        <ul class=\"horizontal\">\n        <% _.each(search.metadata, function(msg, mid) { %>\n          <% _.each(msg.tag_tids, function(tid) { var tag = _.findWhere(Mailpile.instance.tags, {tid: tid}) %>\n            <% if (_.indexOf(shown_tags, tid) == -1 && _.indexOf(['priority', 'tag', 'archive'], tag.display) > -1) { shown_tags.push(tid) %>\n            <li class=\"half-right\"><a href=\"/search/?q=in:<%= tag.slug %>\" target=\"_blank\" title=\"Search for Tag\">\n              <span class=\"<%= tag.icon %>\"></span> <%= tag.name %></a> &nbsp;\n            </li>\n            <% } %>\n          <% }) %>\n        <% }) %>\n        </ul>\n        <ul>\n        <% _.each(search.metadata, function(msg, mid) { %>\n        <li>\n          <hr class=\"half-top half-bottom\">\n          <h4 class=\"half-bottom\">\n            <a href=\"<%= msg.urls.thread %>\"><%= msg.subject %> <span class=\"icon-lock-closed <%= msg.crypto.encryption %>\"></span></a> \n          </h4>\n          <span class=\"color:#999\"><%= msg.body.snippet %>... <a href=\"<%= msg.urls.thread %>\" target=\"_blank\">View Thread</a></span>\n        </li>\n        <% }) %>\n        </ul>\n        <% } else { %>\n        No results found for address: <%= email %>\n        <% } %>\n      </div>\n    </div>\n  </div>\n</script>\n\n\n<script>\n$(document).ready(function() {\n\tvar data = {{result|json|safe}};\n\tMailpile.plugins.forcegrapher.draw(data);\n});\n</script>\n{% endif %}\n{% endblock %}"
  },
  {
    "path": "shared-data/contrib/forcegrapher/forcegrapher.js",
    "content": "/* Use The Force, Grapher\n    - Renders your current search result set as a force directed graph\n    - Built using D3\n*/\n\nreturn {\n    draw: function(graph) {\n\n        // Determine & Set Height\n        var available_height = $(window).height() - ($('#header').height() + $('#content-tools').height());\n        var available_width = $(\"#content-wide\").width();\n\n        // Hide if tools empty (for alt themes like ArchivePile)\n        if ($('#content-tools').height() == 0) {\n          $('#content-tools').hide();\n          $(\"#content-wide\").css({ position: 'absolute', top: $('#header').height(), left: 0 });\n        }\n\n        $('#pile-graph-canvas').height(available_height);\n        $('#pile-graph-canvas').width(available_width);\n        $(\"#pile-graph-canvas-svg\").attr('height', available_height).height(available_height);\n\n        var width = available_width;\n        var height = available_height;\n        var force = d3.layout.force()\n                    .charge(-300)\n                    .linkDistance(100)\n                    .size([width, height]);\n\n        var svg = d3.select(\"#pile-graph-canvas-svg\");\n        $(\"#pile-graph-canvas-svg\").empty();\n\n        var color = d3.scale.category20();\n\n        var tooltip = d3.select(\"body\")\n            .append(\"div\")\n            .style(\"position\", \"absolute\")\n            .style(\"z-index\", \"10\")\n            .style(\"visibility\", \"hidden\")\n            .text(\"a simple tooltip\");\n\n        force\n            .nodes(graph.nodes)\n            .links(graph.links)\n            .start();\n\n        var link = svg.selectAll(\".link\")\n            .data(graph.links)\n            .enter().append(\"line\")\n            .style(\"stroke\", \"#333333\")\n            .style(\"stroke-width\", '1px');\n\n        // Calculate # of connections\n        // function(d) { return Math.sqrt(3*d.value); }\n\n        var node = svg.selectAll(\".node\")\n              .data(graph.nodes)\n              .enter().append(\"g\")\n              .attr(\"class\", \"node\")\n              .call(force.drag);\n\n        node.append(\"circle\")\n            .attr(\"r\", 8)\n            .style(\"fill\", function(d) { return color(\"#337FB2\"); })\n\n        node.append(\"text\")\n            .attr(\"x\", 12)\n            .attr(\"dy\", \"0.35em\")\n            .style({\"opacity\": \"0.3\", \"font-size\": \"12px\"})\n            .text(function(d) {\n              if (d.name !== undefined) {\n                return d.name;\n              } else {\n                var email_name = d.email.split('@');\n                return email_name[0];\n              }\n            });\n\n        link.append(\"text\").attr(\"x\", 12).attr(\"dy\", \".35em\").text(function(d) { return d.type; })\n\n        node.on(\"click\", function(d, m, q) {\n\n            // d.attr(\"toggled\", !d.attr(\"toggled\"));\n            // d.style(\"color\", \"#f00\");\n            if (Mailpile.graphselected.indexOf(d[\"email\"]) < 0) {\n                d3.select(node[q][m]).selectAll(\"circle\").style(\"fill\", \"#4B9441\");\n                Mailpile.graphselected.push(d[\"email\"]);\n            } else {\n                Mailpile.graphselected.pop(d[\"email\"]);\n                d3.select(node[q][m]).selectAll(\"circle\").style(\"fill\", \"#337FB2\");\n            }\n\n            Mailpile.plugins.forcegrapher.node_click(d)\n        });\n        node.on(\"mouseover\", function(d, m, q) {\n            d3.select(node[q][m]).selectAll(\"text\").style(\"opacity\", \"1\");\n        });\n        node.on(\"mouseout\", function(d, m, q) {\n            d3.select(node[q][m]).selectAll(\"text\").style(\"opacity\", \"0.2\");\n        });\n\n        force.on(\"tick\", function() {\n            node.attr(\"transform\", function(d) {\n                if (d.x < 0) { d.x = 0; }\n                if (d.y < 0) { d.y = 0; }\n                if (d.x > width) { d.x = width; }\n                if (d.y > height) { d.y = height; }\n                return \"translate(\" + d.x + \",\" + d.y + \")\";\n            });\n            link.attr(\"x1\", function(d) { return d.source.x; })\n                .attr(\"y1\", function(d) { return d.source.y; })\n                .attr(\"x2\", function(d) { return d.target.x; })\n                .attr(\"y2\", function(d) { return d.target.y; });\n        });\n    },\n    node_click: function(contact) {\n\n      contact['search'] = {};\n\n      var show_contact_modal = function(contact) {\n        var modal_template = _.template($('#modal-grapher-node-detail').html());\n        $('#modal-full').html(modal_template(contact));\n        $('#modal-full').modal(Mailpile.UI.ModalOptions);\n      }\n\n      var clean_email_addresses = function(input) {\n        var separateEmailsBy = \", \";\n        var email = \"<none>\";\n        var emailsArray = input.match(/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\\.[a-zA-Z0-9._-]+)/gi);\n        if (emailsArray) {\n            email = \"\";\n            for (var i = 0; i < emailsArray.length; i++) {\n                if (i != 0) email += separateEmailsBy;\n                email += emailsArray[i];\n            }\n        }\n        return email;\n      }\n\n      if (contact.email) {\n\n        // Clean Junk\n        contact.email = clean_email_addresses(contact.email);\n\n        // Do Search\n        $.ajax({\n  \t\t\t  url: Mailpile.api.search + '?q=from:' + contact.email,\n          type: 'GET',\n          dataType: 'json',\n  \t\t  \tsuccess: function(result) {\n            console.log(result);\n            if (result.result && result.result.data) {\n              contact.search = result.result.data;\n              show_contact_modal(contact);\n            } else if (result.result) {\n              show_contact_modal(contact);              \n            }\n  \t\t  \t}\n  \t\t  });\n\n      }\n\n    }\n}\n"
  },
  {
    "path": "shared-data/contrib/forcegrapher/forcegrapher.py",
    "content": "import datetime\nimport re\nimport time\n\nfrom mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.commands import Command\nfrom mailpile.mailutils import Email, ExtractEmails\nfrom mailpile.search import MailIndex\nfrom mailpile.util import *\n\nfrom mailpile.plugins.search import Search\n\n\nclass Graph(Search):\n    \"\"\"Get a graph of the network in the current search results.\"\"\"\n    ORDER = ('Searching', 1)\n    HTTP_CALLABLE = ('GET', )\n    UI_CONTEXT = \"search\"\n\n    def command(self, search=None):\n        session, idx = self._do_search(search=search)\n\n        nodes = []\n        links = []\n        res = {}\n\n        for messageid in session.results:\n            msg = self._idx().get_msg_at_idx_pos(messageid)\n            msgfrom = msg[self._idx().MSG_FROM]\n            msgto = [self._idx().EMAILS[int(x, 36)]\n                     for x in msg[self._idx().MSG_TO].split(\",\") if x != \"\"]\n            m = re.match(\"((.*) ){0,1}\\<(.*)\\>\", msgfrom)\n            if m:\n                name = m.groups(0)[1]\n                email = m.groups(0)[2]\n            else:\n                name = None\n                email = msgfrom\n\n            if email not in [m[\"email\"] for m in nodes]:\n                n = {\"email\": email}\n                if name:\n                    n[\"name\"] = name\n                nodes.append(n)\n\n            for address in msgto:\n                if address not in [m[\"email\"] for m in nodes]:\n                    nodes.append({\"email\": address})\n\n            curnodes = [x[\"email\"] for x in nodes]\n            fromid = curnodes.index(email)\n            searchspace = [m for m in links if m[\"source\"] == fromid]\n            for address in msgto:\n                index = curnodes.index(address)\n                link = [m for m in searchspace if m[\"target\"] == index]\n                if len(link) == 0:\n                    links.append({\"source\": fromid,\n                                  \"target\": index,\n                                  \"value\": 1})\n                elif len(link) == 1:\n                    link[0][\"value\"] += 1\n                else:\n                    raise ValueError(\"Too many links! - This should never \"\n                                     \"happen.\")\n\n            if len(nodes) >= 300:\n                # Let's put a hard upper limit on how many nodes we can\n                # have, for performance reasons.\n                # There might be a better way to do this though...\n                res[\"limit_hit\"] = True\n                break\n\n        res[\"nodes\"] = nodes\n        res[\"links\"] = links\n        res[\"searched\"] = session.searched\n        if \"limit_hit\" not in res:\n            res[\"limit_hit\"] = False\n\n        return self._success(_('Generated graph view'), res)\n"
  },
  {
    "path": "shared-data/contrib/forcegrapher/manifest.json",
    "content": "# This is a Mailpile plugin manifest, describing the `forcegrapher` plugin.\n{\n    \"name\": \"forcegrapher\",\n    \"author\": \"The Mailpile Team <team@mailpile.is>\",\n    \"description\": \"Generates a simple force directed graph for a given search query\",\n    \"config\": {},\n    \"code\": {\n        \"python\": [\"forcegrapher.py\"],\n        \"javascript\": [\"forcegrapher.js\", \"d3.js\"],\n        \"css\": [\"forcegrapher.css\"]\n    },\n    \"routes\": {\n        \"/graph/\": {\"file\": \"forcegrapher.html\", \"api\": 0}\n    },\n    \"user_interface\": {\n        \"display_modes\": [\n            {\n                \"context\": [\"/search/\"],\n                \"name\": \"graph\",\n                \"text\": \"Graph\",\n                \"description\": \"Show contact relationships\",\n                \"icon\": \"force-graph\",\n                \"url\": \"/graph/\"\n            }\n        ]\n    },\n    \"commands\": [\n        {\n            \"class\": \"Graph\",\n            \"name\": \"graph\",\n            \"url\": \"graph\"\n        }\n    ]\n}\n"
  },
  {
    "path": "shared-data/contrib/hacks/hacks.py",
    "content": "import json\nimport os\nimport traceback\nfrom gettext import gettext as _\nfrom urllib import urlencode, URLopener\n\nimport mailpile.auth\nfrom mailpile.commands import Command\nfrom mailpile.conn_brokers import TcpConnectionBroker as TcpConnBroker\nfrom mailpile.index.search import CachedSearchResultSet\nfrom mailpile.mailutils.headerprint import *\nfrom mailpile.mailutils.emails import *\nfrom mailpile.mailutils import *\nfrom mailpile.plugins.core import Help\nfrom mailpile.search import *\nfrom mailpile.util import *\nfrom mailpile.vcard import *\n\n\nclass Hacks(Command):\n    \"\"\"Various hacks ...\"\"\"\n    SYNOPSIS = (None, 'hacks', None, None)\n    ORDER = ('Internals', 9)\n    HTTP_CALLABLE = ()\n\n    def command(self):\n        return self._success('OK', Help(self.session, arg=['hacks']).run())\n\n\nclass FixIndex(Hacks):\n    \"\"\"Do various things to try and fix broken indexes\"\"\"\n    SYNOPSIS = (None, 'hacks/fixindex', None, None)\n    LOG_PROGRESS = True\n\n    def command(self):\n        session, index = self.session, self._idx()\n\n        session.ui.mark('Checking index for duplicate MSG IDs...')\n        found = {}\n        for i in range(0, len(index.INDEX)):\n            msg_id = index.get_msg_at_idx_pos(i)[index.MSG_ID]\n            if msg_id in found:\n                found[msg_id].append(i)\n            else:\n                found[msg_id] = [i]\n\n        session.ui.mark('Attempting to fix dups with bad location...')\n        for msg_id in found:\n            if len(found[msg_id]) > 1:\n                good, bad = [], []\n                for idx_pos in found[msg_id]:\n                    msg = Email(index, idx_pos).get_msg()\n                    if msg:\n                        good.append(idx_pos)\n                    else:\n                        bad.append(idx_pos)\n                if good and bad:\n                    good_info = index.get_msg_at_idx_pos(good[0])\n                    for bad_idx in bad:\n                        bad_info = index.get_msg_at_idx_pos(bad_idx)\n                        bad_info[index.MSG_PTRS] = good_info[index.MSG_PTRS]\n                        index.set_msg_at_idx_pos(bad_idx, bad_info)\n\n        return self._success(_('Tried to fix metadata index'))\n\n\nclass PyCLI(Hacks):\n    \"\"\"Launch a Python REPL\"\"\"\n    SYNOPSIS = (None, 'hacks/pycli', None, None)\n    LOG_PROGRESS = True\n\n    def command(self):\n        import code\n        import readline\n        import mailpile.util\n        from mailpile import Mailpile\n\n        variables = globals()\n        variables['session'] = self.session\n        variables['config'] = self.session.config\n        variables['index'] = self.session.config.index\n        variables['mp'] = Mailpile(session=self.session)\n\n        try:\n            mailpile.util.QUITTING = True\n            self.session.config.stop_workers()\n            self.session.ui.block()\n        finally:\n            mailpile.util.QUITTING = False\n\n        code.InteractiveConsole(locals=variables).interact(\"\"\"\\\nThis is Python inside of Mailpile inside of Python.\n\n   - The `mp` variable is a Pythonic API to the current pile of mail.\n   - The `session` variable is the current UI session.\n   - The `config` variable contains the current configuration.\n   - Press CTRL+D to return to the normal CLI.\n\"\"\")\n        self.session.ui.unblock()\n        self.session.config.prepare_workers(self.session, daemons=True)\n\n        return self._success(_('That was fun!'))\n\n\nclass ViewMetadata(Hacks):\n    \"\"\"Display the raw metadata for a message\"\"\"\n    SYNOPSIS = (None, 'hacks/metadata', None, '[<message>]')\n\n    def _explain(self, i):\n        idx, cfg = self._idx(), self.session.config\n        raw_info = idx.INDEX[i]\n        info = idx.get_msg_at_idx_pos(i)\n        ptags = [cfg.get_tag(t) or t\n                 for t in info[idx.MSG_TAGS].split(',') if t]\n        ptags = [t.name for t in ptags if hasattr(t, 'name')]\n        pptrs = ['%s -> %s' % (cfg.sys.mailbox.get(p[:MBX_ID_LEN],\n                                                   p[:MBX_ID_LEN] + '?'),\n                               p[MBX_ID_LEN:])\n                 for p in info[idx.MSG_PTRS].split(',') if p]\n        to = idx.expand_to_list(info)\n        cc = idx.expand_to_list(info, idx.MSG_CC)\n        body = info[idx.MSG_BODY]\n        if body[:1] == '{' and body[-1:] == '}':\n            try:\n                body_info = json.loads(body)\n            except:\n                body_info = body\n        else:\n            body_info = {'snippet': body}\n        return {\n            'mid': info[idx.MSG_MID],\n            'ptrs': info[idx.MSG_PTRS],\n            'id': info[idx.MSG_ID],\n            'date': info[idx.MSG_DATE],\n            'from': info[idx.MSG_FROM],\n            'to': info[idx.MSG_TO],\n            'cc': info[idx.MSG_CC],\n            'kb': info[idx.MSG_KB],\n            'subject': info[idx.MSG_SUBJECT],\n            'tags': info[idx.MSG_TAGS],\n            'replies': info[idx.MSG_REPLIES],\n            'thread_mid': info[idx.MSG_THREAD_MID],\n            'parsed': {\n                'body_info': body_info,\n                'date': friendly_datetime(long(info[idx.MSG_DATE], 36)),\n                'tags': ', '.join(ptags),\n                'to': to,\n                'cc': cc,\n                'ptrs': pptrs\n            },\n            'metadata_raw': raw_info,\n            'metadata_bytes': len(raw_info)\n        }\n\n    def command(self):\n        return self._success(_('Displayed raw metadata'),\n            [self._explain(i) for i in self._choose_messages(self.args)])\n\n\nclass EditMetadata(ViewMetadata):\n    \"\"\"Edit the raw metadata for a message (DANGEROUS!)\"\"\"\n    SYNOPSIS = (None, 'hacks/metadata/edit', None, '[<message>] [--ptrs=...|--metadata=...]')\n    SPLIT_ARG = False\n\n    def _new_raw_data(self, op):\n        raw_data = op.split('=', 1)[-1].strip()\n        if op[0] == op[-1] == '\"':\n            raw_data = json.loads(op)\n        return raw_data\n\n    def _edit(self, op, i):\n        idx, cfg = self._idx(), self.session.config\n        info = idx.get_msg_at_idx_pos(i)\n        orig = idx.INDEX[i]\n\n        try:\n            if op.startswith('metadata='):\n                idx.INDEX[i] = self._new_raw_data(op)\n                info = idx.get_msg_at_idx_pos(i)\n\n            elif op.startswith('ptrs='):\n                info[idx.MSG_PTRS] = self._new_raw_data(op)\n\n            else:\n                raise ValueError('Unknown edit op: %s' % op)\n\n            idx.set_msg_at_idx_pos(i, info)\n            idx.update_ptrs_and_msgids(self.session)\n            ClearParseCache(full=True)\n\n            return self._explain(i)\n\n        except:\n            idx.INDEX[i] = orig\n            raise\n\n    def command(self):\n        arg, op = (' '.join(self.args)).split('--')\n        args = arg.strip().split(' ')\n        return self._success(_('Edited raw metadata'),\n            [self._edit(op, i) for i in self._choose_messages(args)])\n\n\nclass ViewKeywords(Hacks):\n    \"\"\"Display the keywords for a message\"\"\"\n    SYNOPSIS = (None, 'hacks/keywords', None, '[<message>]')\n\n    def _explain(self, i):\n        idx = self._idx()\n        info = idx.get_msg_at_idx_pos(i)\n        msg = Email(idx, i).get_msg()\n        return sorted(list(idx.read_message(\n            self.session,\n            info[idx.MSG_MID], info[idx.MSG_ID], msg,\n            long(info[idx.MSG_KB], 36) * 1024,\n            long(info[idx.MSG_DATE], 36))[0]))\n\n    def command(self):\n        return self._success(_('Displayed message keywords'),\n            [self._explain(i) for i in self._choose_messages(self.args)])\n\n\nclass ViewHeaderPrint(Hacks):\n    \"\"\"Display the HeaderPrint for a message\"\"\"\n    SYNOPSIS = (None, 'hacks/headerprint', None, '[<message>]')\n\n    def _explain(self, i):\n        msg = Email(self._idx(), i).get_msg()\n        mta = HeaderPrintMTADetails(msg)\n        mua = HeaderPrintMUADetails(msg, mta=mta)\n        return {\n            'headerprints': HeaderPrints(msg),\n            'details': {\n                'header': HeaderPrintGenericDetails(msg),\n                'mua': mua,\n                'mta': mta\n            }\n        }\n\n    def command(self):\n        return self._success(_('Displayed message HeaderPrint'),\n            [self._explain(i) for i in self._choose_messages(self.args)])\n\n\nHACKS_SESSION_ID = None\n\nclass Http(Hacks):\n    \"\"\"Send HTTP requests to the web server\"\"\"\n    SYNOPSIS = (None, 'hacks/http', None,\n                '<GET|POST> </url/> [<Q|P> <var>=<val> ...]')\n\n#    class CommandResult(Hacks.CommandResult):\n#        def as_text(self):\n#            pass\n\n    def command(self):\n        args = list(self.args)\n        method, url = args[0:2]\n\n        if not url.startswith('http'):\n            url = 'http://%s:%s%s' % (self.session.config.sys.http_host,\n                                      self.session.config.sys.http_port,\n                                      ('/' + url).replace('//', '/'))\n\n        # FIXME: The python URLopener doesn't seem to support other verbs,\n        #        which is really quite lame.\n        method = method.upper()\n        assert(method in ('GET', 'POST'))\n\n        qv, pv = [], []\n        if method == 'POST':\n            which = pv\n        else:\n            which = qv\n        for arg in args[2:]:\n            if '=' in arg:\n                which.append(tuple(arg.split('=', 1)))\n            elif arg.upper()[0] == 'P':\n                which = pv\n            elif arg.upper()[0] == 'Q':\n                which = qv\n\n        if qv:\n            qv = urlencode(qv)\n            url += ('?' in url and '&' or '?') + qv\n\n        # Log us in automagically!\n        httpd = self.session.config.http_worker.httpd\n        global HACKS_SESSION_ID\n        if HACKS_SESSION_ID is None:\n            HACKS_SESSION_ID = httpd.make_session_id(None)\n        mailpile.auth.SetLoggedIn(None,\n                                  user='Hacks plugin HTTP client',\n                                  session_id=HACKS_SESSION_ID)\n        cookie = httpd.session_cookie\n\n        try:\n            uo = URLopener()\n            uo.addheader('Cookie', '%s=%s' % (cookie, HACKS_SESSION_ID))\n            with TcpConnBroker().context(need=[TcpConnBroker.OUTGOING_HTTP],\n                                         oneshot=True):\n                if method == 'POST':\n                    (fn, hdrs) = uo.retrieve(url, data=urlencode(pv))\n                else:\n                    (fn, hdrs) = uo.retrieve(url)\n            hdrs = unicode(hdrs)\n            data = open(fn, 'rb').read().strip()\n            if data.startswith('{') and 'application/json' in hdrs:\n                data = json.loads(data)\n            return self._success('%s %s' % (method, url), result={\n                'headers': hdrs.splitlines(),\n                'data': data\n            })\n        except:\n            self._ignore_exception()\n            return self._error('%s %s' % (method, url))\n\n\nclass CheckMailbox(Hacks):\n    \"\"\"Sanity-check and optionally fix a mailbox\"\"\"\n    SYNOPSIS = (None, 'hacks/chkmbx', None, '[-force] [-noremote] '\n                                            '[-auto|-index|-clean|-dedup] '\n                                            '[all|<ID>]')\n\n    def command(self):\n        session, config, idx = self.session, self.session.config, self._idx()\n        flags = [a[1:] for a in self.args if a[:1] == '-']\n        if 'auto' in flags:\n            flags.extend(['clean', 'dedup', 'index'])\n\n        mbxids = [a for a in self.args if a[:1] != '-']\n        if 'all' in mbxids:\n            mbxids = config.sys.mailbox.keys()\n\n        results = {}\n        errors = {}\n        for mbx_id in mbxids:\n            result = results[mbx_id] = {\n                'messages': None,\n                'finalized': [],\n                'unindexed': [],\n                'duplicates': [],\n                'source_map': False\n            }\n            seen = {}\n            msgids = {}\n            indexed = {}\n            try:\n                session.ui.mark('%s: Opening mailbox' % mbx_id)\n                mbx = config.open_mailbox(session, mbx_id, prefer_local=True)\n                try:\n                    remote = config.open_mailbox(session, mbx_id,\n                                                 prefer_local=False)\n                except:\n                    remote = None\n\n                def _mark_progress(what, counts):\n                    counts[0] += 1\n                    i, n = counts[0], counts[1] or 1  # Avoid divide by zero\n                    if i > max(10, (n/25)) and 0 == i % max(1, (n//397)):\n                        session.ui.mark('%s: %s: Message %d/%d (%d%%)'\n                                        % (mbx_id, what, i, n, 100 * i / n))\n\n                # We do the scan with the mailbox locked, just to be a bit\n                # paranoid. Not doing this was a good way to find bugs...\n                with mbx:\n                    mbx.update_toc()\n                    result['messages'] = len(mbx)\n                    session.ui.mark('%s: Checking %d messages'\n                                    % (mbx_id, len(mbx)))\n                    counts = [0, len(mbx)]\n                    for key in list(mbx.keys()):\n                        _mark_progress('Checking', counts)\n                        try:\n                            message = mbx[key]  # FIXME: only need header...\n                        except KeyError:\n                            traceback.print_exc()\n                            session.ui.notify('%s: Not found in mailbox: %s'\n                                              % (mbx_id, key))\n                            continue\n\n                        enc_msgid = idx.get_msg_id(message, 'bogus')\n                        msgids[key] = enc_msgid\n                        if enc_msgid in seen:\n                            seen[enc_msgid].add(key)\n                        else:\n                            seen[enc_msgid] = set([key])\n                        msg_idx_pos = idx.MSGIDS.get(enc_msgid)\n                        if msg_idx_pos is None:\n                            session.ui.notify('%s: Not in index: %s %s'\n                                              % (mbx_id, key, enc_msgid))\n                            result['unindexed'].append((key, enc_msgid))\n                        else:\n                            msg_info = idx.get_msg_at_idx_pos(msg_idx_pos)\n                            for ptr in msg_info[idx.MSG_PTRS].split(','):\n                                if ptr[:MBX_ID_LEN] == mbx_id:\n                                    indexed[enc_msgid] = ptr[MBX_ID_LEN:]\n\n                        if 'x-mp-internal-readonly' in message:\n                            result['finalized'].append(key)\n\n                for msg_id, keys in seen.iteritems():\n                    if len(keys) > 1:\n                        result['duplicates'].append([msg_id] + list(keys))\n\n                if hasattr(mbx, 'source_map'):\n                    result['source_map'] = len(mbx.source_map)\n                elif mbx.is_local:\n                    try:\n                        mbx.source_map = {}\n                        result['source_map'] = 0\n                    except AttributeError:\n                        session.ui.warning('%s: Failed to add source_map'\n                                           % mbx_id)\n\n                if remote and 'noremote' not in flags:\n                    if hasattr(mbx, 'source_map') and len(mbx.source_map) > 0:\n                        session.ui.mark('%s: Comparing with source' % mbx_id)\n                        result['source_unknown'] = []\n                        result['source_missing'] = []\n                        result['source_mismatch'] = []\n\n                        mapped = mbx.source_map.values()\n                        for k in mbx.iterkeys():\n                            if k not in mapped:\n                                result['source_unknown'].append(k)\n\n                        counts = [0, len(mapped)]\n                        for sk in mbx.source_map.iteritems():\n                            _mark_progress('Comparing', counts)\n                            source_id, key = sk\n                            try:\n                                # FIXME: Can we grab only the header?\n                                src_msg = remote[source_id]\n                                loc_msgid = msgids[key]\n                                if idx.get_msg_id(src_msg, 'x') != loc_msgid:\n                                    session.ui.notify(\n                                        '%s: Source mismatch: %s %s'\n                                        % (mbx_id, source_id, key))\n                                    result['source_missing'].append(sk)\n                            except (IndexError, KeyError):\n                                session.ui.notify(\n                                    '%s: Source missing: %s %s'\n                                    % (mbx_id, source_id, key))\n                                result['source_missing'].append(sk)\n\n                if 'index' in flags:\n                    reindex = result['unindexed'][:]\n                else:\n                    reindex = []\n\n                if 'clean' in flags or 'dedup' in flags:\n                    if mbx.is_local or mbx.editable or 'force' in flags:\n                        result['removed'] = []\n                        session.ui.mark('%s: Removing autosaved drafts'\n                                        % mbx_id)\n                        counts = [0, len(result['duplicates'])]\n                        for dups in result['duplicates'][:]:\n                            _mark_progress('Clean', counts)\n                            dlist = dups[1:]\n                            for k in dlist:\n                                if k in result['finalized']:\n                                    if indexed[msgids[k]] != k:\n                                        # Make sure this gets rescanned\n                                        reindex.append((k, msgids[k]))\n                                    dlist.remove(k)\n                                    for k in dlist:\n                                        mbx.remove(k)\n                                        result['removed'].append(k)\n                                    result['duplicates'].remove(dups)\n                                    break\n                    else:\n                        session.ui.warning('Use -force if you are sure you '\n                                           'want to modify this mailbox.')\n\n                if 'dedup' in flags:\n                    if mbx.is_local or mbx.editable or 'force' in flags:\n                        session.ui.mark('%s: Removing duplicate messages'\n                                        % mbx_id)\n                        if 'removed' not in result:\n                            result['removed'] = []\n                        counts = [0, len(result['duplicates'])]\n                        for dups in result['duplicates'][:]:\n                            _mark_progress('Dedup', counts)\n                            dlist = dups[1:]\n                            msgid = msgids[dups[1]]\n                            if msgid in indexed:\n                                # Remove all except the one that is already\n                                # in the metadata index.\n                                dlist.remove(indexed[msgid])\n                            else:\n                                 dlist = dups[1:-1]\n                            for k in dlist:\n                                mbx.remove(k)\n                                result['removed'].append(k)\n                            result['duplicates'].remove(dups)\n                    else:\n                        session.ui.warning('Use -force if you are sure you '\n                                           'want to modify this mailbox.')\n\n                if reindex:\n                    session.ui.mark('%s: Indexing unindexed messages'\n                                    % mbx_id)\n                    result['indexed'] = []\n                    counts = [0, len(reindex)]\n                    for key, message_id in reindex:\n                        _mark_progress('Index', counts)\n                        config.index.scan_one_message(\n                            session, mbx_id, mbx, key, wait=True)\n\n            except KeyboardInterrupt:\n                mbx.update_toc()\n                errors[mbx_id] = ('Interrupted', '')\n                break\n            except:\n                errors[mbx_id] = ('Failed', traceback.format_exc())\n\n            mbx.update_toc()\n\n        if errors:\n            return self._error('Checked %d mailboxes' % len(results),\n                               info=errors, result=results)\n        else:\n            return self._success('Checked %d mailboxes' % len(results),\n                                 result=results)\n\n\n_REAL_MAKEMESSAGEDATE = None\n\nclass FakeSendingDates(Hacks):\n    \"\"\"Fake the dates of outgoing messages\"\"\"\n    SYNOPSIS = (None, 'hacks/fakedates', None, ' [<minutes> --] <date>')\n\n    def command(self):\n        import subprocess\n        import mailpile.mailutils.emails\n\n        global _REAL_MAKEMESSAGEDATE\n        if _REAL_MAKEMESSAGEDATE is None:\n            _REAL_MAKEMESSAGEDATE = mailpile.mailutils.emails.MakeMessageDate\n\n        if self.args[1] == '--':\n            minutes = float(self.args[0])\n            args = self.args[2:]\n        else:\n            minutes = 5\n            args = self.args\n\n        faked_ts = int(subprocess.check_output(\n            ['date', '+%s', '--date', ' '.join(args)]\n            ).decode('utf-8').strip())\n\n        deadline = time.time() + (minutes * 60)\n\n        def _HackedMMD(ts=None):\n            if time.time() > deadline:\n                mailpile.mailutils.emails.MakeMessageDate = _REAL_MAKEMESSAGEDATE\n                return _REAL_MAKEMESSAGEDATE(ts=ts)\n            else:\n                return _REAL_MAKEMESSAGEDATE(ts=faked_ts)\n\n        mailpile.mailutils.emails.MakeMessageDate = _HackedMMD\n        return self._success(\n            'Dates will be faked as %s for the next %.1f minutes'\n                % (faked_ts, minutes))\n"
  },
  {
    "path": "shared-data/contrib/hacks/manifest.json",
    "content": "# This is a Mailpile plugin manifest, describing the `hacks` plugin.\n{\n    \"name\": \"hacks\",\n    \"author\": \"The Mailpile Team <team@mailpile.is>\",\n    \"description\": \"Tools for hackers and developers. This plugin adds some low-level CLI commands for exploring Mailpile data structures and internals.\",\n    \"display\": true,\n    \"public\": false,\n    \"code\": {\n        \"python\": [\"hacks.py\"],\n        \"javascript\": [],\n        \"css\": []\n    },\n\n    # This section defines URL routes and MIME types.\n    \"routes\": {},\n\n    # Please see https://github.com/pagekite/Mailpile/wiki/Config for\n    # details about the configuration file syntax.\n    \"config\": {},\n\n    # These are our Python-related hooks\n    \"commands\": [\n        {\n            \"class\": \"Hacks\",\n            \"name\": \"hacks\"\n        },\n        {\n            \"class\": \"FixIndex\",\n            \"name\": \"hacks/fixindex\"\n        },\n        {\n            \"class\": \"PyCLI\",\n            \"name\": \"hacks/pycli\"\n        },\n        {\n            \"class\": \"ViewMetadata\",\n            \"name\": \"hacks/metadata\"\n        },\n        {\n            \"class\": \"EditMetadata\",\n            \"name\": \"hacks/metadata/edit\"\n        },\n        {\n            \"class\": \"ViewKeywords\",\n            \"name\": \"hacks/keywords\"\n        },\n        {\n            \"class\": \"ViewHeaderPrint\",\n            \"name\": \"hacks/headerprint\"\n        },\n        {\n            \"class\": \"Http\",\n            \"name\": \"hacks/http\"\n        },\n        {\n            \"class\": \"CheckMailbox\",\n            \"name\": \"hacks/chkmbx\"\n        },\n        {\n            \"class\": \"FakeSendingDates\",\n            \"name\": \"hacks/fakedates\"\n        }\n    ]\n}\n"
  },
  {
    "path": "shared-data/contrib/hints/hints/autotagging.html",
    "content": "<!--\nThis file provides the user with a defintion of autotagging,\nand directions on how to adjust or create an automated tag.\n-->\n\n{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% block title %}{{_(\"Autotagging\")}}{% endblock %}\n{% block content %}\n<div class=\"content-normal\" style=\"max-width: 46em;\">\n\n  <h1><span class=\"icon-tag\"></span>\n    {{_(\"Tag Automation\")}}\n  </h1>\n   <p>\n    {{_(\"Tag automation can be used to tag or untag messages automatically.\")}}\n  </p>\n  <p>\n    <b>{{_(\"How to change an existing tags' automation settings: \")}}</b>\n    <br>\n    <ul class=\"list\" style=\"margin-left: 2em; list-style: disc\">\n      <li>{{_(\"Select the tag you want to edit.\")}}</li>\n      <li>{{_(\"Click \\\"Edit\\\".\")}}</li>\n      <li>{{_(\"Click \\\"Automation\\\".\")}}</li>\n      <li>{{_(\"Under <i>Tagging</i>, select the \\\"Automatic tagging\\\" option if you want automatic tagging.\")}}</li>\n      <li>{{_(\"Under <i>Untagging</i>, select when and where you want messages to be moved.\")}}</li>\n    </ul>\n  </p>\n  <p>\n    <b>{{_(\"How to make a new tag with automation:\")}}</b>\n                <br>\n    <ul class=\"list\" style=\"margin-left: 2em; list-style: disc\">\n      <li>{{_(\"Click the Add button. (It's in the lower left corner!)\")}}</li>\n      <li>{{_(\"Choose the desired settings for \\\"Tag Settings\\\" and \\\"Technical Settings\\\".\")}}</li>\n      <li>{{_(\"Click \\\"Automation\\\".\")}}</li>\n      <li>{{_(\"Under <i>Tagging</i>, select the \\\"Automatic tagging\\\" option if you want automatic tagging.\")}}</li>\n      <li>{{_(\"Under <i>Untagging</i>, select when and where you want messages to be moved.\")}}</li>\n    </ul>\n  </p>\n  <p>\n    {{_(\"You can adjust most existing and new tags to use automation to make your Mailpile even better!\")}}\n    {{_(\"Tags that do not support automation will not have an \\\"Automation\\\" section in their settings.\")}}\n  </p>\n\n        <h4 style=\"margin: 2em 0 1em 0;\">{{_(\"How does it work?\")}}</h4>\n  <p>\n    <a class=\"auto-modal\" href=\"{{ U(\"/page/hints/spam.html\") }}\">{{_(\"The default Spam tag is an example of both automated tagging and un-tagging.\")}}</a>\n    {{_(\"Spam automatically tags incoming messages based on statistical analysis of your mail and spam, and then automatically untags the message again (moving them to Trash) after a month has passed.\")}}\n    {{_(\"You can use the same tools to manage other types of e-mail, notifications and promotions are popular choices.\")}}\n  </p>\n  <p>\n    {{_(\"Tag automation can be used to tag messages or untag them. These behaviours can be configured separately or used together.\")}}\n  </p>\n  <p>\n    {{_(\"Automated tagging is based on bayesian analysis; you start by tagging messages by hand and over time Mailpile will learn which messages belong and which don't.\")}}\n    {{_(\"It will then tag incoming mail automatically!\")}}\n    {{_(\"Much like any automated system, the automated tagging may make mistakes. Correcting these mistakes by manually tagging (or untagging) messages will help train the system. Over time it will become more accurate and need less manual intervention.\")}}\n  </p>\n  <p>\n    {{_(\"Automated untagging is based on time and activity.\")}}\n    {{_(\"Once a message has been untouched for a while (you choose the interval), it can be automatically untagged, deleted or moved to another tag.\")}}\n  </p>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/contrib/hints/hints/backups.html",
    "content": "{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% block title %}{{_(\"Backup Reminder\")}}{% endblock %}\n{% block content %}\n<div class=\"content-normal\" style=\"max-width: 46em;\">\n\n  <h1><span class=\"icon-archive\"></span>\n    {{_(\"Backup Reminder\")}}\n  </h1>\n  <p>\n    {{_(\"Your Mailpile now has {t} tags and {e} e-mails.\")\n        .format(t=(config.tags|length - 36), e=mailpile_size)}}\n  </p>\n  <ul class=\"list\" style=\"margin-left: 2em; list-style: disc\">\n    <li>{{_(\"There is no password reset function.\")}}</li>\n    <li>{{_(\"If your encryption keys are lost, e-mail may become permanently inaccessible.\")}}</li>\n{% if config.prefs.allow_deletion %}\n    <li><a class=\"auto-modal\" href=\"/page/hints/deletion.html\">{{_(\"E-mail deletion is enabled.\")}}</a></li>\n{% endif %}\n  </ul>\n\n  <p>\n    {{_(\"Please make sure you have good backups!\")}}\n    <br>\n    {{_(\"Your Mailpile data is in these locations:\")}}\n  </p>\n  <ul class=\"list\" style=\"margin-left: 2em; list-style: disc\">\n    <li><a target=_blank href=\"file://{{ config.workdir }}\">{{ config.workdir }}</a>\n{%- if config.gnupghome != config.workdir %} ({{_(\"Mailpile data\")}})</li>\n    <li><a target=_blank href=\"file://{{ config.gnupghome }}\">{{ config.gnupghome }}</a> ({{_(\"PGP keys\")}})\n{%- endif %}</li>\n  </ul>\n  <p>\n    {{_(\"It is also a good idea to write down your Mailpile password, if you think there is any chance you might forget it.\")}}</li>\n  </p>\n\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/contrib/hints/hints/deletion.html",
    "content": "{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% block title %}{{_(\"E-mail Deletion\")}}{% endblock %}\n{% block content %}\n<div class=\"content-normal\" style=\"max-width: 46em;\">\n\n  <h1><span class=\"icon-trash\"></span>\n    {{_(\"E-Mail Deletion\")}}\n  </h1>\n{% if config.prefs.allow_deletion %}\n  <p>\n    {{_(\"Your Mailpile is configured to allow permanent deletion of e-mail.\")}}\n  </p>\n  <ul class=\"list\" style=\"margin-left: 2em; list-style: disc\">\n    <li>{{_(\"Depending on your preferences, messages may be permanently deleted from remote IMAP or POP3 servers after they have been downloaded.\")}}</li>\n    <li>{{_(\"When e-mail expires from the Trash (or you empty the trash manually), the messages will be gone forever unless you have kept backups.\")}}</li>\n  </ul>\n  <p>\n    {{_(\"This is good for your privacy and may also conserve some disk space.\")}}\n  </p>\n{% else %}\n  <p>\n    {{_(\"Deletion of e-mail is currently disabled.\")}}\n  </p>\n  <ul class=\"list\" style=\"margin-left: 2em; list-style: disc\">\n    <li>{{_(\"Original copies of all e-mail will be left intact on any IMAP or POP3 servers.\")}}</li>\n    <li>{{_(\"The Trash cannot be emptied.\")}}</li>\n  </ul>\n  <p>\n    {{_(\"This is fine if you are just testing Mailpile.\")}}\n  </p>\n{% endif %}\n  <p><a data-dismiss=modal href=\"/settings/privacy.html#data-security\">\n    {{_(\"Click here to change your e-mail deletion settings.\")}}\n  </a></p>\n\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/contrib/hints/hints/dragging-tags.html",
    "content": "{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% block title %}{{_(\"Dragging and dropping tags\")}}{% endblock %}\n{% block content %}\n<div class=\"content-normal\" style=\"max-width: 46em;\">\n\n  <h1><span class=\"icon-tag\"></span>\n    {{_(\"Dragging and dropping tags\")}}\n  </h1>\n  <p>\n    {{_(\"You can drag and drop tags or messages to add or change tags.\")}}<br>\n    {{_(\"The rules are:\")}}\n  </p>\n  <ul class=\"list\" style=\"margin-left: 2em; list-style: disc;\">\n    <li>{{_(\"If you drag a message from a search result view onto a tag, any searched tags are removed from the message and the tag you drop the message on is added.\")}}</li>\n    <li>{{_(\"If you drag a tag from the sidebar onto a message, the tag is added to the message.\")}}\n            {{_(\"No tags are removed.\")}}</li>\n  </ul>\n  <p>{{_(\"For example:\")}}<br></p>\n  <ul class=\"list\" style=\"margin-left: 2em; list-style: disc;\">\n    <li>{{_(\"If you're looking at the search 'in:inbox in:job', which is the intersection of messages in both Inbox and Job, dragging a message onto the Trash tag will remove both Inbox and Job tags and add the Trash tag.\")}}</li>\n    <li>{{_(\"If you're looking at the search 'in:inbox', dragging a School tag from the sidebar to a message will add the School tag to the message, while also leaving it in the Inbox.\")}}</li>\n  </ul>\n\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/contrib/hints/hints/gravatar.html",
    "content": "{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% block title %}{{_(\"Gravatar Profiles\")}}{% endblock %}\n{% block content %}\n<div class=\"content-normal\" style=\"max-width: 46em;\">\n  <h1><span class=\"icon-archive\"></span>\n    {{_(\"Gravatar Profiles\")}}\n  </h1>\n  <p>\n    {{_(\"Mailpile integrates with Gravatar (from the makers of Wordpress) to download thumbnail images of your contacts.\")}}\n  </p>\n  <ul class=\"list\" style=\"margin-left: 2em; list-style: disc\">\n    <li>{{_(\"Visit Gravatar to create your own public profile\")}}:\n        <a target=_blank href=\"https://www.gravatar.com\">gravatar.com</a></li>\n    <li>{{_(\"Once your Gravatar is configured, people using Gravatar enabled applications will be able to see your image if they know your email address\")}}</li>\n    <li>{{_(\"Mailpile keeps local copies of the thumbnails (for privacy and performance reasons), but checks periodically for updates\")}}</li>\n  </ul>\n  <p>\n    {{_(\"You can disable or configure Gravatar in your Privacy Settings\")}}:\n    <a data-dismiss=modal href=\"{{ U('/settings/privacy.html') }}\">\n      {{_(\"Privacy\")}}\n    </a>\n  </p>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/contrib/hints/hints/keyboard-shortcuts.html",
    "content": "{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% block title %}{{_(\"Keyboard Shortcuts\")}}{% endblock %}\n{% block content %}\n<div class=\"content-normal\" style=\"max-width: 46em;\">\n\n  <h1><span class=\"icon-lightbulb\"></span>\n    {{_(\"Keyboard Shortcuts\")}}\n  </h1>\n  <p>\n    {{_(\"Mailpile has keyboard shortcuts which let you perform common operations using only the keyboard.\")}}\n    {{_(\"These are disabled by default to prevent random key presses from causing accidents.\")}}\n  </p>\n  <p>\n    {{_(\"Once keyboard shortcuts have been enabled, you can press `?` at any time to get help.\")}}\n  </p>\n  <p>\n    {{_(\"To enable keyboard shortcuts, visit the preferences page.\")}}<br>\n    <a data-dismiss=\"modal\" href=\"/settings/preferences.html\">\n      <button class=\"button-secondary right\">\n        <span class=\"icon icon-animals\"></span>\n        {{_(\"Preferences\")}}\n      </button>\n    </a>\n  </p>\n\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/contrib/hints/hints/organize-sidebar.html",
    "content": "{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% block title %}{{_(\"Organizing Your Sidebar\")}}{% endblock %}\n{% block content %}\n<div class=\"content-normal\" style=\"max-width: 46em;\">\n  <h1><span class=\"icon-tag\"></span>\n    {{_(\"Organizing Your Sidebar\")}}\n  </h1>\n  <p>\n    {{_(\"Mailpile lets you organize your mailbox the way you want it be.\")}}<br>\n    {{_(\"You can start customizing your experince by rearranging the sidebar on the left.\")}}\n  </p>\n  <ul class=\"list\" style=\"margin-left: 2em; list-style: disc\">\n    <li>{{_(\"In the bottom left, click:\")}} <b>{{_(\"Organize\")}}.</b></li>\n    <li>{{_(\"From here you can click and drag any of the tags (Inbox, Outbox, etc.) up or down to change its order in the sidebar.\")}}</li>\n    <li>{{_(\"To customize each tag individually, click on one of the gears next to any tag.\")}}</li>\n    <ul style=\"margin-left: 2em; list-style: circle\">\n      <li>{{_(\"Under Tag Settings you can change the name and color of a tag as well as where it's located and if you want it in your searches.\")}}</li>\n      <li>{{_(\"Under Technical Settings you can make sub-tags so that they're more organized.\")}} {{_(\"For example:\")}}</li>\n      <li>{{_(\"Receipts\")}}</li>\n      <ul style=\"margin-left: 2em; list-style: circle\">\n        <li>{{_(\"Bills\")}}</li>\n        <li>{{_(\"Groceries\")}}</li>\n        <li>{{_(\"Personal\")}}</li>\n        <li>{{_(\"Work\")}}</li> \n      </ul>      \n    </ul>\n  </ul>\n  <p>\n    {{_(\"Stay tuned for more helpful hints to guide you in customizing your Mailpile!\")}}\n  </p>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/contrib/hints/hints/spam.html",
    "content": "{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% block title %}{{_(\"Managing Spam\")}}{% endblock %}\n{% block content %}\n<div class=\"content-normal\" style=\"max-width: 46em;\">\n\n  <h1><span class=\"icon-spam\"></span>\n    {{_(\"Managing Spam\")}}\n  </h1>\n  <p>\n    {{_(\"Mailpile uses statistical analysis to recognize spam and keep it out of your Inbox.\")}}\n  </p>\n  <p>\n    {{_(\"Although powerful, the system is not perfect and it does need to some training to keep it working optimally.\")}}\n    {{_(\"If you periodically correct the mistakes made by the spam filter, it will become more accurate.\")}}\n  </p>\n  <p>\n    <b>{{_(\"If Mailpile didn't catch a spam e-mail, move the messages to Spam.\")}}</b>\n    <br>\n    <ul class=\"list\" style=\"margin-left: 2em; list-style: disc\">\n      <li>{{_(\"Go to your Inbox.\")}}</li>\n      <li>{{_(\"Click the checkbox to select the message(s) you want to move.\")}}</li>\n      <li>{{_(\"Click the Spam icon \")}} <span class=\"icon-spam\"></span></li>\n      <li>{{_(\"Yay, no more spam!\")}}</li>\n    </ul>\n  </p>\n  <p>\n    <b>{{_(\"If Mailpile marked legitimate e-mail as Spam, remove the Spam tag.\")}}</b>\n    <br>\n    <ul class=\"list\" style=\"margin-left: 2em; list-style: disc\">\n      <li>{{_(\"Go to Spam.\")}}</li>\n      <li>{{_(\"Click the checkbox to select the message(s) you want to move.\")}}</li>\n      <li>{{_(\"Click the Untag Selection icon\")}} <span class=\"icon-circle-x\"></span></li>\n    </ul>\n  </p>\n  <p>\n    {{_(\"Sorry about that; the spam filter isn't perfect!\")}}\n  </p>\n  <p>\n    {{_(\"Further reading:\")}}\n    <a class=\"auto-modal\" href=\"{{ U(\"/page/hints/autotagging.html\") }}\">{{_(\"Learn more about automatic e-mail classification.\")}}</a>\n  </p>\n\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/contrib/hints/hints.html",
    "content": "{% extends \"logs/layout.html\" %}\n{% block content %}\n<div class=\"content-normal\" style=\"max-width: 46em;\">\n\n  <h1><span class=\"icon-lightbulb\"></span> {{message}}</h1>\n  <br>\n\n  <p>\n    {{_(\"Mailpile will by default give hints now and then to help you get more out of the application.\")}}\n    {{_(\"Here you can see a list of all the hints and when they are scheduled for display.\")}}\n    {{_(\"Clicking on a hint will display more information.\")}}\n  </p>\n  <table><tbody>\n    <tr>\n      <th>{{_(\"Days\")}}</th><th>{{_(\"Hint\")}}</th><th></th>\n    </tr>\n  {% for hint in result.hints %}\n    <tr>\n      <td>\n        {%- if hint.in_days < 1 %}\n          due\n        {%- else %}\n          {{- hint.in_days }}\n        {%- endif %}\n      </td>\n      <td><a class=\"{{ hint.action_cls }}\" {% if hint.action_js -%}\n        {{ hint.action_js|safe -}}\n      {% else -%}\n        href=\"{{ U(hint.action_url) }}\"\n      {%- endif %}>\n        {%- if not hint.applies %}<del>{% endif %}\n          {{ hint.message2 }}\n        {%- if not hint.applies %}</del>{% endif %}\n      </td>\n      <td>\n        {%- if not hint.applies %}\n          <span class=\"icon icon-x color-01-gray-mid\"></span>\n        {%- elif hint.displayed %}\n          <span class=\"icon icon-checkmark color-08-green\"></span>\n        {%- endif %}\n      </td>\n    </tr>\n  {% endfor %}\n  </tbody></table><br>\n\n  <p>\n    {{_(\"Note: Some hints are context sensitive and may no longer apply; those hints are still listed, but crossed out.\")}}\n  </p>\n\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/contrib/hints/hints.js",
    "content": "/* Mailpile.plugins.hints */\n\n$(document).ready(function() {\n  // Display initial hint randomly 1 to 5 minutes after page load\n  setTimeout(Mailpile.plugins.hints.hint, (1 + 4 * Math.random()) * 60000);\n});\n\nreturn {\n  hint: function() {\n    // Check for new hints every 3.5 - 4.5 hours\n    setTimeout(Mailpile.plugins.hints.hint, (3.5 + Math.random()) * 3600000);\n\n    // Using the POST method will record the hint as having been displayed.\n    // FIXME: Should we use GET, and then POST if the user interacts?\n    Mailpile.API.logs_hints_post({\n      now: true,\n    }, function(response) {\n      if (response.result.hints.length) {\n        var hint = response.result.hints[0];\n        hint['status'] = 'info';\n        hint['icon'] = 'icon-lightbulb';\n        if (hint['action_url'].indexOf('javascript:') != 0) {\n          hint['action_url'] = Mailpile.API.U(hint['action_url']);\n        }\n        Mailpile.notification(hint);\n      }\n    });\n  },\n  release_notes: function(ev) {\n    $('#release_notes').eq(0).trigger('click');\n  },\n  keybindings: function() {\n    Mailpile.display_keybindings();\n  },\n};\n\n"
  },
  {
    "path": "shared-data/contrib/hints/hints.py",
    "content": "# This is our very own Clippy replacement...\n\nimport time\n\nfrom gettext import gettext\nfrom mailpile.plugins import PluginManager\n\n_ = lambda t: t\n\n\n##[ Pluggable commands and data views ]#######################################\n\nimport random\nfrom mailpile.config.defaults import APPVER\nfrom mailpile.commands import Command\nfrom mailpile.util import md5_hex, safe_assert\n\n\nTIMESTAMPS = None\n\n\nclass hintsCommand(Command):\n    \"\"\"Provide periodic hints to the user\"\"\"\n    SYNOPSIS_ARGS = '[now|reset]'\n    SPLIT_ARG = True\n    HTTP_CALLABLE = ('GET', 'POST')\n    HTTP_QUERY_VARS = {\n       'now': 'Request a single due hint',\n       'context': 'Current UI context'\n    }\n\n    ALL_HINTS = [\n        # ID, Min age (in days), Interval, Short hint, Details, [Precondition]\n        (APPVER, 0, 99999,\n            _('This is Mailpile version %s') % APPVER,\n            # Note: The style of quotes matters here, because the JS sucks\n            #       a bit. Single quotes only please!\n            \"javascript:Mailpile.plugins.hints.release_notes();\"),\n\n        ('deletion', 3, 90,\n            _('Your Mailpile is configured to never delete e-mail'),\n            '/page/hints/deletion.html',\n            lambda cfg, ctx: not cfg.prefs.allow_deletion),\n\n        ('keyboard', 4, 180,\n            _('Mailpile has keyboard shortcuts!'),\n            \"/page/hints/keyboard-shortcuts.html\",\n            lambda cfg, cgx: not cfg.web.keybindings),\n\n        # Remind the user to manage their spam every 3 months.\n        # FIXME: Allow user to somehow say \"I know, shutup\".\n        ('spam', 5, 90,\n            _('Learn how to get the most out of Mailpile\\'s spam filter'),\n            '/page/hints/spam.html'),\n\n\t# Show the user how to organize their sidebar after 6 days.\n\t('organize-sidebar', 6, 99999,\n\t    _('Rearrange your sidebar to organize how you see your e-mail'),\n\t    '/page/hints/organize-sidebar.html'),\n\n        # Show the user how dragging and dropping tags or messages works\n        # after 7 days.\n        ('dragging', 7, 365,\n            _('Learn how dragging and dropping tags works'),\n            '/page/hints/dragging-tags.html'),\n\n        # Introduce Gravatar integration after 10 days, and yearly repetition.\n        # Remind of the privacy implications\n        ('gravatar', 10, 365,\n            _('Mailpile uses Gravatar thumbnails!'),\n            '/page/hints/gravatar.html'),\n\n        # Don't bother the user about backups unless they've been using the\n        # app for at least 2 weeks. After that, only bug them every 6 months.\n        # FIXME: Allow user to somehow say \"I have backups, shutup\".\n        ('backups', 14, 180,\n            _('You really should make backups of your Mailpile'),\n            '/page/hints/backups.html'),\n\n        # Introduce autotagging after 3 weeks, remind the user once per year.\n        # This isn't something that justifies much nagging.\n        ('autotagging', 21, 365,\n            _('Mailpile can automatically tag or untag any kind of e-mail!'),\n            '/page/hints/autotagging.html')]\n\n    def _today(self):\n        return int(time.time() // (24*3600))\n\n    def timestamps(self):\n        global TIMESTAMPS\n        if TIMESTAMPS is None:\n            try:\n                TIMESTAMPS = self.session.config.load_pickle('hints.dat')\n            except:\n                TIMESTAMPS = {'initial': self._today()}\n                self.save_timestamps()\n        return TIMESTAMPS\n\n    def save_timestamps(self):\n        global TIMESTAMPS\n        if TIMESTAMPS:\n            self.session.config.save_pickle(TIMESTAMPS, 'hints.dat')\n\n    def _days(self):\n        return int(self._today() - self.timestamps()['initial'])\n\n    def _hint_days(self, hint):\n        # This will allow the user to postpone a hint; we check the timestamp\n        # data file before falling back to the hardcoded default.\n        return int(self.timestamps().get('days:%s' % hint[0], hint[1]))\n\n    def _postpone_hint(self, hint, days=None):\n        ts = self.timestamps()\n        ts['days:%s' % hint[0]] = int(self._days() + (days or hint[2]))\n        ts['last_displayed'] = self._today()\n        self.save_timestamps()\n\n    def _hint_applies(self, hint, ctx):\n        if len(hint) > 5:\n            return hint[5](self.session.config, ctx)\n        else:\n            return True\n\n    def _hint_event(self, ctx, hint):\n        applies = self._hint_applies(hint, ctx)\n\n        in_days = max(0, self._hint_days(hint) - self._days())\n        if in_days > 9999:\n            in_days = _('never')\n\n        action_url, action_cls = hint[4], ''\n        if action_url.startswith('/page/'):\n            action_cls = 'auto-modal'\n\n        return {\n            'action_cls': action_cls,\n            'action_url': action_url,\n            'action_text': _('learn more') if hint[3] else '',\n            'applies': applies,\n            'message': _('Did you know') + ' ...',\n            'message2': _(hint[3]),\n            'in_days': in_days,\n            'interval': hint[2],\n            'data': {},\n            'name': hint[0]}\n\n    def _choose_hint(self, ctx):\n        if self._today() == self.timestamps().get('last_displayed'):\n            return None\n\n        days = self._days()\n        hints = [(self._hint_days(h) - days, h)\n                 for h in self.ALL_HINTS if self._hint_applies(h, ctx)]\n\n        if hints:\n            oldest = min(hints)\n            if oldest[0] <= 0:\n                return oldest[1]\n\n        return None\n\n    def command(self):\n        ctx = self.data.get('context')\n\n        if 'reset' in self.args:\n            safe_assert(self.data.get('_method', 'POST') == 'POST')\n            ts = self.timestamps()\n            for k in ts.keys():\n                del ts[k]\n            ts['initial'] = self._today()\n\n        elif 'next' in self.args:\n            safe_assert(self.data.get('_method', 'POST') == 'POST')\n            self.timestamps()['last_displayed'] = 0\n            self.timestamps()['initial'] -= 30\n\n        if 'now' in self.args or 'now' in self.data:\n            hint = self._choose_hint(ctx)\n            if hint:\n                if 'POST' == self.data.get('_method', 'POST'):\n                    self._postpone_hint(hint)\n                return self._success(hint[3], result={\n                    'hints': [self._hint_event(ctx, hint)]})\n            else:\n                return self._success(_('Nothing Happened'), result={\n                    'hints': []})\n        else:\n            return self._success(_('Did you know') + ' ...', result={\n                'today': self._today(),\n                'days': self._days(),\n                'ts': self.timestamps(),\n                'hints': [self._hint_event(ctx, h) for h in self.ALL_HINTS]})\n\n\n_ = gettext\n# EOF #\n"
  },
  {
    "path": "shared-data/contrib/hints/manifest.json",
    "content": "# This is a Mailpile plugin manifest, describing the `hints` plugin.\n#\n{\n    \"name\": \"hints\",\n    \"author\": \"The Mailpile Team <team@mailpile.is>\",\n    \"description\": \"Incremental learning! This plugin provides periodic tips and hints on how to make the most of your Mailpile.\",\n    \"display\": true,\n    \"public\": true,\n    \"code\": {\n        \"python\": [\"hints.py\"],\n        \"javascript\": [\"hints.js\"]\n    },\n    \"routes\": {\n        \"/logs/hints/\": {\"file\": \"hints.html\"},\n        \"/page/hints/deletion.html\": {\"file\": \"hints/deletion.html\"},\n        \"/page/hints/backups.html\": {\"file\": \"hints/backups.html\"},\n        \"/page/hints/gravatar.html\": {\"file\": \"hints/gravatar.html\"},\n        \"/page/hints/organize-sidebar.html\": {\"file\": \"hints/organize-sidebar.html\"},\n        \"/page/hints/spam.html\": {\"file\": \"hints/spam.html\"},\n        \"/page/hints/keyboard-shortcuts.html\": {\"file\": \"hints/keyboard-shortcuts.html\"},\n        \"/page/hints/autotagging.html\": {\"file\": \"hints/autotagging.html\"},\n        \"/page/hints/dragging-tags.html\": {\"file\": \"hints/dragging-tags.html\"}\n    },\n    \"user_interface\": {\n        \"activities\": [\n            {\n                \"context\": [\n                    \"/logs/events/\",\n                    \"/logs/network/\",\n                    \"/logs/hints/\",\n                    \"/settings/\",\n                    \"/settings/set/password/\",\n                    \"/crypto/tls/getcert/\"\n                ],\n                \"name\": \"hints\",\n                \"text\": \"Hints\",\n                \"icon\": \"lightbulb\",\n                \"description\": \"Tips and tricks\",\n                \"url\": \"/logs/hints/\"\n            }\n        ]\n    },\n    \"commands\": [\n        {\n            \"class\": \"hintsCommand\",\n            \"url\": \"logs/hints\",\n            \"name\": \"hints\"\n        }\n    ]\n}\n"
  },
  {
    "path": "shared-data/contrib/i18nhelper/i18nhelper.js",
    "content": "return {\n    activity_setup: function() {\n        $(\".plugin-activity-i18nhelper\").click(function() {\n            Mailpile.API.i18n_recent_get({}, function(data) {\n                console.log(data);\n                var rows = \"\";\n                for (key in data.result) {\n                    value = data.result[key];\n                    rows += \"<tr><td>\" + key + \"</td><td>\" + value + \"</td></tr>\";\n                }\n                $('#modal-full').modal({ backdrop: true, keyboard: true, show: true, remote: false });\n                $('#modal-full .modal-header').append(\"Recently Translated Strings\");\n                $('#modal-full .modal-body').html(\n                  \"<table>\"\n                + \"<tr><th>Original string</th><th>Translated string</th></tr>\"\n                + rows\n                + \"</table>\");\n            });\n        });\n    }\n};\n"
  },
  {
    "path": "shared-data/contrib/i18nhelper/i18nhelper.py",
    "content": "from mailpile.i18n import gettext as _\nfrom mailpile.i18n import ngettext as _n\nfrom mailpile.i18n import ACTIVE_TRANSLATION, RECENTLY_TRANSLATED\nfrom mailpile.commands import Command\n\n\nclass I18NRecent(Command):\n    \"\"\"Show recently translated string in context\"\"\"\n    ORDER = ('', 0)\n    SYNOPSIS = (None, 'i18n/recent', 'i18n/recent', '')\n    HTTP_CALLABLE = ('GET', )\n    HTTP_QUERY_VARS = {}\n\n    class CommandResult(Command.CommandResult):\n        def as_text(self):\n            if self.result:\n                return '\\n'.join([\"%s: %s\" % (key, value) for key, value in self.result.iteritems()])\n            else:\n                return _(\"Nothing recently translated\")\n\n    def command(self):\n        return dict(map(lambda x: (x, _(x)), RECENTLY_TRANSLATED))\n\n"
  },
  {
    "path": "shared-data/contrib/i18nhelper/manifest.json",
    "content": "# This is a Mailpile plugin manifest, describing the `i18nhelper` plugin.\n{\n    \"name\": \"i18nhelper\",\n    \"author\": \"The Mailpile Team <team@mailpile.is>\",\n    \"code\": {\n        \"python\": [\"i18nhelper.py\"],\n        \"javascript\": [\"i18nhelper.js\"],\n        \"css\": []\n    },\n\n    # This section defines URL routes and MIME types.\n    \"routes\": {\n        \"/static/i18nhelper/icon.png\": {\"file\": \"i18n.png\"}\n    },\n\n    \"user_interface\": {\n        \"activities\": [\n            {\n                \"context\": [\"/\"],\n                \"name\": \"i18nhelper\",\n                \"text\": \"Internationalization\",\n                \"icon\": \"/static/i18nhelper/icon.png\",\n                \"description\": \"i18n helper functions\",\n                \"url\": \"#\",\n                \"javascript_setup\": \"activity_setup\"\n            }\n        ]\n    },\n\n    # Please see https://github.com/pagekite/Mailpile/wiki/Config for\n    # details about the configuration file syntax.\n    \"config\": {},\n\n    # These are our Python-related hooks\n    \"commands\": [\n        {\n            \"class\": \"I18NRecent\",\n            \"name\": \"i18n/recent\"\n        }\n    ]\n}\n"
  },
  {
    "path": "shared-data/contrib/maildeck/maildeck.css",
    "content": "/* MailDeck.css */\n\n.piledeck-container {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  top: 85px;\n  bottom: 0px;\n  overflow-x: hidden;\n  overflow-y: hidden;\n  white-space: nowrap;\n}\n\n.piledeck-column-container {\n    position:   relative;\n    top:        0px;\n    display:    inline-block;\n    width: 100%;\n    height: 100%;\n    overflow-x: scroll;\n    overflow-y: hidden;\n    margin-bottom: -50px;\n}\n\n.piledeck-column {\n    display:    inline-block;\n    width:      300px;\n    height:     100%;\n    background: #FFFFFF;\n    overflow-y: scroll;\n    overflow-x: hidden;\n    border: 1px solid #b3b3b3;\n    padding:    0;\n    margin:     0 0 0 20px;\n}\n\n.piledeck-column-header {\n    position:  relative;\n    background: #F6F6F6;\n    font-family: Mailpile-300,HelveticaNeue,\"Helvetica Neue\",Helvetica,Arial,sans-serif;\n    font-size:   18px;\n    padding:    10px;\n    text-align: center;\n    border-bottom: 1px solid #b3b3b3;\n    margin: 0;\n}\n\n.piledeck-column-header .title {\n    font-family: Mailpile-700,HelveticaNeue,\"Helvetica Neue\",Helvetica,Arial,sans-serif;\n    padding-top:    10px;\n}\n\n.piledeck-column-header .refresh {\n    font-size:      8px;\n    position:       absolute;\n    right:          3px;\n    top:            3px;\n}\n\n.piledeck-entry:first-child {\n  border-top: 0px;\n}\n\n.piledeck-entry {\n  border-top: 1px solid #b3b3b3;\n  display:    block;\n  min-height: 30px;\n  max-height: 200px;\n  padding:    10px;\n}\n\n.piledeck-entry:hover {\n  background-color: #F6F6F6;\n  transition: background 0.2s linear;\n}\n\n.piledeck-entry .avatar {\n  display: inline-block;\n  float: left;\n  margin-right: 10px;  \n}\n\n.piledeck-entry .avatar .icon-user {\n  font-size: 21px;\n}\n\n.piledeck-entry .avatar img {\n  width: 24px;\n  border-radius: 3px;\n}\n\n.piledeck-entry .subject {\n    font-family: HelveticaNeue,\"Helvetica Neue\",Helvetica,Arial,sans-serif;\n    white-space: normal;\n    font-size:  14px;\n}\n\n.piledeck-entry .subject a {\n    font-weight: normal;\n}\n\n.piledeck-entry .from {\n    font-size:   14px;\n    font-weight: bold;\n    padding-bottom: -1px;\n}\n\n.piledeck-entry .body {\n    white-space: normal;\n}\n\n.piledeck-entry .actions {\n    opacity:    0.2;\n    float: right;\n}\n\n.piledeck-entry:hover .actions {\n    opacity:    1.0;\n    transition: opacity 0.2s linear;\n}\n"
  },
  {
    "path": "shared-data/contrib/maildeck/maildeck.html",
    "content": "{% extends \"layouts/full-wide.html\" %}\n{% block title %}Maildeck{% endblock %}\n{% block content %}\n\n<div class=\"piledeck-container\">\n  <div class=\"piledeck-column-container\">\n  </div>\n</div>\n\n<script id=\"template-maildeck-column-header\" type=\"text/template\">\n  <div id=\"<%= id %>\" class=\"piledeck-column\">\n    <div class=\"piledeck-column-header\">\n      <span class=\"type\"><span class=\"icon-search\"></span> <%= type %></span>\n      <span class=\"title\"><%= search %></span>\n      <a class=\"column-delete\" data-id=\"<%= id %>\"><span class=\"icon-circle-x\"></span></a>\n      <span class=\"refresh\"></span>\n    </div>\n    <div class=\"entries\"></div>\n  </div>\n</script>\n\n<script id=\"template-maildeck-column-item\" type=\"text/template\">\n  <div class=\"piledeck-entry<%= classes %>\" id=\"mid_<%= mid %>\">\n    <div class=\"clearfix\">\n      <div class=\"left\">\n      <div class=\"avatar\"><%= avatar %></div>\n      <div class=\"from\"><%= from %></div>\n      </div>\n      <div class=\"actions right\">\n        <a href=\"#\"><span class=\"icon-reply\"></span></a>\n        <a href=\"#\"><span class=\"icon-archive\"></span></a>\n        <a href=\"#\"><span class=\"icon-forward\"></span></a>\n        <a href=\"#\"><span class=\"icon-trash\"></span></a>\n      </div>\n    </div>\n    <div class=\"subject\"><a href=\"http://localhost:33411/thread/=<%= mid %>/\"><%= subject %></a></div>\n  </div>\n</script>\n\n<script>\n$(document).ready(function(){\n  Mailpile.plugins.maildeck.load();\n});\n</script>\n{% endblock %}"
  },
  {
    "path": "shared-data/contrib/maildeck/maildeck.js",
    "content": "/* MailDeck.js is the javascript code\n   The name of the returned class will be `mailpile.plugins.maildeck`.\n */\nreturn {\n    load: function(e) {\n        Mailpile.plugins.maildeck.column_add('in:github');\n        Mailpile.plugins.maildeck.column_add('javascript');\n        Mailpile.plugins.maildeck.column_add('indieweb');\n        Mailpile.plugins.maildeck.column_add('in:berlin');\n        \n        // Add Prefix to search box\n        $('#search-query').val('maildeck: ');\n    },\n    activity_setup: function(e) {\n\n      $('.column-delete').on('click', function(){\n          Mailpile.plugins.maildeck.column_delete($(this).data('id'));\n      });\n    },\n    makeid: function() {\n        var text = \"\";\n        var possible = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\";\n\n        for( var i=0; i < 5; i++ ) {\n            text += possible.charAt(Math.floor(Math.random() * possible.length));\n        }\n\n        return text;\n    },\n    columns: {},\n    column_add: function(search) {\n        for (s in this.columns) {\n            if (this.columns[s].search == search) {\n                $(\"#\" + s)\n                    .animate( { opacity: 0.4 }, 300 )\n                    .animate( { opacity: 1.0 }, 300 );\n                return s;\n            }\n        }\n        var id = \"col\" + Mailpile.plugins.maildeck.makeid();\n\n        // Add HTML Column\n        var template_html = $('#template-maildeck-column-header').html();\n        var header_html = _.template(template_html, { type: \"Search:\", search: search, id: id });\n        $(\".piledeck-column-container\").append(header_html);\n\n        this.columns[id] = {\n            search:     search,\n            lastresult: null,\n            countdown:  5,\n        };\n        this.column_refresh(id);\n        this.column_start_refresh(id);\n        return id;\n    },\n    column_delete: function(id) {\n        this.column_stop_refresh(id);\n        delete this.columns[id];\n        $(\"#\" + id).remove();\n    },\n    refresh: function() {\n        for (var id in this.columns) {\n            this.refresh_column(id);\n        }\n    },\n    column_refresh: function(id) {\n        var col = this.columns[id];\n        var self = this;\n        Mailpile.API.search_get({ q: col[\"search\"]}, function(data) {\n            self.columns[id][\"lastresult\"] = data;\n            self.column_render(id);\n        });\n    },\n    column_render: function(id) {\n        var result = this.columns[id].lastresult.result;\n        var thread_ids = result.thread_ids.reverse();\n\n        // Add HTML Item\n        var template_html = $('#template-maildeck-column-item').html();\n\n        for (mid in thread_ids) {\n            mid = thread_ids[mid];\n            var metadata = result.data.metadata[mid];\n            var messages = result.data.messages[mid];\n\n            if ($(\"#\" + id + \" #mid_\" + mid).length) {\n                // Do nothing...\n            } else {\n\n                var subject = metadata.subject.substr(0, 80);\n                if (metadata.subject.length > 80) { \n                    subject += \"...\";\n                }\n                tagclasses = \"\";\n                for (tid in metadata.tag_tids) {\n                    tid = metadata.tag_tids[tid];\n                    tagclasses += \" in_\" + result.data.tags[tid].slug;\n                }\n\n                var avatar =  '<span class=\"icon-user\"></span>';\n                if (metadata.from.photo !== undefined) {\n                  avatar = '<img src=\"' + metadata.from.photo + '\">';\n                }\n                var item_data = {\n                  mid: mid,\n                  classes: tagclasses,\n                  from: metadata.from.fn,\n                  subject: subject,\n                  avatar: avatar\n                }\n \n                // Add HTML Item\n                var item_html = _.template(template_html, item_data);\n                $(\"#\"+id + \" .entries\").prepend(item_html);\n            }\n        }\n    },\n    column_stop_refresh: function(id) {\n        window.clearInterval(this.columns[id].refresh);\n        this.columns[id].countdown = 5;\n    },\n    column_start_refresh: function(id) {\n        var self = this;\n        window.setInterval(function() { \n            if (self.columns[id].countdown == 0) {\n                self.column_refresh(id); \n                self.columns[id].countdown = 5;\n            } else {\n                self.columns[id].countdown--;\n            }\n            //$(\"#\" + id + \" .refresh\").html(self.columns[id].countdown);\n        }, 1000);\n    },\n    runsearch: function() {\n        this.column_add($('#search-query').val());\n        $('#search-query').val(\"\");\n        return false;\n    }\n};"
  },
  {
    "path": "shared-data/contrib/maildeck/maildeck.py",
    "content": "from mailpile.commands import Command\n\nclass maildeckCommand(Command):\n           HTTP_CALLABLE = ('GET',)"
  },
  {
    "path": "shared-data/contrib/maildeck/manifest.json",
    "content": "{\n    \"name\": \"maildeck\",\n    \"author\": \"The Mailpile Team <team@mailpile.is>\",\n    \"code\": {\n        \"python\": [\"maildeck.py\"],\n        \"javascript\": [\"maildeck.js\"],\n        \"css\": [\"maildeck.css\"]\n    },\n    \"routes\": {\n        \"/maildeck/\": {\"file\": \"maildeck.html\", \"api\": 0},\n        \"/static/maildeck/icon.png\": {\"file\": \"viking.png\"}\n    },\n    \"user_interface\": {\n        \"activities\": [\n            {\n                \"context\": [\"/\"],\n                \"name\": \"maildeck\",\n                \"text\": \"Maildeck\",\n                \"icon\": \"/static/maildeck/icon.png\",\n                \"description\": \"Maildeck\",\n                \"url\": \"/maildeck/\",\n                \"javascript_setup\": \"activity_setup\"\n            }\n        ],\n        \"display_modes\": [\n            {\n                \"context\": [\"/maildeck/\"],\n                \"name\": \"something\",\n                \"text\": \"Something\",\n                \"description\": \"Something, very useful\",\n                \"url\": \"#something\"\n            },\n            {\n                \"context\": [\"/maildeck/\"],\n                \"name\": \"other\",\n                \"text\": \"Other\",\n                \"description\": \"Other, more useful\",\n                \"url\": \"#other\"\n            }\n        ]\n    },\n    \"commands\": [\n        {\n            \"class\": \"maildeckCommand\",\n            \"url\": \"maildeck\",\n            \"name\": \"maildeck\"\n        }\n    ]    \n}\n"
  },
  {
    "path": "shared-data/contrib/remoteaccess/manifest.json",
    "content": "# This is a Mailpile plugin manifest, describing the `remoteaccess` plugin.\n{\n    \"name\": \"remoteaccess\",\n    \"author\": \"The Mailpile Team <team@mailpile.is>\",\n    \"code\": {\n        \"python\": [],\n        \"javascript\": [],\n        \"css\": []\n    },\n\n    # This section defines URL routes and MIME types.\n    \"routes\": {\n        \"/settings/remote.html\": {\"file\": \"settings-remote.html\"}\n    },\n\n    # Please see https://github.com/pagekite/Mailpile/wiki/Config for\n    # details about the configuration file syntax.\n    \"config\": {\"sections\": {\n        \"remoteaccess\": [\"Remote Access settings\", false, {\n            \"pagekite\":      [\"PageKite settings\", false, {\n                \"enabled\":   [\"Enable PageKite remote access\", \"bool\", false],\n                \"kitename\":  [\"Name of public DNS\", \"str\", \"\"],\n                \"account\":   [\"Account login\", \"str\", \"\"],\n                \"password\":  [\"Kite secret\", \"str\", \"\"],\n                \"frontends\": [\"--frontends\", \"str\", \"\"],\n                \"dyndns\":    [\"--dyndns\", \"str\", \"\"],\n                \"xmlrpc\":    [\"XML-RPC server for signups\", \"url\",\n                              \"https://pagekite.net/xmlrpc/\"]\n            }]\n        }]\n    }},\n\n    # These are our Python-related hooks\n    \"commands\": [\n    ],\n\n    # Hook into the UI\n    \"user_interface\": {\n        \"activities\": [\n            {\n                \"context\": [\"/profiles/\"],\n                \"name\": \"remoteaccess\",\n                \"text\": \"Remote Access\",\n                \"icon\": \"force-graph\",\n                \"description\": \"Access your Mailpile over the Internet\",\n                \"aclass\": \"auto-modal\",\n                \"url\": \"/settings/remoteaccess/remote.html\"\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "shared-data/contrib/remoteaccess/settings-remote.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block title %}{{_(\"Remote Access\")}}{% endblock %}\n{% block content %}\n<div class=\"content-normal\">\n<form id=\"form-remote-access\" class=\"standard\" method=\"POST\"\n      style=\"position: relative; max-width: 60em;\"\n      action=\"{{ U('/settings/remote.html') }}\">\n  <h1><span class=\"icon-force-graph\"></span> {{_(\"Remote Access\")}}</h1>\n\n  <script>\n    var _rac = (function() {\n      return {\n        section: function(which, sub) {\n          $('.section').slideUp();\n          $('.section.'+which).slideDown();\n          _rac.subsection(sub || 'main');\n          return false;\n        },\n        subsection: function(which) {\n          $('.subsection').slideUp();\n          $('.subsection.'+which).slideDown();\n          return false;\n        }\n      };\n    })();\n  </script>\n\n  <p class=\"message paragraph-important\"\n     onclick=\"javascript:_rac.section('ra-remote-access');\">\n    <span class=\"icon icon-force-graph\"></span> {{_(\"Remote Access\")}}\n  </p>\n  <div class=\"section ra-remote-access\" style=\"position: relative;\">\n    <table style=\"margin: 5px 0;\">\n      <tr>\n        <th></th>\n        <th width=\"40%\">method</th>\n        <th width=\"50%\">address</th>\n        <th></th>\n      </tr>\n      <tr onclick=\"javascript:return _rac.section('ra-pagekite');\">\n        <td><input type=\"checkbox\"></td>\n        <td>PageKite Relay</td>\n        {%- if result.pagekite and result.pagekite.kitename %}\n        <td><a href=\"https://{{ result.pagekite.kitename }}{{ settings.http_path }}/\"\n               class=\"pagekite kitename\">{{ result.pagekite.kitename }}</a></td>\n        {%- else %}\n        <td><a href=\"#\" class=\"pagekite kitename\"><i>yourname</i>.my.mailpile.is</a></td>\n        {%- endif %}\n        <td><span class=\"icon icon-x\"></span></td>\n      </tr>\n      <tr onclick=\"javascript:return _rac.section('tor');\">\n        <td><input type=\"checkbox\"></td>\n        <td>Tor Hidden Service</td>\n        <td><a><i>1234567890abcdef</i>.onion</a></td>\n        <td><span class=\"icon icon-x\"></span></td>\n      </tr>\n      <tr onclick=\"javascript:return _rac.section('direct');\">\n        <td><input type=\"checkbox\"></td>\n        <td>Direct Access</td>\n        <td><a>10.0.0.1</a></td>\n        <td><span class=\"icon icon-x\"></span></td>\n      </tr>\n    </table>\n    <p class=\"text-center\" style=\"margin: 1em 0;\">\n      <button onclick=\"javascript:return _rac.section('ra-pagekite', 'signup');\"\n              class=\"button button-secondary\">Sign up for <i>my.mailpile.is</i></button>\n    </p>\n    <p>\n      {{_(\"You can configure your Mailpile to allow access over the network, making it your own personal web-mail service.\")}}\n      {{_(\"Different methods have different security properties, costs, pros and cons.\")}}\n      {{_(\"Tick the boxes to explore and configure each one.\")}}\n    </p>\n  </div>\n\n  <div class=\"section ra-pagekite hide\" style=\"position: relative;\">\n    <p class=\"message paragraph-important\"\n       onclick=\"javascript:_rac.section('ra-pagekite');\">\n      <span class=\"icon icon-settings\"></span> {{_(\"PageKite settings\")}}\n    </p>\n    <p>\n      {{_(\"PageKite uses an in-the-cloud relay to make your local Mailpile accessible over the Internet.\")}}\n      {{_(\"Traffic is encrypted end-to-end so the relay cannot see your mail.\")}}\n    </p>\n    <div class=\"subsection main\">\n      <p class=\"text-center\">\n        <button onclick=\"javascript:return _rac.subsection('configure');\"\n                class=\"buttin\">I have PageKite</button> &nbsp;\n        <button onclick=\"javascript:return _rac.subsection('signup');\"\n                class=\"button button-secondary\">Sign up for <i>my.mailpile.is</i></button>\n      </p>\n      <p>\n        {{_(\"Techies can run their own relays, but there is also an affordable public relay provided by the people behind Mailpile.\")}}\n        {{_(\"Using the Mailpile relay supports ongoing development.\")}}\n      </p>\n    </div>\n    <div class=\"subsection signup\" style=\"position: relative;\">\n      <div class=\"left\" style=\"width: 10em; margin-right: 1em;\">\n        <label>E-mail</label>\n        <select class=\"signup-email\">\n          <option>foo@klaki.net</option>\n          <option>bar@klaki.net</option>\n        </select>\n      </div>\n      <div>\n        <label>Username</label>\n        <input type=\"text\" class=\"signup-username\" placeholder=\"username\"\n               style=\"display: inline; width: 7em; text-align: right;\"> <b>.my.mailpile.is</b>\n      </div>\n      <label>Payment plan</label>\n      <ul>\n        <li><input type=\"radio\" name=\"plan\" value=\"benefactor\">\n            <span class=\"checkbox\"><b>{{_(\"Benefactor\")}}:</b> {{_(\"$20 USD/month for yourself while sponsoring 10 freebies\")}}</span></li>\n        <li><input type=\"radio\" name=\"plan\" value=\"activist\">\n            <span class=\"checkbox\"><b>{{_(\"Activist\")}}:</b> {{_(\"$6 USD/month for yourself while sponsoring 2 freebies\")}}</span></li>\n        <li><input type=\"radio\" name=\"plan\" value=\"backer\">\n            <span class=\"checkbox\"><b>{{_(\"Backer\")}}:</b> {{_(\"Free trial month, then $3 USD/month\")}}</span></li>\n        <li><input type=\"radio\" name=\"plan\" value=\"freebie\">\n            <span class=\"checkbox\"><b>{{_(\"Freebie\")}}:</b> {{_(\"Sponsored by community, limited availability\")}}</span></li>\n      </ul>\n      <div>\n        <button onclick=\"javascript:return _rac.pagekite_signup();\"\n                style=\"position: absolute; right: 0; bottom: 0;\"\n                class=\"button button-secondary\">Sign up!</button>\n        <p style=\"width: 28em;\"><i>\n          {{_(\"Using the Mailpile relay supports ongoing development.\")}}\n        </i></p>\n      </div>\n    </div>\n    <div class=\"subsection configure\">\n      <p class=\"text-center\" style=\"margin: 0; padding: 1em;\">\n        <i>CONFIG: WORK IN PROGRESS...</i>\n      </p>\n    </div>\n  </div>\n\n  <p class=\"message paragraph-important\"\n     onclick=\"javascript:_rac.section('ra-authentication');\">\n    <span class=\"icon icon-lock-closed\"></span> {{_(\"Passwords and Authentication\")}}\n  </p>\n  <div class=\"section ra-authentication hide\" style=\"position: relative;\">\n    <p class=\"text-center\" style=\"margin: 0; padding: 1em;\">\n      <i>WORK IN PROGRESS...</i>\n    </p>\n    <br clear=\"both\">\n  </div>\n\n  <div class=\"right\">\n    <button class=\"button button-primary\">\n      <span class=\"icon icon-checkmark\"></span> {{_(\"Save\")}}\n    </button>\n  </div>\n  <p style=\"width: 28em;\">\n    <b>WARNING:</b>\n    This is all EXPERIMENTAL and probably still INSECURE.\n    We're working on it!\n  </p>\n\n</form></div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/contrib/unthread/i18n-stubs.html",
    "content": "{#\n # This is not a real template, but it duplicates strings from the manifest\n # to ensure they get translated.\n #}\n{{ _(\"Add a button to the message view, making it possible to override the default threading and re-classify an e-mail as the beginning of a new conversation.\") }}\n{{ _(\"Move this e-mail to a new conversation.\") }}\n"
  },
  {
    "path": "shared-data/contrib/unthread/manifest.json",
    "content": "# This is a Mailpile plugin manifest, describing the `unthread` plugin.\n#\n{\n    \"name\": \"unthread\",\n    \"author\": \"The Mailpile Team <team@mailpile.is>\",\n    \"description\": \"Add a button to the message view, making it possible to override the default threading and re-classify an e-mail as the beginning of a new conversation.\",\n    \"display\": true,\n    \"public\": true,\n    \"code\": {\n        \"javascript\": [\"unthread.js\"]\n    },\n    \"routes\": {\n        \"/message/unthread/modal.html\": {\"file\": \"modal.html\", \"api\": 0}\n    },\n    \"user_interface\": {\n        \"thread_activities\": [\n            {\n                \"context\": [\n                    \"/search/\",\n                    \"/message/\"\n                ],\n                \"name\": \"unthread\",\n                \"text\": \"New Thread\",\n                \"icon\": \"forum\",\n                \"description\": \"Move this e-mail to a new conversation.\",\n                \"url\": \"#\"\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "shared-data/contrib/unthread/modal.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block content %}\n{%- set mid = state.query_args.q[0][1:] %}\n{%- set metadata = result.data.metadata[mid] %}\n<form method=\"POST\" action=\"{{ U(state.command_url) }}\">{{ csrf_field|safe }}\n  <input type=\"hidden\" name=\"mid\" value=\"{{ mid }}\">\n  <h1>{{_(\"New Thread\")}}</h1>\n  <p>\n    {{_(\"You are about to move the e-mail from {person} ({subject}) and any replies, to a new conversation.\"\n        ).format(person=metadata.from.fn, subject=metadata.subject)}}\n  </p>\n  <p style=\"text-align: center\">\n    <b>{{ _(\"New Subject\") }}:</b>\n    <input name=\"subject\" style=\"width: 22em;\" value=\"\" placeholder=\"{{ metadata.subject }}\">\n    <span style=\"opacity: 0.6;\">({{ _(\"optional\") }})</span>\n  </p>\n  <p>\n    {{_(\"These changes are purely local (visible to you only), but will allow you to search and tag these messages separately from others in the current thread.\")}}\n    {{_(\"Changing the subject does not modify the original e-mail, but the new subject will be displayed in search result listings and proposed as a default when composing replies.\")}}\n  </p>\n\n  <button class=\"button button-info\" data-dismiss=\"modal\"\n          type=cancel>{{_(\"Cancel\")}}</button>\n  <button class=\"button button-primary submit-modal-unthread right\"\n          type=submit>{{_(\"Move to a New Thread\")}}</button>\n</form> \n{% endblock %}\n"
  },
  {
    "path": "shared-data/contrib/unthread/unthread.js",
    "content": "/* Mailpile.plugins.unthread */\n\nfunction _message_click_handler(e) {\n  e.preventDefault();\n  var mid = $(this).closest('.has-mid').data('mid');\n  Mailpile.auto_modal({\n    method: 'GET',\n    url: Mailpile.API.U('/message/unthread/=' + mid + '/modal.html'),\n  });\n}\n\nfunction _modal_submit_handler(e) {\n  e.preventDefault();\n  var $form = $(this).closest('form');\n\n  var mid = $form.find('input[name=mid]').val();\n  var args = {mid: mid};\n\n  var subject = $form.find('input[name=subject]').val();\n  if (subject) args['subject'] = subject;\n\n  Mailpile.API.message_unthread_post(args, function(result) {\n    Mailpile.UI.hide_modal();\n    Mailpile.go(Mailpile.urls.thread + mid + '/');\n  });\n};\n\n$(document).on('click', '.message-action-unthread', _message_click_handler);\n$(document).on('click', '.submit-modal-unthread', _modal_submit_handler);\n\nreturn {\n  'message_click_handler': _message_click_handler,\n  'modal_submit_handler': _modal_submit_handler,\n}\n"
  },
  {
    "path": "shared-data/default-theme/README.md",
    "content": "Mailpile \"Default\" Theme\n========================\n\nThis is a \"Default\" theme that ships with Mailpile and is rendered in the web interface.\n\nTo help develop or extend this theme, please follow our [FrontEnd Development Guide](https://github.com/mailpile/Mailpile/wiki/Front-End-Development-Guide)\n\nEventually, we will flesh out the ability to create alternate themes that are wholly unique Mailpile themes.\n"
  },
  {
    "path": "shared-data/default-theme/css/default.css",
    "content": "/* Mailpile - Default Theme\n*  Version 0.4.1\n*\t Designed and built by @brennannovak and others\n*/\n/* Global - Must be included before the config file incase you want specify custom fonts or backgrounds */\n/* #Fonts\n================================================== */\n/* @license\n * Mailpile font designed for Mailpile\n *\n * You may obtain a copy of the license at the URLs below.\n *\n * Webfont: Mailpile by Brennan Novak\n * URL: http://github.com/mailpile/fonts\n *\n * Mailpile font is licensed under the SIL Open Font License (OFL)\n * http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL\n *\n * © 2013 Mailpile\n*/\n@font-face {\n  font-family: 'Mailpile-300';\n  src: url('../webfonts/Mailpile-Normal.eot');\n  src: url('../webfonts/Mailpile-Normal.eot?#iefix') format('embedded-opentype'), url('../webfonts/Mailpile-Normal.woff') format('font-woff'), url('../webfonts/Mailpile-Normal.ttf') format('truetype'), url('../webfonts/Mailpile-Normal.svg#wf') format('svg');\n}\n@font-face {\n  font-family: 'Mailpile-500';\n  src: url('../webfonts/Mailpile-500.eot');\n  src: url('../webfonts/Mailpile-500.eot?#iefix') format('embedded-opentype'), url('../webfonts/Mailpile-500.woff') format('font-woff'), url('../webfonts/Mailpile-500.ttf') format('truetype'), url('../webfonts/Mailpile-500.svg#wf') format('svg');\n}\n@font-face {\n  font-family: 'Mailpile-700';\n  src: url('../webfonts/Mailpile-700.eot');\n  src: url('../webfonts/Mailpile-700.eot?#iefix') format('embedded-opentype'), url('../webfonts/Mailpile-700.woff') format('font-woff'), url('../webfonts/Mailpile-700.ttf') format('truetype'), url('../webfonts/Mailpile-700.svg#wf') format('svg');\n}\n@font-face {\n  font-family: 'Mailpile-Interface';\n  src: url('../webfonts/Mailpile-Interface.eot');\n  src: url('../webfonts/Mailpile-Interface.eot') format('embedded-opentype'), url('../webfonts/Mailpile-Interface.woff') format('woff'), url('../webfonts/Mailpile-Interface.ttf') format('truetype'), url('../webfonts/Mailpile-Interface.svg#Mailpile-Interface') format('svg');\n  font-weight: normal;\n  font-style: normal;\n}\n[class^=\"icon-\"],\n[class*=\" icon-\"] {\n  font-family: 'Mailpile-Interface';\n  speak: none;\n  font-style: normal;\n  font-weight: normal;\n  font-variant: normal;\n  text-transform: none;\n  line-height: 1;\n  /* Better Font Rendering =========== */\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n.icon-addresses:before {\n  content: \"\\e600\";\n}\n.icon-ads:before {\n  content: \"\\e601\";\n}\n.icon-alerts:before {\n  content: \"\\e602\";\n}\n.icon-animals:before {\n  content: \"\\e603\";\n}\n.icon-archive:before {\n  content: \"\\e604\";\n}\n.icon-arrow-down:before {\n  content: \"\\e605\";\n}\n.icon-arrow-left:before {\n  content: \"\\e606\";\n}\n.icon-arrow-right:before {\n  content: \"\\e607\";\n}\n.icon-arrow-up:before {\n  content: \"\\e608\";\n}\n.icon-attachment:before {\n  content: \"\\e609\";\n}\n.icon-calendar:before {\n  content: \"\\e60a\";\n}\n.icon-checkmark:before {\n  content: \"\\e60b\";\n}\n.icon-circle-dotted:before {\n  content: \"\\e60c\";\n}\n.icon-circle-info:before {\n  content: \"\\e60d\";\n}\n.icon-circle-x:before {\n  content: \"\\e60e\";\n}\n.icon-clock:before {\n  content: \"\\e60f\";\n}\n.icon-code:before {\n  content: \"\\e610\";\n}\n.icon-collapse:before {\n  content: \"\\e611\";\n}\n.icon-columns:before {\n  content: \"\\e612\";\n}\n.icon-comment:before {\n  content: \"\\e613\";\n}\n.icon-compose:before {\n  content: \"\\e614\";\n}\n.icon-dislike:before {\n  content: \"\\e615\";\n}\n.icon-document:before {\n  content: \"\\e616\";\n}\n.icon-donate:before {\n  content: \"\\e617\";\n}\n.icon-download:before {\n  content: \"\\e618\";\n}\n.icon-expand:before {\n  content: \"\\e619\";\n}\n.icon-eye:before {\n  content: \"\\e61a\";\n}\n.icon-filters:before {\n  content: \"\\e61b\";\n}\n.icon-fingerprint:before {\n  content: \"\\e61c\";\n}\n.icon-flashlight:before {\n  content: \"\\e61d\";\n}\n.icon-food:before {\n  content: \"\\e61e\";\n}\n.icon-force-graph:before {\n  content: \"\\e61f\";\n}\n.icon-forum:before {\n  content: \"\\e620\";\n}\n.icon-forward:before {\n  content: \"\\e621\";\n}\n.icon-geopoint:before {\n  content: \"\\e622\";\n}\n.icon-graph:before {\n  content: \"\\e623\";\n}\n.icon-groups:before {\n  content: \"\\e624\";\n}\n.icon-help:before {\n  content: \"\\e625\";\n}\n.icon-home:before {\n  content: \"\\e626\";\n}\n.icon-hosting:before {\n  content: \"\\e627\";\n}\n.icon-image:before {\n  content: \"\\e628\";\n}\n.icon-inbox:before {\n  content: \"\\e629\";\n}\n.icon-key:before {\n  content: \"\\e62a\";\n}\n.icon-later:before {\n  content: \"\\e62b\";\n}\n.icon-lightbulb:before {\n  content: \"\\e62c\";\n}\n.icon-like:before {\n  content: \"\\e62d\";\n}\n.icon-links:before {\n  content: \"\\e62e\";\n}\n.icon-list:before {\n  content: \"\\e62f\";\n}\n.icon-lock-closed:before {\n  content: \"\\e630\";\n}\n.icon-lock-error:before {\n  content: \"\\e631\";\n}\n.icon-lock-open:before {\n  content: \"\\e632\";\n}\n.icon-logo:before {\n  content: \"\\e633\";\n}\n.icon-logout:before {\n  content: \"\\e634\";\n}\n.icon-mailsource:before {\n  content: \"\\e635\";\n}\n.icon-map:before {\n  content: \"\\e636\";\n}\n.icon-merge:before {\n  content: \"\\e637\";\n}\n.icon-minus:before {\n  content: \"\\e638\";\n}\n.icon-money:before {\n  content: \"\\e639\";\n}\n.icon-move:before {\n  content: \"\\e63a\";\n}\n.icon-music:before {\n  content: \"\\e63b\";\n}\n.icon-new:before {\n  content: \"\\e63c\";\n}\n.icon-news:before {\n  content: \"\\e63d\";\n}\n.icon-not-spam:before {\n  content: \"\\e63e\";\n}\n.icon-notifications:before {\n  content: \"\\e63f\";\n}\n.icon-outbox:before {\n  content: \"\\e640\";\n}\n.icon-photos:before {\n  content: \"\\e641\";\n}\n.icon-plus:before {\n  content: \"\\e642\";\n}\n.icon-preferences:before {\n  content: \"\\e643\";\n}\n.icon-privacy:before {\n  content: \"\\e644\";\n}\n.icon-profiles:before {\n  content: \"\\e645\";\n}\n.icon-purchases:before {\n  content: \"\\e646\";\n}\n.icon-receipts:before {\n  content: \"\\e647\";\n}\n.icon-reply-all:before {\n  content: \"\\e648\";\n}\n.icon-reply:before {\n  content: \"\\e649\";\n}\n.icon-robot:before {\n  content: \"\\e64a\";\n}\n.icon-routes:before {\n  content: \"\\e64b\";\n}\n.icon-rss:before {\n  content: \"\\e64c\";\n}\n.icon-search:before {\n  content: \"\\e64d\";\n}\n.icon-sent:before {\n  content: \"\\e64e\";\n}\n.icon-settings:before {\n  content: \"\\e64f\";\n}\n.icon-signature-expired:before {\n  content: \"\\e650\";\n}\n.icon-signature-invalid:before {\n  content: \"\\e651\";\n}\n.icon-signature-none:before {\n  content: \"\\e652\";\n}\n.icon-signature-revoked:before {\n  content: \"\\e653\";\n}\n.icon-signature-unknown:before {\n  content: \"\\e654\";\n}\n.icon-signature-unverified:before {\n  content: \"\\e655\";\n}\n.icon-signature-verified:before {\n  content: \"\\e656\";\n}\n.icon-social:before {\n  content: \"\\e657\";\n}\n.icon-spam:before {\n  content: \"\\e658\";\n}\n.icon-speed:before {\n  content: \"\\e659\";\n}\n.icon-spreadsheet:before {\n  content: \"\\e65a\";\n}\n.icon-star:before {\n  content: \"\\e65b\";\n}\n.icon-tag:before {\n  content: \"\\e65c\";\n}\n.icon-tags:before {\n  content: \"\\e65d\";\n}\n.icon-text:before {\n  content: \"\\e65e\";\n}\n.icon-themes:before {\n  content: \"\\e65f\";\n}\n.icon-tor:before {\n  content: \"\\e660\";\n}\n.icon-transit:before {\n  content: \"\\e661\";\n}\n.icon-trash:before {\n  content: \"\\e662\";\n}\n.icon-travel:before {\n  content: \"\\e663\";\n}\n.icon-trophy:before {\n  content: \"\\e664\";\n}\n.icon-unknown:before {\n  content: \"\\e665\";\n}\n.icon-upload:before {\n  content: \"\\e666\";\n}\n.icon-user:before {\n  content: \"\\e667\";\n}\n.icon-video:before {\n  content: \"\\e668\";\n}\n.icon-work:before {\n  content: \"\\e669\";\n}\n.icon-x:before {\n  content: \"\\e66a\";\n}\n.icon-zip:before {\n  content: \"\\e66b\";\n}\n/* Sidebar - makes icons switch to dotted circle on drag */\n.sidebar-tags-draggable-hover .icon-mailsource:before,\n.sidebar-tags-draggable-hover .icon-inbox:before,\n.sidebar-tags-draggable-hover .icon-sent:before,\n.sidebar-tags-draggable-hover .icon-spam:before,\n.sidebar-tags-draggable-hover .icon-trash:before,\n.sidebar-tags-draggable-hover .icon-alerts:before,\n.sidebar-tags-draggable-hover .icon-animals:before,\n.sidebar-tags-draggable-hover .icon-calendar:before,\n.sidebar-tags-draggable-hover .icon-checkmark:before,\n.sidebar-tags-draggable-hover .icon-clock:before,\n.sidebar-tags-draggable-hover .icon-code:before,\n.sidebar-tags-draggable-hover .icon-comment:before,\n.sidebar-tags-draggable-hover .icon-columns:before,\n.sidebar-tags-draggable-hover .icon-document:before,\n.sidebar-tags-draggable-hover .icon-donate:before,\n.sidebar-tags-draggable-hover .icon-download:before,\n.sidebar-tags-draggable-hover .icon-flashlight:before,\n.sidebar-tags-draggable-hover .icon-food:before,\n.sidebar-tags-draggable-hover .icon-forum:before,\n.sidebar-tags-draggable-hover .icon-force-graph:before,\n.sidebar-tags-draggable-hover .icon-geopoint:before,\n.sidebar-tags-draggable-hover .icon-groups:before,\n.sidebar-tags-draggable-hover .icon-graph:before,\n.sidebar-tags-draggable-hover .icon-help:before,\n.sidebar-tags-draggable-hover .icon-home:before,\n.sidebar-tags-draggable-hover .icon-image:before,\n.sidebar-tags-draggable-hover .icon-key:before,\n.sidebar-tags-draggable-hover .icon-links:before,\n.sidebar-tags-draggable-hover .icon-list:before,\n.sidebar-tags-draggable-hover .icon-lock-closed:before,\n.sidebar-tags-draggable-hover .icon-map:before,\n.sidebar-tags-draggable-hover .icon-money:before,\n.sidebar-tags-draggable-hover .icon-music:before,\n.sidebar-tags-draggable-hover .icon-new:before,\n.sidebar-tags-draggable-hover .icon-news:before,\n.sidebar-tags-draggable-hover .icon-photos:before,\n.sidebar-tags-draggable-hover .icon-privacy:before,\n.sidebar-tags-draggable-hover .icon-purchases:before,\n.sidebar-tags-draggable-hover .icon-receipts:before,\n.sidebar-tags-draggable-hover .icon-spreadsheet:before,\n.sidebar-tags-draggable-hover .icon-rss:before,\n.sidebar-tags-draggable-hover .icon-robot:before,\n.sidebar-tags-draggable-hover .icon-star:before,\n.sidebar-tags-draggable-hover .icon-tag:before,\n.sidebar-tags-draggable-hover .icon-tags:before,\n.sidebar-tags-draggable-hover .icon-text:before,\n.sidebar-tags-draggable-hover .icon-themes:before,\n.sidebar-tags-draggable-hover .icon-transit:before,\n.sidebar-tags-draggable-hover .icon-travel:before,\n.sidebar-tags-draggable-hover .icon-trophy:before,\n.sidebar-tags-draggable-hover .icon-upload:before,\n.sidebar-tags-draggable-hover .icon-video:before,\n.sidebar-tags-draggable-hover .icon-user:before,\n.sidebar-tags-draggable-hover .icon-work:before,\n.sidebar-tags-draggable-hover .icon-zip:before {\n  content: \"\\e60c\";\n  color: #333333;\n}\n/* Mimetype - icons */\n.icon-mime:before,\n.icon-mime[type=\"application/octet-stream\"]:before,\n.icon-mime[type=\"application/mac-binhex40\"]:before,\n.icon-mime[type=\"application/x-shockwave-flash\"]:before,\n.icon-mime[type=\"application/x-director\"]:before,\n.icon-mime[type=\"application/x-x509-ca-cert\"]:before,\n.icon-mime[type=\"application/x-director\"]:before,\n.icon-mime[type=\"application/x-msdownload\"]:before,\n.icon-mime[type=\"application/x-director\"]:before {\n  content: \"\\e609\";\n}\n.icon-mime[type=\"application/mbox\"]:before {\n  content: \"\\e635\";\n}\n.icon-mime[type=\"application/x-compress\"]:before,\n.icon-mime[type=\"application/x-compressed\"]:before,\n.icon-mime[type=\"application/x-tar\"]:before,\n.icon-mime[type=\"application/zip\"]:before,\n.icon-mime[type=\"application/x-stuffit\"]:before,\n.icon-mime[type=\"application/x-gzip\"]:before,\n.icon-mime[type=\"application/x-gzip-compressed\"]:before,\n.icon-mime[type=\"application/x-tar\"]:before,\n.icon-mime[type=\"application/x-winzip\"]:before,\n.icon-mime[type=\"application/x-zip\"]:before,\n.icon-mime[type=\"application/x-zip-compressed\"]:before,\n.icon-mime[type=\"application/x-rar-compressed\"]:before {\n  content: \"\\e66b\";\n}\n.icon-mime[type=\"audio/amr\"]:before,\n.icon-mime[type=\"audio/mp3\"]:before,\n.icon-mime[type=\"audio/midi\"]:before,\n.icon-mime[type=\"audio/mid\"]:before,\n.icon-mime[type=\"audio/mpeg\"]:before,\n.icon-mime[type=\"audio/basic\"]:before,\n.icon-mime[type=\"audio/x-aiff\"]:before,\n.icon-mime[type=\"audio/x-pn-realaudio\"]:before,\n.icon-mime[type=\"audio/x-pn-realaudio\"]:before,\n.icon-mime[type=\"audio/mid\"]:before,\n.icon-mime[type=\"audio/basic\"]:before,\n.icon-mime[type=\"audio/x-wav\"]:before,\n.icon-mime[type=\"audio/x-mpegurl\"]:before,\n.icon-mime[type=\"audio/wave\"]:before,\n.icon-mime[type=\"audio/wav\"]:before,\n.icon-mime[type=\"audio/mp4a-latm\"]:before {\n  content: \"\\e63b\";\n}\n.icon-mime[type=\"text/calendar\"]:before,\n.icon-mime[type=\"application/ics\"]:before,\n.icon-mime[type=\"text/x-vcalendar\"]:before {\n  content: \"\\e60a\";\n}\n.icon-mime[type=\"text/directory\"]:before,\n.icon-mime[type=\"text/x-vcard\"]:before,\n.icon-mime[type=\"text/x-ms-contact\"]:before {\n  content: \"\\e600\";\n}\n.icon-mime[type=\"image/gif\"]:before,\n.icon-mime[type=\"image/png\"]:before,\n.icon-mime[type=\"image/jpeg\"]:before,\n.icon-mime[type=\"image/cis-cod\"]:before,\n.icon-mime[type=\"image/ief\"]:before,\n.icon-mime[type=\"image/pipeg\"]:before,\n.icon-mime[type=\"image/tiff\"]:before,\n.icon-mime[type=\"image/x-cmx\"]:before,\n.icon-mime[type=\"image/x-cmu-raster\"]:before,\n.icon-mime[type=\"image/x-rgb\"]:before,\n.icon-mime[type=\"image/x-icon\"]:before,\n.icon-mime[type=\"image/x-xbitmap\"]:before,\n.icon-mime[type=\"image/x-xpixmap\"]:before,\n.icon-mime[type=\"image/x-xwindowdump\"]:before,\n.icon-mime[type=\"image/x-portable-anymap\"]:before,\n.icon-mime[type=\"image/x-portable-graymap\"]:before,\n.icon-mime[type=\"image/x-portable-pixmap\"]:before,\n.icon-mime[type=\"image/x-portable-bitmap\"]:before,\n.icon-mime[type=\"image/svg+xml\"]:before,\n.icon-mime[type=\"application/x-photoshop\"]:before,\n.icon-mime[type=\"application/postscript\"]:before {\n  content: \"\\e641\";\n}\n.icon-mime[type=\"application/pgp-signature\"]:before {\n  content: \"\\e656\";\n}\n.icon-mime[type=\"application/pgp-keys\"]:before {\n  content: \"\\e62a\";\n}\n.icon-mime[type=\"application/x-mobipocket-ebook\"]:before,\n.icon-mime[type=\"application/epub+zip\"]:before,\n.icon-mime[type=\"application/rtf\"]:before,\n.icon-mime[type=\"application/vnd.ms-works\"]:before,\n.icon-mime[type=\"application/msword\"]:before,\n.icon-mime[type=\"application/pdf\"]:before,\n.icon-mime[type=\"application/x-download\"]:before,\n.icon-mime[type=\"message/rfc822\"]:before,\n.icon-mime[type=\"text/x-log\"]:before,\n.icon-mime[type=\"text/scriptlet\"]:before,\n.icon-mime[type=\"text/plain\"]:before,\n.icon-mime[type=\"text/iuls\"]:before,\n.icon-mime[type=\"text/plain\"]:before,\n.icon-mime[type=\"text/richtext\"]:before,\n.icon-mime[type=\"text/x-setext\"]:before,\n.icon-mime[type=\"text/x-component\"]:before,\n.icon-mime[type=\"text/webviewhtml\"]:before,\n.icon-mime[type=\"text/h323\"]:before,\n.icon-mime[type=\"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"]:before,\n.icon-mime[type=\"application/vnd.oasis.opendocument.text\"]:before,\n.icon-mime[type=\"application/vnd.oasis.opendocument.text-template\"]:before,\n.icon-mime[type=\"application/vnd.sun.xml.writer\"]:before,\n.icon-mime[type=\"application/vnd.sun.xml.writer.template\"]:before,\n.icon-mime[type=\"application/vnd.sun.xml.writer.global\"]:before,\n.icon-mime[type=\"application/vnd.stardivision.writer\"]:before,\n.icon-mime[type=\"application/vnd.stardivision.writer-global\"]:before,\n.icon-mime[type=\"application/x-starwriter\"]:before {\n  content: \"\\e616\";\n}\n.icon-mime[type=\"application/json\"]:before,\n.icon-mime[type=\"application/x-javascript\"]:before,\n.icon-mime[type=\"text/html\"]:before,\n.icon-mime[type=\"text/css\"]:before,\n.icon-mime[type=\"text/xml\"]:before,\n.icon-mime[type=\"text/json\"]:before {\n  content: \"\\e610\";\n}\n.icon-mime[type=\"application/excel\"]:before,\n.icon-mime[type=\"application/msexcel\"]:before,\n.icon-mime[type=\"application/vnd.ms-excel\"]:before,\n.icon-mime[type=\"application/vnd.msexcel\"]:before,\n.icon-mime[type=\"application/csv\"]:before,\n.icon-mime[type=\"application/x-csv\"]:before,\n.icon-mime[type=\"text/tab-separated-values\"]:before,\n.icon-mime[type=\"text/x-comma-separated-values\"]:before,\n.icon-mime[type=\"text/comma-separated-values\"]:before,\n.icon-mime[type=\"text/csv\"]:before,\n.icon-mime[type=\"text/x-csv\"]:before,\n.icon-mime[type=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\"]:before,\n.icon-mime[type=\"application/vnd.oasis.opendocument.spreadsheet\"]:before,\n.icon-mime[type=\"application/vnd.oasis.opendocument.spreadsheet-template\"]:before,\n.icon-mime[type=\"application/vnd.sun.xml.calc\"]:before,\n.icon-mime[type=\"application/vnd.sun.xml.calc.template\"]:before,\n.icon-mime[type=\"application/vnd.stardivision.calc\"]:before,\n.icon-mime[type=\"application/x-starcalc\"]:before {\n  content: \"\\e65a\";\n}\n.icon-mime[type=\"application/powerpoint\"]:before,\n.icon-mime[type=\"application/vnd.ms-powerpoint\"]:before .icon-mime[type=\"application/vnd.oasis.opendocument.presentation\"]:before,\n.icon-mime[type=\"application/vnd.oasis.opendocument.presentation-template\"]:before,\n.icon-mime[type=\"application/vnd.openxmlformats-officedocument.presentationml.presentation\"]:before,\n.icon-mime[type=\"application/vnd.sun.xml.impress\"]:before,\n.icon-mime[type=\"application/vnd.sun.xml.impress.template\"]:before,\n.icon-mime[type=\"application/vnd.stardivision.impress\"]:before,\n.icon-mime[type=\"application/vnd.stardivision.impress-packed\"]:before,\n.icon-mime[type=\"application/x-starimpress\"]:before {\n  content: \"\\e65f\";\n}\n.icon-mime[type=\"video/quicktime\"]:before,\n.icon-mime[type=\"video/x-sgi-movie\"]:before,\n.icon-mime[type=\"video/mpeg\"]:before,\n.icon-mime[type=\"video/x-la-asf\"]:before,\n.icon-mime[type=\"video/x-ms-asf\"]:before,\n.icon-mime[type=\"video/x-msvideo\"]:before,\n.icon-mime[type=\"video/mp4\"]:before,\n.icon-mime[type=\"video/mp2\"]:before,\n.icon-mime[type=\"video/avi\"]:before {\n  content: \"\\e668\";\n}\n/* Config - Change the settings in this file change the look and feel of your style */\n/* Mailpile v0.1.0\n * config.less\n */\n/* Mailpile */\n/* Colors */\n/* Body */\n/* Container */\n/* Margins, Paddings, and Spacings */\n/* Text */\n/* Links */\n/* Headings */\n/* Navigation */\n/* Lists */\n/* Separators */\n/* Buttons */\n/* Button - Primary */\n/* Button - Secondary */\n/* Button - Info */\n/* Button - Alert */\n/* Button - Warning */\n/* Forms */\n/* Validation */\n/* Tables */\n/* Grid */\n/* Boxes */\n/* Rectangles */\n/* Bootstrap */\n/* Bootstrap - Dropdown */\n/* Bootstrap - Modal */\n/* Bower */\n@charset \"UTF-8\";\n/*!\nAnimate.css - http://daneden.me/animate\nLicensed under the MIT license\n\nCopyright (c) 2013 Daniel Eden\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n*/\n.animated {\n  -webkit-animation-duration: 1s;\n  animation-duration: 1s;\n  -webkit-animation-fill-mode: both;\n  animation-fill-mode: both;\n}\n.animated.infinite {\n  -webkit-animation-iteration-count: infinite;\n  animation-iteration-count: infinite;\n}\n.animated.hinge {\n  -webkit-animation-duration: 2s;\n  animation-duration: 2s;\n}\n@-webkit-keyframes bounce {\n  0%,\n  20%,\n  50%,\n  80%,\n  100% {\n    -webkit-transform: translateY(0);\n    transform: translateY(0);\n  }\n  40% {\n    -webkit-transform: translateY(-30px);\n    transform: translateY(-30px);\n  }\n  60% {\n    -webkit-transform: translateY(-15px);\n    transform: translateY(-15px);\n  }\n}\n@keyframes bounce {\n  0%,\n  20%,\n  50%,\n  80%,\n  100% {\n    -webkit-transform: translateY(0);\n    -ms-transform: translateY(0);\n    transform: translateY(0);\n  }\n  40% {\n    -webkit-transform: translateY(-30px);\n    -ms-transform: translateY(-30px);\n    transform: translateY(-30px);\n  }\n  60% {\n    -webkit-transform: translateY(-15px);\n    -ms-transform: translateY(-15px);\n    transform: translateY(-15px);\n  }\n}\n.bounce {\n  -webkit-animation-name: bounce;\n  animation-name: bounce;\n}\n@-webkit-keyframes flash {\n  0%,\n  50%,\n  100% {\n    opacity: 1;\n  }\n  25%,\n  75% {\n    opacity: 0;\n  }\n}\n@keyframes flash {\n  0%,\n  50%,\n  100% {\n    opacity: 1;\n  }\n  25%,\n  75% {\n    opacity: 0;\n  }\n}\n.flash {\n  -webkit-animation-name: flash;\n  animation-name: flash;\n}\n/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */\n@-webkit-keyframes pulse {\n  0% {\n    -webkit-transform: scale(1);\n    transform: scale(1);\n  }\n  50% {\n    -webkit-transform: scale(1.1);\n    transform: scale(1.1);\n  }\n  100% {\n    -webkit-transform: scale(1);\n    transform: scale(1);\n  }\n}\n@keyframes pulse {\n  0% {\n    -webkit-transform: scale(1);\n    -ms-transform: scale(1);\n    transform: scale(1);\n  }\n  50% {\n    -webkit-transform: scale(1.1);\n    -ms-transform: scale(1.1);\n    transform: scale(1.1);\n  }\n  100% {\n    -webkit-transform: scale(1);\n    -ms-transform: scale(1);\n    transform: scale(1);\n  }\n}\n.pulse {\n  -webkit-animation-name: pulse;\n  animation-name: pulse;\n}\n@-webkit-keyframes rubberBand {\n  0% {\n    -webkit-transform: scale(1);\n    transform: scale(1);\n  }\n  30% {\n    -webkit-transform: scaleX(1.25) scaleY(0.75);\n    transform: scaleX(1.25) scaleY(0.75);\n  }\n  40% {\n    -webkit-transform: scaleX(0.75) scaleY(1.25);\n    transform: scaleX(0.75) scaleY(1.25);\n  }\n  60% {\n    -webkit-transform: scaleX(1.15) scaleY(0.85);\n    transform: scaleX(1.15) scaleY(0.85);\n  }\n  100% {\n    -webkit-transform: scale(1);\n    transform: scale(1);\n  }\n}\n@keyframes rubberBand {\n  0% {\n    -webkit-transform: scale(1);\n    -ms-transform: scale(1);\n    transform: scale(1);\n  }\n  30% {\n    -webkit-transform: scaleX(1.25) scaleY(0.75);\n    -ms-transform: scaleX(1.25) scaleY(0.75);\n    transform: scaleX(1.25) scaleY(0.75);\n  }\n  40% {\n    -webkit-transform: scaleX(0.75) scaleY(1.25);\n    -ms-transform: scaleX(0.75) scaleY(1.25);\n    transform: scaleX(0.75) scaleY(1.25);\n  }\n  60% {\n    -webkit-transform: scaleX(1.15) scaleY(0.85);\n    -ms-transform: scaleX(1.15) scaleY(0.85);\n    transform: scaleX(1.15) scaleY(0.85);\n  }\n  100% {\n    -webkit-transform: scale(1);\n    -ms-transform: scale(1);\n    transform: scale(1);\n  }\n}\n.rubberBand {\n  -webkit-animation-name: rubberBand;\n  animation-name: rubberBand;\n}\n@-webkit-keyframes shake {\n  0%,\n  100% {\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n  10%,\n  30%,\n  50%,\n  70%,\n  90% {\n    -webkit-transform: translateX(-10px);\n    transform: translateX(-10px);\n  }\n  20%,\n  40%,\n  60%,\n  80% {\n    -webkit-transform: translateX(10px);\n    transform: translateX(10px);\n  }\n}\n@keyframes shake {\n  0%,\n  100% {\n    -webkit-transform: translateX(0);\n    -ms-transform: translateX(0);\n    transform: translateX(0);\n  }\n  10%,\n  30%,\n  50%,\n  70%,\n  90% {\n    -webkit-transform: translateX(-10px);\n    -ms-transform: translateX(-10px);\n    transform: translateX(-10px);\n  }\n  20%,\n  40%,\n  60%,\n  80% {\n    -webkit-transform: translateX(10px);\n    -ms-transform: translateX(10px);\n    transform: translateX(10px);\n  }\n}\n.shake {\n  -webkit-animation-name: shake;\n  animation-name: shake;\n}\n@-webkit-keyframes swing {\n  20% {\n    -webkit-transform: rotate(15deg);\n    transform: rotate(15deg);\n  }\n  40% {\n    -webkit-transform: rotate(-10deg);\n    transform: rotate(-10deg);\n  }\n  60% {\n    -webkit-transform: rotate(5deg);\n    transform: rotate(5deg);\n  }\n  80% {\n    -webkit-transform: rotate(-5deg);\n    transform: rotate(-5deg);\n  }\n  100% {\n    -webkit-transform: rotate(0deg);\n    transform: rotate(0deg);\n  }\n}\n@keyframes swing {\n  20% {\n    -webkit-transform: rotate(15deg);\n    -ms-transform: rotate(15deg);\n    transform: rotate(15deg);\n  }\n  40% {\n    -webkit-transform: rotate(-10deg);\n    -ms-transform: rotate(-10deg);\n    transform: rotate(-10deg);\n  }\n  60% {\n    -webkit-transform: rotate(5deg);\n    -ms-transform: rotate(5deg);\n    transform: rotate(5deg);\n  }\n  80% {\n    -webkit-transform: rotate(-5deg);\n    -ms-transform: rotate(-5deg);\n    transform: rotate(-5deg);\n  }\n  100% {\n    -webkit-transform: rotate(0deg);\n    -ms-transform: rotate(0deg);\n    transform: rotate(0deg);\n  }\n}\n.swing {\n  -webkit-transform-origin: top center;\n  -ms-transform-origin: top center;\n  transform-origin: top center;\n  -webkit-animation-name: swing;\n  animation-name: swing;\n}\n@-webkit-keyframes tada {\n  0% {\n    -webkit-transform: scale(1);\n    transform: scale(1);\n  }\n  10%,\n  20% {\n    -webkit-transform: scale(0.9) rotate(-3deg);\n    transform: scale(0.9) rotate(-3deg);\n  }\n  30%,\n  50%,\n  70%,\n  90% {\n    -webkit-transform: scale(1.1) rotate(3deg);\n    transform: scale(1.1) rotate(3deg);\n  }\n  40%,\n  60%,\n  80% {\n    -webkit-transform: scale(1.1) rotate(-3deg);\n    transform: scale(1.1) rotate(-3deg);\n  }\n  100% {\n    -webkit-transform: scale(1) rotate(0);\n    transform: scale(1) rotate(0);\n  }\n}\n@keyframes tada {\n  0% {\n    -webkit-transform: scale(1);\n    -ms-transform: scale(1);\n    transform: scale(1);\n  }\n  10%,\n  20% {\n    -webkit-transform: scale(0.9) rotate(-3deg);\n    -ms-transform: scale(0.9) rotate(-3deg);\n    transform: scale(0.9) rotate(-3deg);\n  }\n  30%,\n  50%,\n  70%,\n  90% {\n    -webkit-transform: scale(1.1) rotate(3deg);\n    -ms-transform: scale(1.1) rotate(3deg);\n    transform: scale(1.1) rotate(3deg);\n  }\n  40%,\n  60%,\n  80% {\n    -webkit-transform: scale(1.1) rotate(-3deg);\n    -ms-transform: scale(1.1) rotate(-3deg);\n    transform: scale(1.1) rotate(-3deg);\n  }\n  100% {\n    -webkit-transform: scale(1) rotate(0);\n    -ms-transform: scale(1) rotate(0);\n    transform: scale(1) rotate(0);\n  }\n}\n.tada {\n  -webkit-animation-name: tada;\n  animation-name: tada;\n}\n/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */\n@-webkit-keyframes wobble {\n  0% {\n    -webkit-transform: translateX(0%);\n    transform: translateX(0%);\n  }\n  15% {\n    -webkit-transform: translateX(-25%) rotate(-5deg);\n    transform: translateX(-25%) rotate(-5deg);\n  }\n  30% {\n    -webkit-transform: translateX(20%) rotate(3deg);\n    transform: translateX(20%) rotate(3deg);\n  }\n  45% {\n    -webkit-transform: translateX(-15%) rotate(-3deg);\n    transform: translateX(-15%) rotate(-3deg);\n  }\n  60% {\n    -webkit-transform: translateX(10%) rotate(2deg);\n    transform: translateX(10%) rotate(2deg);\n  }\n  75% {\n    -webkit-transform: translateX(-5%) rotate(-1deg);\n    transform: translateX(-5%) rotate(-1deg);\n  }\n  100% {\n    -webkit-transform: translateX(0%);\n    transform: translateX(0%);\n  }\n}\n@keyframes wobble {\n  0% {\n    -webkit-transform: translateX(0%);\n    -ms-transform: translateX(0%);\n    transform: translateX(0%);\n  }\n  15% {\n    -webkit-transform: translateX(-25%) rotate(-5deg);\n    -ms-transform: translateX(-25%) rotate(-5deg);\n    transform: translateX(-25%) rotate(-5deg);\n  }\n  30% {\n    -webkit-transform: translateX(20%) rotate(3deg);\n    -ms-transform: translateX(20%) rotate(3deg);\n    transform: translateX(20%) rotate(3deg);\n  }\n  45% {\n    -webkit-transform: translateX(-15%) rotate(-3deg);\n    -ms-transform: translateX(-15%) rotate(-3deg);\n    transform: translateX(-15%) rotate(-3deg);\n  }\n  60% {\n    -webkit-transform: translateX(10%) rotate(2deg);\n    -ms-transform: translateX(10%) rotate(2deg);\n    transform: translateX(10%) rotate(2deg);\n  }\n  75% {\n    -webkit-transform: translateX(-5%) rotate(-1deg);\n    -ms-transform: translateX(-5%) rotate(-1deg);\n    transform: translateX(-5%) rotate(-1deg);\n  }\n  100% {\n    -webkit-transform: translateX(0%);\n    -ms-transform: translateX(0%);\n    transform: translateX(0%);\n  }\n}\n.wobble {\n  -webkit-animation-name: wobble;\n  animation-name: wobble;\n}\n@-webkit-keyframes bounceIn {\n  0% {\n    opacity: 0;\n    -webkit-transform: scale(0.3);\n    transform: scale(0.3);\n  }\n  50% {\n    opacity: 1;\n    -webkit-transform: scale(1.05);\n    transform: scale(1.05);\n  }\n  70% {\n    -webkit-transform: scale(0.9);\n    transform: scale(0.9);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: scale(1);\n    transform: scale(1);\n  }\n}\n@keyframes bounceIn {\n  0% {\n    opacity: 0;\n    -webkit-transform: scale(0.3);\n    -ms-transform: scale(0.3);\n    transform: scale(0.3);\n  }\n  50% {\n    opacity: 1;\n    -webkit-transform: scale(1.05);\n    -ms-transform: scale(1.05);\n    transform: scale(1.05);\n  }\n  70% {\n    -webkit-transform: scale(0.9);\n    -ms-transform: scale(0.9);\n    transform: scale(0.9);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: scale(1);\n    -ms-transform: scale(1);\n    transform: scale(1);\n  }\n}\n.bounceIn {\n  -webkit-animation-name: bounceIn;\n  animation-name: bounceIn;\n}\n@-webkit-keyframes bounceInDown {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateY(-2000px);\n    transform: translateY(-2000px);\n  }\n  60% {\n    opacity: 1;\n    -webkit-transform: translateY(30px);\n    transform: translateY(30px);\n  }\n  80% {\n    -webkit-transform: translateY(-10px);\n    transform: translateY(-10px);\n  }\n  100% {\n    -webkit-transform: translateY(0);\n    transform: translateY(0);\n  }\n}\n@keyframes bounceInDown {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateY(-2000px);\n    -ms-transform: translateY(-2000px);\n    transform: translateY(-2000px);\n  }\n  60% {\n    opacity: 1;\n    -webkit-transform: translateY(30px);\n    -ms-transform: translateY(30px);\n    transform: translateY(30px);\n  }\n  80% {\n    -webkit-transform: translateY(-10px);\n    -ms-transform: translateY(-10px);\n    transform: translateY(-10px);\n  }\n  100% {\n    -webkit-transform: translateY(0);\n    -ms-transform: translateY(0);\n    transform: translateY(0);\n  }\n}\n.bounceInDown {\n  -webkit-animation-name: bounceInDown;\n  animation-name: bounceInDown;\n}\n@-webkit-keyframes bounceInLeft {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateX(-2000px);\n    transform: translateX(-2000px);\n  }\n  60% {\n    opacity: 1;\n    -webkit-transform: translateX(30px);\n    transform: translateX(30px);\n  }\n  80% {\n    -webkit-transform: translateX(-10px);\n    transform: translateX(-10px);\n  }\n  100% {\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n@keyframes bounceInLeft {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateX(-2000px);\n    -ms-transform: translateX(-2000px);\n    transform: translateX(-2000px);\n  }\n  60% {\n    opacity: 1;\n    -webkit-transform: translateX(30px);\n    -ms-transform: translateX(30px);\n    transform: translateX(30px);\n  }\n  80% {\n    -webkit-transform: translateX(-10px);\n    -ms-transform: translateX(-10px);\n    transform: translateX(-10px);\n  }\n  100% {\n    -webkit-transform: translateX(0);\n    -ms-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n.bounceInLeft {\n  -webkit-animation-name: bounceInLeft;\n  animation-name: bounceInLeft;\n}\n@-webkit-keyframes bounceInRight {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateX(2000px);\n    transform: translateX(2000px);\n  }\n  60% {\n    opacity: 1;\n    -webkit-transform: translateX(-30px);\n    transform: translateX(-30px);\n  }\n  80% {\n    -webkit-transform: translateX(10px);\n    transform: translateX(10px);\n  }\n  100% {\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n@keyframes bounceInRight {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateX(2000px);\n    -ms-transform: translateX(2000px);\n    transform: translateX(2000px);\n  }\n  60% {\n    opacity: 1;\n    -webkit-transform: translateX(-30px);\n    -ms-transform: translateX(-30px);\n    transform: translateX(-30px);\n  }\n  80% {\n    -webkit-transform: translateX(10px);\n    -ms-transform: translateX(10px);\n    transform: translateX(10px);\n  }\n  100% {\n    -webkit-transform: translateX(0);\n    -ms-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n.bounceInRight {\n  -webkit-animation-name: bounceInRight;\n  animation-name: bounceInRight;\n}\n@-webkit-keyframes bounceInUp {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateY(2000px);\n    transform: translateY(2000px);\n  }\n  60% {\n    opacity: 1;\n    -webkit-transform: translateY(-30px);\n    transform: translateY(-30px);\n  }\n  80% {\n    -webkit-transform: translateY(10px);\n    transform: translateY(10px);\n  }\n  100% {\n    -webkit-transform: translateY(0);\n    transform: translateY(0);\n  }\n}\n@keyframes bounceInUp {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateY(2000px);\n    -ms-transform: translateY(2000px);\n    transform: translateY(2000px);\n  }\n  60% {\n    opacity: 1;\n    -webkit-transform: translateY(-30px);\n    -ms-transform: translateY(-30px);\n    transform: translateY(-30px);\n  }\n  80% {\n    -webkit-transform: translateY(10px);\n    -ms-transform: translateY(10px);\n    transform: translateY(10px);\n  }\n  100% {\n    -webkit-transform: translateY(0);\n    -ms-transform: translateY(0);\n    transform: translateY(0);\n  }\n}\n.bounceInUp {\n  -webkit-animation-name: bounceInUp;\n  animation-name: bounceInUp;\n}\n@-webkit-keyframes bounceOut {\n  0% {\n    -webkit-transform: scale(1);\n    transform: scale(1);\n  }\n  25% {\n    -webkit-transform: scale(0.95);\n    transform: scale(0.95);\n  }\n  50% {\n    opacity: 1;\n    -webkit-transform: scale(1.1);\n    transform: scale(1.1);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: scale(0.3);\n    transform: scale(0.3);\n  }\n}\n@keyframes bounceOut {\n  0% {\n    -webkit-transform: scale(1);\n    -ms-transform: scale(1);\n    transform: scale(1);\n  }\n  25% {\n    -webkit-transform: scale(0.95);\n    -ms-transform: scale(0.95);\n    transform: scale(0.95);\n  }\n  50% {\n    opacity: 1;\n    -webkit-transform: scale(1.1);\n    -ms-transform: scale(1.1);\n    transform: scale(1.1);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: scale(0.3);\n    -ms-transform: scale(0.3);\n    transform: scale(0.3);\n  }\n}\n.bounceOut {\n  -webkit-animation-name: bounceOut;\n  animation-name: bounceOut;\n}\n@-webkit-keyframes bounceOutDown {\n  0% {\n    -webkit-transform: translateY(0);\n    transform: translateY(0);\n  }\n  20% {\n    opacity: 1;\n    -webkit-transform: translateY(-20px);\n    transform: translateY(-20px);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateY(2000px);\n    transform: translateY(2000px);\n  }\n}\n@keyframes bounceOutDown {\n  0% {\n    -webkit-transform: translateY(0);\n    -ms-transform: translateY(0);\n    transform: translateY(0);\n  }\n  20% {\n    opacity: 1;\n    -webkit-transform: translateY(-20px);\n    -ms-transform: translateY(-20px);\n    transform: translateY(-20px);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateY(2000px);\n    -ms-transform: translateY(2000px);\n    transform: translateY(2000px);\n  }\n}\n.bounceOutDown {\n  -webkit-animation-name: bounceOutDown;\n  animation-name: bounceOutDown;\n}\n@-webkit-keyframes bounceOutLeft {\n  0% {\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n  20% {\n    opacity: 1;\n    -webkit-transform: translateX(20px);\n    transform: translateX(20px);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateX(-2000px);\n    transform: translateX(-2000px);\n  }\n}\n@keyframes bounceOutLeft {\n  0% {\n    -webkit-transform: translateX(0);\n    -ms-transform: translateX(0);\n    transform: translateX(0);\n  }\n  20% {\n    opacity: 1;\n    -webkit-transform: translateX(20px);\n    -ms-transform: translateX(20px);\n    transform: translateX(20px);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateX(-2000px);\n    -ms-transform: translateX(-2000px);\n    transform: translateX(-2000px);\n  }\n}\n.bounceOutLeft {\n  -webkit-animation-name: bounceOutLeft;\n  animation-name: bounceOutLeft;\n}\n@-webkit-keyframes bounceOutRight {\n  0% {\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n  20% {\n    opacity: 1;\n    -webkit-transform: translateX(-20px);\n    transform: translateX(-20px);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateX(2000px);\n    transform: translateX(2000px);\n  }\n}\n@keyframes bounceOutRight {\n  0% {\n    -webkit-transform: translateX(0);\n    -ms-transform: translateX(0);\n    transform: translateX(0);\n  }\n  20% {\n    opacity: 1;\n    -webkit-transform: translateX(-20px);\n    -ms-transform: translateX(-20px);\n    transform: translateX(-20px);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateX(2000px);\n    -ms-transform: translateX(2000px);\n    transform: translateX(2000px);\n  }\n}\n.bounceOutRight {\n  -webkit-animation-name: bounceOutRight;\n  animation-name: bounceOutRight;\n}\n@-webkit-keyframes bounceOutUp {\n  0% {\n    -webkit-transform: translateY(0);\n    transform: translateY(0);\n  }\n  20% {\n    opacity: 1;\n    -webkit-transform: translateY(20px);\n    transform: translateY(20px);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateY(-2000px);\n    transform: translateY(-2000px);\n  }\n}\n@keyframes bounceOutUp {\n  0% {\n    -webkit-transform: translateY(0);\n    -ms-transform: translateY(0);\n    transform: translateY(0);\n  }\n  20% {\n    opacity: 1;\n    -webkit-transform: translateY(20px);\n    -ms-transform: translateY(20px);\n    transform: translateY(20px);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateY(-2000px);\n    -ms-transform: translateY(-2000px);\n    transform: translateY(-2000px);\n  }\n}\n.bounceOutUp {\n  -webkit-animation-name: bounceOutUp;\n  animation-name: bounceOutUp;\n}\n@-webkit-keyframes fadeIn {\n  0% {\n    opacity: 0;\n  }\n  100% {\n    opacity: 1;\n  }\n}\n@keyframes fadeIn {\n  0% {\n    opacity: 0;\n  }\n  100% {\n    opacity: 1;\n  }\n}\n.fadeIn {\n  -webkit-animation-name: fadeIn;\n  animation-name: fadeIn;\n}\n@-webkit-keyframes fadeInDown {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateY(-20px);\n    transform: translateY(-20px);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: translateY(0);\n    transform: translateY(0);\n  }\n}\n@keyframes fadeInDown {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateY(-20px);\n    -ms-transform: translateY(-20px);\n    transform: translateY(-20px);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: translateY(0);\n    -ms-transform: translateY(0);\n    transform: translateY(0);\n  }\n}\n.fadeInDown {\n  -webkit-animation-name: fadeInDown;\n  animation-name: fadeInDown;\n}\n@-webkit-keyframes fadeInDownBig {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateY(-2000px);\n    transform: translateY(-2000px);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: translateY(0);\n    transform: translateY(0);\n  }\n}\n@keyframes fadeInDownBig {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateY(-2000px);\n    -ms-transform: translateY(-2000px);\n    transform: translateY(-2000px);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: translateY(0);\n    -ms-transform: translateY(0);\n    transform: translateY(0);\n  }\n}\n.fadeInDownBig {\n  -webkit-animation-name: fadeInDownBig;\n  animation-name: fadeInDownBig;\n}\n@-webkit-keyframes fadeInLeft {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateX(-20px);\n    transform: translateX(-20px);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n@keyframes fadeInLeft {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateX(-20px);\n    -ms-transform: translateX(-20px);\n    transform: translateX(-20px);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: translateX(0);\n    -ms-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n.fadeInLeft {\n  -webkit-animation-name: fadeInLeft;\n  animation-name: fadeInLeft;\n}\n@-webkit-keyframes fadeInLeftBig {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateX(-2000px);\n    transform: translateX(-2000px);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n@keyframes fadeInLeftBig {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateX(-2000px);\n    -ms-transform: translateX(-2000px);\n    transform: translateX(-2000px);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: translateX(0);\n    -ms-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n.fadeInLeftBig {\n  -webkit-animation-name: fadeInLeftBig;\n  animation-name: fadeInLeftBig;\n}\n@-webkit-keyframes fadeInRight {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateX(20px);\n    transform: translateX(20px);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n@keyframes fadeInRight {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateX(20px);\n    -ms-transform: translateX(20px);\n    transform: translateX(20px);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: translateX(0);\n    -ms-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n.fadeInRight {\n  -webkit-animation-name: fadeInRight;\n  animation-name: fadeInRight;\n}\n@-webkit-keyframes fadeInRightBig {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateX(2000px);\n    transform: translateX(2000px);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n@keyframes fadeInRightBig {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateX(2000px);\n    -ms-transform: translateX(2000px);\n    transform: translateX(2000px);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: translateX(0);\n    -ms-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n.fadeInRightBig {\n  -webkit-animation-name: fadeInRightBig;\n  animation-name: fadeInRightBig;\n}\n@-webkit-keyframes fadeInUp {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateY(20px);\n    transform: translateY(20px);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: translateY(0);\n    transform: translateY(0);\n  }\n}\n@keyframes fadeInUp {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateY(20px);\n    -ms-transform: translateY(20px);\n    transform: translateY(20px);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: translateY(0);\n    -ms-transform: translateY(0);\n    transform: translateY(0);\n  }\n}\n.fadeInUp {\n  -webkit-animation-name: fadeInUp;\n  animation-name: fadeInUp;\n}\n@-webkit-keyframes fadeInUpBig {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateY(2000px);\n    transform: translateY(2000px);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: translateY(0);\n    transform: translateY(0);\n  }\n}\n@keyframes fadeInUpBig {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateY(2000px);\n    -ms-transform: translateY(2000px);\n    transform: translateY(2000px);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: translateY(0);\n    -ms-transform: translateY(0);\n    transform: translateY(0);\n  }\n}\n.fadeInUpBig {\n  -webkit-animation-name: fadeInUpBig;\n  animation-name: fadeInUpBig;\n}\n@-webkit-keyframes fadeOut {\n  0% {\n    opacity: 1;\n  }\n  100% {\n    opacity: 0;\n  }\n}\n@keyframes fadeOut {\n  0% {\n    opacity: 1;\n  }\n  100% {\n    opacity: 0;\n  }\n}\n.fadeOut {\n  -webkit-animation-name: fadeOut;\n  animation-name: fadeOut;\n}\n@-webkit-keyframes fadeOutDown {\n  0% {\n    opacity: 1;\n    -webkit-transform: translateY(0);\n    transform: translateY(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateY(20px);\n    transform: translateY(20px);\n  }\n}\n@keyframes fadeOutDown {\n  0% {\n    opacity: 1;\n    -webkit-transform: translateY(0);\n    -ms-transform: translateY(0);\n    transform: translateY(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateY(20px);\n    -ms-transform: translateY(20px);\n    transform: translateY(20px);\n  }\n}\n.fadeOutDown {\n  -webkit-animation-name: fadeOutDown;\n  animation-name: fadeOutDown;\n}\n@-webkit-keyframes fadeOutDownBig {\n  0% {\n    opacity: 1;\n    -webkit-transform: translateY(0);\n    transform: translateY(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateY(2000px);\n    transform: translateY(2000px);\n  }\n}\n@keyframes fadeOutDownBig {\n  0% {\n    opacity: 1;\n    -webkit-transform: translateY(0);\n    -ms-transform: translateY(0);\n    transform: translateY(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateY(2000px);\n    -ms-transform: translateY(2000px);\n    transform: translateY(2000px);\n  }\n}\n.fadeOutDownBig {\n  -webkit-animation-name: fadeOutDownBig;\n  animation-name: fadeOutDownBig;\n}\n@-webkit-keyframes fadeOutLeft {\n  0% {\n    opacity: 1;\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateX(-20px);\n    transform: translateX(-20px);\n  }\n}\n@keyframes fadeOutLeft {\n  0% {\n    opacity: 1;\n    -webkit-transform: translateX(0);\n    -ms-transform: translateX(0);\n    transform: translateX(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateX(-20px);\n    -ms-transform: translateX(-20px);\n    transform: translateX(-20px);\n  }\n}\n.fadeOutLeft {\n  -webkit-animation-name: fadeOutLeft;\n  animation-name: fadeOutLeft;\n}\n@-webkit-keyframes fadeOutLeftBig {\n  0% {\n    opacity: 1;\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateX(-2000px);\n    transform: translateX(-2000px);\n  }\n}\n@keyframes fadeOutLeftBig {\n  0% {\n    opacity: 1;\n    -webkit-transform: translateX(0);\n    -ms-transform: translateX(0);\n    transform: translateX(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateX(-2000px);\n    -ms-transform: translateX(-2000px);\n    transform: translateX(-2000px);\n  }\n}\n.fadeOutLeftBig {\n  -webkit-animation-name: fadeOutLeftBig;\n  animation-name: fadeOutLeftBig;\n}\n@-webkit-keyframes fadeOutRight {\n  0% {\n    opacity: 1;\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateX(20px);\n    transform: translateX(20px);\n  }\n}\n@keyframes fadeOutRight {\n  0% {\n    opacity: 1;\n    -webkit-transform: translateX(0);\n    -ms-transform: translateX(0);\n    transform: translateX(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateX(20px);\n    -ms-transform: translateX(20px);\n    transform: translateX(20px);\n  }\n}\n.fadeOutRight {\n  -webkit-animation-name: fadeOutRight;\n  animation-name: fadeOutRight;\n}\n@-webkit-keyframes fadeOutRightBig {\n  0% {\n    opacity: 1;\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateX(2000px);\n    transform: translateX(2000px);\n  }\n}\n@keyframes fadeOutRightBig {\n  0% {\n    opacity: 1;\n    -webkit-transform: translateX(0);\n    -ms-transform: translateX(0);\n    transform: translateX(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateX(2000px);\n    -ms-transform: translateX(2000px);\n    transform: translateX(2000px);\n  }\n}\n.fadeOutRightBig {\n  -webkit-animation-name: fadeOutRightBig;\n  animation-name: fadeOutRightBig;\n}\n@-webkit-keyframes fadeOutUp {\n  0% {\n    opacity: 1;\n    -webkit-transform: translateY(0);\n    transform: translateY(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateY(-20px);\n    transform: translateY(-20px);\n  }\n}\n@keyframes fadeOutUp {\n  0% {\n    opacity: 1;\n    -webkit-transform: translateY(0);\n    -ms-transform: translateY(0);\n    transform: translateY(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateY(-20px);\n    -ms-transform: translateY(-20px);\n    transform: translateY(-20px);\n  }\n}\n.fadeOutUp {\n  -webkit-animation-name: fadeOutUp;\n  animation-name: fadeOutUp;\n}\n@-webkit-keyframes fadeOutUpBig {\n  0% {\n    opacity: 1;\n    -webkit-transform: translateY(0);\n    transform: translateY(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateY(-2000px);\n    transform: translateY(-2000px);\n  }\n}\n@keyframes fadeOutUpBig {\n  0% {\n    opacity: 1;\n    -webkit-transform: translateY(0);\n    -ms-transform: translateY(0);\n    transform: translateY(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateY(-2000px);\n    -ms-transform: translateY(-2000px);\n    transform: translateY(-2000px);\n  }\n}\n.fadeOutUpBig {\n  -webkit-animation-name: fadeOutUpBig;\n  animation-name: fadeOutUpBig;\n}\n@-webkit-keyframes flip {\n  0% {\n    -webkit-transform: perspective(400px) translateZ(0) rotateY(0) scale(1);\n    transform: perspective(400px) translateZ(0) rotateY(0) scale(1);\n    -webkit-animation-timing-function: ease-out;\n    animation-timing-function: ease-out;\n  }\n  40% {\n    -webkit-transform: perspective(400px) translateZ(150px) rotateY(170deg) scale(1);\n    transform: perspective(400px) translateZ(150px) rotateY(170deg) scale(1);\n    -webkit-animation-timing-function: ease-out;\n    animation-timing-function: ease-out;\n  }\n  50% {\n    -webkit-transform: perspective(400px) translateZ(150px) rotateY(190deg) scale(1);\n    transform: perspective(400px) translateZ(150px) rotateY(190deg) scale(1);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n  }\n  80% {\n    -webkit-transform: perspective(400px) translateZ(0) rotateY(360deg) scale(0.95);\n    transform: perspective(400px) translateZ(0) rotateY(360deg) scale(0.95);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n  }\n  100% {\n    -webkit-transform: perspective(400px) translateZ(0) rotateY(360deg) scale(1);\n    transform: perspective(400px) translateZ(0) rotateY(360deg) scale(1);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n  }\n}\n@keyframes flip {\n  0% {\n    -webkit-transform: perspective(400px) translateZ(0) rotateY(0) scale(1);\n    -ms-transform: perspective(400px) translateZ(0) rotateY(0) scale(1);\n    transform: perspective(400px) translateZ(0) rotateY(0) scale(1);\n    -webkit-animation-timing-function: ease-out;\n    animation-timing-function: ease-out;\n  }\n  40% {\n    -webkit-transform: perspective(400px) translateZ(150px) rotateY(170deg) scale(1);\n    -ms-transform: perspective(400px) translateZ(150px) rotateY(170deg) scale(1);\n    transform: perspective(400px) translateZ(150px) rotateY(170deg) scale(1);\n    -webkit-animation-timing-function: ease-out;\n    animation-timing-function: ease-out;\n  }\n  50% {\n    -webkit-transform: perspective(400px) translateZ(150px) rotateY(190deg) scale(1);\n    -ms-transform: perspective(400px) translateZ(150px) rotateY(190deg) scale(1);\n    transform: perspective(400px) translateZ(150px) rotateY(190deg) scale(1);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n  }\n  80% {\n    -webkit-transform: perspective(400px) translateZ(0) rotateY(360deg) scale(0.95);\n    -ms-transform: perspective(400px) translateZ(0) rotateY(360deg) scale(0.95);\n    transform: perspective(400px) translateZ(0) rotateY(360deg) scale(0.95);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n  }\n  100% {\n    -webkit-transform: perspective(400px) translateZ(0) rotateY(360deg) scale(1);\n    -ms-transform: perspective(400px) translateZ(0) rotateY(360deg) scale(1);\n    transform: perspective(400px) translateZ(0) rotateY(360deg) scale(1);\n    -webkit-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n  }\n}\n.animated.flip {\n  -webkit-backface-visibility: visible;\n  -ms-backface-visibility: visible;\n  backface-visibility: visible;\n  -webkit-animation-name: flip;\n  animation-name: flip;\n}\n@-webkit-keyframes flipInX {\n  0% {\n    -webkit-transform: perspective(400px) rotateX(90deg);\n    transform: perspective(400px) rotateX(90deg);\n    opacity: 0;\n  }\n  40% {\n    -webkit-transform: perspective(400px) rotateX(-10deg);\n    transform: perspective(400px) rotateX(-10deg);\n  }\n  70% {\n    -webkit-transform: perspective(400px) rotateX(10deg);\n    transform: perspective(400px) rotateX(10deg);\n  }\n  100% {\n    -webkit-transform: perspective(400px) rotateX(0deg);\n    transform: perspective(400px) rotateX(0deg);\n    opacity: 1;\n  }\n}\n@keyframes flipInX {\n  0% {\n    -webkit-transform: perspective(400px) rotateX(90deg);\n    -ms-transform: perspective(400px) rotateX(90deg);\n    transform: perspective(400px) rotateX(90deg);\n    opacity: 0;\n  }\n  40% {\n    -webkit-transform: perspective(400px) rotateX(-10deg);\n    -ms-transform: perspective(400px) rotateX(-10deg);\n    transform: perspective(400px) rotateX(-10deg);\n  }\n  70% {\n    -webkit-transform: perspective(400px) rotateX(10deg);\n    -ms-transform: perspective(400px) rotateX(10deg);\n    transform: perspective(400px) rotateX(10deg);\n  }\n  100% {\n    -webkit-transform: perspective(400px) rotateX(0deg);\n    -ms-transform: perspective(400px) rotateX(0deg);\n    transform: perspective(400px) rotateX(0deg);\n    opacity: 1;\n  }\n}\n.flipInX {\n  -webkit-backface-visibility: visible !important;\n  -ms-backface-visibility: visible !important;\n  backface-visibility: visible !important;\n  -webkit-animation-name: flipInX;\n  animation-name: flipInX;\n}\n@-webkit-keyframes flipInY {\n  0% {\n    -webkit-transform: perspective(400px) rotateY(90deg);\n    transform: perspective(400px) rotateY(90deg);\n    opacity: 0;\n  }\n  40% {\n    -webkit-transform: perspective(400px) rotateY(-10deg);\n    transform: perspective(400px) rotateY(-10deg);\n  }\n  70% {\n    -webkit-transform: perspective(400px) rotateY(10deg);\n    transform: perspective(400px) rotateY(10deg);\n  }\n  100% {\n    -webkit-transform: perspective(400px) rotateY(0deg);\n    transform: perspective(400px) rotateY(0deg);\n    opacity: 1;\n  }\n}\n@keyframes flipInY {\n  0% {\n    -webkit-transform: perspective(400px) rotateY(90deg);\n    -ms-transform: perspective(400px) rotateY(90deg);\n    transform: perspective(400px) rotateY(90deg);\n    opacity: 0;\n  }\n  40% {\n    -webkit-transform: perspective(400px) rotateY(-10deg);\n    -ms-transform: perspective(400px) rotateY(-10deg);\n    transform: perspective(400px) rotateY(-10deg);\n  }\n  70% {\n    -webkit-transform: perspective(400px) rotateY(10deg);\n    -ms-transform: perspective(400px) rotateY(10deg);\n    transform: perspective(400px) rotateY(10deg);\n  }\n  100% {\n    -webkit-transform: perspective(400px) rotateY(0deg);\n    -ms-transform: perspective(400px) rotateY(0deg);\n    transform: perspective(400px) rotateY(0deg);\n    opacity: 1;\n  }\n}\n.flipInY {\n  -webkit-backface-visibility: visible !important;\n  -ms-backface-visibility: visible !important;\n  backface-visibility: visible !important;\n  -webkit-animation-name: flipInY;\n  animation-name: flipInY;\n}\n@-webkit-keyframes flipOutX {\n  0% {\n    -webkit-transform: perspective(400px) rotateX(0deg);\n    transform: perspective(400px) rotateX(0deg);\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform: perspective(400px) rotateX(90deg);\n    transform: perspective(400px) rotateX(90deg);\n    opacity: 0;\n  }\n}\n@keyframes flipOutX {\n  0% {\n    -webkit-transform: perspective(400px) rotateX(0deg);\n    -ms-transform: perspective(400px) rotateX(0deg);\n    transform: perspective(400px) rotateX(0deg);\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform: perspective(400px) rotateX(90deg);\n    -ms-transform: perspective(400px) rotateX(90deg);\n    transform: perspective(400px) rotateX(90deg);\n    opacity: 0;\n  }\n}\n.flipOutX {\n  -webkit-animation-name: flipOutX;\n  animation-name: flipOutX;\n  -webkit-backface-visibility: visible !important;\n  -ms-backface-visibility: visible !important;\n  backface-visibility: visible !important;\n}\n@-webkit-keyframes flipOutY {\n  0% {\n    -webkit-transform: perspective(400px) rotateY(0deg);\n    transform: perspective(400px) rotateY(0deg);\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform: perspective(400px) rotateY(90deg);\n    transform: perspective(400px) rotateY(90deg);\n    opacity: 0;\n  }\n}\n@keyframes flipOutY {\n  0% {\n    -webkit-transform: perspective(400px) rotateY(0deg);\n    -ms-transform: perspective(400px) rotateY(0deg);\n    transform: perspective(400px) rotateY(0deg);\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform: perspective(400px) rotateY(90deg);\n    -ms-transform: perspective(400px) rotateY(90deg);\n    transform: perspective(400px) rotateY(90deg);\n    opacity: 0;\n  }\n}\n.flipOutY {\n  -webkit-backface-visibility: visible !important;\n  -ms-backface-visibility: visible !important;\n  backface-visibility: visible !important;\n  -webkit-animation-name: flipOutY;\n  animation-name: flipOutY;\n}\n@-webkit-keyframes lightSpeedIn {\n  0% {\n    -webkit-transform: translateX(100%) skewX(-30deg);\n    transform: translateX(100%) skewX(-30deg);\n    opacity: 0;\n  }\n  60% {\n    -webkit-transform: translateX(-20%) skewX(30deg);\n    transform: translateX(-20%) skewX(30deg);\n    opacity: 1;\n  }\n  80% {\n    -webkit-transform: translateX(0%) skewX(-15deg);\n    transform: translateX(0%) skewX(-15deg);\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform: translateX(0%) skewX(0deg);\n    transform: translateX(0%) skewX(0deg);\n    opacity: 1;\n  }\n}\n@keyframes lightSpeedIn {\n  0% {\n    -webkit-transform: translateX(100%) skewX(-30deg);\n    -ms-transform: translateX(100%) skewX(-30deg);\n    transform: translateX(100%) skewX(-30deg);\n    opacity: 0;\n  }\n  60% {\n    -webkit-transform: translateX(-20%) skewX(30deg);\n    -ms-transform: translateX(-20%) skewX(30deg);\n    transform: translateX(-20%) skewX(30deg);\n    opacity: 1;\n  }\n  80% {\n    -webkit-transform: translateX(0%) skewX(-15deg);\n    -ms-transform: translateX(0%) skewX(-15deg);\n    transform: translateX(0%) skewX(-15deg);\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform: translateX(0%) skewX(0deg);\n    -ms-transform: translateX(0%) skewX(0deg);\n    transform: translateX(0%) skewX(0deg);\n    opacity: 1;\n  }\n}\n.lightSpeedIn {\n  -webkit-animation-name: lightSpeedIn;\n  animation-name: lightSpeedIn;\n  -webkit-animation-timing-function: ease-out;\n  animation-timing-function: ease-out;\n}\n@-webkit-keyframes lightSpeedOut {\n  0% {\n    -webkit-transform: translateX(0%) skewX(0deg);\n    transform: translateX(0%) skewX(0deg);\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform: translateX(100%) skewX(-30deg);\n    transform: translateX(100%) skewX(-30deg);\n    opacity: 0;\n  }\n}\n@keyframes lightSpeedOut {\n  0% {\n    -webkit-transform: translateX(0%) skewX(0deg);\n    -ms-transform: translateX(0%) skewX(0deg);\n    transform: translateX(0%) skewX(0deg);\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform: translateX(100%) skewX(-30deg);\n    -ms-transform: translateX(100%) skewX(-30deg);\n    transform: translateX(100%) skewX(-30deg);\n    opacity: 0;\n  }\n}\n.lightSpeedOut {\n  -webkit-animation-name: lightSpeedOut;\n  animation-name: lightSpeedOut;\n  -webkit-animation-timing-function: ease-in;\n  animation-timing-function: ease-in;\n}\n@-webkit-keyframes rotateIn {\n  0% {\n    -webkit-transform-origin: center center;\n    transform-origin: center center;\n    -webkit-transform: rotate(-200deg);\n    transform: rotate(-200deg);\n    opacity: 0;\n  }\n  100% {\n    -webkit-transform-origin: center center;\n    transform-origin: center center;\n    -webkit-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n}\n@keyframes rotateIn {\n  0% {\n    -webkit-transform-origin: center center;\n    -ms-transform-origin: center center;\n    transform-origin: center center;\n    -webkit-transform: rotate(-200deg);\n    -ms-transform: rotate(-200deg);\n    transform: rotate(-200deg);\n    opacity: 0;\n  }\n  100% {\n    -webkit-transform-origin: center center;\n    -ms-transform-origin: center center;\n    transform-origin: center center;\n    -webkit-transform: rotate(0);\n    -ms-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n}\n.rotateIn {\n  -webkit-animation-name: rotateIn;\n  animation-name: rotateIn;\n}\n@-webkit-keyframes rotateInDownLeft {\n  0% {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate(-90deg);\n    transform: rotate(-90deg);\n    opacity: 0;\n  }\n  100% {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n}\n@keyframes rotateInDownLeft {\n  0% {\n    -webkit-transform-origin: left bottom;\n    -ms-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate(-90deg);\n    -ms-transform: rotate(-90deg);\n    transform: rotate(-90deg);\n    opacity: 0;\n  }\n  100% {\n    -webkit-transform-origin: left bottom;\n    -ms-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate(0);\n    -ms-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n}\n.rotateInDownLeft {\n  -webkit-animation-name: rotateInDownLeft;\n  animation-name: rotateInDownLeft;\n}\n@-webkit-keyframes rotateInDownRight {\n  0% {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate(90deg);\n    transform: rotate(90deg);\n    opacity: 0;\n  }\n  100% {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n}\n@keyframes rotateInDownRight {\n  0% {\n    -webkit-transform-origin: right bottom;\n    -ms-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate(90deg);\n    -ms-transform: rotate(90deg);\n    transform: rotate(90deg);\n    opacity: 0;\n  }\n  100% {\n    -webkit-transform-origin: right bottom;\n    -ms-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate(0);\n    -ms-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n}\n.rotateInDownRight {\n  -webkit-animation-name: rotateInDownRight;\n  animation-name: rotateInDownRight;\n}\n@-webkit-keyframes rotateInUpLeft {\n  0% {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate(90deg);\n    transform: rotate(90deg);\n    opacity: 0;\n  }\n  100% {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n}\n@keyframes rotateInUpLeft {\n  0% {\n    -webkit-transform-origin: left bottom;\n    -ms-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate(90deg);\n    -ms-transform: rotate(90deg);\n    transform: rotate(90deg);\n    opacity: 0;\n  }\n  100% {\n    -webkit-transform-origin: left bottom;\n    -ms-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate(0);\n    -ms-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n}\n.rotateInUpLeft {\n  -webkit-animation-name: rotateInUpLeft;\n  animation-name: rotateInUpLeft;\n}\n@-webkit-keyframes rotateInUpRight {\n  0% {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate(-90deg);\n    transform: rotate(-90deg);\n    opacity: 0;\n  }\n  100% {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n}\n@keyframes rotateInUpRight {\n  0% {\n    -webkit-transform-origin: right bottom;\n    -ms-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate(-90deg);\n    -ms-transform: rotate(-90deg);\n    transform: rotate(-90deg);\n    opacity: 0;\n  }\n  100% {\n    -webkit-transform-origin: right bottom;\n    -ms-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate(0);\n    -ms-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n}\n.rotateInUpRight {\n  -webkit-animation-name: rotateInUpRight;\n  animation-name: rotateInUpRight;\n}\n@-webkit-keyframes rotateOut {\n  0% {\n    -webkit-transform-origin: center center;\n    transform-origin: center center;\n    -webkit-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform-origin: center center;\n    transform-origin: center center;\n    -webkit-transform: rotate(200deg);\n    transform: rotate(200deg);\n    opacity: 0;\n  }\n}\n@keyframes rotateOut {\n  0% {\n    -webkit-transform-origin: center center;\n    -ms-transform-origin: center center;\n    transform-origin: center center;\n    -webkit-transform: rotate(0);\n    -ms-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform-origin: center center;\n    -ms-transform-origin: center center;\n    transform-origin: center center;\n    -webkit-transform: rotate(200deg);\n    -ms-transform: rotate(200deg);\n    transform: rotate(200deg);\n    opacity: 0;\n  }\n}\n.rotateOut {\n  -webkit-animation-name: rotateOut;\n  animation-name: rotateOut;\n}\n@-webkit-keyframes rotateOutDownLeft {\n  0% {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate(90deg);\n    transform: rotate(90deg);\n    opacity: 0;\n  }\n}\n@keyframes rotateOutDownLeft {\n  0% {\n    -webkit-transform-origin: left bottom;\n    -ms-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate(0);\n    -ms-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform-origin: left bottom;\n    -ms-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate(90deg);\n    -ms-transform: rotate(90deg);\n    transform: rotate(90deg);\n    opacity: 0;\n  }\n}\n.rotateOutDownLeft {\n  -webkit-animation-name: rotateOutDownLeft;\n  animation-name: rotateOutDownLeft;\n}\n@-webkit-keyframes rotateOutDownRight {\n  0% {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate(-90deg);\n    transform: rotate(-90deg);\n    opacity: 0;\n  }\n}\n@keyframes rotateOutDownRight {\n  0% {\n    -webkit-transform-origin: right bottom;\n    -ms-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate(0);\n    -ms-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform-origin: right bottom;\n    -ms-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate(-90deg);\n    -ms-transform: rotate(-90deg);\n    transform: rotate(-90deg);\n    opacity: 0;\n  }\n}\n.rotateOutDownRight {\n  -webkit-animation-name: rotateOutDownRight;\n  animation-name: rotateOutDownRight;\n}\n@-webkit-keyframes rotateOutUpLeft {\n  0% {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate(-90deg);\n    transform: rotate(-90deg);\n    opacity: 0;\n  }\n}\n@keyframes rotateOutUpLeft {\n  0% {\n    -webkit-transform-origin: left bottom;\n    -ms-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate(0);\n    -ms-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform-origin: left bottom;\n    -ms-transform-origin: left bottom;\n    transform-origin: left bottom;\n    -webkit-transform: rotate(-90deg);\n    -ms-transform: rotate(-90deg);\n    transform: rotate(-90deg);\n    opacity: 0;\n  }\n}\n.rotateOutUpLeft {\n  -webkit-animation-name: rotateOutUpLeft;\n  animation-name: rotateOutUpLeft;\n}\n@-webkit-keyframes rotateOutUpRight {\n  0% {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate(90deg);\n    transform: rotate(90deg);\n    opacity: 0;\n  }\n}\n@keyframes rotateOutUpRight {\n  0% {\n    -webkit-transform-origin: right bottom;\n    -ms-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate(0);\n    -ms-transform: rotate(0);\n    transform: rotate(0);\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform-origin: right bottom;\n    -ms-transform-origin: right bottom;\n    transform-origin: right bottom;\n    -webkit-transform: rotate(90deg);\n    -ms-transform: rotate(90deg);\n    transform: rotate(90deg);\n    opacity: 0;\n  }\n}\n.rotateOutUpRight {\n  -webkit-animation-name: rotateOutUpRight;\n  animation-name: rotateOutUpRight;\n}\n@-webkit-keyframes slideInDown {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateY(-2000px);\n    transform: translateY(-2000px);\n  }\n  100% {\n    -webkit-transform: translateY(0);\n    transform: translateY(0);\n  }\n}\n@keyframes slideInDown {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateY(-2000px);\n    -ms-transform: translateY(-2000px);\n    transform: translateY(-2000px);\n  }\n  100% {\n    -webkit-transform: translateY(0);\n    -ms-transform: translateY(0);\n    transform: translateY(0);\n  }\n}\n.slideInDown {\n  -webkit-animation-name: slideInDown;\n  animation-name: slideInDown;\n}\n@-webkit-keyframes slideInLeft {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateX(-2000px);\n    transform: translateX(-2000px);\n  }\n  100% {\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n@keyframes slideInLeft {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateX(-2000px);\n    -ms-transform: translateX(-2000px);\n    transform: translateX(-2000px);\n  }\n  100% {\n    -webkit-transform: translateX(0);\n    -ms-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n.slideInLeft {\n  -webkit-animation-name: slideInLeft;\n  animation-name: slideInLeft;\n}\n@-webkit-keyframes slideInRight {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateX(2000px);\n    transform: translateX(2000px);\n  }\n  100% {\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n@keyframes slideInRight {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateX(2000px);\n    -ms-transform: translateX(2000px);\n    transform: translateX(2000px);\n  }\n  100% {\n    -webkit-transform: translateX(0);\n    -ms-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n.slideInRight {\n  -webkit-animation-name: slideInRight;\n  animation-name: slideInRight;\n}\n@-webkit-keyframes slideOutLeft {\n  0% {\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateX(-2000px);\n    transform: translateX(-2000px);\n  }\n}\n@keyframes slideOutLeft {\n  0% {\n    -webkit-transform: translateX(0);\n    -ms-transform: translateX(0);\n    transform: translateX(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateX(-2000px);\n    -ms-transform: translateX(-2000px);\n    transform: translateX(-2000px);\n  }\n}\n.slideOutLeft {\n  -webkit-animation-name: slideOutLeft;\n  animation-name: slideOutLeft;\n}\n@-webkit-keyframes slideOutRight {\n  0% {\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateX(2000px);\n    transform: translateX(2000px);\n  }\n}\n@keyframes slideOutRight {\n  0% {\n    -webkit-transform: translateX(0);\n    -ms-transform: translateX(0);\n    transform: translateX(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateX(2000px);\n    -ms-transform: translateX(2000px);\n    transform: translateX(2000px);\n  }\n}\n.slideOutRight {\n  -webkit-animation-name: slideOutRight;\n  animation-name: slideOutRight;\n}\n@-webkit-keyframes slideOutUp {\n  0% {\n    -webkit-transform: translateY(0);\n    transform: translateY(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateY(-2000px);\n    transform: translateY(-2000px);\n  }\n}\n@keyframes slideOutUp {\n  0% {\n    -webkit-transform: translateY(0);\n    -ms-transform: translateY(0);\n    transform: translateY(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateY(-2000px);\n    -ms-transform: translateY(-2000px);\n    transform: translateY(-2000px);\n  }\n}\n.slideOutUp {\n  -webkit-animation-name: slideOutUp;\n  animation-name: slideOutUp;\n}\n@-webkit-keyframes slideInUp {\n  0% {\n    -webkit-transform: translateY(2000px);\n    transform: translateY(2000px);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateY(0);\n    transform: translateY(0);\n  }\n}\n@keyframes slideInUp {\n  0% {\n    -webkit-transform: translateY(2000px);\n    -ms-transform: translateY(2000px);\n    transform: translateY(2000px);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateY(0);\n    -ms-transform: translateY(0);\n    transform: translateY(0);\n  }\n}\n.slideInUp {\n  -webkit-animation-name: slideInUp;\n  animation-name: slideInUp;\n}\n@-webkit-keyframes slideOutDown {\n  0% {\n    -webkit-transform: translateY(0);\n    transform: translateY(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateY(2000px);\n    transform: translateY(2000px);\n  }\n}\n@keyframes slideOutDown {\n  0% {\n    -webkit-transform: translateY(0);\n    -ms-transform: translateY(0);\n    transform: translateY(0);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateY(2000px);\n    -ms-transform: translateY(2000px);\n    transform: translateY(2000px);\n  }\n}\n.slideOutDown {\n  -webkit-animation-name: slideOutDown;\n  animation-name: slideOutDown;\n}\n@-webkit-keyframes hinge {\n  0% {\n    -webkit-transform: rotate(0);\n    transform: rotate(0);\n    -webkit-transform-origin: top left;\n    transform-origin: top left;\n    -webkit-animation-timing-function: ease-in-out;\n    animation-timing-function: ease-in-out;\n  }\n  20%,\n  60% {\n    -webkit-transform: rotate(80deg);\n    transform: rotate(80deg);\n    -webkit-transform-origin: top left;\n    transform-origin: top left;\n    -webkit-animation-timing-function: ease-in-out;\n    animation-timing-function: ease-in-out;\n  }\n  40% {\n    -webkit-transform: rotate(60deg);\n    transform: rotate(60deg);\n    -webkit-transform-origin: top left;\n    transform-origin: top left;\n    -webkit-animation-timing-function: ease-in-out;\n    animation-timing-function: ease-in-out;\n  }\n  80% {\n    -webkit-transform: rotate(60deg) translateY(0);\n    transform: rotate(60deg) translateY(0);\n    -webkit-transform-origin: top left;\n    transform-origin: top left;\n    -webkit-animation-timing-function: ease-in-out;\n    animation-timing-function: ease-in-out;\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform: translateY(700px);\n    transform: translateY(700px);\n    opacity: 0;\n  }\n}\n@keyframes hinge {\n  0% {\n    -webkit-transform: rotate(0);\n    -ms-transform: rotate(0);\n    transform: rotate(0);\n    -webkit-transform-origin: top left;\n    -ms-transform-origin: top left;\n    transform-origin: top left;\n    -webkit-animation-timing-function: ease-in-out;\n    animation-timing-function: ease-in-out;\n  }\n  20%,\n  60% {\n    -webkit-transform: rotate(80deg);\n    -ms-transform: rotate(80deg);\n    transform: rotate(80deg);\n    -webkit-transform-origin: top left;\n    -ms-transform-origin: top left;\n    transform-origin: top left;\n    -webkit-animation-timing-function: ease-in-out;\n    animation-timing-function: ease-in-out;\n  }\n  40% {\n    -webkit-transform: rotate(60deg);\n    -ms-transform: rotate(60deg);\n    transform: rotate(60deg);\n    -webkit-transform-origin: top left;\n    -ms-transform-origin: top left;\n    transform-origin: top left;\n    -webkit-animation-timing-function: ease-in-out;\n    animation-timing-function: ease-in-out;\n  }\n  80% {\n    -webkit-transform: rotate(60deg) translateY(0);\n    -ms-transform: rotate(60deg) translateY(0);\n    transform: rotate(60deg) translateY(0);\n    -webkit-transform-origin: top left;\n    -ms-transform-origin: top left;\n    transform-origin: top left;\n    -webkit-animation-timing-function: ease-in-out;\n    animation-timing-function: ease-in-out;\n    opacity: 1;\n  }\n  100% {\n    -webkit-transform: translateY(700px);\n    -ms-transform: translateY(700px);\n    transform: translateY(700px);\n    opacity: 0;\n  }\n}\n.hinge {\n  -webkit-animation-name: hinge;\n  animation-name: hinge;\n}\n/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */\n@-webkit-keyframes rollIn {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateX(-100%) rotate(-120deg);\n    transform: translateX(-100%) rotate(-120deg);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: translateX(0px) rotate(0deg);\n    transform: translateX(0px) rotate(0deg);\n  }\n}\n@keyframes rollIn {\n  0% {\n    opacity: 0;\n    -webkit-transform: translateX(-100%) rotate(-120deg);\n    -ms-transform: translateX(-100%) rotate(-120deg);\n    transform: translateX(-100%) rotate(-120deg);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: translateX(0px) rotate(0deg);\n    -ms-transform: translateX(0px) rotate(0deg);\n    transform: translateX(0px) rotate(0deg);\n  }\n}\n.rollIn {\n  -webkit-animation-name: rollIn;\n  animation-name: rollIn;\n}\n/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */\n@-webkit-keyframes rollOut {\n  0% {\n    opacity: 1;\n    -webkit-transform: translateX(0px) rotate(0deg);\n    transform: translateX(0px) rotate(0deg);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateX(100%) rotate(120deg);\n    transform: translateX(100%) rotate(120deg);\n  }\n}\n@keyframes rollOut {\n  0% {\n    opacity: 1;\n    -webkit-transform: translateX(0px) rotate(0deg);\n    -ms-transform: translateX(0px) rotate(0deg);\n    transform: translateX(0px) rotate(0deg);\n  }\n  100% {\n    opacity: 0;\n    -webkit-transform: translateX(100%) rotate(120deg);\n    -ms-transform: translateX(100%) rotate(120deg);\n    transform: translateX(100%) rotate(120deg);\n  }\n}\n.rollOut {\n  -webkit-animation-name: rollOut;\n  animation-name: rollOut;\n}\n/*---------------------------------------------------\n    LESS Elements 0.9\n  ---------------------------------------------------\n    A set of useful LESS mixins\n    More info at: http://lesselements.com\n  ---------------------------------------------------*/\n/* Rebar - these files reset and style all the basic HTML elements */\n/* #Reset & Basics (Inspired by E. Meyers)\n================================================== */\nhtml,\nbody,\ndiv,\nspan,\napplet,\nobject,\niframe,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\np,\nblockquote,\npre,\na,\nabbr,\nacronym,\naddress,\nbig,\ncite,\ncode,\ndel,\ndfn,\nem,\nimg,\nins,\nkbd,\nq,\ns,\nsamp,\nsmall,\nstrike,\nstrong,\nsub,\nsup,\ntt,\nvar,\nb,\nu,\ni,\ncenter,\ndl,\ndt,\ndd,\nol,\nul,\nli,\nfieldset,\nform,\nlabel,\nlegend,\ntable,\ncaption,\ntbody,\ntfoot,\nthead,\ntr,\nth,\ntd,\narticle,\naside,\ncanvas,\ndetails,\nembed,\nfigure,\nfigcaption,\nfooter,\nheader,\nhgroup,\nmenu,\nnav,\noutput,\nruby,\nsection,\nsummary,\ntime,\nmark,\naudio,\nvideo {\n  margin: 0;\n  padding: 0;\n  border: 0;\n  font-size: 100%;\n  font: inherit;\n  vertical-align: baseline;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmenu,\nnav,\nsection {\n  display: block;\n}\nbody {\n  line-height: 1;\n}\nol,\nul {\n  list-style: none;\n}\nblockquote,\nq {\n  quotes: none;\n}\nblockquote:before,\nblockquote:after,\nq:before,\nq:after {\n  content: '';\n  content: none;\n}\ntable {\n  border-collapse: separate;\n  border-spacing: 0;\n}\n/* #Body Styles\n================================================== */\nbody {\n  background:    #ffffff;\n  font-size: 14px;\n  font-weight: normal;\n  font-family: \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  line-height: 24px;\n  color: #333333;\n  -webkit-font-smoothing: antialiased;\n  /* Fix for webkit rendering */\n  -webkit-text-size-adjust: 100%;\n}\n/* #Buttons\n================================================== */\n/* Global */\nbutton,\nbutton:hover,\nbutton:active,\ninput[type=\"submit\"],\ninput[type=\"submit\"]:hover,\ninput[type=\"submit\"]:active,\ninput[type=\"reset\"],\ninput[type=\"reset\"]:hover,\ninput[type=\"reset\"]:active,\ninput[type=\"button\"],\ninput[type=\"button\"]:hover,\ninput[type=\"button\"]:active,\n.button-primary,\n.button-secondary,\n.button-info,\n.button-alert,\n.button-warning {\n  font-family: Mailpile-500, \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif !important;\n  font-size: 16px !important;\n  font-weight: 300 !important;\n  line-height: inherit;\n  text-decoration: none;\n  margin: 0px;\n  padding: 5px 15px;\n  display: inline-block;\n  cursor: pointer;\n  box-sizing: border-box;\n  user-select: none;\n  transition-duration: 0.2s;\n  outline: none;\n}\na.button-primary,\na.button-primary:visited,\na.button-primary:hover,\na.button-secondary,\na.button-secondary:visited,\na.button-secondary:hover,\na.button-info,\na.button-info:visited,\na.button-info:hover,\na.button-alert,\na.button-alert:visited,\na.button-alert:hover,\na.button-warning,\na.button-warning:visited,\na.button-warning:hover {\n  text-decoration: none;\n}\n.button-small,\nbutton.button-small,\ninput[type=\"submit\"].button-small,\ninput[type=\"reset\"].button-small,\ninput[type=\"button\"].button-small {\n  font-family: Mailpile-500, \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif !important;\n  font-size: 12px !important;\n  font-weight: bold !important;\n  line-height: inherit;\n  text-decoration: none;\n  margin: 0px;\n  padding: 5px 8px;\n  display: inline-block;\n  cursor: pointer;\n  box-sizing: border-box;\n  user-select: none;\n  transition-duration: 0.2s;\n}\n/* Primary */\nbutton,\nbutton.button-primary,\ninput[type=\"submit\"],\ninput[type=\"reset\"],\ninput[type=\"button\"],\ninput[type=\"submit\"].button-primary,\ninput[type=\"reset\"].button-primary,\ninput[type=\"button\"].button-primary,\na.button-primary {\n  color: #ffffff !important;\n  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.4);\n  border: 1px solid #28638a;\n  border-radius: 4px;\n  background: #337fb2;\n  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4), 0 0px 0px rgba(0, 0, 0, 0.2);\n}\nbutton:hover,\ninput[type=\"submit\"]:hover,\ninput[type=\"reset\"]:hover,\ninput[type=\"button\"]:hover,\na.button-primary:hover {\n  border-color: #225577;\n  background-color: #2d719e;\n}\nbutton:active,\ninput[type=\"submit\"]:active,\ninput[type=\"reset\"]:active,\ninput[type=\"button\"]:active,\na.button-primary:active {\n  border-color: #225577;\n  background-color: #2d719e;\n  box-shadow: inset 0 0.17em 0.1em rgba(0, 0, 0, 0.3);\n}\n/* Secondary */\nbutton.button-secondary,\ninput[type=\"submit\"].button-secondary,\ninput[type=\"reset\"].button-secondary,\ninput[type=\"button\"].button-secondary,\na.button-secondary {\n  color: #ffffff !important;\n  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.4);\n  border: 1px solid #397131;\n  border-radius: 4px;\n  background: #4b9441;\n  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4), 0 0px 0px rgba(0, 0, 0, 0.2);\n}\n.button-secondary:hover,\ninput[type=\"submit\"].button-secondary:hover,\ninput[type=\"reset\"].button-secondary:hover,\ninput[type=\"button\"].button-secondary:hover,\na.button-secondary:hover {\n  border-color: #397131;\n  background-color: #428239;\n}\nbutton.button-secondary:active,\ninput[type=\"submit\"].button-secondary:active,\ninput[type=\"reset\"].button-secondary:active,\ninput[type=\"button\"].button-secondary:active,\na.button-secondary:active {\n  border-color: #397131;\n  background-color: #428239;\n  box-shadow: inset 0 0.17em 0.1em rgba(0, 0, 0, 0.3);\n}\n/* Info */\nbutton.button-info,\ninput[type=\"submit\"].button-info,\ninput[type=\"reset\"].button-info,\ninput[type=\"button\"].button-info,\na.button-info {\n  color: #333333 !important;\n  text-shadow: 0px 0px 0px;\n  border: 1px solid #cccccc;\n  border-radius: 4px;\n  background: #ffffff;\n  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4), 0 0px 0px rgba(0, 0, 0, 0.2);\n}\nbutton.button-info:hover,\ninput[type=\"submit\"].button-info:hover,\ninput[type=\"reset\"].button-info:hover,\ninput[type=\"button\"].button-info:hover,\na.button-info:hover {\n  border-color: #b3b3b3;\n  background-color: #dcdcdc;\n}\nbutton.button-info:active,\ninput[type=\"submit\"].button-info:active,\ninput[type=\"reset\"].button-info:active,\ninput[type=\"button\"].button-info:active,\na.button-info:active {\n  border-color: #9a9a9a;\n  background-color: #dcdcdc;\n  box-shadow: inset 0 0.17em 0.1em rgba(0, 0, 0, 0.3);\n}\n/* Alert */\nbutton.button-alert,\ninput[type=\"submit\"].button-alert,\ninput[type=\"reset\"].button-alert,\ninput[type=\"button\"].button-alert,\na.button-alert {\n  color: #ffffff !important;\n  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.4);\n  border: 1px solid #fa9c09;\n  border-radius: 4px;\n  background: #fbb03b;\n  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4), 0 0px 0px rgba(0, 0, 0, 0.2);\n}\nbutton.button-alert:hover,\ninput[type=\"submit\"].button-alert:hover,\ninput[type=\"reset\"].button-alert:hover,\ninput[type=\"button\"].button-alert:hover,\na.button-alert:hover {\n  border-color: #e58d05;\n  background-color: #faa013;\n}\nbutton.button-alert:active,\ninput[type=\"submit\"].button-alert:active,\ninput[type=\"reset\"].button-alert:active,\ninput[type=\"button\"].button-alert:active,\na.button-alert:active {\n  border-color: #f49705;\n  background-color: #faa622;\n  box-shadow: inset 0 0.17em 0.1em rgba(0, 0, 0, 0.3);\n}\n/* Warning */\nbutton.button-warning,\ninput[type=\"submit\"].button-warning,\ninput[type=\"reset\"].button-warning,\ninput[type=\"button\"].button-warning,\na.button-warning {\n  color: #ffffff !important;\n  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.4);\n  border: 1px solid #921519;\n  border-radius: 4px;\n  background: #be1c21;\n  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4), 0 0px 0px rgba(0, 0, 0, 0.2);\n}\nbutton.button-warning:hover,\ninput[type=\"submit\"].button-warning:hover,\ninput[type=\"reset\"].button-warning:hover,\ninput[type=\"button\"].button-warning:hover,\na.button-warning:hover {\n  border-color: #921519;\n  background-color: #9f171c;\n}\nbutton.button-warning:active,\ninput[type=\"submit\"].button-warning:active,\ninput[type=\"reset\"].button-warning:active,\ninput[type=\"button\"].button-warning:active,\na.button-warning:active {\n  border-color: #9a171b;\n  background-color: #a8191d;\n  box-shadow: inset 0 0.17em 0.1em rgba(0, 0, 0, 0.3);\n}\n/* Icons */\n.button span,\n.button-secondary span,\n.button-alert span,\n.button-warning span {\n  margin-right: 5px;\n}\n/* Full Width */\n.button.full-width,\n.button-primary.full-width,\n.button-secondary.full-width,\n.button-alert.full-width,\n.button-warning.full-width,\n.button-big-primary.full-width,\n.button-big-secondary.full-width,\nbutton.full-width,\ninput[type=\"submit\"].full-width,\ninput[type=\"reset\"].full-width,\ninput[type=\"button\"].full-width {\n  width: 100%;\n  padding-left: 0 !important;\n  padding-right: 0 !important;\n  text-align: center;\n}\n/* Fix for odd Mozilla border & padding issues */\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n  border: 0;\n  padding: 0;\n}\n/* --- Clearing --- */\n.container:after {\n  content: \"\\0020\";\n  display: block;\n  height: 0;\n  clear: both;\n  visibility: hidden;\n}\n/* Clearfix helper */\n.clearfix,\n.modal-footer {\n  zoom: 1;\n}\n.clearfix:before,\n.clearfix:after,\n.modal-footer:before,\n.modal-footer:after {\n  content: \"\";\n  display: table;\n}\n.clearfix:after,\n.modal-footer:after {\n  clear: both;\n}\n/* Vertical Margins & Padding */\n.remove-top {\n  margin-top: 0 !important;\n}\n.half-top {\n  margin-top: 10px !important;\n}\n.add-top {\n  margin-top: 20px !important;\n}\n.double-top {\n  margin-top: 40px !important;\n}\n.remove-bottom {\n  margin-bottom: 0 !important;\n}\n.half-bottom {\n  margin-bottom: 10px !important;\n}\n.add-bottom {\n  margin-bottom: 20px !important;\n}\n.double-bottom {\n  margin-top: 40px !important;\n}\n.remove-right {\n  margin-right: 0px !important;\n}\n.add-right {\n  margin-right: 20px !important;\n}\n.add-right-half {\n  margin-right: 10px !important;\n}\n.add-right-double {\n  margin-left: 40px !important;\n}\n.remove-left {\n  margin-left: 0px !important;\n}\n.add-left {\n  margin-left: 20px !important;\n}\n.add-left-half {\n  margin-left: 10px !important;\n}\n.add-left-double {\n  margin-left: 40px !important;\n}\n/* Centering */\n.left {\n  float: left;\n}\n.center {\n  margin-left: auto !important;\n  margin-right: auto !important;\n}\n.right {\n  float: right;\n}\n.top {\n  vertical-align: top !important;\n}\n.middle {\n  vertical-align: middle !important;\n}\n.bottom {\n  vertical-align: bottom !important;\n}\n.text-left {\n  text-align: left !important;\n}\n.text-center {\n  text-align: center !important;\n}\n.text-right {\n  text-align: right !important;\n}\n.text-top {\n  vertical-align: text-top !important;\n}\n.text-bottom {\n  vertical-align: text-bottom !important;\n}\n/* Display */\n.hide {\n  display: none;\n}\n.block {\n  display: block !important;\n}\n.inline-block {\n  display: inline-block !important;\n}\n.table {\n  display: table !important;\n}\n.table-cell {\n  display: table-cell !important;\n}\n/* #Forms\n================================================== */\n.standard form {\n  margin-bottom: 20px;\n}\n.standard fieldset {\n  margin-bottom: 20px;\n}\n.standard input[type=\"text\"],\n.standard input[type=\"password\"],\n.standard input[type=\"email\"],\n.standard input[type=\"url\"],\n.standard input[type=\"phone\"],\n.standard input[type=\"address\"],\n.standard textarea,\n.standard select {\n  margin-bottom: 20px;\n  padding: 10px 12px;\n  outline: none;\n  font-size: 16px;\n  font-family: \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  color: #666666;\n  width: 210px;\n  max-width: 100%;\n  display: block;\n  background: #ffffff;\n  border-radius: 3px;\n  border: 1px solid #b3b3b3;\n  box-sizing: border-box;\n}\n.standard input[type=\"text\"]:focus,\n.standard input[type=\"password\"]:focus,\n.standard input[type=\"email\"]:focus,\n.standard textarea:focus {\n  border: 1px solid #4d4d4d;\n  color: #333333;\n  box-shadow: 0 0 5px rgba(0, 0, 0, 0.4);\n}\n.standard textarea {\n  min-height: 60px;\n}\n.standard label,\n.standard legend {\n  display: block;\n  font-weight: normal;\n  font-size: 16px;\n  font-style: italic;\n  margin: 0 0 5px 0;\n  color: #4d4d4d;\n}\n.standard select {\n  color: #4d4d4d;\n  width: 220px;\n  font-size: 14.4px;\n}\n.standard input[type=\"checkbox\"] {\n  display: inline;\n}\n.standard label span,\n.standard legend span {\n  font-weight: normal;\n  font-size: 16px;\n  color: #4d4d4d;\n}\n.standard a {\n  font-style: normal;\n}\n.standard label span.form_error {\n  margin: 0 0 0 3px;\n  color: #4d4d4d;\n}\ninput[type=\"text\"].tiny,\ninput[type=\"password\"].tiny,\ninput[type=\"email\"].tiny,\ninput[type=\"url\"].tiny,\ninput[type=\"phone\"].tiny,\ninput[type=\"address\"].tiny,\ntextarea.tiny,\nselect.tiny {\n  width: 75px;\n}\ninput[type=\"text\"].small,\ninput[type=\"password\"].small,\ninput[type=\"email\"].small,\ninput[type=\"url\"].small,\ninput[type=\"phone\"].small,\ninput[type=\"address\"].small,\ntextarea.small,\nselect.small {\n  width: 135px;\n}\ninput[type=\"text\"].medium,\ninput[type=\"password\"].medium,\ninput[type=\"email\"].medium,\ninput[type=\"url\"].medium,\ninput[type=\"phone\"].medium,\ninput[type=\"address\"].medium,\ntextarea.medium,\nselect.medium {\n  width: 350px;\n}\ninput[type=\"text\"].large,\ninput[type=\"password\"].large,\ninput[type=\"email\"].large,\ninput[type=\"url\"].large,\ninput[type=\"phone\"].large,\ninput[type=\"address\"].large,\ntextarea.large,\nselect.large {\n  width: 500px;\n}\ninput[type=\"text\"].full,\ninput[type=\"password\"].full,\ninput[type=\"email\"].full,\ninput[type=\"url\"].full,\ninput[type=\"phone\"].full,\ninput[type=\"address\"].full,\ntextarea.full,\nselect.full {\n  width: 100%;\n}\nlabel.checkbox {\n  margin-bottom: 20px;\n}\n/* #Images\n================================================== */\nimg {\n  max-width: 100%;\n  height: auto;\n}\nimg.scale-with-grid {\n  max-width: 100%;\n  height: auto;\n}\n/* #Links\n================================================== */\na,\na:visited {\n  font-size: 14px;\n  color: #4d4d4d;\n  font-weight: bold;\n  text-decoration: none;\n  outline: 0;\n}\na:hover,\na:focus {\n  color: #337fb2;\n  text-decoration: none;\n}\np a,\np a:visited {\n  line-height: inherit;\n}\n/* #Lists\n================================================== */\nul,\nol {\n  margin: 0 0 20px 0;\n  padding: 0px;\n}\nul {\n  list-style: none outside;\n}\nol {\n  list-style: none outside;\n}\nul ul,\nul ol,\nol ol,\nol ul {\n  margin: 0 0 20px 0;\n}\nul ul li,\nul ol li,\nol ol li,\nol ul li {\n  margin-bottom: 5px;\n  line-height: 14px;\n}\nli {\n  margin-bottom: 5px;\n  line-height: 14px;\n}\n/* Lists - Square, Circle, Disc, None */\nul.square,\nul.circle,\nul.disc,\nul.none,\nol.square,\nol.circle,\nol.disc,\nol.none {\n  margin: 0 0 20px 0;\n}\nul.square,\nol.square {\n  list-style: square outside;\n}\nul.circle,\nol.square {\n  list-style: circle outside;\n}\nul.disc,\nol.disc {\n  list-style: disc outside;\n}\nul.none,\nol.none {\n  list-style: none outside;\n}\n/* Lists - Large & Small */\nul li p,\nul.large li,\nol.large li {\n  font-size: 21px;\n  line-height: 21px;\n}\nul li p,\nul.small li,\nol.small li {\n  font-size: 11.2px;\n  line-height: 11.2px;\n}\n/* Lists - Horizontal */\nul.horizontal,\nol.horizontal {\n  list-style: none inside;\n  margin: 0px;\n}\nul.horizontal li,\nol.horizontal li {\n  display: inline-block;\n  margin: 0px;\n}\nul.horizontal li:first-child,\nol.horizontal li:first-child {\n  margin-left: 0px;\n}\nul.horizontal li:last-child,\nol.horizontal li:last-child {\n  margin-right: 0px;\n}\n/**\n * @name Rebar Rectangles\n * @description\n *   - web max 1140\n *   - web 960\n *   - tablet\n *   - mobile\n */\n.rectangles-outer {\n  position: relative;\n  float: left;\n  display: block;\n}\n.rectangles-inner h1,\n.rectangles-inner h2,\n.rectangles-inner h3,\n.rectangles-inner h4 {\n  font-size: 18px;\n  margin-bottom: 10px;\n}\n.rectangles-inner p {\n  font-size: 12px;\n}\n.rectangles-inner p a {\n  font-size: 12px;\n  color: #b3b3b3;\n}\n.rectangles-inner a.bottom {\n  position: absolute;\n  bottom: 10px;\n}\n.rectangles-inner a.bottom.right {\n  right: 10px;\n}\n/* --- LESS mixin to generate multiple sizes --- */\n/* Five Column, 1140 web max  */\n@media only screen and (min-width: 1140px) {\n  .container .rectangles-outer {\n    width: 19.1%;\n    height: 70px;\n    margin-right: 1.1%;\n    margin-bottom: 1.2%;\n  }\n  .container .rectangles-outer:nth-child( 6),\n  .container .rectangles-outer:nth-child( 11),\n  .container .rectangles-outer:nth-child( 16),\n  .container .rectangles-outer:nth-child( 21),\n  .container .rectangles-outer:nth-child( 26),\n  .container .rectangles-outer:nth-child( 31),\n  .container .rectangles-outer:nth-child( 36),\n  .container .rectangles-outer:nth-child( 41),\n  .container .rectangles-outer:nth-child( 46),\n  .container .rectangles-outer:nth-child( 51),\n  .container .rectangles-outer:nth-child( 56),\n  .container .rectangles-outer:nth-child( 61),\n  .container .rectangles-outer:nth-child( 66),\n  .container .rectangles-outer:nth-child( 71),\n  .container .rectangles-outer:nth-child( 76),\n  .container .rectangles-outer:nth-child( 81),\n  .container .rectangles-outer:nth-child( 86),\n  .container .rectangles-outer:nth-child( 91),\n  .container .rectangles-outer:nth-child( 96),\n  .container .rectangles-outer:nth-child( 101),\n  .container .rectangles-outer:nth-child( 106),\n  .container .rectangles-outer:nth-child( 111),\n  .container .rectangles-outer:nth-child( 116),\n  .container .rectangles-outer:nth-child( 121),\n  .container .rectangles-outer:nth-child( 126),\n  .container .rectangles-outer:nth-child( 131),\n  .container .rectangles-outer:nth-child( 136),\n  .container .rectangles-outer:nth-child( 141),\n  .container .rectangles-outer:nth-child( 146),\n  .container .rectangles-outer:nth-child( 151),\n  .container .rectangles-outer:nth-child( 156),\n  .container .rectangles-outer:nth-child( 161),\n  .container .rectangles-outer:nth-child( 166),\n  .container .rectangles-outer:nth-child( 171),\n  .container .rectangles-outer:nth-child( 176),\n  .container .rectangles-outer:nth-child( 181),\n  .container .rectangles-outer:nth-child( 186),\n  .container .rectangles-outer:nth-child( 191),\n  .container .rectangles-outer:nth-child( 196),\n  .container .rectangles-outer:nth-child( 201) {\n    margin-right: 0px;\n  }\n  .container .rectangles-inner {\n    position: absolute;\n    left: 1px;\n    top: 1px;\n    bottom: 1px;\n    right: 1px;\n    padding: 10px;\n    overflow: hidden;\n  }\n}\n/* Four Column, 960 web */\n@media only screen and (min-width: 960px) and (max-width: 1139px) {\n  .container .rectangles-outer {\n    width: 23.1%;\n    height: 70px;\n    margin-right: 2.25%;\n    margin-bottom: 2%;\n  }\n  .container .rectangles-outer:nth-child( 5),\n  .container .rectangles-outer:nth-child( 9),\n  .container .rectangles-outer:nth-child( 13),\n  .container .rectangles-outer:nth-child( 17),\n  .container .rectangles-outer:nth-child( 21),\n  .container .rectangles-outer:nth-child( 25),\n  .container .rectangles-outer:nth-child( 29),\n  .container .rectangles-outer:nth-child( 33),\n  .container .rectangles-outer:nth-child( 37),\n  .container .rectangles-outer:nth-child( 41),\n  .container .rectangles-outer:nth-child( 45),\n  .container .rectangles-outer:nth-child( 49),\n  .container .rectangles-outer:nth-child( 53),\n  .container .rectangles-outer:nth-child( 57),\n  .container .rectangles-outer:nth-child( 61),\n  .container .rectangles-outer:nth-child( 65),\n  .container .rectangles-outer:nth-child( 69),\n  .container .rectangles-outer:nth-child( 73),\n  .container .rectangles-outer:nth-child( 77),\n  .container .rectangles-outer:nth-child( 81),\n  .container .rectangles-outer:nth-child( 85),\n  .container .rectangles-outer:nth-child( 89),\n  .container .rectangles-outer:nth-child( 93),\n  .container .rectangles-outer:nth-child( 97),\n  .container .rectangles-outer:nth-child( 101),\n  .container .rectangles-outer:nth-child( 105),\n  .container .rectangles-outer:nth-child( 109),\n  .container .rectangles-outer:nth-child( 113),\n  .container .rectangles-outer:nth-child( 117),\n  .container .rectangles-outer:nth-child( 121),\n  .container .rectangles-outer:nth-child( 125),\n  .container .rectangles-outer:nth-child( 129),\n  .container .rectangles-outer:nth-child( 133),\n  .container .rectangles-outer:nth-child( 137),\n  .container .rectangles-outer:nth-child( 141),\n  .container .rectangles-outer:nth-child( 145),\n  .container .rectangles-outer:nth-child( 149),\n  .container .rectangles-outer:nth-child( 153),\n  .container .rectangles-outer:nth-child( 157),\n  .container .rectangles-outer:nth-child( 161) {\n    margin-right: 0px;\n  }\n  .container .rectangles-inner {\n    position: absolute;\n    left: 1px;\n    top: 1px;\n    bottom: 1px;\n    right: 1px;\n    padding: 10px;\n    overflow: hidden;\n  }\n}\n/* Three Column, tablet landscape  */\n@media only screen and (min-width: 768px) and (max-width: 959px) {\n  .container .rectangles-outer {\n    width: 31.25%;\n    height: 70px;\n    margin-right: 2.75%;\n    margin-bottom: 2.25%;\n  }\n  .container .rectangles-outer:nth-child( 4),\n  .container .rectangles-outer:nth-child( 7),\n  .container .rectangles-outer:nth-child( 10),\n  .container .rectangles-outer:nth-child( 13),\n  .container .rectangles-outer:nth-child( 16),\n  .container .rectangles-outer:nth-child( 19),\n  .container .rectangles-outer:nth-child( 22),\n  .container .rectangles-outer:nth-child( 25),\n  .container .rectangles-outer:nth-child( 28),\n  .container .rectangles-outer:nth-child( 31),\n  .container .rectangles-outer:nth-child( 34),\n  .container .rectangles-outer:nth-child( 37),\n  .container .rectangles-outer:nth-child( 40),\n  .container .rectangles-outer:nth-child( 43),\n  .container .rectangles-outer:nth-child( 46),\n  .container .rectangles-outer:nth-child( 49),\n  .container .rectangles-outer:nth-child( 52),\n  .container .rectangles-outer:nth-child( 55),\n  .container .rectangles-outer:nth-child( 58),\n  .container .rectangles-outer:nth-child( 61),\n  .container .rectangles-outer:nth-child( 64),\n  .container .rectangles-outer:nth-child( 67),\n  .container .rectangles-outer:nth-child( 70),\n  .container .rectangles-outer:nth-child( 73),\n  .container .rectangles-outer:nth-child( 76),\n  .container .rectangles-outer:nth-child( 79),\n  .container .rectangles-outer:nth-child( 82),\n  .container .rectangles-outer:nth-child( 85),\n  .container .rectangles-outer:nth-child( 88),\n  .container .rectangles-outer:nth-child( 91),\n  .container .rectangles-outer:nth-child( 94),\n  .container .rectangles-outer:nth-child( 97),\n  .container .rectangles-outer:nth-child( 100),\n  .container .rectangles-outer:nth-child( 103),\n  .container .rectangles-outer:nth-child( 106),\n  .container .rectangles-outer:nth-child( 109),\n  .container .rectangles-outer:nth-child( 112),\n  .container .rectangles-outer:nth-child( 115),\n  .container .rectangles-outer:nth-child( 118),\n  .container .rectangles-outer:nth-child( 121) {\n    margin-right: 0px;\n  }\n  .container .rectangles-inner {\n    position: absolute;\n    left: 1px;\n    top: 1px;\n    bottom: 1px;\n    right: 1px;\n    padding: 10px;\n    overflow: hidden;\n  }\n}\n/* Two Column, mobile portrait & landscape */\n@media only screen and (max-width: 767px) {\n  .container .rectangles-outer {\n    width: 48.5%;\n    height: 70px;\n    margin-right: 3%;\n    margin-bottom: 3%;\n  }\n  .container .rectangles-outer:nth-child( 3),\n  .container .rectangles-outer:nth-child( 5),\n  .container .rectangles-outer:nth-child( 7),\n  .container .rectangles-outer:nth-child( 9),\n  .container .rectangles-outer:nth-child( 11),\n  .container .rectangles-outer:nth-child( 13),\n  .container .rectangles-outer:nth-child( 15),\n  .container .rectangles-outer:nth-child( 17),\n  .container .rectangles-outer:nth-child( 19),\n  .container .rectangles-outer:nth-child( 21),\n  .container .rectangles-outer:nth-child( 23),\n  .container .rectangles-outer:nth-child( 25),\n  .container .rectangles-outer:nth-child( 27),\n  .container .rectangles-outer:nth-child( 29),\n  .container .rectangles-outer:nth-child( 31),\n  .container .rectangles-outer:nth-child( 33),\n  .container .rectangles-outer:nth-child( 35),\n  .container .rectangles-outer:nth-child( 37),\n  .container .rectangles-outer:nth-child( 39),\n  .container .rectangles-outer:nth-child( 41),\n  .container .rectangles-outer:nth-child( 43),\n  .container .rectangles-outer:nth-child( 45),\n  .container .rectangles-outer:nth-child( 47),\n  .container .rectangles-outer:nth-child( 49),\n  .container .rectangles-outer:nth-child( 51),\n  .container .rectangles-outer:nth-child( 53),\n  .container .rectangles-outer:nth-child( 55),\n  .container .rectangles-outer:nth-child( 57),\n  .container .rectangles-outer:nth-child( 59),\n  .container .rectangles-outer:nth-child( 61),\n  .container .rectangles-outer:nth-child( 63),\n  .container .rectangles-outer:nth-child( 65),\n  .container .rectangles-outer:nth-child( 67),\n  .container .rectangles-outer:nth-child( 69),\n  .container .rectangles-outer:nth-child( 71),\n  .container .rectangles-outer:nth-child( 73),\n  .container .rectangles-outer:nth-child( 75),\n  .container .rectangles-outer:nth-child( 77),\n  .container .rectangles-outer:nth-child( 79),\n  .container .rectangles-outer:nth-child( 81) {\n    margin-right: 0px;\n  }\n  .container .rectangles-inner {\n    position: absolute;\n    left: 1px;\n    top: 1px;\n    bottom: 1px;\n    right: 1px;\n    padding: 10px;\n    overflow: hidden;\n  }\n}\n/* Two Column, mobile portrait */\n@media only screen and (max-width: 480px) {\n  .container .rectangles-outer {\n    width: 100%;\n    height: 70px;\n    margin-right: 4%;\n    margin-bottom: 2%;\n  }\n  .container .rectangles-outer:nth-child( 2),\n  .container .rectangles-outer:nth-child( 3),\n  .container .rectangles-outer:nth-child( 4),\n  .container .rectangles-outer:nth-child( 5),\n  .container .rectangles-outer:nth-child( 6),\n  .container .rectangles-outer:nth-child( 7),\n  .container .rectangles-outer:nth-child( 8),\n  .container .rectangles-outer:nth-child( 9),\n  .container .rectangles-outer:nth-child( 10),\n  .container .rectangles-outer:nth-child( 11),\n  .container .rectangles-outer:nth-child( 12),\n  .container .rectangles-outer:nth-child( 13),\n  .container .rectangles-outer:nth-child( 14),\n  .container .rectangles-outer:nth-child( 15),\n  .container .rectangles-outer:nth-child( 16),\n  .container .rectangles-outer:nth-child( 17),\n  .container .rectangles-outer:nth-child( 18),\n  .container .rectangles-outer:nth-child( 19),\n  .container .rectangles-outer:nth-child( 20),\n  .container .rectangles-outer:nth-child( 21),\n  .container .rectangles-outer:nth-child( 22),\n  .container .rectangles-outer:nth-child( 23),\n  .container .rectangles-outer:nth-child( 24),\n  .container .rectangles-outer:nth-child( 25),\n  .container .rectangles-outer:nth-child( 26),\n  .container .rectangles-outer:nth-child( 27),\n  .container .rectangles-outer:nth-child( 28),\n  .container .rectangles-outer:nth-child( 29),\n  .container .rectangles-outer:nth-child( 30),\n  .container .rectangles-outer:nth-child( 31),\n  .container .rectangles-outer:nth-child( 32),\n  .container .rectangles-outer:nth-child( 33),\n  .container .rectangles-outer:nth-child( 34),\n  .container .rectangles-outer:nth-child( 35),\n  .container .rectangles-outer:nth-child( 36),\n  .container .rectangles-outer:nth-child( 37),\n  .container .rectangles-outer:nth-child( 38),\n  .container .rectangles-outer:nth-child( 39),\n  .container .rectangles-outer:nth-child( 40),\n  .container .rectangles-outer:nth-child( 41) {\n    margin-right: 0px;\n  }\n  .container .rectangles-inner {\n    position: absolute;\n    left: 1px;\n    top: 1px;\n    bottom: 1px;\n    right: 1px;\n    padding: 10px;\n    overflow: hidden;\n  }\n}\n/* Separators\n================================================== */\nhr {\n  border: solid #b3b3b3;\n  border-width: 1px 0 0;\n  clear: both;\n  margin: 35px 0;\n  height: 0;\n}\nhr.medium {\n  border: none;\n  height: 2px;\n  margin: 35px 0;\n  background: #b3b3b3;\n}\nhr.large {\n  border: none;\n  height: 4px;\n  margin: 35px 0;\n  background: #b3b3b3;\n}\nhr.giant {\n  border: none;\n  height: 8px;\n  margin: 35px 0;\n  background: #b3b3b3;\n}\n/* #Tables\n================================================== */\ntable {\n  background-color: #ffffff;\n  border-right: 1px solid #cccccc;\n  border-left: 1px solid #cccccc;\n  border-bottom: 1px solid #cccccc;\n}\ntable th,\ntable td,\ntable tfoot {\n  border-top: 1px solid #cccccc;\n}\ntable td {\n  padding: 10px;\n  font-size: 16px;\n  line-height: 16px;\n}\ntable th {\n  padding: 5px 10px;\n  background-color: #ffffff;\n  text-align: left;\n  font-style: italic;\n  font-size: 14px;\n}\ntable tr.foot td {\n  padding: 5px 10px;\n  background-color: #ffffff;\n  font-style: italic;\n  font-size: 14px;\n}\n/* Table - Full */\ntable.full {\n  width: 100%;\n}\ntable.full td,\ntable.full th {\n  padding: 10px;\n}\n/* Table - Rounded */\ntable.rounded {\n  border-radius: 5px;\n}\ntable.rounded td:first-child,\ntable.rounded th:first-child {\n  border-left: none;\n}\ntable.rounded th:first-child {\n  border-radius: 5px 0 0 0;\n}\ntable.rounded th:last-child {\n  border-radius: 0 5px 0 0;\n}\ntable.rounded th:only-child {\n  border-radius: 5px 5px 0 0;\n}\ntable.rounded tr:last-child td:first-child {\n  border-radius: 0 0 0 5px;\n}\ntable.rounded tr:last-child td:last-child {\n  border-radius: 0 0 5px 0;\n}\ntable.rounded tr:last-child td:only-child {\n  border-radius: 0 0 5px 5px;\n}\n/* Table - Zebra Striping */\ntable.zebra {\n  border-right: 1px solid #cccccc;\n  border-left: 1px solid #cccccc;\n}\ntable.zebra td,\ntable.zebra th {\n  padding: 10px;\n}\ntable.zebra tr:nth-child(odd) {\n  background-color: #f9f9f9;\n}\ntable.zebra tbody tr:nth-child(even) {\n  background-color: #fefefe;\n  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8) inset;\n}\ntable.zebra th {\n  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);\n  background-color: #eee;\n  background-image: linear-gradient(top, #f5f5f5, #eeeeee);\n}\ntable.zebra th:first-child {\n  border-radius: 6px 0 0 0;\n}\ntable.zebra th:last-child {\n  border-radius: 0 6px 0 0;\n}\ntable.zebra th:only-child {\n  border-radius: 6px 6px 0 0;\n}\ntable.zebra tfoot td {\n  border-bottom: 0;\n  border-top: 1px solid #fff;\n  background-color: #f1f1f1;\n}\ntable.zebra tfoot td:first-child {\n  border-radius: 0 0 0 6px;\n}\ntable.zebra tfoot td:last-child {\n  border-radius: 0 0 6px 0;\n}\ntable.zebra tfoot td:only-child {\n  border-radius: 0 0 6px 6px;\n}\n/* Table - Rouded & Zebra */\ntable tr:hover,\ntable.rounded tr:hover,\ntable.zebra tr:hover {\n  background: #f5f6db;\n  transition: all 0.1s ease-in-out;\n}\n/* #Typography\n================================================== */\n/* Headings */\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n  color: #333333;\n  font-family: Mailpile-700, \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-weight: normal;\n  letter-spacing: 0px;\n}\nh1 a,\nh2 a,\nh3 a,\nh4 a,\nh5 a,\nh6 a {\n  font-weight: inherit;\n}\nh1 {\n  font-size: 36px;\n  line-height: 36px;\n  margin-bottom: 0px;\n}\nh2 {\n  font-size: 30px;\n  line-height: 30px;\n  margin-bottom: 0px;\n}\nh3 {\n  font-size: 24px;\n  line-height: 24px;\n  margin-bottom: 0px;\n}\nh4 {\n  font-size: 21px;\n  line-height: 21px;\n  margin-bottom: 0px;\n}\nh5 {\n  font-size: 18px;\n  line-height: 18px;\n  margin-bottom: 0px;\n}\nh6 {\n  font-size: 14px;\n  line-height: 14px;\n  margin-bottom: 0px;\n}\n.subheader {\n  color: #777;\n}\n/* Non Headings */\np {\n  margin: 0 0 20px 0;\n}\np img {\n  margin: 0;\n}\np.lead {\n  font-size: 21px;\n  line-height: 27px;\n  color: #777;\n}\ni,\nem {\n  font-style: italic;\n}\nb,\nstrong {\n  font-family: \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-weight: bold;\n  color: inherit;\n}\nsmall {\n  font-size: 80%;\n}\n/*\tBlockquotes  */\nblockquote,\nblockquote p {\n  font-size: 18px;\n  line-height: 24px;\n  color: #666666;\n  font-style: italic;\n  margin: 0 0 10px 0;\n}\nblockquote {\n  margin: 0 0 20px;\n  padding: 9px 20px 0 19px;\n  border-left: 4px solid #c9c9c9;\n}\nblockquote cite {\n  display: block;\n  font-size: 12px;\n  color: #555;\n}\nblockquote cite:before {\n  content: \"\\2014 \\0020\";\n}\nblockquote cite a,\nblockquote cite a:visited,\nblockquote cite a:visited {\n  color: #555;\n}\n/* Pre & Code */\npre {\n  word-wrap: normal;\n  display: block;\n  padding: 4px 8px;\n}\npre,\n.pre {\n  margin-bottom: 20px;\n  background: #e9e9e9;\n  color: #333333;\n  border: 1px solid #b3b3b3;\n  border-radius: 3px;\n  font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;\n  font-size: 14px;\n  font-weight: normal;\n}\ncode {\n  display: inline;\n  border: 1px solid #b3b3b3;\n  border-radius: 3px;\n  background: #e9e9e9;\n  padding: 2px 5px;\n  font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;\n  font-size: 70%;\n  font-weight: normal;\n}\npre code {\n  border: 0px;\n  font-size: 100%;\n}\n/* Special Classes */\n.text-detail {\n  color: #b3b3b3;\n}\na.link-detail,\na.link-detail:visited {\n  color: #b3b3b3;\n  font-weight: normal !important;\n}\na.link-detail:hover {\n  color: #4d4d4d;\n}\na.disabled {\n  pointer-events: none;\n  cursor: default;\n}\n/* Validation */\n.validation-message {\n  font-style: italic;\n}\n/* Success */\nspan.validation-success,\nlabel.validation-success {\n  color: #4b9441;\n}\ninput.validation-success,\ntextarea.validation-success,\nselect.validation-success {\n  border: 1px solid #4b9441 !important;\n}\n/* Warning */\nspan.validation-warning,\nlabel.validation-warning {\n  color: #fbb03b;\n}\ninput.validation-warning,\ntextarea.validation-warning,\nselect.validation-warning {\n  border: 1px solid #fbb03b !important;\n}\n/* Error */\nspan.validation-error,\nlabel.validation-error {\n  color: #be1c21;\n}\ninput.validation-error,\ntextarea.validation-error,\nselect.validation-error {\n  border: 1px solid #be1c21 !important;\n}\n/* Libraries */\n/*\nVersion: 3.5.4 Timestamp: Sun Aug 30 13:30:32 EDT 2015\n*/\n.select2-container {\n  margin: 0;\n  position: relative;\n  display: inline-block;\n  vertical-align: middle;\n}\n.select2-container,\n.select2-drop,\n.select2-search,\n.select2-search input {\n  /*\n    Force border-box so that % widths fit the parent\n    container without overlap because of margin/padding.\n    More Info : http://www.quirksmode.org/css/box.html\n  */\n  -webkit-box-sizing: border-box;\n  /* webkit */\n  -moz-box-sizing: border-box;\n  /* firefox */\n  box-sizing: border-box;\n  /* css3 */\n}\n.select2-container .select2-choice {\n  display: block;\n  height: 26px;\n  padding: 0 0 0 8px;\n  overflow: hidden;\n  position: relative;\n  border: 1px solid #aaa;\n  white-space: nowrap;\n  line-height: 26px;\n  color: #444;\n  text-decoration: none;\n  border-radius: 4px;\n  background-clip: padding-box;\n  -webkit-touch-callout: none;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n  background-color: #fff;\n  background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.5, #ffffff));\n  background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, #ffffff 50%);\n  background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, #ffffff 50%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0);\n  background-image: linear-gradient(to top, #eeeeee 0%, #ffffff 50%);\n}\nhtml[dir=\"rtl\"] .select2-container .select2-choice {\n  padding: 0 8px 0 0;\n}\n.select2-container.select2-drop-above .select2-choice {\n  border-bottom-color: #aaa;\n  border-radius: 0 0 4px 4px;\n  background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.9, #ffffff));\n  background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, #ffffff 90%);\n  background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, #ffffff 90%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0);\n  background-image: linear-gradient(to bottom, #eeeeee 0%, #ffffff 90%);\n}\n.select2-container.select2-allowclear .select2-choice .select2-chosen {\n  margin-right: 42px;\n}\n.select2-container .select2-choice > .select2-chosen {\n  margin-right: 26px;\n  display: block;\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  float: none;\n  width: auto;\n}\nhtml[dir=\"rtl\"] .select2-container .select2-choice > .select2-chosen {\n  margin-left: 26px;\n  margin-right: 0;\n}\n.select2-container .select2-choice abbr {\n  display: none;\n  width: 12px;\n  height: 12px;\n  position: absolute;\n  right: 24px;\n  top: 8px;\n  font-size: 1px;\n  text-decoration: none;\n  border: 0;\n  background: url('../css/select2.png') right top no-repeat;\n  cursor: pointer;\n  outline: 0;\n}\n.select2-container.select2-allowclear .select2-choice abbr {\n  display: inline-block;\n}\n.select2-container .select2-choice abbr:hover {\n  background-position: right -11px;\n  cursor: pointer;\n}\n.select2-drop-mask {\n  border: 0;\n  margin: 0;\n  padding: 0;\n  position: fixed;\n  left: 0;\n  top: 0;\n  min-height: 100%;\n  min-width: 100%;\n  height: auto;\n  width: auto;\n  opacity: 0;\n  z-index: 9998;\n  /* styles required for IE to work */\n  background-color: #fff;\n  filter: alpha(opacity=0);\n}\n.select2-drop {\n  width: 100%;\n  margin-top: -1px;\n  position: absolute;\n  z-index: 9999;\n  top: 100%;\n  background: #fff;\n  color: #000;\n  border: 1px solid #aaa;\n  border-top: 0;\n  border-radius: 0 0 4px 4px;\n  -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15);\n  box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15);\n}\n.select2-drop.select2-drop-above {\n  margin-top: 1px;\n  border-top: 1px solid #aaa;\n  border-bottom: 0;\n  border-radius: 4px 4px 0 0;\n  -webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, 0.15);\n  box-shadow: 0 -4px 5px rgba(0, 0, 0, 0.15);\n}\n.select2-drop-active {\n  border: 1px solid #5897fb;\n  border-top: none;\n}\n.select2-drop.select2-drop-above.select2-drop-active {\n  border-top: 1px solid #5897fb;\n}\n.select2-drop-auto-width {\n  border-top: 1px solid #aaa;\n  width: auto;\n}\n.select2-container .select2-choice .select2-arrow {\n  display: inline-block;\n  width: 18px;\n  height: 100%;\n  position: absolute;\n  right: 0;\n  top: 0;\n  border-left: 1px solid #aaa;\n  border-radius: 0 4px 4px 0;\n  background-clip: padding-box;\n  background: #ccc;\n  background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #cccccc), color-stop(0.6, #eeeeee));\n  background-image: -webkit-linear-gradient(center bottom, #cccccc 0%, #eeeeee 60%);\n  background-image: -moz-linear-gradient(center bottom, #cccccc 0%, #eeeeee 60%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#cccccc', GradientType=0);\n  background-image: linear-gradient(to top, #cccccc 0%, #eeeeee 60%);\n}\nhtml[dir=\"rtl\"] .select2-container .select2-choice .select2-arrow {\n  left: 0;\n  right: auto;\n  border-left: none;\n  border-right: 1px solid #aaa;\n  border-radius: 4px 0 0 4px;\n}\n.select2-container .select2-choice .select2-arrow b {\n  display: block;\n  width: 100%;\n  height: 100%;\n  background: url('../css/select2.png') no-repeat 0 1px;\n}\nhtml[dir=\"rtl\"] .select2-container .select2-choice .select2-arrow b {\n  background-position: 2px 1px;\n}\n.select2-search {\n  display: inline-block;\n  width: 100%;\n  min-height: 26px;\n  margin: 0;\n  padding: 4px 4px 0 4px;\n  position: relative;\n  z-index: 10000;\n  white-space: nowrap;\n}\n.select2-search input {\n  width: 100%;\n  height: auto !important;\n  min-height: 26px;\n  padding: 4px 20px 4px 5px;\n  margin: 0;\n  outline: 0;\n  font-family: sans-serif;\n  font-size: 1em;\n  border: 1px solid #aaa;\n  border-radius: 0;\n  -webkit-box-shadow: none;\n  box-shadow: none;\n  background: #ffffff url('../css/select2.png') no-repeat 100% -22px;\n  background: url('../css/select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #ffffff), color-stop(0.99, #eeeeee));\n  background: url('../css/select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, #ffffff 85%, #eeeeee 99%);\n  background: url('../css/select2.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, #ffffff 85%, #eeeeee 99%);\n  background: url('../css/select2.png') no-repeat 100% -22px, linear-gradient(to bottom, #ffffff 85%, #eeeeee 99%) 0 0;\n}\nhtml[dir=\"rtl\"] .select2-search input {\n  padding: 4px 5px 4px 20px;\n  background: #ffffff url('../css/select2.png') no-repeat -37px -22px;\n  background: url('../css/select2.png') no-repeat -37px -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #ffffff), color-stop(0.99, #eeeeee));\n  background: url('../css/select2.png') no-repeat -37px -22px, -webkit-linear-gradient(center bottom, #ffffff 85%, #eeeeee 99%);\n  background: url('../css/select2.png') no-repeat -37px -22px, -moz-linear-gradient(center bottom, #ffffff 85%, #eeeeee 99%);\n  background: url('../css/select2.png') no-repeat -37px -22px, linear-gradient(to bottom, #ffffff 85%, #eeeeee 99%) 0 0;\n}\n.select2-search input.select2-active {\n  background: #ffffff url('../css/select2-spinner.gif') no-repeat 100%;\n  background: url('../css/select2-spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #ffffff), color-stop(0.99, #eeeeee));\n  background: url('../css/select2-spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, #ffffff 85%, #eeeeee 99%);\n  background: url('../css/select2-spinner.gif') no-repeat 100%, -moz-linear-gradient(center bottom, #ffffff 85%, #eeeeee 99%);\n  background: url('../css/select2-spinner.gif') no-repeat 100%, linear-gradient(to bottom, #ffffff 85%, #eeeeee 99%) 0 0;\n}\n.select2-container-active .select2-choice,\n.select2-container-active .select2-choices {\n  border: 1px solid #5897fb;\n  outline: none;\n  -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);\n  box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);\n}\n.select2-dropdown-open .select2-choice {\n  border-bottom-color: transparent;\n  -webkit-box-shadow: 0 1px 0 #fff inset;\n  box-shadow: 0 1px 0 #fff inset;\n  border-bottom-left-radius: 0;\n  border-bottom-right-radius: 0;\n  background-color: #eee;\n  background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ffffff), color-stop(0.5, #eeeeee));\n  background-image: -webkit-linear-gradient(center bottom, #ffffff 0%, #eeeeee 50%);\n  background-image: -moz-linear-gradient(center bottom, #ffffff 0%, #eeeeee 50%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0);\n  background-image: linear-gradient(to top, #ffffff 0%, #eeeeee 50%);\n}\n.select2-dropdown-open.select2-drop-above .select2-choice,\n.select2-dropdown-open.select2-drop-above .select2-choices {\n  border: 1px solid #5897fb;\n  border-top-color: transparent;\n  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #ffffff), color-stop(0.5, #eeeeee));\n  background-image: -webkit-linear-gradient(center top, #ffffff 0%, #eeeeee 50%);\n  background-image: -moz-linear-gradient(center top, #ffffff 0%, #eeeeee 50%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0);\n  background-image: linear-gradient(to bottom, #ffffff 0%, #eeeeee 50%);\n}\n.select2-dropdown-open .select2-choice .select2-arrow {\n  background: transparent;\n  border-left: none;\n  filter: none;\n}\nhtml[dir=\"rtl\"] .select2-dropdown-open .select2-choice .select2-arrow {\n  border-right: none;\n}\n.select2-dropdown-open .select2-choice .select2-arrow b {\n  background-position: -18px 1px;\n}\nhtml[dir=\"rtl\"] .select2-dropdown-open .select2-choice .select2-arrow b {\n  background-position: -16px 1px;\n}\n.select2-hidden-accessible {\n  border: 0;\n  clip: rect(0 0 0 0);\n  height: 1px;\n  margin: -1px;\n  overflow: hidden;\n  padding: 0;\n  position: absolute;\n  width: 1px;\n}\n/* results */\n.select2-results {\n  max-height: 200px;\n  padding: 0 0 0 4px;\n  margin: 4px 4px 4px 0;\n  position: relative;\n  overflow-x: hidden;\n  overflow-y: auto;\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nhtml[dir=\"rtl\"] .select2-results {\n  padding: 0 4px 0 0;\n  margin: 4px 0 4px 4px;\n}\n.select2-results ul.select2-result-sub {\n  margin: 0;\n  padding-left: 0;\n}\n.select2-results li {\n  list-style: none;\n  display: list-item;\n  background-image: none;\n}\n.select2-results li.select2-result-with-children > .select2-result-label {\n  font-weight: bold;\n}\n.select2-results .select2-result-label {\n  padding: 3px 7px 4px;\n  margin: 0;\n  cursor: pointer;\n  min-height: 1em;\n  -webkit-touch-callout: none;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n.select2-results-dept-1 .select2-result-label {\n  padding-left: 20px;\n}\n.select2-results-dept-2 .select2-result-label {\n  padding-left: 40px;\n}\n.select2-results-dept-3 .select2-result-label {\n  padding-left: 60px;\n}\n.select2-results-dept-4 .select2-result-label {\n  padding-left: 80px;\n}\n.select2-results-dept-5 .select2-result-label {\n  padding-left: 100px;\n}\n.select2-results-dept-6 .select2-result-label {\n  padding-left: 110px;\n}\n.select2-results-dept-7 .select2-result-label {\n  padding-left: 120px;\n}\n.select2-results .select2-highlighted {\n  background: #3875d7;\n  color: #fff;\n}\n.select2-results li em {\n  background: #feffde;\n  font-style: normal;\n}\n.select2-results .select2-highlighted em {\n  background: transparent;\n}\n.select2-results .select2-highlighted ul {\n  background: #fff;\n  color: #000;\n}\n.select2-results .select2-no-results,\n.select2-results .select2-searching,\n.select2-results .select2-ajax-error,\n.select2-results .select2-selection-limit {\n  background: #f4f4f4;\n  display: list-item;\n  padding-left: 5px;\n}\n/*\ndisabled look for disabled choices in the results dropdown\n*/\n.select2-results .select2-disabled.select2-highlighted {\n  color: #666;\n  background: #f4f4f4;\n  display: list-item;\n  cursor: default;\n}\n.select2-results .select2-disabled {\n  background: #f4f4f4;\n  display: list-item;\n  cursor: default;\n}\n.select2-results .select2-selected {\n  display: none;\n}\n.select2-more-results.select2-active {\n  background: #f4f4f4 url('../css/select2-spinner.gif') no-repeat 100%;\n}\n.select2-results .select2-ajax-error {\n  background: rgba(255, 50, 50, 0.2);\n}\n.select2-more-results {\n  background: #f4f4f4;\n  display: list-item;\n}\n/* disabled styles */\n.select2-container.select2-container-disabled .select2-choice {\n  background-color: #f4f4f4;\n  background-image: none;\n  border: 1px solid #ddd;\n  cursor: default;\n}\n.select2-container.select2-container-disabled .select2-choice .select2-arrow {\n  background-color: #f4f4f4;\n  background-image: none;\n  border-left: 0;\n}\n.select2-container.select2-container-disabled .select2-choice abbr {\n  display: none;\n}\n/* multiselect */\n.select2-container-multi .select2-choices {\n  height: auto !important;\n  height: 1%;\n  margin: 0;\n  padding: 0 5px 0 0;\n  position: relative;\n  border: 1px solid #aaa;\n  cursor: text;\n  overflow: hidden;\n  background-color: #fff;\n  background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff));\n  background-image: -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%);\n  background-image: -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%);\n  background-image: linear-gradient(to bottom, #eeeeee 1%, #ffffff 15%);\n}\nhtml[dir=\"rtl\"] .select2-container-multi .select2-choices {\n  padding: 0 0 0 5px;\n}\n.select2-locked {\n  padding: 3px 5px 3px 5px !important;\n}\n.select2-container-multi .select2-choices {\n  min-height: 26px;\n}\n.select2-container-multi.select2-container-active .select2-choices {\n  border: 1px solid #5897fb;\n  outline: none;\n  -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);\n  box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);\n}\n.select2-container-multi .select2-choices li {\n  float: left;\n  list-style: none;\n}\nhtml[dir=\"rtl\"] .select2-container-multi .select2-choices li {\n  float: right;\n}\n.select2-container-multi .select2-choices .select2-search-field {\n  margin: 0;\n  padding: 0;\n  white-space: nowrap;\n}\n.select2-container-multi .select2-choices .select2-search-field input {\n  padding: 5px;\n  margin: 1px 0;\n  font-family: sans-serif;\n  font-size: 100%;\n  color: #666;\n  outline: 0;\n  border: 0;\n  -webkit-box-shadow: none;\n  box-shadow: none;\n  background: transparent !important;\n}\n.select2-container-multi .select2-choices .select2-search-field input.select2-active {\n  background: #ffffff url('../css/select2-spinner.gif') no-repeat 100% !important;\n}\n.select2-default {\n  color: #999 !important;\n}\n.select2-container-multi .select2-choices .select2-search-choice {\n  padding: 3px 5px 3px 18px;\n  margin: 3px 0 3px 5px;\n  position: relative;\n  line-height: 13px;\n  color: #333;\n  cursor: default;\n  border: 1px solid #aaaaaa;\n  border-radius: 3px;\n  -webkit-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0, 0, 0, 0.05);\n  box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0, 0, 0, 0.05);\n  background-clip: padding-box;\n  -webkit-touch-callout: none;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n  background-color: #e4e4e4;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#f4f4f4', GradientType=0);\n  background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));\n  background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);\n  background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);\n  background-image: linear-gradient(to bottom, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);\n}\nhtml[dir=\"rtl\"] .select2-container-multi .select2-choices .select2-search-choice {\n  margin: 3px 5px 3px 0;\n  padding: 3px 18px 3px 5px;\n}\n.select2-container-multi .select2-choices .select2-search-choice .select2-chosen {\n  cursor: default;\n}\n.select2-container-multi .select2-choices .select2-search-choice-focus {\n  background: #d4d4d4;\n}\n.select2-search-choice-close {\n  display: block;\n  width: 12px;\n  height: 13px;\n  position: absolute;\n  right: 3px;\n  top: 4px;\n  font-size: 1px;\n  outline: none;\n  background: url('../css/select2.png') right top no-repeat;\n}\nhtml[dir=\"rtl\"] .select2-search-choice-close {\n  right: auto;\n  left: 3px;\n}\n.select2-container-multi .select2-search-choice-close {\n  left: 3px;\n}\nhtml[dir=\"rtl\"] .select2-container-multi .select2-search-choice-close {\n  left: auto;\n  right: 2px;\n}\n.select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover {\n  background-position: right -11px;\n}\n.select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close {\n  background-position: right -11px;\n}\n/* disabled styles */\n.select2-container-multi.select2-container-disabled .select2-choices {\n  background-color: #f4f4f4;\n  background-image: none;\n  border: 1px solid #ddd;\n  cursor: default;\n}\n.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice {\n  padding: 3px 5px 3px 5px;\n  border: 1px solid #ddd;\n  background-image: none;\n  background-color: #f4f4f4;\n}\n.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close {\n  display: none;\n  background: none;\n}\n/* end multiselect */\n.select2-result-selectable .select2-match,\n.select2-result-unselectable .select2-match {\n  text-decoration: underline;\n}\n.select2-offscreen,\n.select2-offscreen:focus {\n  clip: rect(0 0 0 0) !important;\n  width: 1px !important;\n  height: 1px !important;\n  border: 0 !important;\n  margin: 0 !important;\n  padding: 0 !important;\n  overflow: hidden !important;\n  position: absolute !important;\n  outline: 0 !important;\n  left: 0px !important;\n  top: 0px !important;\n}\n.select2-display-none {\n  display: none;\n}\n.select2-measure-scrollbar {\n  position: absolute;\n  top: -10000px;\n  left: -10000px;\n  width: 100px;\n  height: 100px;\n  overflow: scroll;\n}\n/* Retina-ize icons */\n@media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 2dppx) {\n  .select2-search input,\n  .select2-search-choice-close,\n  .select2-container .select2-choice abbr,\n  .select2-container .select2-choice .select2-arrow b {\n    background-image: url('../css/select2x2.png') !important;\n    background-repeat: no-repeat !important;\n    background-size: 60px 40px !important;\n  }\n  .select2-search input {\n    background-position: 100% -21px !important;\n  }\n}\n/*\n * qTip2 - Pretty powerful tooltips - v2.1.1\n * http://qtip2.com\n *\n * Copyright (c) 2013 Craig Michael Thompson\n * Released under the MIT, GPL licenses\n * http://jquery.org/license\n *\n * Date: Wed Sep 11 2013 11:17 GMT+0100+0100\n * Plugins: None\n * Styles: None\n */\n.qtip {\n  position: absolute;\n  left: -28000px;\n  top: -28000px;\n  display: none;\n  max-width: 280px;\n  min-width: 50px;\n  font-size: 10.5px;\n  line-height: 12px;\n  direction: ltr;\n  box-shadow: none;\n  padding: 0;\n}\n.qtip-content {\n  position: relative;\n  padding: 5px 9px;\n  overflow: hidden;\n  text-align: left;\n  word-wrap: break-word;\n}\n.qtip-titlebar {\n  position: relative;\n  padding: 5px 35px 5px 10px;\n  overflow: hidden;\n  border-width: 0 0 1px;\n  font-weight: bold;\n}\n.qtip-titlebar + .qtip-content {\n  border-top-width: 0 !important;\n}\n/* Default close button class */\n.qtip-close {\n  position: absolute;\n  right: -9px;\n  top: -9px;\n  cursor: pointer;\n  outline: medium none;\n  border-width: 1px;\n  border-style: solid;\n  border-color: transparent;\n}\n.qtip-titlebar .qtip-close {\n  right: 4px;\n  top: 50%;\n  margin-top: -9px;\n}\n* html .qtip-titlebar .qtip-close {\n  top: 16px;\n}\n/* IE fix */\n.qtip-titlebar .ui-icon,\n.qtip-icon .ui-icon {\n  display: block;\n  text-indent: -1000em;\n  direction: ltr;\n}\n.qtip-icon,\n.qtip-icon .ui-icon {\n  -moz-border-radius: 3px;\n  -webkit-border-radius: 3px;\n  border-radius: 3px;\n  text-decoration: none;\n}\n.qtip-icon .ui-icon {\n  width: 18px;\n  height: 14px;\n  line-height: 14px;\n  text-align: center;\n  text-indent: 0;\n  font: normal bold 10px/13px Tahoma, sans-serif;\n  color: inherit;\n  background: transparent none no-repeat -100em -100em;\n}\n/* Applied to 'focused' tooltips e.g. most recently displayed/interacted with */\n/* Applied on hover of tooltips i.e. added/removed on mouseenter/mouseleave respectively */\n/* Default tooltip style */\n.qtip-default {\n  border-width: 1px;\n  border-style: solid;\n  border-color: #F1D031;\n  background-color: #FFFFA3;\n  color: #555;\n}\n.qtip-default .qtip-titlebar {\n  background-color: #FFEF93;\n}\n.qtip-default .qtip-icon {\n  border-color: #CCC;\n  background: #F1F1F1;\n  color: #777;\n}\n.qtip-default .qtip-titlebar .qtip-close {\n  border-color: #AAA;\n  color: #111;\n}\n.caret {\n  display: inline-block;\n  width: 0;\n  height: 0;\n  margin-left: 2px;\n  vertical-align: middle;\n  border-top: 4px solid;\n  border-right: 4px solid transparent;\n  border-left: 4px solid transparent;\n}\n.dropdown {\n  position: relative;\n}\n.dropdown-toggle:focus {\n  outline: 0;\n}\n.dropdown-menu {\n  position: absolute;\n  top: 100%;\n  left: 0;\n  z-index: 1000;\n  display: none;\n  float: left;\n  min-width: 160px;\n  padding: 5px 0;\n  margin: 2px 0 0;\n  list-style: none;\n  font-size: 14px;\n  text-align: left;\n  background-color: #ffffff;\n  border: 1px solid #cccccc;\n  border: 1px solid rgba(0, 0, 0, 0.15);\n  border-radius: 4px;\n  -moz-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n  -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n  background-clip: padding-box;\n}\n.dropdown-menu.pull-right {\n  right: 0;\n  left: auto;\n}\n.dropdown-menu .divider {\n  height: 1px;\n  margin: 9px 0;\n  overflow: hidden;\n  background-color: #cccccc;\n}\n.dropdown-menu > li > a {\n  display: block;\n  padding: 3px 20px;\n  clear: both;\n  font-weight: normal;\n  line-height: 20px;\n  color: #4d4d4d;\n  white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n  text-decoration: none;\n  color: #ffffff;\n  background-color: #4d4d4d;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n  color: #be1c21;\n  text-decoration: none;\n  outline: 0;\n  background-color: #4d4d4d;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n  color: #e9e9e9;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n  text-decoration: none;\n  background-color: transparent;\n  background-image: none;\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  cursor: not-allowed;\n}\n.open > .dropdown-menu {\n  display: block;\n}\n.open > a {\n  outline: 0;\n}\n.dropdown-menu-right {\n  left: auto;\n  right: 0;\n}\n.dropdown-menu-left {\n  left: 0;\n  right: auto;\n}\n.dropdown-header {\n  display: block;\n  padding: 3px 20px;\n  font-size: 12px;\n  line-height: 20px;\n  color: #e9e9e9;\n  white-space: nowrap;\n}\n.dropdown-backdrop {\n  position: fixed;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  top: 0;\n  z-index: 990;\n}\n.pull-right > .dropdown-menu {\n  right: 0;\n  left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n  border-top: 0;\n  border-bottom: 4px solid;\n  content: \"\";\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n  top: auto;\n  bottom: 100%;\n  margin-bottom: 1px;\n}\n@media (min-width: 768px) {\n  .navbar-right .dropdown-menu {\n    left: auto;\n    right: 0;\n  }\n  .navbar-right .dropdown-menu-left {\n    left: 0;\n    right: auto;\n  }\n}\n.modal-open {\n  overflow: hidden;\n}\n.modal {\n  display: none;\n  overflow: hidden;\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 1050;\n  -webkit-overflow-scrolling: touch;\n  outline: 0;\n}\n.modal.fade .modal-dialog {\n  -webkit-transform: translate(0, -25%);\n  -moz-transform: translate(0, -25%);\n  -o-transform: translate(0, -25%);\n  -ms-transform: translate(0, -25%);\n  transform: translate(0, -25%);\n  -webkit-transition: -webkit-transform 0.3s ease-out;\n  -moz-transition: -moz-transform 0.3s ease-out;\n  -o-transition: -o-transform 0.3s ease-out;\n  transition: transform 0.3s ease-out;\n}\n.modal.in .modal-dialog {\n  -webkit-transform: translate(0, 0);\n  -moz-transform: translate(0, 0);\n  -o-transform: translate(0, 0);\n  -ms-transform: translate(0, 0);\n  transform: translate(0, 0);\n}\n.modal-open .modal {\n  overflow-x: hidden;\n  overflow-y: auto;\n}\n.modal-dialog {\n  position: relative;\n  width: auto;\n  margin: 10px;\n}\n.modal-content {\n  position: relative;\n  background-color: #ffffff;\n  border: 1px solid #ffffff;\n  border: 1px solid #cccccc;\n  border-radius: 4px;\n  -moz-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n  -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n  box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n  background-clip: padding-box;\n  outline: 0;\n}\n.modal-backdrop {\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  background-color: #333333;\n  z-index: 1049;\n}\n.modal-backdrop.fade {\n  -moz-opacity: 0;\n  -khtml-opacity: 0;\n  -webkit-opacity: 0;\n  opacity: 0;\n  -ms-filter: progid:DXImageTransform.Microsoft.Alpha(opacity=0);\n  filter: alpha(opacity=0);\n}\n.modal-backdrop.in {\n  -moz-opacity: 0.5;\n  -khtml-opacity: 0.5;\n  -webkit-opacity: 0.5;\n  opacity: 0.5;\n  -ms-filter: progid:DXImageTransform.Microsoft.Alpha(opacity=50);\n  filter: alpha(opacity=50);\n}\n.modal-header {\n  padding: 15px;\n  border-bottom: 1px solid #cccccc;\n  min-height: 16.42857143px;\n}\n.modal-header .close {\n  margin-top: -2px;\n  float: right;\n}\n.modal-title {\n  margin: 0;\n  line-height: 1.42857143;\n}\n.modal-body {\n  position: relative;\n  padding: 20px;\n}\n.modal-footer {\n  padding: 20px;\n  text-align: right;\n  border-top: 1px solid #cccccc;\n}\n.modal-footer .btn + .btn {\n  margin-left: 5px;\n  margin-bottom: 0;\n}\n.modal-footer .btn-group .btn + .btn {\n  margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n  margin-left: 0;\n}\n.modal-scrollbar-measure {\n  position: absolute;\n  top: -9999px;\n  width: 50px;\n  height: 50px;\n  overflow: scroll;\n}\n@media (min-width: 768px) {\n  .modal-dialog {\n    width: 600px;\n    margin: 30px auto;\n  }\n  .modal-content {\n    -moz-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n    -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n  }\n  .modal-sm {\n    width: 300px;\n  }\n}\n@media (min-width: 1024px) {\n  .modal-lg {\n    width: 900px;\n  }\n}\n.twitter-typeahead {\n  width: 282px;\n  float: left;\n}\n.tt-dropdown-menu {\n  width: 305px;\n  background: #ffffff;\n  border-right: 1px solid #b3b3b3;\n  border-bottom: 1px solid #b3b3b3;\n  border-left: 1px solid #b3b3b3;\n  border-bottom-left-radius: 6px;\n  border-bottom-right-radius: 6px;\n  box-shadow: 1px 1px 2px #cccccc;\n}\n.tt-suggestion {\n  font-size: 14px;\n}\n.tt-suggestion .separator {\n  border-top: 1px solid #cccccc;\n}\n.tt-suggestion .helper {\n  color: #b3b3b3;\n}\n.tt-suggestion .avatar {\n  width: 24px;\n  border-radius: 3px;\n  margin-right: 5px;\n}\n.tt-suggestion p {\n  padding: 5px 15px;\n  margin: 0px;\n}\n.tt-cursor {\n  background: #cccccc;\n}\n/* FIXME: This overrides some library defaults, not sure this is the\n *        right way to do this! -bre */\n/* Compose - Custom Styles */\n.select2-hidden-accessible {\n  visibility: hidden;\n}\n.select2-result-label .compose-select-avatar {\n  display: inline-block;\n  margin-right: 8px;\n}\n.select2-result-label .compose-select-avatar img {\n  width: 32px;\n  height: 32px;\n}\n.select2-result-label .compose-select-avatar .icon-user {\n  font-size: 32px;\n  line-height: 32px;\n}\n.select2-result-label .compose-select-name {\n  display: inline-block;\n  margin-top: 0px;\n  padding-top: 0px;\n  font-size: 14px;\n  font-weight: bold;\n  line-height: 14px;\n  color: #4d4d4d;\n}\n.select2-result-label .icon-lock-closed {\n  color: #4b9441;\n  margin-left: 8px;\n}\n.select2-result-label .compose-select-address {\n  font-family: Helvetica, Arial, sans-serif;\n  font-size: 12px;\n  font-weight: normal;\n  line-height: 12px;\n  color: #8c8c8c;\n}\n.select2-search-choice .compose-choice-name {\n  display: table-cell;\n  vertical-align: middle;\n  font-size: 14px;\n  font-weight: bold;\n  line-height: 14px;\n  color: #4d4d4d;\n}\n.select2-search-choice .avatar {\n  display: table-cell;\n  vertical-align: middle;\n  padding-right: 10px;\n}\n.select2-search-choice .avatar img {\n  width: 24px;\n  height: 24px;\n}\n.select2-search-choice .icon-user {\n  font-size: 24px;\n  line-height: 24px;\n  margin-right: 10px;\n}\n.select2-search-choice .icon-blank {\n  width: 0px;\n  height: 14px;\n  display: inline-block;\n}\n.select2-search-choice .icon-lock-closed {\n  color: #4b9441;\n  margin-left: 8px;\n}\n/* Tipped style */\n.qtip-tipped {\n  border: 0px solid #444;\n  -moz-border-radius: 3px;\n  -webkit-border-radius: 3px;\n  border-radius: 3px;\n  background-color: #444;\n  color: #ffffff;\n  font-size: 12px;\n  font-weight: bold;\n  font-family: Arial;\n  line-height: 14px;\n  padding: 4px 6px;\n}\n.qtip-tipped .qtip-titlebar {\n  border-bottom-width: 0;\n  color: white;\n  background: #3A79B8;\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#3a79b8), to(#2e629d));\n  background-image: -webkit-linear-gradient(top, #3a79b8, #2e629d);\n  background-image: -moz-linear-gradient(top, #3a79b8, #2e629d);\n  background-image: -ms-linear-gradient(top, #3a79b8, #2e629d);\n  background-image: -o-linear-gradient(top, #3a79b8, #2e629d);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#3a79b8, endColorstr=#2e629d);\n  -ms-filter: \"progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D)\";\n}\n.qtip-tipped .qtip-content {\n  text-align: center;\n}\n.qtip-tipped .qtip-icon {\n  border: 2px solid #285589;\n  background: #285589;\n}\n.qtip-tipped .qtip-icon .ui-icon {\n  background-color: #FBFBFB;\n  color: #555;\n}\n/* IE9 fix - removes all filters */\n.qtip:not(.ie9haxors) div.qtip-content,\n.qtip:not(.ie9haxors) div.qtip-titlebar {\n  filter: none;\n  -ms-filter: none;\n}\n.qtip .qtip-tip {\n  margin: 0 auto;\n  overflow: hidden;\n  z-index: 10;\n}\n/* Opera bug #357 - Incorrect tip position https://github.com/Craga89/qTip2/issues/367 */\nx:-o-prefocus,\n.qtip .qtip-tip {\n  visibility: hidden;\n}\n.qtip .qtip-tip,\n.qtip .qtip-tip .qtip-vml,\n.qtip .qtip-tip canvas {\n  position: absolute;\n  color: #123456;\n  background: transparent;\n  border: 0 dashed transparent;\n}\n.qtip .qtip-tip canvas {\n  top: 0;\n  left: 0;\n}\n.qtip .qtip-tip .qtip-vml {\n  behavior: url(#default#VML);\n  display: inline-block;\n  visibility: visible;\n}\n/* App */\n/* Mailpile - Setup\n*  Version 0.1.0\n*\t Designed and built by @brennannovak and others\n*/\n/* Config - Change the settings in this file change the look and feel of your style */\n/* Topbar */\n.topbar-middle {\n  position: relative;\n  top: 15px;\n  text-align: center;\n  margin-left: auto;\n  margin-right: auto;\n  font-size: 30px;\n  line-height: 30px;\n}\n.topbar-middle .title {\n  position: relative;\n  top: 0px;\n  left: -95px;\n  font-family: Mailpile-700, \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n}\n.topbar-middle .icon {\n  float: right;\n  position: relative;\n  top: 0px;\n  right: 20px;\n  display: block;\n  font-size: 30px;\n  line-height: 30px;\n}\n/* Setup */\n#setup-container {\n  position: relative;\n  top: 65px;\n  left: 0px;\n}\ndiv.setup-box {\n  margin: 0px auto;\n  padding: 25px;\n  border-radius: 5px;\n  background: #e9e9e9;\n  border: 1px solid #cccccc;\n}\ndiv.setup-box-small {\n  width: 400px;\n}\ndiv.setup-box-medium {\n  width: 600px;\n}\ndiv.setup-box-large {\n  width: 800px;\n}\ndiv.setup-box h3 {\n  margin: 0 0 12px 0;\n}\n.setup-text-detail-large {\n  color: #b3b3b3;\n  font-size: 24px;\n}\n.setup-table {\n  background: #ffffff;\n}\n/* Setup Progress */\n#setup-progress {\n  width: 520px;\n  position: relative;\n  top: 10px;\n  left: 15%;\n  text-align: center;\n}\na.setup-progress-circle {\n  display: table-cell;\n  height: 38px;\n  width: 38px;\n  vertical-align: middle;\n  border-radius: 25px;\n  background: #ffffff;\n  border: 1px solid #b3b3b3;\n}\na.setup-progress-circle:hover,\na.setup-progress-circle:hover span.icon {\n  color: #4d4d4d;\n  background: #d0d0d0;\n}\na.setup-progress-circle span.icon {\n  display: inline-block;\n  color: #b3b3b3;\n}\na.setup-progress-circle.on {\n  background: #337fb2;\n  border: 1px solid #e9e9e9;\n}\na.setup-progress-circle.complete {\n  background: #4b9441;\n  border: 1px solid #e9e9e9;\n}\na.setup-progress-circle.on span.icon,\na.setup-progress-circle.complete span.icon {\n  color: #ffffff;\n}\na.setup-progress-circle.on:hover span.icon,\na.setup-progress-circle.complete:hover span.icon {\n  background: transparent;\n}\nspan.setup-progress-line {\n  width: 90px;\n  display: block;\n  border-bottom: 1px solid #cccccc;\n  margin: 20px 8px 0px 8px;\n}\n/* Setup - Details */\nlabel span.setup-help-tooltip {\n  color: #b3b3b3;\n  cursor: pointer;\n}\na.setup-check-connection {\n  font-weight: normal;\n}\na.setup-check-connection:hover {\n  color: #4b9441;\n}\n/* Setup - Welcome */\n#setup-welcome {\n  width: 100%;\n  height: 100%;\n}\n.welcome-logo {\n  width: 25%;\n}\n.welcome-icons {\n  width: 470px;\n  font-size: 40px;\n  line-height: 40px;\n  display: inline-block;\n  margin-left: auto;\n  margin-right: auto;\n}\n.welcome-icons li {\n  margin: 0px 13.33333333px;\n}\n/* Setup - Crypto */\n#identity-vault-lock {\n  margin: 20px 0;\n  position: relative;\n  left: 32%;\n  top: 0px;\n}\n.setup-cryto-fingerprint-icon {\n  font-size: 48px;\n  line-height: 48px;\n  display: inline-block;\n}\n.setup-crypto-fingerprint-fingerprint {\n  font-size: 21px;\n  line-height: 24px;\n  font-weight: normal;\n  font-style: italic;\n  display: inline-block;\n  width: 320px;\n}\nlabel.radio-list-item div.radio {\n  width: 30px;\n}\nlabel.radio-list-item div.icon {\n  width: 30px;\n}\nlabel.radio-list-item .icon-key {\n  font-size: 30px;\n  line-height: 30px;\n}\n.setup-list-items {\n  background: #ffffff;\n  border-radius: 5px;\n  border: 1px solid #cccccc;\n}\n.setup-list-items li:first-child {\n  border-top: 0px solid;\n}\n.setup-item {\n  padding: 15px;\n  border-top: 1px solid #cccccc;\n}\n.setup-item ul {\n  margin-bottom: 0px;\n}\n.setup-item ul li {\n  margin-left: 20px;\n}\n.setup-item .avatar {\n  width: 50px;\n  display: inline-block;\n  float: left;\n  margin-right: 20px;\n}\n.setup-item .avatar img {\n  width: 50px;\n  border-radius: 3px;\n}\n.setup-item .name {\n  display: block;\n  margin-bottom: 5px;\n  vertical-align: text-top;\n  font-size: 18px;\n  font-weight: bold;\n  line-height: 18px;\n  color: #4d4d4d;\n}\n.setup-item .email {\n  font-size: 14px;\n  line-height: 14px;\n  color: #b3b3b3;\n}\n.setup-item.disabled,\n.setup-item.disabled .name,\n.setup-item.disabled .email,\n.setup-item.disabled .setup-actions a {\n  color: #cccccc;\n}\n.setup-item-notice {\n  background: #f8f3b9;\n  padding: 15px;\n  margin-bottom: 0px;\n  border-radius: 4.5px;\n}\n/* Setup - Sources Settings */\n#setup-source-settings {\n  background: #ffffff;\n  border-radius: 3px;\n  padding: 15px;\n  border: 1px solid #cccccc;\n}\n#setup-source-settings div.left {\n  width: 275px;\n}\n/* Setup - Complete */\n#setup-complete-message {\n  line-height: 48px;\n}\n#setup-complete-icon {\n  font-size: 100px;\n  line-height: 100px;\n}\n/* Mailpile - Settings */\ndiv.settings-page,\n.settings-page .setting-group,\n.settings-page .setting-group .settings {\n  max-width: 80em;\n  position: relative;\n}\n.settings-page .setting-group .settings {\n  max-width: 50%;\n}\n.settings-page .setting-group {\n  border-top: 1px solid #cccccc;\n  margin: 15px 0;\n  padding: 10px 0;\n}\n.settings-page .notices {\n  font-size: 20px;\n  line-height: 24px;\n  padding: 0 0 6px 30px;\n}\n.settings-page h3 {\n  margin: 0 0 15px 0;\n  padding: 5px 0 0 0;\n  width: 50%;\n  float: left;\n}\n.settings-page p {\n  margin: 0 0 0.5em 0;\n  padding: 0;\n}\n.settings-page h4,\n.settings-page h5,\n.settings-page h6 {\n  margin: 1.25em 0 0.5em 0;\n  padding: 0;\n}\n.settings-page div.explanation {\n  margin: 0 25px 0 0;\n  width: 40%;\n  float: right;\n  opacity: 0.55;\n}\n.settings-page div.explanation p {\n  position: relative;\n  padding: 0 0 6px 30px;\n  margin: 5px 0 0 0;\n}\n.settings-page div.notices span.icon,\n.settings-page div.explanation span.icon {\n  position: relative;\n  font-size: 24px;\n  line-height: 24px;\n}\n.settings-page span.icon-checkmark {\n  color: #337fb2;\n}\n.settings-page .what span.icon {\n  color: #4b9441;\n}\n.settings-page .risks span.icon {\n  color: #f15a24;\n}\n.settings-page .actions span.icon {\n  color: #7d4837;\n}\n.settings-page span.icon.default {\n  color: #337fb2;\n  font-size: 12px;\n  line-height: 12px;\n  position: relative;\n  top: -2px;\n}\n.settings-page div.explanation p span.icon {\n  position: absolute;\n  left: 0;\n}\n.settings-page div.settings,\n.settings-page p.settings {\n  display: block;\n  padding: 10px 25px;\n  line-height: 20px;\n  clear: left;\n}\n.settings-page ul.notes {\n  list-style-type: circle;\n  font-size: 13px;\n}\n.settings-page .subsection,\n.settings-page ul.notes {\n  display: block;\n  padding: 0;\n  margin: 0 0 0 2em;\n}\n/* Event Log specifics */\n.settings-page p.event-summary span {\n  margin-right: 1em;\n}\n.settings-page p.event-summary .event-source {\n  float: right;\n  font-size: 11px;\n  color: #777;\n}\n/* Mailpile - Settings */\n.content-normal .actions {\n  float: right;\n}\n.content-normal section {\n  margin-left: -3px;\n  margin-right: -3px;\n  display: inline-block;\n  margin-top: 1.7em;\n  vertical-align: top;\n}\n.content-normal section h3 {\n  margin-top: 0.7em;\n  margin-bottom: 0.7em;\n  padding-left: 4px;\n}\n@media only screen and (min-width: 1600px), (orientation: portrait) {\n  .content-normal section {\n    max-width: 50%;\n    width: 100%;\n  }\n}\nsection.account-list {\n  margin-right: 5px;\n}\nsection.account-list table {\n  margin: auto;\n  background: none;\n  border: 0;\n  text-align: right;\n  width: 100%;\n}\nsection.account-list table td#email-address,\nsection.account-list table td#first-name {\n  text-align: left;\n}\nsection.account-list table th {\n  background: none;\n  border: 0;\n  text-align: left;\n}\ntr.message {\n  background: #ffffff;\n}\nsection.message {\n  width: 100%;\n  max-width: 100%;\n  background: #ffffff;\n  border-radius: 3px;\n  border: 1px solid #cccccc;\n}\nsection.account-list .icon-logo {\n  font-size: 1.3em;\n}\n@media only screen and (min-width: 1600px), (orientation: portrait) {\n  section.motd {\n    padding-left: 4%;\n  }\n}\nsection.motd p,\nsection.motd h4 {\n  margin: 0.5em 0 0 0;\n}\nsection.motd.recent,\nsection.motd.recent h3 {\n  color: #225577;\n}\nsection.motd.recent p.motd,\nsection.motd.recent p.motd a.motd-signed {\n  color: #4b9441;\n}\nsection.motd .updated {\n  color: #be1c21;\n}\nsection.motd p.motd {\n  margin-left: 1em;\n}\nsection.motd a.motd-signed {\n  display: block;\n  margin-top: 0.5em;\n  text-align: right;\n  padding-right: 2em;\n}\nsection.motd .version-info {\n  padding-left: 0.5em;\n}\n/* Connection Down */\n#connection-down {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  z-index: 2000;\n  background: #000000;\n  filter: alpha(opacity=100);\n  opacity: 100;\n  text-align: center;\n}\n#connection-down .message {\n  width: 480px;\n  text-align: center;\n  position: absolute;\n  font-family: Helvetica, Arial, Sans-Serif;\n  font-size: 18px;\n  line-height: 18px;\n  color: #b3b3b3;\n  top: 100px;\n  left: 50%;\n  margin-top: 0px;\n  margin-left: -200px;\n  background: #e9e9e9;\n  border-radius: 6px;\n  z-index: 2001;\n}\n#connection-down .message h1 {\n  color: #4d4d4d;\n  font-size: 36px;\n  line-height: 48px;\n}\n#connection-down .message-normal {\n  color: #eeeeee !important;\n}\n#connection-down .message-success {\n  color: #eeeeee !important;\n}\n#connection-down .message-error {\n  color: #b20a0a !important;\n}\n/* Login */\n#login {\n  width: 100%;\n  border: 8px solid #cccccc;\n  box-sizing: border-box;\n}\n#login-left {\n  width: 65%;\n  height: 100px;\n  background: #e9e9e9;\n  border-right: 4px solid #cccccc;\n  box-sizing: border-box;\n  display: inline-block;\n  float: left;\n}\n#login-right {\n  width: 35%;\n  height: 100%;\n  background: #e9e9e9;\n  border-left: 2px solid #cccccc;\n  box-sizing: border-box;\n  display: inline-block;\n  float: right;\n}\n#login-logo {\n  width: 223px;\n  position: absolute;\n  top: 13%;\n  left: 20%;\n}\n#login-logo #logo-icon {\n  width: 150px;\n  height: 100px;\n  display: block;\n  margin: 0px auto 35px auto;\n}\n#login-logo #logo-name {\n  width: 223px;\n  height: 72px;\n  display: block;\n  margin: 0 auto;\n}\n#login-messages {\n  position: absolute;\n  top: 35%;\n  margin: 20px auto;\n  width: 400px;\n  font-weight: bold;\n}\n#login-vault-lock {\n  margin: 0 auto;\n  position: absolute;\n  top: 10%;\n  left: 55%;\n}\n#login-details {\n  width: 485px;\n  margin: 0px auto;\n  position: absolute;\n  top: 50%;\n  left: 33%;\n}\n.form-text {\n  display: inline-block;\n  position: relative;\n  vertical-align: top;\n  top: 15px;\n  left: 0px;\n  font-family: Mailpile-700, \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-size: 18px;\n  line-height: 18px;\n  color: #4d4d4d;\n}\n#form-login {\n  width: 246px;\n  display: inline-block;\n}\n.form-login {\n  width: 246px;\n  height: 36px;\n  display: inline-block;\n  margin-top: 5px;\n  margin-left: 15px;\n  border-radius: 5px;\n  border: 1px solid #b3b3b3;\n}\n.form-login input {\n  width: 178px;\n  height: 18px;\n  padding: 9px 12px;\n  margin-bottom: 5px;\n  float: left;\n  font: normal 18px \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  border: 0;\n  background: #ffffff;\n  border-radius: 5px 0 0 5px;\n  color: #cccccc;\n}\n.form-login input#login-username {\n  border-radius: 5px 5px 5px 5px;\n  width: 222px;\n}\n.form-login input:focus {\n  outline: 0;\n  background: #fff;\n  box-shadow: 0 0 1px #4d4d4d inset;\n  color: #4d4d4d;\n}\n.form-login input::-webkit-input-placeholder,\n.form-login input:-moz-placeholder,\n.form-login input:-ms-input-placeholder {\n  color: #999;\n  font-weight: normal;\n  font-style: italic;\n}\n.form-login button {\n  overflow: visible;\n  position: relative;\n  float: right;\n  border: 0;\n  padding: 0;\n  cursor: pointer;\n  height: 36px;\n  width: 44px;\n  font: bold 18px/40px \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  color: #ffffff;\n  background: #337fb2;\n  border-radius: 0 3px 3px 0;\n  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3);\n}\n.form-login button:hover {\n  background: #2d719e;\n}\n.form-login button:active,\n.form-login button:focus {\n  background: #225577;\n  outline: 0;\n}\n.form-login button::-moz-focus-inner {\n  /* remove extra button spacing for Mozilla Firefox */\n  border: 0;\n  padding: 0;\n}\n.form-login button .icon-key {\n  font-size: 24px;\n  line-height: 24px;\n}\n.login-wrong-passphrase {\n  background: #fbb03b;\n  display: inline-block;\n  margin: 20px auto 20px 23%;\n  padding: 15px;\n  border-radius: 6px;\n  text-align: center;\n  font-weight: bold;\n  font-size: 14px;\n  line-height: 14px;\n  color: #ffffff;\n}\n.logged-out-message {\n  background: #337fb2;\n  display: inline-block;\n  margin: 20px auto 20px 23%;\n  padding: 15px;\n  border-radius: 6px;\n  text-align: center;\n  font-weight: bold;\n  font-size: 14px;\n  line-height: 14px;\n  color: #ffffff;\n}\n.still-running {\n  margin: 2em auto 0 auto;\n  padding: 1em 4em 0 0;\n  max-width: 26em;\n  color: #777;\n}\n.still-running .icon {\n  color: #aaa;\n  float: right;\n  margin: 0 2px 5px 2px;\n  padding-top: 2px;\n}\n.still-running .icon.bigger {\n  color: #bbb;\n  font-size: 2em;\n  padding-top: 0px;\n}\n.still-running .icon.smaller {\n  font-size: 0.7em;\n  padding-top: 4px;\n}\n/* Body */\nbody {\n  overflow: hidden;\n}\n/* Content */\n#content {\n  position: fixed;\n  top: 62px;\n  left: 225px;\n  right: 0px;\n  bottom: 0px;\n  overflow: hidden;\n}\n#content-wide {\n  width: 100%;\n  margin-top: 62px;\n}\n#content-tools {\n  position: relative;\n  z-index: 10;\n}\n/* Copyright Footer Navigation */\n.footer-nav {\n  margin-top: 20px;\n  margin-bottom: 5px;\n}\n.footer-nav,\n.footer-nav a {\n  font-family: Mailpile-300, \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-size: 12px;\n  color: #b3b3b3;\n}\n/* Sub Navigation */\n.sub-navigation {\n  width: 100%;\n  height: 46px;\n  display: block;\n  background: #e9e9e9;\n  border-bottom: 1px solid #b3b3b3;\n  box-sizing: border-box;\n}\n.sub-navigation > ul {\n  margin: 10px;\n}\n.sub-navigation > ul > li {\n  margin: 0px 5px;\n  padding: 0px 5px;\n}\n.sub-navigation > ul > li > a {\n  display: block;\n  padding: 5px;\n  font-family: \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-weight: normal;\n  color: #4d4d4d;\n  line-height: 14px;\n}\n.sub-navigation > ul > li > a:hover,\n.sub-navigation > ul > li > a:hover span.navigation-icon {\n  color: #be1c21;\n}\n.sub-navigation > ul > li > a img {\n  height: 14px;\n}\n.sub-navigation > ul > li > ul.dropdown-menu li {\n  display: block;\n  float: none;\n}\n.sub-navigation > ul > li > ul.dropdown-menu li .selected:before {\n  position: absolute;\n  left: 5px;\n  content: \"\\2713\";\n}\n.navigation-icon {\n  margin: 0;\n  color: #4d4d4d;\n  font-weight: normal;\n}\n.navigation-text {\n  margin: 0 0 0 8px;\n}\n/* Bulk Actions */\n.bulk-actions {\n  height: 38px;\n  position: relative;\n  top: 0px;\n  left: 0px;\n  background: #ffffff;\n  border-bottom: 1px solid #b3b3b3;\n  box-sizing: border-box;\n  color: #4d4d4d;\n  font-size: 14px;\n  line-height: 16px;\n  padding: 11px 5px;\n}\n.bulk-actions div {\n  margin: 0px 15px;\n}\n.bulk-actions ul {\n  margin: -5px 15px;\n}\n.bulk-actions li {\n  padding: 0 15px;\n}\n.bulk-actions li.left {\n  padding-left: 0px;\n}\n.bulk-actions ul.right {\n  text-align: right;\n}\n.bulk-actions ul.right li {\n  padding-right: 0px;\n}\n.bulk-actions a {\n  color: #4d4d4d;\n  line-height: 16px;\n}\n.bulk-actions a img {\n  padding: 0;\n  margin-top: 0;\n  height: 16px;\n}\n.bulk-actions a:hover {\n  color: #be1c21;\n}\n.bulk-actions a span.icon {\n  padding: 0;\n  margin-top: 0;\n  font-size: 16px;\n  line-height: 16px;\n}\n.bulk-actions-hints a span.icon {\n  font-size: 14px;\n  line-height: 16px;\n}\n.bulk-actions li.hide {\n  visibility: hidden;\n}\n/* Content View */\n#content-tall-view,\n#content-view {\n  position: absolute;\n  top: 84px;\n  bottom: 0;\n  right: 0;\n  left: 0;\n  overflow-y: scroll;\n  z-index: 5;\n  background: #e9e9e9;\n}\n#content-tall-view {\n  top: 0px;\n}\ndiv.content-normal {\n  margin: 25px 20px;\n}\ndiv.content-small {\n  max-width: 400px;\n}\ndiv.content-medium {\n  max-width: 600px;\n}\ndiv.content-large {\n  max-width: 800px;\n}\n/* Debug */\n#debug {\n  width: 100%;\n  font-size: 14px;\n  font-family: Helvetica, Arial, sans-serif;\n  text-align: center;\n  color: #666666;\n  line-height: 14px;\n}\n#debug p {\n  margin: 0px 5px;\n  padding: 0px;\n}\n/* Images */\n.img-border {\n  border: 1px solid #cccccc;\n  -webkit-transition-duration: 0.3s;\n  -moz-transition-duration: 0.3s;\n  transition-duration: 0.3s;\n}\n.img-border:hover {\n  border: 1px solid #4d4d4d;\n  -webkit-transition-duration: 0.3s;\n  -moz-transition-duration: 0.3s;\n  transition-duration: 0.3s;\n}\n/* Text */\n/* REBAR - move to rebar */\n.text-detail {\n  color: #b3b3b3;\n}\n.text-detail a {\n  color: #b3b3b3;\n}\n.text-detail a:hover {\n  color: #337fb2;\n}\na.link-detail,\na.link-detail:visited {\n  color: #b3b3b3;\n  font-weight: normal !important;\n}\na.link-detail:hover {\n  color: #4d4d4d;\n}\na.disabled {\n  pointer-events: none;\n  cursor: default;\n}\np.paragraph-success,\np.paragraph-important,\np.paragraph-alert,\np.paragraph-warning {\n  padding: 7.5px 15px;\n  border-radius: 3px;\n  font-weight: bold;\n}\np.paragraph-success {\n  background: #4b9441;\n  color: #ffffff;\n}\np.paragraph-important {\n  background: #337fb2;\n  color: #ffffff;\n}\np.paragraph-alert {\n  background: #fbb03b;\n  color: #ffffff;\n}\np.paragraph-warning {\n  background: #be1c21;\n  color: #ffffff;\n}\n/* Form Inputs */\n/* REBAR: move this rebar/forms.less library at some point */\nul.radio-list {\n  background: #ffffff;\n  border: 1px solid #b3b3b3;\n  border-radius: 3px;\n}\nul.radio-list li {\n  margin-bottom: 0px;\n  border-top: 1px solid #b3b3b3;\n}\nul.radio-list li:first-child {\n  border-top: 0px;\n}\nlabel.radio-list-item {\n  width: 100%;\n  height: 100%;\n  display: table;\n  margin-bottom: 0px;\n  box-sizing: content-box;\n}\nlabel.radio-list-item:hover {\n  background: #faf7d0;\n  cursor: pointer;\n}\nlabel.radio-list-item div {\n  display: table-cell;\n  padding: 15px;\n  vertical-align: middle;\n}\nspan.radio-list-item-detail {\n  color: #e9e9e9;\n}\n/* List Items */\n/* REBAR: Move these more global styles to Rebar */\nul.items {\n  margin: 20px 0;\n}\nul.items.grouped {\n  background: #ffffff;\n  border-radius: 3px;\n  border: 1px solid #b3b3b3;\n}\nul.items li.separate {\n  background: #ffffff;\n  margin-bottom: 20px;\n  padding: 15px;\n  border: 1px solid #b3b3b3;\n  border-radius: 3px;\n}\nul.items.grouped li.grouped:first-child {\n  border-top: 0px;\n}\nul.items.grouped li.grouped {\n  border-top: 1px solid #b3b3b3;\n  padding: 15px;\n}\nul.items li.separate h5,\nul.items li.grouped h5 {\n  margin-bottom: 13.33333333px;\n}\n/* Response Rectangles - Tags & Contacts */\n.rectangles-container {\n  width: 97%;\n  margin-top: 1.5%;\n  margin-bottom: 1.5%;\n}\n.rectangles-outer {\n  background: #ffffff;\n  border: 1px solid #cccccc;\n  box-sizing: border-box;\n  border-radius: 3px;\n}\n.rectangles-outer:hover {\n  background: #e9e9e9;\n}\n/* User Card - Avatar, Name, Address */\n.global-user-avatar {\n  display: inline-block;\n  width: 45px;\n  margin-right: 10px;\n}\n.global-user-avatar-img {\n  width: 45px;\n  border-radius: 3px;\n}\n.user .avatar {\n  display: inline-block;\n  width: 45px;\n  margin-right: 10px;\n}\n.user .avatar img {\n  width: 45px;\n  border-radius: 3px;\n}\n.user .name {\n  width: 120px;\n  display: inline-block;\n  vertical-align: top;\n}\n.user .name a {\n  display: inline-block;\n  font-size: 14px;\n  font-weight: bold;\n  line-height: 16px;\n  word-break: break-word;\n  color: #4d4d4d;\n  vertical-align: top;\n  margin-bottom: 5px;\n}\n.user .address {\n  display: inline-block;\n  color: #b3b3b3;\n  font-size: 12px;\n  font-weight: normal;\n}\n/* Crypto */\n.vault-lock-outer {\n  display: inline-block;\n  width: 225px;\n  height: 225px;\n  border-radius: 112.5px;\n  border: 1px solid #4d4d4d;\n  box-sizing: border-box;\n  background: #b3b3b3;\n}\n.vault-lock-inner {\n  display: inline-block;\n  width: 171px;\n  height: 171px;\n  border-radius: 85.5px;\n  border: 1px solid #4d4d4d;\n  box-sizing: border-box;\n  background: #ffffff;\n  position: relative;\n  top: 27px;\n  left: 27px;\n}\n.vault-lock {\n  display: inline-block;\n  font-size: 72px;\n  line-height: 72px;\n  position: relative;\n  top: 42px;\n  left: 47px;\n}\n/* Keyboard shortcuts */\nkbd {\n  display: inline-block;\n  padding: 5px 8px;\n  margin: 3px;\n  min-width: 10px;\n  border: 1px #b3b3b3 solid;\n  border-bottom: 2px #b3b3b3 solid;\n  border-radius: 3px;\n  background-color: #e9e9e9;\n  font-size: 14px;\n  text-align: center;\n  font-style: normal;\n}\nkbd::first-letter {\n  text-transform: capitalize;\n}\n/* Some general helper classes */\n/*\n  Sets cursor to pointer on a tags\n  functioning as buttons\n */\na.clickable {\n  cursor: pointer;\n  -webkit-touch-callout: none;\n  -webkit-user-select: none;\n  -khtml-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n/*\n  As they were HTML5 labels\n */\nspan.checkbox {\n  cursor: default;\n  -webkit-touch-callout: none;\n  -webkit-user-select: none;\n  -khtml-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n/* Notification Bubbles */\n#notifications {\n  position: fixed;\n  bottom: 0px;\n  right: 15px;\n  width: 250px;\n  display: inline-block;\n  z-index: 1000;\n  box-sizing: content-box;\n  max-height: 30%;\n  overflow: auto;\n}\n#notification-bubbles,\n#notifications-header {\n  background: #4d4d4d;\n  color: #b3b3b3;\n  font-weight: normal;\n  font-size: 14px;\n}\n#notifications-header {\n  margin: 0;\n  border-top-left-radius: 3px;\n  border-top-right-radius: 3px;\n  border-bottom: 0;\n  padding: 3.75px 15px;\n  padding-bottom: 2px;\n  box-sizing: padding-box;\n  font-size: 15px;\n  font-weight: bold;\n}\ndiv.notification-bubble {\n  display: table;\n  margin-top: 0;\n  padding: 7.5px 15px;\n  border-top: 1px solid #333333;\n  box-sizing: padding-box;\n}\ndiv.notification-bubble span.icon {\n  display: table-cell;\n  vertical-align: text-top;\n  color: #ffffff;\n  margin-right: 5px;\n  font-size: 14px;\n  line-height: 14px;\n}\ndiv.notification-bubble.error .icon {\n  color: #be1c21;\n}\ndiv.notification-bubble.warning .icon {\n  color: #fbb03b;\n}\ndiv.notification-bubble.success .icon {\n  color: #4b9441;\n}\n#notifications-header span.text,\ndiv.notification-bubble span.text {\n  width: 100%;\n  display: table-cell;\n  vertical-align: text-top;\n  padding-left: 10px;\n  padding-bottom: 3px;\n  line-height: 18px;\n  color: #ffffff;\n}\n#notifications-header span.text {\n  padding-bottom: 0;\n  margin: 0;\n}\ndiv.notification-bubble span.message {\n  font-weight: bold;\n}\ndiv.notification-bubble span.action {\n  font-weight: normal;\n  font-style: italic;\n  color: #b3b3b3;\n}\n#notifications a {\n  color: #b3b3b3;\n}\n#notifications a:visited {\n  color: #b3b3b3;\n}\n#notifications a:hover {\n  color: #e9e9e9;\n}\n#notifications a.notifications-close-all,\n#notifications a.notification-close {\n  display: table-cell;\n  vertical-align: text-top;\n}\n/* Navigation */\n.navigation-on {\n  background: #d9d9d9;\n  border-radius: 3px;\n}\n.navigation-on > a {\n  cursor: default;\n  color: #4d4d4d;\n}\n.navigation-on > a:hover,\n.navigation-on > a:hover > span {\n  color: #4d4d4d;\n}\n/* Selects */\n.checkbox-item-picker {\n  background: #e9e9e9;\n  margin: 0px 20px 20px 0px;\n  padding: 5px;\n  display: inline-block;\n  border-radius: 4px;\n  border: 1px solid #cccccc;\n}\n.checkbox-item-picker:hover {\n  background: #cccccc;\n  cursor: pointer;\n}\n.checkbox-item-picker-selected {\n  background: #faf7d0;\n}\n.checkbox-item-picker-selected:hover {\n  background: #f8f3b9;\n}\n/* Topbar */\n.topbar {\n  width: 100%;\n  height: 62px;\n  display: table;\n  position: fixed;\n  top: 0px;\n  left: 0px;\n  z-index: 100;\n  min-width: 800px;\n  border-bottom: 1px solid #b3b3b3;\n  box-sizing: border-box;\n  background: #e9e9e9;\n}\n.topbar-logo {\n  width: 67px;\n  display: table-cell;\n  box-sizing: border-box;\n  vertical-align: middle;\n  text-align: center;\n}\n.topbar-logo #logo-icon {\n  display: block;\n  margin: 0 10px 0 15px;\n  height: 37px;\n}\n.topbar-logo span#page-title-icon {\n  font-size: 37px;\n  line-height: 37px;\n}\n.topbar-logo-name {\n  position: relative;\n  width: 157px;\n  height: 40px;\n  display: table-cell;\n  box-sizing: border-box;\n  vertical-align: middle;\n  text-align: left;\n}\n.topbar-logo-name #logo-name {\n  display: block;\n  margin-right: 10px;\n}\n.topbar-logo-name span#page-title-text {\n  display: block;\n  max-width: 75%;\n  position: absolute;\n  font-family: Mailpile-700, \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-weight: normal;\n  font-size: 12px;\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  margin: -7px 0 0 8px;\n  padding: 0;\n}\n.topbar-actions {\n  min-width: 630px;\n  display: table-cell;\n  box-sizing: border-box;\n  vertical-align: middle;\n}\n.form-search {\n  width: 350px;\n  height: 36px;\n  float: left;\n  margin-top: 5px;\n  margin-left: 15px;\n  border-radius: 5px;\n  border: 1px solid #b3b3b3;\n  position: relative;\n}\n.form-search input {\n  width: 282px;\n  height: 20px;\n  padding: 8px 12px;\n  float: left;\n  font: normal 18px \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  border: 0;\n  background: #ffffff;\n  border-radius: 5px 0 0 5px;\n  color: #cccccc;\n}\n.form-search input:focus {\n  outline: 0;\n  background: #fff;\n  box-shadow: 0 0 1px #4d4d4d inset;\n  color: #4d4d4d;\n}\n.form-search input::-webkit-input-placeholder,\n.form-search input:-moz-placeholder,\n.form-search input:-ms-input-placeholder {\n  color: #999;\n  font-weight: normal;\n  font-style: italic;\n}\n.form-search .clear-search {\n  position: absolute;\n  right: 50px;\n  top: 11px;\n  cursor: pointer;\n  color: #cccccc;\n}\n.form-search .clear-search:hover {\n  color: #b3b3b3;\n}\n.form-search button {\n  overflow: visible;\n  position: relative;\n  float: right;\n  border: 0;\n  padding: 0;\n  cursor: pointer;\n  height: 36px;\n  width: 44px;\n  font: bold 18px/40px \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  color: #ffffff;\n  background: #337fb2;\n  border-radius: 0 3px 3px 0;\n  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3);\n}\n.form-search button:hover {\n  background: #2d719e;\n}\n.form-search button:active,\n.form-search button:focus {\n  background: #225577;\n  outline: 0;\n}\n.form-search button::-moz-focus-inner {\n  /* remove extra button spacing for Mozilla Firefox */\n  border: 0;\n  padding: 0;\n}\n.topbar-nav {\n  float: right;\n  position: relative;\n  top: 0px;\n  right: 20px;\n}\n.topbar-nav ul {\n  list-style: none;\n}\n.topbar-nav ul:after {\n  clear: both;\n}\n.topbar-nav > ul > li {\n  float: left;\n  margin-left: 15px;\n  text-align: center;\n}\n.topbar-nav > ul > li > a {\n  width: 32px;\n  height: 32px;\n  display: block;\n  margin: 6px 8px;\n  font-weight: normal;\n  color: #4d4d4d;\n  -webkit-transition-duration: 0.3s;\n  -moz-transition-duration: 0.3s;\n  transition-duration: 0.3s;\n}\n.topbar-nav > ul > li > a:hover {\n  color: #337fb2;\n  -webkit-transition-duration: 0.2s;\n  -moz-transition-duration: 0.2s;\n  transition-duration: 0.2s;\n}\n.topbar-nav > ul > li > a img {\n  height: 32px;\n}\n.topbar-nav > ul > li > a.donate:hover {\n  color: #be1c21;\n}\n.topbar-nav > ul > li > a span.link-icon {\n  display: block;\n  font-size: 32px;\n  line-height: 32px;\n}\n.topbar-nav > ul > li.navigation-on {\n  background: #cccccc;\n  border-radius: 3px;\n}\n.topbar-nav > ul > li.navigation-on > a {\n  color: #4d4d4d;\n  cursor: default;\n}\n.topbar-nav > ul > li.navigation-on.nav-dropdown > a:hover {\n  cursor: pointer !important;\n}\n.topbar-nav > ul > li.nav-dropdown ul.dropdown-menu li {\n  display: block;\n  float: none;\n  text-align: left;\n}\n.topbar-nav .nav-search {\n  display: none;\n}\n/* Sidebar */\n#sidebar {\n  position: fixed;\n  background: #e9e9e9;\n  top: 62px;\n  bottom: 0px;\n  width: 225px;\n  margin: 0px;\n  padding: 0px;\n  box-sizing: border-box;\n  border-right: 1px solid #b3b3b3;\n  border-spacing: 0px;\n}\n#sidebar-wrapper {\n  position: absolute;\n  top: 0px;\n  bottom: 0px;\n  width: 224px;\n  padding: 0px;\n  margin: 0px;\n}\n#sidebar-scroll-area {\n  position: absolute;\n  top: 0px;\n  bottom: 45px;\n  width: 100%;\n  padding: 5px 0 0 0;\n  margin: 0px;\n  overflow-x: hidden;\n  overflow-y: auto;\n}\n#sidebar-lists {\n  padding: 0px;\n  margin: 0px;\n}\n#sidebar-bottom {\n  position: absolute;\n  width: 100%;\n  height: 25px;\n  bottom: 0px;\n  padding-top: 10px;\n  padding-bottom: 10px;\n  background: #e9e9e9;\n  border-top: 1px solid #b3b3b3;\n}\n#sidebar-bottom a {\n  display: inline-block;\n  margin: 0px 10px;\n  font-size: 14px;\n  line-height: 14px;\n  font-weight: normal;\n  color: #4d4d4d;\n}\n#sidebar-bottom a:hover {\n  color: #337fb2;\n}\n#sidebar hr {\n  margin: 10px 0px;\n}\n#sidebar ul {\n  margin: 0px;\n  padding: 0px;\n}\n#sidebar ul li {\n  margin: 3px 0px;\n  padding: 0px 0px;\n  transition-duration: 0.3s;\n}\n#sidebar.cozy ul li {\n  margin-top: 2px;\n  margin-bottom: 3px;\n  padding-top: 2px;\n  padding-bottom: 2px;\n}\n#sidebar.snug ul li {\n  margin-top: 1px;\n  margin-bottom: 2px;\n  padding-top: 1px;\n  padding-bottom: 1px;\n}\n#sidebar ul li.show-subtags {\n  background: #ffffff;\n  border-top: 1px solid #cccccc;\n  border-bottom: 1px solid #cccccc;\n  padding-top: 5px;\n  transition-duration: 0.3s;\n}\n#sidebar a.sidebar-tag {\n  position: relative;\n  display: block;\n  margin: 0px 7px;\n  padding: 6px 5px;\n  vertical-align: middle;\n  text-align: left;\n  white-space: nowrap;\n  transition-duration: 0.2s;\n  box-sizing: content-box;\n}\n#sidebar a.sidebar-tag:hover {\n  color: #b3b3b3;\n}\n#sidebar.cozy a.sidebar-tag {\n  padding: 3px 0px;\n}\n#sidebar.snug a.sidebar-tag {\n  padding: 0px 0px;\n}\n#sidebar li.is-editing,\n#sidebar li.is-editing a.sidebar-tag {\n  cursor: move;\n}\n#sidebar a.sidebar-tag span.icon {\n  margin-left: 5px;\n  width: 24px;\n  height: 18px;\n  display: inline-block;\n  vertical-align: middle;\n  text-align: center;\n  font-weight: normal;\n  font-size: 18px;\n  line-height: 18px;\n}\n#sidebar.cozy a.sidebar-tag span.icon {\n  font-size: 18px;\n  line-height: 18px;\n}\n#sidebar.snug a.sidebar-tag span.icon {\n  font-size: 16px;\n  line-height: 16px;\n}\n#sidebar a.sidebar-tag > span.name,\n#sidebar a.sidebar-tag > span.notification {\n  vertical-align: middle;\n  font-family: Mailpile-300, \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-weight: normal;\n  font-size: 18px;\n  line-height: 24px;\n}\n#sidebar a.sidebar-tag span.name {\n  display: inline-block;\n  max-width: 135px;\n  padding-left: 5px;\n  padding-right: 0px;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n#sidebar a.sidebar-tag span.notification {\n  display: inline-block;\n  letter-spacing: -0.5px;\n  color: #b3b3b3;\n}\n#sidebar.cozy span.name,\n#sidebar.cozy span.notification {\n  font-size: 16px;\n  line-height: 18px;\n}\n#sidebar.snug span.name,\n#sidebar.snug span.notification {\n  font-size: 14px;\n  line-height: 16px;\n}\n#sidebar a.sidebar-tag.has-unread span.name,\n#sidebar a.sidebar-tag.has-unread span.notification {\n  font-family: Mailpile-700, \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-weight: normal;\n}\n#sidebar li.sidebar-tag {\n  position: relative;\n}\n#sidebar a.sidebar-tag-expand {\n  position: absolute;\n  display: inline-block;\n  color: #cccccc;\n  top: 20%;\n}\n#sidebar a.sidebar-tag-settings {\n  position: absolute;\n  display: inline-block;\n  color: #cccccc;\n  right: 4px;\n  top: 20%;\n}\n/* Sidebar - Subtags */\n#sidebar ul.sidebar-subtags {\n  margin-bottom: 8px;\n  padding: 0px;\n}\n#sidebar ul.sidebar-subtags li.sidebar-subtag {\n  margin-top: 0px;\n  margin-bottom: 0px;\n  padding-top: 0px;\n  padding-bottom: 0px;\n}\n#sidebar li.sidebar-subtag a.sidebar-tag.has-unread span.name,\n#sidebar li.sidebar-subtag a.sidebar-tag.has-unread span.notification {\n  font-family: \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-weight: bold;\n}\n#sidebar li.sidebar-subtag a.sidebar-tag span.icon {\n  margin-left: 16px;\n  font-size: 16px;\n  line-height: 16px;\n}\n#sidebar li.sidebar-subtag a.sidebar-tag span.name,\n#sidebar li.sidebar-subtag a.sidebar-tag span.notification {\n  vertical-align: middle;\n  font-weight: normal;\n  font-size: 14px;\n  font-family: \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  line-height: 16px;\n}\n/* Sidebar - Edit */\na.sidebar-tag span.sidebar-tag-archive {\n  cursor: pointer;\n  background: #b3b3b3;\n  color: #ffffff;\n  vertical-align: middle;\n  padding: 5px;\n  position: absolute;\n  top: 4px;\n  right: 0px;\n  font-size: 8px;\n  line-height: 8px;\n  border-radius: 10px;\n}\na.sidebar-tag span.sidebar-tag-archive:hover {\n  background: #fbb03b;\n}\n/* Sidebar - Drag & Drop */\n.sidebar-tags-draggable {\n  border-radius: 3px;\n}\n.sidebar-tags-draggable-hover {\n  transition-duration: 0.3s;\n}\n.sidebar-tags-draggable-active,\n.sidebar-tags-draggable-active.show-subtags {\n  background: #cccccc;\n  transition-duration: 0.3s;\n}\n.sidebar-tags-draggable-highlight {\n  transition-duration: 0.3s;\n}\n/* Sidebar - Sortable */\n.sidebar-tags-sortable {\n  height: 29px;\n  padding: 5px 10px;\n  margin: 4px 0;\n  border-radius: 3px;\n  background: #cccccc;\n}\n/* Sidebar - Tag Drag */\n.sidebar-tag-drag {\n  background: #ffffff;\n  border: 1px solid #b3b3b3;\n  border-radius: 4px;\n  padding: 5px 10px;\n  font-size: 14px;\n  font-weight: bold;\n  z-index: 9999;\n}\n/* Attachments (used in both Compose & Thread) */\n.attachment-image {\n  display: block;\n  width: 150px;\n  height: 125px;\n  border: 1px solid #b3b3b3;\n  margin: 0px;\n  padding: 0px;\n  overflow: hidden;\n  vertical-align: text-top;\n  -webkit-touch-callout: none;\n  -webkit-user-select: none;\n  -khtml-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n.attachment-image div.preview {\n  display: block;\n  width: 100%;\n  height: 125px;\n  background-size: cover;\n  background-position: center center;\n  background-repeat: no-repeat;\n}\n.attachment {\n  display: block;\n  width: 150px;\n  height: 125px;\n  margin: 0px;\n  padding: 0px;\n  border: 1px solid #b3b3b3;\n  text-align: center;\n  vertical-align: text-top;\n  -webkit-touch-callout: none;\n  -webkit-user-select: none;\n  -khtml-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n.attachment div.preview {\n  height: 97px;\n  display: inline-table;\n}\n.attachment div.preview span.icon-mime {\n  width: 60px;\n  display: table-cell;\n  vertical-align: middle;\n  color: #b3b3b3;\n  font-size: 40px;\n  line-height: 40px;\n}\n.attachment div.preview span.extension {\n  display: table-cell;\n  vertical-align: middle;\n  text-align: center;\n  color: #b3b3b3;\n  text-transform: uppercase;\n  font-size: 18px;\n  font-weight: bold;\n}\n.attachment div.filename {\n  width: 100%;\n  height: 12px;\n  display: block;\n  padding: 7.5px 0;\n  box-sizing: content-box;\n  border-top: 1px solid #b3b3b3;\n  background: #e9e9e9;\n  font-size: 12px;\n  font-weight: bold;\n  line-height: 12px;\n  color: #4d4d4d;\n}\n.attachment:hover {\n  background: #e9e9e9;\n  border: 1px solid #b3b3b3;\n}\n.attachment:hover div.filename {\n  color: #4d4d4d;\n}\n.attachment:hover span.icon-mime,\n.attachment:hover span.extension {\n  color: #4d4d4d;\n}\n.attachment-progress-bar {\n  padding-left: 10px;\n}\n/* Crypto */\n.compose-crypto-signature.none {\n  color: #b3b3b3;\n}\n.compose-crypto-signature.signed {\n  color: #4b9441;\n}\n.compose-crypto-signature.error {\n  color: #be1c21;\n}\n.compose-crypto-encryption.none {\n  color: #b3b3b3;\n}\n.crypto-none,\n.compose-crypto-encryption.none {\n  color: #b3b3b3;\n}\n.crypto-warning,\n.compose-crypto-encryption.cannot {\n  color: #fbb03b;\n}\n.crypto-encrypted,\n.compose-crypto-encryption.encrypted {\n  color: #4b9441;\n}\n.crypto-color-error,\n.compose-crypto-encryption.error {\n  color: #be1c21;\n}\n/* Compose */\n.form-compose {\n  padding: 15px 20px 10px 20px;\n  background: #e9e9e9;\n}\n.form-compose label {\n  display: block;\n  margin-bottom: 3px;\n  font-size: 14px;\n  font-weight: normal;\n}\n.form-compose label a {\n  font-size: 14px;\n  font-weight: normal;\n}\n.form-compose label a:hover {\n  color: #be1c21;\n}\n.form-compose label span,\n.form-compose label a.compose-hide-field {\n  color: #b3b3b3;\n}\n.compose-headers,\n.compose-subject,\n.compose-options,\n.compose-body {\n  width: 100%;\n  max-width: 50em;\n}\n/* Compose - Headers */\n.compose-headers {\n  padding-bottom: 15px;\n}\na.compose-show-field {\n  font-family: \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-size: 18px;\n  font-style: normal;\n  font-variant: normal;\n  font-weight: bold;\n  color: #4d4d4d;\n  margin-left: 10px;\n}\n/* Compose - Subject */\n.compose-subject input[type=text] {\n  width: 100%;\n  margin-bottom: 10px;\n  padding: 10px;\n  border: 1px solid #cccccc;\n  border-radius: 4px;\n  box-sizing: border-box;\n  font-family: Helvetica, Arial, sans-serif;\n  font-size: 14px;\n  line-height: 18px;\n}\n.compose-subject input[type=text]:focus {\n  outline: none;\n  border: 1px solid #b3b3b3;\n  box-shadow: 0 0 3px #b3b3b3;\n  -moz-box-shadow: 0 0 3px #b3b3b3;\n  -webkit-box-shadow: 0 0 3px #b3b3b3;\n}\n/* Compose - Options */\n.compose-options {\n  margin-top: -3px;\n  margin-bottom: 0px;\n  padding: 0 0 0px 0;\n}\n.compose-options-size {\n  font-size: 14px;\n  font-weight: normal;\n  line-height: 14px;\n  color: #b3b3b3;\n}\n.compose-options-crypto {\n  width: 74px;\n  position: relative;\n  top: 2px;\n  display: inline-block;\n  text-align: center;\n  background: #ffffff;\n  border-left: 1px solid #cccccc;\n  border-bottom: 1px solid #cccccc;\n  border-right: 1px solid #cccccc;\n  border-bottom-left-radius: 4px;\n  border-bottom-right-radius: 4px;\n  float: right;\n  padding-top: 5px;\n  font-size: 14px;\n  font-weight: normal;\n  line-height: 14px;\n  color: #b3b3b3;\n}\n.compose-options-crypto .compose-message-settings,\n.compose-options-crypto .compose-crypto-encryption,\n.compose-options-crypto .compose-crypto-signature {\n  display: inline-block;\n  margin: 10px 6px;\n}\n.compose-message-settings:hover,\n.compose-crypto-signature:hover,\n.compose-crypto-encryption:hover {\n  cursor: pointer;\n}\n.compose-options ul {\n  display: inline-block;\n  margin-bottom: 0px;\n  padding: 0px;\n}\n.compose-options ul li {\n  margin: 0px 10px 0px 0px;\n  font-size: 14px;\n  font-weight: normal;\n  line-height: 14px;\n  color: #b3b3b3;\n}\n.compose-options ul li a,\n.compose-options label.right,\n.compose-options a.right {\n  font-size: 14px;\n  font-weight: normal;\n  line-height: 14px;\n  color: #b3b3b3;\n}\n.compose-options ul li a:hover,\n.compose-options label.right:hover,\n.compose-options a.right:hover {\n  color: #4d4d4d;\n}\n.compose-options label.right {\n  position: relative;\n  top: 9px;\n  right: 10px;\n  padding-bottom: 0px;\n  margin-bottom: 0px;\n  cursor: pointer;\n  font-style: italic;\n}\n.compose-options a.right {\n  position: relative;\n  top: 9px;\n  right: 5px;\n  margin-left: 10px;\n}\n.compose-to-summary {\n  max-width: 500px;\n  word-wrap: normal;\n  word-break: normal;\n  white-space: nowrap;\n  overflow: hidden;\n}\n/* Compose - Body */\n.compose-body {\n  border-radius: 4px;\n  background: #ffffff;\n  border: 1px solid #cccccc;\n  padding-bottom: 0px;\n}\n.compose-body textarea {\n  width: 97%;\n  display: block;\n  min-height: 75px;\n  margin: 12px auto 0px auto;\n  padding: 0px 0px 12px 0px;\n  border: 0px;\n  border-radius: 4px;\n  -webkit-box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  box-sizing: border-box;\n  font-family: Helvetica, Arial, sans-serif;\n  font-size: 14px;\n  line-height: 18px;\n  resize: none;\n}\n.compose-body textarea:focus {\n  outline: none;\n}\n/* Compose - Attachments */\ndiv.compose-attachments {\n  margin-top: 0px;\n  padding: 0px 10px 10px 10px;\n  font-size: 14px;\n}\ndiv.compose-attachments ul.horizontal {\n  margin-bottom: 0px;\n}\nul.compose-attachments li {\n  margin-right: 15px;\n  margin-bottom: 15px;\n}\na.compose-attachment-remove {\n  float: right;\n  display: inline;\n  position: relative;\n  top: 5px;\n  right: 5px;\n  padding: 4px;\n  color: #b3b3b3;\n  font-size: 14px;\n  line-height: 14px;\n  border-radius: 5px;\n}\na.compose-attachment-remove:hover {\n  background: #be1c21;\n  color: #ffffff;\n}\n/* Attachment Browser */\n.attachment-browswer-unsupported {\n  color: #b3b3b3;\n  font-style: italic;\n}\n.attachment-browswer-unsupported a {\n  color: #b3b3b3;\n}\nlabel.compose-attach-key {\n  color: #4d4d4d;\n  cursor: pointer;\n  font-weight: bold;\n}\nlabel.compose-attach-key:hover,\nlabel.compose-attach-key:hover span.icon-key {\n  color: #be1c21;\n}\nlabel.compose-attach-key span.icon-key {\n  color: #4d4d4d;\n  font-weight: normal;\n}\n/* Compose - From Menu */\n.compose-from-select {\n  display: block;\n  width: 18em;\n  background: #ffffff;\n  border: 1px solid #cccccc;\n  border-radius: 3px;\n  padding: 4px 6px;\n  line-height: 14px;\n}\n.compose-from-select:hover {\n  cursor: pointer;\n  background: #4d4d4d;\n  color: #ffffff;\n}\n.compose-from-select:hover .name {\n  color: #ffffff;\n}\n.compose-from-selected {\n  display: inline-block;\n  vertical-align: middle;\n}\n.compose-from-caret {\n  display: inline-block;\n  padding-left: 5px;\n  float: right;\n}\n.compose-from-selected .avatar,\n.compose-from .avatar {\n  width: 24px;\n  display: inline-block;\n  margin-right: 5px;\n  float: left;\n}\n.compose-from-selected .avatar img {\n  width: 24px;\n  border-radius: 3px;\n}\n.compose-from-selected .name-and-address,\n.compose-from .name-and-address {\n  float: left;\n}\n.compose-from-selected .name {\n  display: inline-block;\n  vertical-align: middle;\n  font-size: 14px;\n  font-family: Helvetica, Arial, sans-serif;\n  font-weight: bold;\n  line-height: 14px;\n}\n.compose-from-selected .address {\n  font-size: 12px;\n  font-family: Helvetica, Arial, sans-serif;\n  font-weight: normal;\n  color: #cccccc;\n  line-height: 12px;\n}\n.compose-from {\n  height: 32px;\n  font-size: 14px;\n  font-weight: bold;\n  margin: 0px;\n  display: block;\n  width: 18em;\n  background: #ffffff;\n  border: 1px solid #cccccc;\n  border-radius: 3px;\n  padding: 0px;\n  line-height: 14px;\n}\n.compose-from .avatar {\n  width: 32px;\n}\n.compose-from .avatar img {\n  width: 32px;\n  border-radius: 3px;\n}\n.compose-from .name {\n  display: inline-block;\n  vertical-align: text-top;\n  font-size: 14px;\n  font-weight: bold;\n  line-height: 14px;\n}\n.compose-from .address {\n  color: #b3b3b3;\n  font-family: Helvetica, Arial, sans-serif;\n  font-size: 12px;\n  font-weight: normal;\n}\n/* Compose - Actions */\n.compose-actions {\n  width: 100%;\n  max-width: 41em;\n  margin-top: -5px;\n  padding-bottom: 15px;\n}\n.compose-buttons {\n  text-align: right;\n}\n.compose-buttons button {\n  margin-left: 10px;\n}\n.compose-actions ul.dropdown-menu {\n  padding: 0;\n}\n.compose-actions ul.dropdown-menu li {\n  margin-bottom: 0px;\n}\n.compose-actions ul.dropdown-menu li a {\n  border-radius: 0px;\n  border: 0px;\n  border-bottom: 1px solid #b3b3b3;\n}\n/* Composer - Changes for in-pile composer */\n.form-compose .pile-message .subject {\n  position: relative;\n  padding: 0 4px 0 0;\n}\n/*** WORKS IN PROGRESS - COMMENTED OUT\n\n  .pile-message .compose-subject-container {\n    display: table;\n    width: 100%;\n  }\n\n  .pile-message .compose-subject-container h3 {\n    display: table-cell;\n    margin-bottom: 5px;\n    padding: 10px 10px 0 0;\n    box-sizing: border-box;\n    font-size: 16px;\n    line-height: 18px;\n    color: @gray;\n  }\n\n  .pile-message .compose-subject-container div.compose-subject {\n    display: table-cell;\n  }\n\n  .pile-message .compose-subject input[type=text] {\n    margin-bottom: 5px;\n  }\n\n  .pile-message .compose-headers {\n    padding-bottom: 0;\n  }\n\n  .pile-message div.thread-reply {\n    border: 0;\n  }\n\n  .pile-message .compose-actions {\n    display: table;\n    border-spacing: 0;\n    border-collapse: collapse;\n    width: 100%;\n    margin-bottom: 5px;\n    margin-top: 5px;\n  }\n\n  .pile-message .compose-actions .dropdown,\n  .pile-message .compose-actions .compose-buttons,\n  .pile-message .compose-actions .compose-options-crypto {\n    display: table-cell;\n    white-space: nowrap;\n    vertical-align: top;\n    padding: 0;\n  }\n\n  .pile-message .compose-actions .dropdown {\n    position: relative;\n    width: 99%;\n  }\n\n  .pile-message .compose-actions .dropdown .dropdown-toggle {\n    border-radius: 4px;\n    padding: 2px 5px;\n    margin: 1px 5px 0 0;\n    width: 100%;\n  }\n\n  .pile-message .compose-body {\n    padding-right: 2px;\n    position: relative;\n    border-bottom-right-radius: 0px;\n    width: auto;\n  }\n\n  .pile-message .compose-options-crypto {\n    width: 111px;\n    position: relative;\n    display: inline-block;\n    text-align: center;\n    background: @white;\n    padding-top: 9px;\n    margin-top: -7px;\n    margin-left: 10px;\n    border-left: 1px solid @grayMid;\n    border-bottom: 1px solid @grayMid;\n    border-right: 1px solid @grayMid;\n    border-bottom-left-radius: 4px;\n    border-bottom-right-radius: 4px;\n    border-top: 0;\n    border-top-left-radius: 0;\n    border-top-right-radius: 0;\n    .compose-options-size();\n    float: right;\n  }\n\n***** END OF WIP SECTION ************* */\n/* Contact - List */\n.contact-card-avatar {\n  display: inline-block;\n  width: 45px;\n  margin-right: 10px;\n}\n.contact-card-avatar img {\n  width: 45px;\n  border-radius: 3px;\n}\n.contact-card-name {\n  max-width: 100px;\n  display: inline-block;\n  border: 0px solid #ffffff;\n  margin-top: 0px;\n  padding-top: 0px;\n  font-size: 14px;\n  line-height: 18px;\n  vertical-align: top;\n  word-break: break-word;\n}\n.contact-card-name:hover {\n  color: #337fb2;\n}\n.contact-card-checkbox {\n  margin-top: 0px;\n  vertical-align: top;\n}\n/* Contact - View */\n#contact-view {\n  margin-bottom: 100px;\n}\n#contact-view .contact-avatar {\n  display: block;\n  margin-right: 20px;\n  border-radius: 3px;\n}\n#contact-view .contact-name {\n  font-family: Mailpile-700, \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  margin-bottom: 5px;\n}\n#contact-view .contact-subname {\n  display: block;\n  float: left;\n  color: #b3b3b3;\n}\n#contact-view h5.contact-key {\n  width: 315px;\n}\n#contact-view .icon-fingerprint {\n  font-size: 36px;\n  margin-right: 10px;\n}\n/* Contact - Details */\n.contact-detail li {\n  background: #ffffff;\n  margin-bottom: 20px;\n  padding: 15px;\n  border: 1px solid #b3b3b3;\n  border-radius: 3px;\n}\n.contact-detail li h5 {\n  margin-bottom: 13.33333333px;\n}\n.contact-detail a span.contact-detail-light {\n  color: #b3b3b3;\n  font-weight: normal;\n}\n.contact-detail a:hover span.contact-detail-light,\n.contact-detail a span.contact-detail-light:hover {\n  color: #be1c21;\n}\n.contact-key-details {\n  margin-top: 20px;\n  font-size: 14px;\n}\n.contact-tag-filter {\n  background: #ffffff;\n  border: 1px solid #b3b3b3;\n  border-radius: 3px;\n  padding: 15px 0;\n}\n.contact-tag-filter li {\n  display: inline;\n  margin: 20px;\n}\n/* Contacts - Conversation */\n.contact-conversation-avatar {\n  width: 45px;\n  display: inline-block;\n  border-radius: 3px;\n}\n.contact-conversation-name {\n  display: inline-block;\n  font-size: 18px;\n  font-weight: bold;\n  line-height: 18px;\n}\n.contact-conversation-address {\n  display: inline-block;\n  font-size: 14px;\n  font-weight: normal;\n  font-family: Helvetica, Arial, sans-serif;\n  line-height: 14px;\n  color: #cccccc;\n}\n/* Contacts - Add */\n.contact-add-fields {\n  float: left;\n  margin-right: 45px;\n}\n.contact-add-search-keyserver {\n  float: left;\n}\n.contact-add-search-item {\n  margin-bottom: 15px;\n  padding: 10px 15px;\n  border-radius: 5px;\n}\n.contact-add-search-item:hover {\n  cursor: pointer;\n  background: #cccccc;\n}\n.contact-add-search-item .name {\n  display: block;\n  font-size: 18px;\n  font-weight: bold;\n}\n.contact-add-search-item .email,\n.contact-add-search-item .key,\n.contact-add-search-item .keysize,\n.contact-add-search-item .keytype,\n.contact-add-search-item .created {\n  display: block;\n  font-size: 14px;\n  font-family: Helvetica, Arial, sans-serif;\n}\n/* Modals */\n.modal-title .title {\n  text-transform: capitalize;\n}\n.modal-body-light-gray {\n  background: #e9e9e9;\n}\n#modal-full .content-normal {\n  padding: 0;\n  margin: 0;\n}\n#modal-full h1 {\n  display: none;\n}\n/* Modal - Table default style */\ntable.default-table {\n  width: 100%;\n  background: #ffffff;\n  border-top: 1px solid #cccccc;\n  border-right: 1px solid #cccccc;\n  border-bottom: 0px;\n  border-left: 1px solid #cccccc;\n  border-radius: 3px;\n}\ntable.default-table tr {\n  width: 100%;\n}\ntable.default-table tr:hover {\n  cursor: pointer;\n  background: #d2e3f7;\n}\ntable.default-table td {\n  border-top: 0px;\n  border-bottom: 1px solid #cccccc;\n  padding: 7.5px;\n  font-style: italic;\n  color: #4d4d4d;\n  padding-left: 10px;\n}\ntr.modal-tag-picker-item td {\n  border-top: 0px;\n  border-bottom: 1px solid #cccccc;\n  padding: 7.5px;\n}\ntr.modal-tag-picker-item td.tag span.text {\n  font-weight: bold;\n}\ntr.modal-tag-picker-item td.selection {\n  color: #b3b3b3;\n  font-style: italic;\n  padding-right: 0px;\n}\ntr.modal-tag-picker-item td.checkbox {\n  width: 30px;\n}\n/* Modal - Results from crypto searchkey command */\n.searchkey-result-item {\n  list-style-type: none;\n  padding: 15px;\n  border: 1px solid #cccccc;\n  border-radius: 3px;\n  margin-bottom: 20px;\n}\n.searchkey-result-item:hover {\n  background: #e9e9e9;\n}\n.searchkey-result-item .avatar {\n  display: inline-block;\n  width: 45px;\n  margin-right: 10px;\n}\n.searchkey-result-item .avatar img {\n  width: 45px;\n  border-radius: 3px;\n}\n.searchkey-result-item .name {\n  width: 200px;\n  display: inline-block;\n  font-weight: bold;\n  word-break: break-word;\n  color: #4d4d4d;\n  vertical-align: top;\n}\n.searchkey-result-item .name span {\n  display: inline-block;\n  color: #b3b3b3;\n  font-size: 12px;\n  font-weight: normal;\n}\n.searchkey-result-item .icon-fingerprint {\n  display: inline-block;\n  font-size: 30px;\n  line-height: 30px;\n  vertical-align: top;\n}\n.searchkey-result-item .fingerprint {\n  display: inline-block;\n  width: 200px;\n  vertical-align: top;\n}\n.searchkey-result-details {\n  font-size: 12px;\n  line-height: 18px;\n}\n.searchkey-result-details table {\n  width: 100%;\n  border: 0px;\n  background: transparent;\n}\n.searchkey-result-details table tr:hover {\n  background: transparent;\n}\n.searchkey-result-details table td {\n  width: 150px;\n  border: 0px;\n  padding: 0px 15px 0px 0px;\n  font-size: 12px;\n}\n.searchkey-result-score {\n  padding: 5px 3px 0 3px;\n  font-weight: normal;\n}\n.searchkey-result-score:hover {\n  opacity: 0.60;\n}\n.searchkey-result-score:hover em,\n.searchkey-result-score:active em,\n.searchkey-result-score:visited em {\n  color: #4d4d4d;\n}\n/* Search */\n#button-search-options {\n  background: red;\n  display: inline-block;\n  height: 32px;\n  position: relative;\n  left: -25px;\n  top: -10px;\n}\n#button-search-options:hover .icon-arrow-down {\n  color: #b3b3b3;\n}\n#button-search-options .icon-arrow-down {\n  position: relative;\n  left: 0px;\n  top: 10px;\n  font-size: 12px;\n  color: #cccccc;\n}\n#search-params {\n  position: absolute;\n  top: 50px;\n  left: 225px;\n  background: #ffffff;\n  border: 1px solid #b3b3b3;\n  z-index: 1000;\n}\n#search-params li {\n  margin: 15px;\n}\n#search-params a {\n  padding: 5px 10px;\n  background: #e9e9e9;\n  color: #4d4d4d;\n  font-family: \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-size: 14px;\n  border-radius: 3px;\n  border: 1px solid #b3b3b3;\n}\n#search-params a:hover {\n  background-color: #c3c3c3;\n}\n/* Pile */\n#pile-results {\n  table-layout: fixed;\n  width: 100%;\n  min-width: 800px;\n  border-collapse: collapse;\n  border: 0px;\n}\n#pile-results tr.result {\n  background: #ffffff;\n}\n#pile-results tr.result:hover {\n  background: #e9e9e9;\n}\n#pile-results tr.result-hover {\n  background: #e9e9e9;\n}\n#pile-results tr.result-on {\n  background: #faf7d0;\n}\n#pile-results tr.result-on:hover {\n  background: #f8f3b9;\n}\n#pile-results td {\n  vertical-align: top;\n  position: relative;\n  border-spacing: 0px;\n  border-top: 0px;\n  border-right: 0px;\n  border-bottom: 1px solid #cccccc;\n  border-left: 0px;\n  box-sizing: padding-box;\n  font-family: \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-size: 14px;\n  line-height: 14px;\n  padding-left: 0px;\n  padding-right: 0px;\n}\n#pile-results.comfy td {\n  padding-top: 13px;\n  padding-bottom: 11px;\n}\n#pile-results.cozy td {\n  padding-top: 9px;\n  padding-bottom: 6px;\n}\n#pile-results.snug td {\n  padding-top: 5px;\n  padding-bottom: 2px;\n}\n#pile-results tr a {\n  font-size: 14px;\n  line-height: 14px;\n  font-weight: normal;\n  color: inherit;\n}\n#pile-results tr.in_new a {\n  font-weight: bold;\n}\n#pile-results td span.pile-message-tag {\n  font-weight: bold;\n  margin-right: 5px;\n  cursor: pointer;\n}\n#pile-results td.draggable {\n  width: 12px;\n  cursor: move;\n}\n#pile-results td.draggable:hover {\n  cursor: move;\n}\n#pile-results td.message-nav {\n  text-align: center;\n  white-space: nowrap;\n}\n#pile-results td.avatar a {\n  display: block;\n  text-align: center;\n}\n#pile-results td.avatar a img {\n  display: inline-block;\n  border-radius: 2px;\n}\n#pile-results.comfy td.avatar,\n#pile-results.cozy td.avatar {\n  padding-top: 6px;\n  padding-bottom: 5px;\n}\n#pile-results.snug td.avatar {\n  padding-top: 4px;\n  padding-bottom: 3px;\n}\n#pile-results.comfy td.message-nav,\n#pile-results.comfy td.avatar {\n  width: 32px;\n  padding-right: 8px;\n}\n#pile-results.cozy td.message-nav,\n#pile-results.cozy td.avatar {\n  width: 24px;\n  padding-right: 6px;\n}\n#pile-results.snug td.message-nav,\n#pile-results.snug td.avatar {\n  width: 18px;\n  padding-right: 4px;\n}\n#pile-results.comfy td.avatar a img {\n  width: 24px;\n  height: 24px;\n}\n#pile-results.cozy td.avatar a img {\n  width: 18px;\n  height: 18px;\n}\n#pile-results.snug td.avatar a img {\n  width: 14px;\n  height: 14px;\n}\n#pile-results td.message-nav a.icon,\n#pile-results td.message-nav span.icon,\n#pile-results td.message-nav {\n  font-size: 18px;\n  line-height: 18px;\n  color: #cccccc;\n}\n#pile-results td.message-nav span.icon {\n  font-size: 16px;\n}\n#pile-results.snug td.message-nav a.icon,\n#pile-results.snug td.message-nav span.icon,\n#pile-results.snug td.message-nav {\n  font-size: 16px;\n  line-height: 16px;\n}\n#pile-results.snug td.message-nav span.icon {\n  font-size: 14px;\n}\n#pile-results td.people {\n  width: 279px /* 255+24 */;\n  overflow-x: hidden;\n  word-wrap: normal;\n  word-break: normal;\n}\n#pile-results td.people a {\n  display: inline-block;\n  white-space: nowrap;\n  border: 0;\n  margin: 0;\n  background: none;\n}\n#pile-results td.people span.rcpt-count {\n  color: #4d4d4d;\n  font-size: 11px;\n  font-weight: bold;\n}\n#pile-results td.people span.conversation-count {\n  text-align: center;\n  vertical-align: top;\n  position: relative;\n  top: 1px;\n  left: 3px;\n  padding: 4px 8px;\n  box-sizing: border-box;\n  color: #4d4d4d;\n  background: #cccccc;\n  border-radius: 3px;\n  font-size: 11px;\n  font-weight: bold;\n  line-height: 11px;\n}\n#pile-results.cozy td.people span.conversation-count {\n  top: 2px;\n  padding: 3px 6px;\n}\n#pile-results.snug td.people span.conversation-count {\n  top: 2px;\n  padding: 2px 4px;\n}\n#pile-results td.from .icon-reply,\n#pile-results td.from .icon-forward,\n#pile-results td.from .icon-compose {\n  position: relative;\n  top: 0px;\n  left: 4px;\n  color: #cccccc;\n}\n#pile-results td.subject {\n  overflow: hidden;\n  word-wrap: normal;\n  word-break: normal;\n}\n#pile-results td.subject a.item-subject {\n  display: block;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n/* This is because relative % positioning inside a table doesn't work\n   * because the table does not yet know its own size.\n   */\n@media only screen and (max-width: 1500px) {\n  #pile-results td.subject .message-container {\n    max-width: 700px;\n  }\n}\n@media only screen and (max-width: 1300px) {\n  #pile-results td.subject .message-container {\n    max-width: 550px;\n  }\n}\n@media only screen and (max-width: 1150px) {\n  #pile-results td.subject .message-container {\n    max-width: 370px;\n  }\n}\n@media only screen and (max-width: 950px) {\n  #pile-results td.subject .message-container {\n    max-width: 310px;\n  }\n}\n@media only screen and (max-width: 800px) {\n  #pile-results td.subject .message-container {\n    max-width: 250px;\n  }\n}\n@media only screen and (max-width: 700px) {\n  #pile-results td.subject .message-container {\n    max-width: 214px;\n  }\n}\n@media only screen and (max-width: 480px) {\n  #pile-results td.subject {\n    max-width: 70%;\n  }\n  #pile-results td.subject .message-container {\n    max-width: 70%;\n  }\n}\n#pile-results td.date {\n  width: 80px;\n  text-align: right;\n  white-space: nowrap;\n  color: #b3b3b3;\n}\n#pile-results td.date.wide-date {\n  width: 140px;\n}\n#pile-results td.checkbox {\n  width: 45px;\n  text-align: center;\n  padding-top: 2px;\n  padding-bottom: 0px;\n  color: #b3b3b3;\n}\n#pile-results.cozy td.checkbox {\n  padding-top: 6px;\n}\n#pile-results.comfy td.checkbox {\n  padding-top: 10px;\n}\n/* Pile Inlined Messages */\n#pile-results tr.full-message td {\n  background: #ffffff;\n}\n#pile-results tr.full-message {\n  border-top: 6px solid;\n  border-bottom: 6px solid;\n  border-color: #e9e9e9;\n}\n#pile-results tr.full-message td.draggable {\n  border-color: #e9e9e9;\n  background: #e9e9e9;\n}\n#pile-results tr.result-on.full-message,\n#pile-results tr.result-on.full-message td.draggable {\n  border-color: #faf7d0;\n  background: #faf7d0;\n}\n#pile-results tr.result-on.full-message:hover,\n#pile-results tr.result-on.full-message:hover td.draggable {\n  background: #f8f3b9;\n  border-color: #f8f3b9;\n}\n#pile-results .message-details,\n#pile-results .thread-container {\n  position: relative;\n  overflow: hidden;\n  margin-bottom: 2em;\n  padding: 4px 4px;\n}\n#pile-results td.subject .message-container h3,\n#pile-results .message-details h3,\n#pile-results .thread-container h3 {\n  text-align: left;\n  font-size: 16px;\n  line-height: 18px;\n  font-weight: bold;\n  padding: 0 2px;\n  margin: 0 10px 6px 10px;\n  color: #b3b3b3;\n}\n#pile-results .message-nav {\n  color: #b3b3b3;\n}\n#pile-results .message-details .header-raw,\n#pile-results .message-details .header-cooked h3,\n#pile-results .message-details .header-cooked .header-value,\n#pile-results .thread-container .thread-message {\n  display: block;\n  width: 264px;\n  margin: 0 10px;\n  padding: 0px 2px;\n  border: 0;\n  word-wrap: normal;\n  word-break: normal;\n  text-align: left;\n  overflow: hidden;\n}\n#pile-results .thread-container {\n  display: block;\n  width: 245px;\n  margin: 10px 4px 0 4px;\n  padding: 0;\n  border: 0;\n  overflow: hidden;\n}\n#pile-results .thread-container .thread-message {\n  overflow: visible;\n  margin: 0 0 0 16px;\n  width: 225px;\n}\n#pile-results .message-details .header-cooked {\n  margin-bottom: 5px;\n}\n#pile-results .message-details .header-cooked .header-value {\n  padding: 2px 10px;\n}\n#pile-results .thread-container .thread-message:hover {\n  background: #e9e9e9;\n  border-radius: 3px;\n}\n#pile-results .message-details .header-raw.hide,\n#pile-results .message-details .header-cooked.hide,\n#pile-results .thread-container .thread-message.hide {\n  display: none;\n}\n#pile-results .thread-container .thread-selected:hover,\n#pile-results .thread-container .thread-selected {\n  background: #b3b3b3;\n  border-radius: 3px;\n}\n#pile-results .message-details .address-avatar img,\n#pile-results .thread-message .thread-avatar img {\n  margin: 0 4px -3px 0;\n  padding: 0;\n  border: 0;\n  width: 30px;\n  height: 30px;\n}\n#pile-results .thread-message .thread-tree {\n  margin: 0;\n  padding: 0;\n  border: 0;\n  position: relative;\n  top: 4px;\n  font-size: 40px;\n  font-family: monospace;\n  line-height: 34px;\n  color: #b3b3b3;\n  white-space: pre;\n}\n#pile-results .thread-container a.show-hide {\n  color: #b3b3b3;\n  font-weight: bold;\n  margin: 10px 0 0 20px;\n}\n#pile-results .thread-container .thread-selected .thread-tree {\n  color: #4d4d4d;\n}\n#pile-results .message-details .header-value,\n#pile-results .thread-message .thread-info {\n  margin: 0;\n  padding: 0;\n  border: 0;\n  position: relative;\n  display: inline-block;\n  height: 28px;\n}\n#pile-results .thread-unread .thread-from {\n  font-weight: bold;\n}\n#pile-results .header-value .address-name,\n#pile-results .header-value .address-email,\n#pile-results .thread-info .thread-from,\n#pile-results .thread-info .thread-details {\n  position: absolute;\n  margin: 0;\n  padding: 0;\n  border: 0;\n  display: inline-block;\n  font-size: 14px;\n}\n#pile-results .header-value .address-name {\n  top: 4px;\n}\n#pile-results .thread-info .thread-from {\n  top: 1px;\n}\n#pile-results .header-value .address-email,\n#pile-results .thread-info .thread-details {\n  bottom: 0px;\n  margin: 0 0 -1px 2px;\n  color: #4d4d4d;\n  font-size: 12px;\n  line-height: 12px;\n}\n#pile-results .header-value .address-email {\n  color: #b3b3b3;\n  margin-bottom: 0px;\n}\n#pile-results .thread-info.thread-simple .thread-from {\n  top: 9px;\n  font-size: 14px;\n}\n#pile-results .thread-info.thread-simple .thread-details {\n  display: none;\n}\n#pile-results .message-details .header-to .header-value,\n#pile-results .message-details .header-from .header-value {\n  height: 35px;\n}\n#pile-results .message-details .header-to .address-name,\n#pile-results .message-details .header-from .address-name {\n  font-size: 18px;\n  line-height: 18px;\n}\n#pile-results td.subject .message-container .message-subject {\n  font-size: 22px;\n  line-height: 22px;\n  margin: 0px 10px 4px 12px;\n  display: block;\n}\n#pile-results .message-details .header-to .address-email,\n#pile-results .message-details .header-from .address-email {\n  margin-bottom: 2px;\n}\n#pile-results .message-details .header-to .address-avatar img,\n#pile-results .message-details .header-from .address-avatar img {\n  width: 36px;\n  height: 36px;\n}\n#pile-results .message-details .header-from ul.message-sender-actions {\n  display: inline-block;\n  margin: 0;\n  padding: 2px;\n  white-space: nowrap;\n  background: #ffffff;\n  transition: opacity 1s;\n  opacity: 0;\n}\n#pile-results .message-details .header-from .message-sender-actions li {\n  display: inline-block;\n}\n#pile-results .message-details .header-from:hover .message-sender-actions {\n  transition: opacity 1s;\n  opacity: 1.0;\n}\n#pile-results .message-details .header-to {\n  opacity: 0.8;\n}\n#pile-results .message-details .header-cc,\n#pile-results .message-details .header-bcc {\n  opacity: 0.6;\n}\n#pile-results .message-details .header-to:hover,\n#pile-results .message-details .header-cc:hover,\n#pile-results .message-details .header-bcc:hover {\n  opacity: 1.0;\n}\n#pile-results .message-details .header-value span.punct {\n  position: absolute;\n  opacity: 0;\n  transition: opacity 1s;\n}\n#pile-results .full-message div.crypto-and-tags {\n  white-space: normal;\n  padding-top: 2px;\n}\n#pile-results .display-attachments div.crypto-and-tags {\n  opacity: 0.33;\n}\n#pile-results .full-message div.crypto-and-tags .pile-message-tag,\n#pile-results .full-message div.crypto-and-tags .icon.crypto {\n  display: inline-block;\n  margin: 0 0 5px 0;\n  padding: 0;\n  border: 0;\n  font-size: 16px;\n  line-height: 16px;\n}\n#pile-results div.crypto-and-tags {\n  float: right;\n  text-align: right;\n}\n#pile-results .full-message div.crypto-and-tags .item-tags {\n  display: block;\n  margin-bottom: 0 0 5px 0;\n}\n#pile-results.snug div.crypto-and-tags {\n  padding-right: 4px;\n}\n#pile-results.cozy div.crypto-and-tags {\n  padding-right: 6px;\n}\n#pile-results.comfy div.crypto-and-tags {\n  padding-right: 8px;\n}\n#pile-results td.subject .message-container h3 {\n  display: inline-block;\n  margin-right: 5px;\n}\n#pile-results td.subject .message-container {\n  display: block;\n  max-width: 50em;\n  padding: 5px 2px 5px 0px;\n  white-space: normal;\n  line-height: 16px;\n  font-size: 14px;\n}\n#pile-results .full-message .pile-message-content {\n  margin: 5px 10px;\n  padding: 0 0 0 10px;\n  border: 0;\n}\n#pile-results .full-message .part-crypto-status {\n  border: 0;\n  padding: 0;\n  margin: 0 0 -4px -10px;\n}\n#pile-results .full-message .part-crypto-status .crypto-color-gray {\n  transition: all 1s ease-in;\n  opacity: 0;\n}\n#pile-results .full-message .part-crypto-status:hover .crypto-color-gray,\n#pile-results .full-message .pile-message-content div.crypto-changed .crypto-color-gray {\n  transition: all 1s ease-in;\n  opacity: 1;\n}\n#pile-results .full-message .part-crypto-status .inline-encryption-info .crypto-color-gray {\n  /* We need this so individual signature status align nicely */\n  display: none;\n}\n#pile-results .full-message .part-crypto-status:hover .inline-encryption-info .crypto-color-gray {\n  /* FIXME: Doesn't fade in, boo hoo */\n  display: inline-block;\n}\n#pile-results .full-message .pile-message-content div.crypto-changed {\n  margin: 10px 0 0 -10px;\n  padding-top: 1px;\n  border-top: 2px dotted;\n  border-top-color: #e9e9e9;\n}\n#pile-results .message-inline-crypto-info {\n  display: inline-block;\n  margin: 0 0 7px 0;\n  text-align: left;\n}\n#pile-results .full-message h3 {\n  position: relative;\n}\n#pile-results .full-message .message-metadata-crypto-info {\n  font-size: 14px;\n  padding: 0 5px;\n  display: inline-block;\n}\n#pile-results .full-message .message-metadata-crypto-info {\n  font-size: 14px;\n  padding: 0 5px;\n  display: inline-block;\n}\n#pile-results .message-subject-container .message-metadata-crypto-info {\n  padding-right: 0px;\n}\n#pile-results .full-message .crypto-color-gray .message-metadata-crypto-info,\n#pile-results .full-message .message-metadata-crypto-info.crypto-color-gray {\n  transition: opacity 1s;\n  opacity: 0;\n}\n#pile-results .full-message .crypto-color-gray:hover .message-metadata-crypto-info,\n#pile-results .full-message .message-subject-container:hover .message-metadata-crypto-info,\n#pile-results .full-message .thread-container:hover .message-metadata-crypto-info,\n#pile-results .full-message .header-cooked:hover .message-metadata-crypto-info {\n  transition: opacity 1s;\n  opacity: 1;\n}\n#pile-results .message-actions-padding {\n  display: block;\n  line-height: 18px;\n  margin: 0;\n  padding: 5px 0 0 0;\n  border: 0;\n}\n#pile-results td .bottom {\n  position: absolute;\n  bottom: 0;\n}\n#pile-results iframe.message-part-html {\n  width: 100%;\n  margin: 0;\n  padding: 0;\n}\n/* Pile Bottom */\n#pile-bottom {\n  margin: 15px 15px 0px 15px;\n}\n#pile-bottom h5 {\n  margin-top: 10px;\n  color: #4d4d4d;\n}\n#pile-bottom a {\n  margin-right: 15px;\n}\n#pile-empty {\n  padding: 15px;\n  background: #ffffff;\n  border-bottom: 1px solid #cccccc;\n  font-size: 14px;\n}\n#pile-empty-search-terms {\n  font-size: 24px;\n  font-weight: bold;\n  color: #b3b3b3;\n}\n/* Pile Speed */\n#pile-speed {\n  margin-bottom: 50px;\n  font-family: Mailpile-700, \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  color: #b3b3b3;\n}\n#pile-speed span {\n  font-size: 21px;\n  margin-right: 10px;\n  position: relative;\n  top: 3px;\n  left: 0px;\n}\n/* Pile  - Drag & Drop */\n#pile-results tr.result:hover td.draggable,\n#pile-results tr.result-hover td.draggable,\n#pile-results tr.result-on:hover td.draggable {\n  background: url('../img/draggable-pattern.png'), #ffffff;\n  opacity: 0.3;\n  filter: alpha(opacity=30);\n}\n.pile-results-drag {\n  background: #ffffff;\n  border: 1px solid #b3b3b3;\n  border-radius: 4px;\n  padding: 5px 10px;\n  z-index: 9999;\n  font-size: 14px;\n  font-weight: bold;\n}\n/* Thread */\n/* Thread Title */\n#thread-title {\n  display: table;\n  text-align: center;\n  padding: 0;\n}\n#thread-title h1 {\n  display: inline-block;\n  font-family: Mailpile-700, \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-size: 21px;\n  line-height: 24px;\n  color: #4d4d4d;\n}\n#thread-title ul li {\n  margin: 0 10px;\n}\n#thread-title ul a {\n  color: #b3b3b3;\n  font-family: Helvetica, Arial, sans-serif;\n  font-size: 12px;\n  font-weight: normal;\n}\n#thread-title ul a:hover {\n  color: #4d4d4d;\n}\n#thread-title div.thread-draggable {\n  width: 12px;\n  height: 100%;\n  display: table-cell;\n  background: url('../img/draggable-pattern.png'), #ffffff;\n  opacity: 0.3;\n  filter: alpha(opacity=30);\n}\n#thread-title div.thread-draggable:hover {\n  cursor: move;\n}\n#thread-title div.thread-details {\n  display: table-cell;\n}\n/* Thread New */\n.thread-snippet.new,\n.thread-message.new {\n  background: #e8f0fb;\n}\n.thread-snippet.new:hover,\n.thread-message.new:hover {\n  background: #d2e3f7;\n}\n.thread-snippet.new a.datetime,\n.thread-snippet.new a.datetime:visited,\n.thread-message.new a.datetime,\n.thread-message.new a.datetime:visited {\n  color: #4d4d4d;\n}\n/* Thread Snippet - Preview of messages */\n.thread-snippet {\n  background: #ffffff;\n  border-bottom: 1px solid #b3b3b3;\n}\n.thread-snippet:hover {\n  background: #d2e3f7;\n  cursor: pointer;\n}\n.thread-snippet:hover .feedback-expand {\n  display: block;\n}\n/* Thread Notification - Non message items */\n.thread-notification {\n  padding: 10px 15px;\n  background: #ffffff;\n  border-bottom: 1px solid #b3b3b3;\n  color: #b3b3b3;\n}\n.thread-notification span.instruction {\n  display: none;\n}\n.thread-notification a {\n  width: 100%;\n  height: 100%;\n  display: block;\n  color: #b3b3b3;\n  font-weight: normal;\n}\n.thread-notification:hover {\n  background: #d2e3f7;\n}\n.thread-notification:hover span.instruction {\n  display: inline;\n}\n.thread-notification:hover a {\n  color: #4d4d4d;\n}\n.thread-notification a:hover,\n.thread-notification:hover a:hover {\n  color: #337fb2;\n}\n/* Thread Message - View of full message */\n.thread-message {\n  background: #ffffff;\n  border-bottom: 1px solid #b3b3b3;\n}\n/* Thread Message Attachments */\ndiv.thread-message-attachments {\n  margin-top: 15px;\n  margin-left: 20px;\n  margin-bottom: 0px;\n}\nul.thread-message-attachments {\n  margin-left: 0px;\n  margin-bottom: 0px;\n}\nul.thread-message-attachments li {\n  margin-right: 15px;\n  margin-bottom: 15px;\n  padding: 0;\n}\n/* Thread - Reply */\ndiv.thread-reply {\n  border-bottom: 1px solid #b3b3b3;\n}\n/* Message */\n.crypto-color-gray {\n  color: #b3b3b3 !important;\n}\n.crypto-color-red {\n  color: #be1c21 !important;\n}\n.crypto-color-orange {\n  color: #fbb03b !important;\n}\n.crypto-color-blue {\n  color: #337fb2 !important;\n}\n.crypto-color-green {\n  color: #4b9441 !important;\n}\n.border-crypto-color-gray {\n  border-color: #b3b3b3 !important;\n}\n.border-crypto-color-red {\n  border-color: #be1c21 !important;\n}\n.border-crypto-color-orange {\n  border-color: #fbb03b !important;\n}\n.border-crypto-color-blue {\n  border-color: #337fb2 !important;\n}\n.border-crypto-color-green {\n  border-color: #4b9441 !important;\n}\n.message-metadata {\n  width: 100%;\n  max-width: 51em;\n  display: table;\n  margin: 0px;\n}\n.message-from-avatar {\n  display: inline-block;\n  width: 45px;\n  margin-right: 10px;\n  display: table-cell;\n  padding-top: 15px;\n  padding-left: 15px;\n  vertical-align: top;\n  text-align: left;\n}\n.message-from-avatar a img {\n  width: 45px;\n  border-radius: 3px;\n}\n.message-from {\n  min-width: 175px;\n  max-width: 200px;\n  display: table-cell;\n  padding-top: 13px;\n  padding-left: 15px;\n  vertical-align: text-top;\n  text-align: left;\n}\n.message-from a.name {\n  display: inline-block;\n  margin-top: 0px;\n  margin-bottom: 5px;\n  padding-top: 0px;\n  color: #4d4d4d;\n  font-size: 16px;\n  font-weight: bold;\n  line-height: 16px;\n}\n.message-from a:hover {\n  color: #337fb2;\n}\n.message-metadata-address {\n  font-size: 12px;\n  line-height: 12px;\n  color: #b3b3b3;\n  display: block;\n}\n.message-details {\n  width: 200px;\n  display: table-cell;\n  vertical-align: top;\n  text-align: right;\n  padding-top: 15px;\n  padding-left: 15px;\n}\n.message-details a.datetime,\n.message-details a.datetime:visited {\n  display: block;\n  margin-bottom: 5px;\n  text-align: right;\n  font-size: 14px;\n  font-weight: bold;\n  color: #b3b3b3;\n  line-height: 14px;\n}\n.message-details a.datetime:active,\n.message-details a.datetime:hover {\n  color: #337fb2;\n}\n.message-details span.icon {\n  display: inline-block;\n  margin-right: 5px;\n  font-size: 14px;\n  cursor: pointer;\n}\n.message-details .icon-circle-info {\n  color: #cccccc;\n}\n.message-details span.datetime.message {\n  color: #4d4d4d;\n}\n.message-details a.outbox {\n  background: #cccccc;\n  padding: 2px 5px;\n  border-radius: 3px;\n  font-size: 11px;\n  font-weight: bold;\n  color: #ffffff;\n}\n.feedback-expand {\n  display: none;\n  color: #b3b3b3;\n  font-family: Helvetica, Arial, sans-serif;\n  font-size: 11px;\n  font-weight: normal;\n  line-height: 12px;\n}\n.message-metadata-details {\n  display: none;\n  padding-bottom: 5px;\n}\n.message-metadata-details ul {\n  margin: 10px 0px 10px 20px;\n}\n.message-metadata-details ul li {\n  display: inline-block;\n  margin-right: 8px;\n  vertical-align: middle;\n  font-size: 14px;\n  font-weight: normal;\n  line-height: 14px;\n}\n.message-metadata-details a:hover {\n  color: #337fb2;\n}\n.message-metadata-details.border-bottom {\n  border-bottom: 1px solid #cccccc;\n}\n.message-metadata-contact {\n  color: #4d4d4d;\n  display: table;\n}\n.message-metadata-contact a {\n  font-size: 14px;\n  line-height: 14px;\n  display: table-cell;\n  vertical-align: middle;\n  padding-right: 5px;\n}\n.message-metadata-contact a span {\n  font-size: 11px;\n  font-weight: normal;\n  line-height: 11px;\n  color: #b3b3b3;\n}\n.message-metadata-contact a img {\n  width: 24px;\n  height: 24px;\n  border-radius: 3px;\n  margin-right: 5px;\n}\n.message-inline-crypto {\n  width: 95%;\n  margin-top: 10px;\n  margin-left: 20px;\n  margin-bottom: 0px;\n}\n.message-inline-crypto-info {\n  margin-right: 10px;\n}\n.message-inline-crypto-info .icon {\n  font-size: 14px;\n}\n.message-inline-crypto-info .text {\n  font-size: 12px;\n  font-family: Helvetica, Arial, sans-serif;\n  font-weight: bold;\n  text-transform: uppercase;\n}\n.message-inline-crypto-error {\n  width: 50%;\n  text-align: center;\n  color: #b3b3b3;\n  margin: 0px auto 20px auto;\n}\n.message-inline-crypto-error p {\n  line-height: 18px;\n}\n.message-inline-crypto-error .icon {\n  font-size: 48px;\n  line-height: 48px;\n  display: block;\n  margin: 20px auto;\n}\n.message-inline-crypto-error .status {\n  margin-bottom: 20px;\n  font-size: 21px;\n  line-height: 24px;\n  font-family: Mailpile-700, \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-weight: bold;\n}\niframe.message-part-html {\n  width: 65%;\n  min-width: 500px;\n  margin-top: 0px;\n  margin-left: 20px;\n  margin-right: 15px;\n  margin-bottom: 15px;\n}\n.message-part-html-text {\n  margin: 0px;\n  padding: 0px;\n  font-family: Helvetica, Arial, sans-serif;\n  font-size: 14px;\n  line-height: 18px;\n  white-space: pre-wrap;\n  word-wrap: break-word;\n}\n.message-part-text {\n  max-width: 50em;\n  margin-top: 5px;\n  margin-left: 20px;\n  margin-right: 15px;\n  margin-bottom: 15px;\n  font-family: Helvetica, Arial, sans-serif;\n  font-size: 14px;\n  line-height: 18px;\n  white-space: pre-wrap;\n  word-wrap: break-word;\n  color: #333333;\n}\n.message-part-text a {\n  color: #337fb2;\n  font-weight: bold;\n  font-size: inherit;\n  line-height: inherit;\n}\n.message-part-text a:hover {\n  color: #225577;\n}\n.message-part-quote,\n.message-part-quote-text {\n  max-width: 50em;\n  margin-left: 20px;\n  margin-bottom: 15px;\n  font-family: Helvetica, Arial, sans-serif;\n  font-size: 14px;\n  line-height: 18px;\n  white-space: pre-wrap;\n  word-wrap: break-word;\n  color: #808080;\n}\n.message-part-quote a,\n.message-part-quote a:visited {\n  color: #999999;\n}\n.message-part-quote a:hover {\n  color: #4d4d4d;\n}\n.message-part-signature {\n  margin-left: 20px;\n  margin-bottom: 15px;\n  font-family: Helvetica, Arial, sans-serif;\n  font-size: 14px;\n  line-height: 18px;\n  white-space: pre-wrap;\n  word-wrap: break-word;\n  border: 1px solid #ffffff;\n}\n#pile-results .message-part-quote,\n#pile-results .message-part-quote-text,\n#pile-results .message-part-signature,\n#pile-results .message-part-text {\n  margin-left: 0;\n  margin-right: 0;\n}\n/* Message HTML controls/widgets */\n.message-app-note {\n  width: 80%;\n  font-size: 13px;\n  text-align: center;\n  border-top: 1px solid #b3b3b3;\n  border-bottom: 1px solid #b3b3b3;\n  margin: 30px auto;\n  padding: 10px;\n  color: #4d4d4d;\n  background: #e9e9e9;\n  opacity: 0.65;\n  white-space: normal;\n  transition: opacity 1s;\n}\n.message-app-note:hover {\n  opacity: 1.0;\n  transition: opacity 1s;\n}\n.message-app-note .icon,\n.html-image-question .icon {\n  display: block !important;\n  font-size: 30px !important;\n  margin-bottom: 10px;\n}\n.message-app-note ul {\n  text-align: left;\n}\n.message-app-note ul li {\n  list-style-type: disc;\n  margin-left: 25px;\n  padding: 0;\n}\n.message-app-note ul li,\n.message-app-note ul li a {\n  white-space: normal !important;\n  vertical-align: text-top;\n}\n.message-app-note a {\n  cursor: pointer;\n}\n#pile-results .display-modes a img,\n#pile-results .alternate-views a img {\n  opacity: 0.5;\n  height: 13px;\n}\n#pile-results .display-modes a:hover img,\n#pile-results .alternate-views a:hover img {\n  opacity: 1.0;\n}\n#pile-results .alternate-views a:hover,\n#pile-results .display-modes a:hover,\n.message-app-note ul li a:hover {\n  color: #333333 !important;\n  cursor: pointer;\n}\n#pile-results .alternate-views,\n#pile-results .display-modes {\n  float: right;\n  margin: 0;\n  padding: 0;\n  margin-right: 4px;\n  color: #b3b3b3;\n}\n#pile-results .alternate-views li,\n#pile-results .display-modes li {\n  display: inline-block;\n  margin: 0px 2px;\n}\n#pile-results .message-subject-container .alternate-views li {\n  opacity: 0;\n  transition: opacity 1s;\n}\n#pile-results .message-subject-container:hover .alternate-views li {\n  opacity: 1;\n  transition: opacity 1s;\n}\n/* Thread Message Actions */\ndiv.message-actions {\n  width: 100%;\n  max-width: 51em;\n}\nul.message-actions {\n  display: inline-block;\n  margin-left: 20px;\n  margin-bottom: 10px;\n}\nul.message-actions.right {\n  margin-right: -18px;\n}\nul.message-actions li.action {\n  margin-right: 20px;\n}\nul.message-actions li.action a {\n  min-width: auto;\n}\nul.message-actions li.action ul.dropdown-menu li {\n  display: block;\n  float: none;\n}\nul.message-actions li.action ul.dropdown-menu li.hide {\n  display: none;\n}\na.message-actions-quote {\n  display: inline-block;\n  padding: 0px 4px;\n  border: 1px solid #cccccc;\n  border-radius: 3px;\n  color: #4d4d4d;\n  cursor: pointer;\n  font-size: 18px;\n  font-weight: normal;\n  line-height: 14px;\n}\na.message-actions-quote:hover {\n  background: #e9e9e9;\n}\n.pile-message-vcal-event {\n  border: 1px solid #ccc;\n  width: 100%;\n  margin-top: 2em;\n  padding: 1em;\n  background: #eee;\n}\n.event-details {\n  display: grid;\n  vertical-align: top;\n  text-align: left;\n  padding-top: 15px;\n  grid-template-columns: 15% 85%;\n}\n/* Tags */\n.tag-card-name {\n  max-width: 175px;\n  display: block;\n  margin-bottom: 10px;\n  font-family: Mailpile-700, \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-weight: normal;\n  font-size: 18px;\n  line-height: 18px;\n  letter-spacing: -0.25px;\n}\n.tag-card-name:hover {\n  color: #337fb2;\n}\n.tag-card-label {\n  font-family: Helvetica, Arial, sans-serif;\n}\n.tag-card-details {\n  min-height: 65px;\n  clear: both;\n  font-size: 14px;\n  line-height: 14px;\n  color: #b3b3b3;\n}\n/* Tags - Edit */\n#tag-editor-icon {\n  font-size: 36px;\n  line-height: 36px;\n  padding: 5px;\n}\nli.modal-tag-icon-option {\n  font-size: 36px;\n  line-height: 36px;\n  margin: 0px 15px 15px 0px;\n  padding: 5px;\n  border-radius: 3px;\n}\nli.modal-tag-icon-option:hover {\n  background-color: #cccccc;\n  cursor: pointer;\n}\n#tag-editor-label-color {\n  width: 48px;\n  height: 48px;\n  display: inline-block;\n  border-radius: 3px;\n}\na.modal-tag-color-option {\n  width: 48px;\n  height: 48px;\n  display: block;\n  margin: 0px 15px 15px 0px;\n  border-radius: 3px;\n  cursor: pointer;\n}\na.modal-tag-color-option:hover {\n  opacity: 0.7;\n}\n/* Files Browser */\n.item-file {\n  width: 150px;\n  float: left;\n  margin: 25px 25px;\n  padding: 20px;\n  text-align: center;\n}\n.item-file:hover {\n  background: #e9e9e9;\n}\n.item-file-icon {\n  display: block;\n  font-size: 125px;\n  margin-bottom: 10px;\n}\n.item-file-name {\n  font-size: 14px;\n  font-family: \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  line-height: 14px;\n}\n/* Tooltips - Global */\n.qtip-tipped small {\n  display: block;\n  font-size: 11px;\n  font-weight: normal;\n  color: #b3b3b3;\n}\n/* Tooltips - Crypto */\n.qtip-thread-crypto {\n  border: 1px solid #b3b3b3;\n  border-radius: 4px;\n  background: #ffffff;\n  padding: 8px 10px;\n  box-shadow: 1px 1px 2px 0px #cccccc;\n}\n.qtip-thread-crypto .qtip-content h4 {\n  text-align: center;\n  margin-top: 5px;\n  margin-bottom: 15px;\n}\n.qtip-thread-crypto .qtip-content h4 span {\n  margin-right: 5px;\n}\n.qtip-thread-crypto .qtip-content p {\n  margin-bottom: 10px;\n  text-align: center;\n  font-size: 14px;\n  font-weight: normal;\n  font-family: Helvetica, Arial, sans-serif;\n  line-height: 18px;\n  color: #4d4d4d;\n}\n.qtip-thread-crypto .qtip-icon {\n  border: 2px solid #285589;\n  background: #285589;\n}\n.qtip-thread-crypto .qtip-icon .ui-icon {\n  background-color: #FBFBFB;\n  color: #555;\n}\n/* Tooltips - Contact Details */\n.qtip-contact-details {\n  border: 1px solid #b3b3b3;\n  border-radius: 4px;\n  background: #ffffff;\n  padding: 5px 10px 0px 10px;\n  box-shadow: 1px 1px 2px 0px #cccccc;\n}\n.qtip-contact-details .qtip-content {\n  width: 215px;\n  margin-top: 5px;\n}\n/* Tooltips - Tag Details */\n.qtip-tag-details {\n  border: 1px solid #b3b3b3;\n  border-radius: 4px;\n  background: #ffffff;\n  padding: 5px 10px 0px 10px;\n  box-shadow: 1px 1px 2px 0px #cccccc;\n}\n.qtip-tag-details .qtip-content {\n  width: 170px;\n  margin-top: 5px;\n}\n.qtip-tag-details .qtip-content a {\n  display: inline-block;\n  margin-bottom: 10px;\n}\n/* =============== Tablet Style =============== */\n@media only screen and (max-width: 1020px) {\n  .tablet-hide {\n    display: none !important;\n  }\n  /* Shrink the sidebar a bit, but keep it. */\n  #sidebar {\n    width: 180px;\n  }\n  #sidebar-wrapper {\n    width: 179px;\n    font-size: 0.7em;\n  }\n  #content {\n    min-width: 0;\n    left: 180px;\n  }\n  /* Apply the cozy values per default, with a smaller font. */\n  #sidebar ul li {\n    margin-top: 2px;\n    margin-bottom: 3px;\n    padding-top: 2px;\n    padding-bottom: 2px;\n  }\n  #sidebar a.sidebar-tag {\n    padding: 3px 0px;\n  }\n  #sidebar a.sidebar-tag > span.name,\n  #sidebar a.sidebar-tag > span.notification,\n  #sidebar a.sidebar-tag span.icon {\n    font-size: 16px;\n    line-height: 18px;\n  }\n  #sidebar span.name,\n  #sidebar span.notification {\n    font-size: 14px;\n    line-height: 16px;\n  }\n  #sidebar-bottom a {\n    font-size: 14px;\n    line-height: 14px;\n  }\n  #sidebar li.sidebar-subtag a.sidebar-tag span.icon,\n  #sidebar li.sidebar-subtag a.sidebar-tag span.notification,\n  #sidebar li.sidebar-subtag a.sidebar-tag span.name {\n    font-size: 14px;\n    line-height: 14px;\n  }\n  /* Pile */\n  #pile-results {\n    min-width: 300px;\n  }\n  #pile-results td.people {\n    width: 220px;\n  }\n  div.bulk-actions-hints {\n    position: fixed;\n    top: -100px;\n  }\n  #pile-results xtd.draggable {\n    display: none;\n  }\n  #pile-results td.avatar {\n    padding-left: 4px;\n  }\n}\n/* Mobile only components to hide under normal circumstances */\n.mobile-block {\n  display: none !important;\n}\n.mobile-inline {\n  display: none !important;\n}\n.mobile-pt-block {\n  display: none !important;\n}\n.mobile-pt-inline {\n  display: none !important;\n}\n/* Mobile - All Sizes (devices and browser) */\n@media only screen and (max-width: 767px), (orientation: portrait) {\n  .mobile-block {\n    display: block !important;\n  }\n  .mobile-inline {\n    display: inline-block !important;\n  }\n  .mobile-hide {\n    display: none !important;\n  }\n  /* Start by making comfy/cozy/snug be identical: */\n  #pile-results.comfy td,\n  #pile-results.cozy td,\n  #pile-results.snug td {\n    padding-top: 13px;\n    padding-bottom: 11px;\n  }\n  #pile-results.comfy td.avatar,\n  #pile-results.cozy td.avatar,\n  #pile-results.snug td.avatar {\n    padding-top: 6px;\n    padding-bottom: 5px;\n  }\n  #pile-results.comfy td.avatar a img,\n  #pile-results.cozy td.avatar a img,\n  #pile-results.snug td.avatar a img {\n    width: 24px;\n    height: 24px;\n  }\n  #pile-results.comfy td.people span.conversation-count,\n  #pile-results.cozy td.people span.conversation-count,\n  #pile-results.snug td.people span.conversation-count {\n    top: 1px;\n    padding: 4px 8px;\n  }\n  #content-view {\n    top: 46px;\n  }\n  .bulk-actions {\n    top: 0px;\n    position: absolute;\n    height: 46px;\n    width: 100%;\n    padding-top: 15px;\n  }\n  /* Make setup and login look less horrible */\n  div.setup-box-medium {\n    width: 100%;\n  }\n  div.setup-box {\n    border: 0px;\n    border-radius: 0px;\n    height: 100%;\n    width: 100%;\n    padding: 0px;\n    padding-top: 20px;\n    padding-bottom: 30px;\n  }\n  div.add-top {\n    margin-top: 0px !important;\n  }\n  div#identity-vault-lock {\n    display: block;\n    left: 0px;\n    margin-left: auto;\n    margin-right: auto;\n  }\n  body {\n    background: #e9e9e9;\n  }\n  #login {\n    display: none;\n  }\n  #login #login-right,\n  #login #login-left {\n    display: none;\n  }\n  #login-logo {\n    zoom: 0.8;\n    left: 0%;\n    width: 100%;\n    height: 100%;\n    display: block;\n  }\n  #login-logo svg {\n    margin: auto;\n  }\n  #login-details {\n    left: 0px;\n    width: 100%;\n  }\n  #login-details .form-text {\n    display: block;\n    top: 0px;\n    left: 0px;\n    text-align: center;\n  }\n  #login-details .form-login {\n    display: block;\n    margin: auto;\n    margin-top: 5px;\n  }\n  #login-vault-lock {\n    display: none;\n  }\n  /* Now for serious business: */\n  .topbar {\n    min-width: 100%;\n  }\n  .topbar-logo {\n    min-width: 1%;\n    max-width: 1%;\n    width: 67px;\n    padding: 0;\n    margin: 0;\n  }\n  .topbar-logo #logo-icon,\n  .topbar-logo-name #logo-name {\n    width: 75%;\n  }\n  .topbar-logo-name {\n    min-width: 30%;\n  }\n  .topbar-actions {\n    min-width: 80%;\n  }\n  .topbar-logo span#page-title-icon {\n    padding: 0 0 0 10px;\n  }\n  .topbar-logo-name span#page-title-text {\n    position: absolute;\n    display: block;\n    top: 0;\n    font-size: 24px;\n    margin: 18px 10px;\n  }\n  .topbar-actions {\n    white-space: nowrap;\n    text-align: right;\n  }\n  .topbar-nav {\n    right: 8px;\n  }\n  .topbar-nav > ul > li {\n    margin: 0 0 0 7px;\n    padding: 0;\n  }\n  .topbar-nav > ul > li > a {\n    width: 22px;\n    height: 22px;\n    margin: 0;\n    padding: 0;\n  }\n  .topbar-nav > ul > li > a span.link-icon {\n    font-size: 22px;\n    line-height: 22px;\n    margin: 0;\n    padding: 8px 0 0 0;\n  }\n  .topbar-nav .nav-search {\n    display: list-item;\n  }\n  .form-search {\n    width: auto;\n    margin: 0 0 0 10px;\n  }\n  .form-search input {\n    width: auto;\n    max-width: 100px;\n  }\n  #sidebar,\n  #sidebar-wrapper {\n    width: 72px;\n  }\n  #sidebar-scroll-area {\n    bottom: 0;\n  }\n  #sidebar a.sidebar-tag > span.name,\n  #sidebar a.sidebar-tag > span.notification {\n    display: none;\n  }\n  #sidebar-bottom {\n    display: none;\n  }\n  #sidebar-lists ul,\n  #sidebar a.sidebar-tag,\n  #sidebar-lists ul li:not(.hide) {\n    display: inline-block;\n    text-align: center;\n    vertical-align: top;\n    padding: 0;\n    margin: 0;\n  }\n  #sidebar-lists ul {\n    display: block;\n  }\n  #sidebar-lists li {\n    width: 72px;\n    height: 56px;\n    padding: 3px;\n    margin: 0;\n    overflow: hidden;\n  }\n  #sidebar li.sidebar-subtag a.sidebar-tag span.name,\n  #sidebar li.sidebar-subtag a.sidebar-tag span.notification,\n  #sidebar a.sidebar-tag > span.name,\n  #sidebar.snug a.sidebar-tag > span.name,\n  #sidebar.cozy a.sidebar-tag > span.name {\n    display: block;\n    font-size: 12px;\n    line-height: 14px;\n    width: 72px;\n    padding: 0;\n    margin: 0;\n  }\n  #sidebar a.sidebar-tag > span.notification,\n  #sidebar.snug a.sidebar-tag > span.notification,\n  #sidebar.cozy a.sidebar-tag > span.notification {\n    display: block;\n    width: 72px;\n    text-align: center;\n    position: absolute;\n    top: 20px;\n    color: #333333;\n    text-shadow: 1px 1px 0px #ffffff, -1px -1px 0px #ffffff, -1px 1px 0px #ffffff, 1px -1px 0px #ffffff;\n    opacity: 0.75;\n    font-size: 11px;\n    font-weight: bold;\n  }\n  #sidebar li.sidebar-subtag a.sidebar-tag span.icon,\n  #sidebar.cozy a.sidebar-tag span.icon,\n  #sidebar.snug a.sidebar-tag span.icon,\n  #sidebar a.sidebar-tag span.icon {\n    display: block;\n    width: auto;\n    height: 28px;\n    font-size: 28px;\n    line-height: 28px;\n    padding: 6px;\n    margin: 0;\n  }\n  #sidebar a.sidebar-tag-expand {\n    top: 25%;\n    left: 0;\n  }\n  #page {\n    margin: 0px !important;\n  }\n  #content {\n    left: 72px;\n  }\n  .content-normal h1,\n  .pile-bottom h1 {\n    font-size: 24px;\n    padding: 4px;\n    padding-left: 10px;\n  }\n  .content-normal h2,\n  .pile-bottom h2 {\n    font-size: 21px;\n    padding: 4px;\n    padding-left: 10px;\n  }\n  .content-normal h3,\n  .pile-bottom h3 {\n    font-size: 16px;\n    padding: 4px;\n    padding-left: 10px;\n  }\n  .content-normal h4,\n  .pile-bottom h4 {\n    font-size: 14px;\n    padding: 4px;\n    padding-left: 10px;\n  }\n  .content-normal h5,\n  .pile-bottom h5 {\n    font-size: 10px;\n    padding: 4px;\n    padding-left: 4px;\n  }\n  div.content-normal {\n    margin: 1px 5px;\n  }\n  div.settings-page,\n  .settings-page .setting-group,\n  .settings-page .setting-group .settings {\n    max-width: 100%;\n  }\n  .settings-page .setting-group .explanation {\n    display: none;\n  }\n  #pile-results td.people {\n    width: 110px;\n    text-overflow: ellipsis;\n  }\n  .content-normal section {\n    width: 100%;\n    max-width: 100%;\n  }\n  .content-normal section table {\n    width: 100%;\n    max-width: 100%;\n  }\n  .content-normal section table th {\n    display: none;\n  }\n  .content-normal section table tr {\n    display: block;\n    border-bottom: 1px solid #cccccc;\n  }\n  .content-normal section table td {\n    display: inline-block;\n  }\n  .content-normal section table td#email-address,\n  .content-normal section table td#first-name {\n    padding-top: 5px;\n    padding-bottom: 0px;\n    display: block;\n  }\n  .content-normal section table td#first-name a {\n    font-size: 20px;\n  }\n  .content-normal section table td#compose-message {\n    float: right;\n    padding-bottom: 0px;\n  }\n  .content-normal section table td#compose-message a {\n    font-size: 24px;\n  }\n  .content-normal section table td#settings-actions a,\n  .content-normal section table td#stats-new a,\n  .content-normal section table td#stats-all a {\n    padding-right: 8px;\n    font-size: 16px;\n  }\n  .content-normal section.motd {\n    padding: 0px;\n  }\n  .content-normal section.motd .version-info {\n    padding-left: 5px;\n  }\n  #pile-bottom h5 {\n    font-size: 12px;\n    margin-top: 0px !important;\n  }\n  #pile-empty .button-primary {\n    display: block;\n  }\n  #pile-results td.checkbox,\n  #pile-results div.crypto-and-tags {\n    display: none;\n  }\n  #pile-results td.rom,\n  #pile-results td.date {\n    padding-right: 3px;\n  }\n  /*  #pile-results td.subject a.item-subject { width: 370px; }*/\n  #content-tools .sub-navigation > ul {\n    margin: 9px;\n  }\n  #content-tools nav li {\n    padding: 0 1px;\n    margin: 0;\n    opacity: 0.95;\n  }\n  #content-tools nav li,\n  #content-tools nav .navigation-icon {\n    font-size: 18px;\n  }\n  #content-tools nav .navigation-text {\n    display: none;\n  }\n  #bulk-actions-message {\n    position: absolute;\n  }\n}\n@media only screen and (max-width: 767px) and (min-width: 350px), (orientation: portrait) and (min-width: 350px) {\n  .topbar-nav > ul > li {\n    margin-left: 10px;\n  }\n}\n@media only screen and (max-width: 767px) and (min-width: 400px), (orientation: portrait) and (min-width: 400px) {\n  .topbar-nav > ul > li {\n    margin-left: 11px;\n  }\n}\n@media only screen and (max-width: 767px) and (min-width: 450px), (orientation: portrait) and (min-width: 450px) {\n  .topbar-nav > ul > li {\n    margin-left: 12px;\n  }\n}\n@media only screen and (max-width: 767px) and (min-width: 500px), (orientation: portrait) and (min-width: 500px) {\n  .topbar-nav > ul > li {\n    margin-left: 13px;\n  }\n}\n@media only screen and (max-width: 767px) and (min-width: 550px), (orientation: portrait) and (min-width: 550px) {\n  .topbar-nav > ul > li {\n    margin-left: 14px;\n  }\n}\n@media only screen and (max-width: 767px) and (min-width: 600px), (orientation: portrait) and (min-width: 600px) {\n  .form-search input {\n    max-width: 150px;\n  }\n}\n@media only screen and (max-width: 767px) and (min-width: 700px), (orientation: portrait) and (min-width: 700px) {\n  .form-search input {\n    max-width: 160px;\n  }\n}\n@media only screen and (max-width: 767px) and (min-width: 550px), (orientation: portrait) and (min-width: 550px) {\n  #pile-results td.people {\n    width: 125px;\n  }\n}\n@media only screen and (max-width: 767px) and (min-width: 630px), (orientation: portrait) and (min-width: 630px) {\n  #pile-results td.people {\n    width: 150px;\n  }\n}\n/* This is too small for the tabular result list, switch to card-style */\n@media only screen and (max-width: 480px) {\n  .mobile-pt-block {\n    display: block !important;\n  }\n  .mobile-pt-inline {\n    display: inline-block !important;\n  }\n  .mobile-pt-hide {\n    display: none !important;\n  }\n  #content {\n    left: 0;\n    bottom: 72px;\n  }\n  #sidebar {\n    position: fixed;\n    display: block;\n    top: auto;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    padding: 0;\n    height: 73px;\n    width: 100%;\n    vertical-align: middle;\n    border-top: 1px solid #b3b3b3;\n    border-right: 0;\n    z-index: 10;\n  }\n  #sidebar-wrapper {\n    width: 100%;\n    height: 72px;\n    margin: 0;\n    padding: 0;\n  }\n  #sidebar-scroll-area {\n    overflow-x: scroll;\n    overflow-y: hidden;\n    padding: 0;\n    margin: 0;\n  }\n  #sidebar-lists {\n    width: 5000px;\n  }\n  #sidebar-lists ul {\n    display: inline-block;\n  }\n  #sidebar-lists li {\n    width: 72px;\n    height: 64px;\n    padding: 3px 3px 0 3px;\n    margin-bottom: 0;\n  }\n  #notifications {\n    bottom: 0;\n    left: 0px;\n    right: 0px;\n    top: auto;\n    width: 100%;\n    max-height: 73px;\n    opacity: 0.95;\n  }\n  div.notification-bubble {\n    padding: 2px 5px;\n  }\n  div.notification-bubble br {\n    display: inline;\n  }\n  div.notification-bubble span.text {\n    padding-left: 5px;\n  }\n  .compose-from-select {\n    position: relative;\n    width: 15em;\n    max-width: 15em;\n    margin-bottom: 0.5em;\n    border-top: 1px solid #cccccc;\n    padding-top: 4px;\n    margin-top: 0px;\n    margin-left: 0px;\n  }\n  /* Stuff we just hide... */\n  #sidebar hr,\n  #notifications-header,\n  #pile-speed {\n    display: none;\n  }\n  .bulk-actions ul {\n    margin-right: 0px;\n    white-space: nowrap;\n  }\n  .bulk-actions li {\n    padding: 0px 8px;\n  }\n  .topbar-logo-name {\n    min-width: 55%;\n  }\n  .topbar-actions {\n    min-width: 45%;\n  }\n  .topbar-nav > ul > li > a span.link-icon {\n    padding: 0 2px;\n  }\n  .topbar-nav > ul > li.nav-search-hide {\n    position: absolute;\n    top: 9px;\n    right: 0px;\n  }\n  .form-search input {\n    max-width: 115px;\n  }\n  #pile-results,\n  #pile-results.comfy {\n    display: block;\n  }\n  #pile-results tbody,\n  #pile-results.comfy tbody {\n    display: block;\n  }\n  #pile-results tbody tr.pile-message,\n  #pile-results.comfy tbody tr.pile-message {\n    display: block;\n    position: relative;\n    border-bottom: 1px solid #cccccc;\n    overflow: hidden;\n  }\n  #pile-results td,\n  #pile-results.comfy td {\n    display: block;\n    border: none;\n  }\n  #pile-results td.avatar,\n  #pile-results.comfy td.avatar {\n    position: absolute;\n    top: 0px;\n    left: 0px;\n    width: 50px;\n    height: 50px;\n  }\n  #pile-results td.avatar a img,\n  #pile-results.comfy td.avatar a img {\n    margin: 5px 5px 5px 0px;\n    width: 40px;\n    height: 40px;\n  }\n  #pile-results div.crypto-and-tags,\n  #pile-results.comfy div.crypto-and-tags {\n    display: none;\n  }\n  #pile-results td.checkbox,\n  #pile-results.comfy td.checkbox {\n    position: absolute;\n    bottom: -3px;\n    right: -5px;\n  }\n  #pile-results td.date,\n  #pile-results.comfy td.date {\n    position: absolute;\n    top: 0px;\n    right: 5px;\n  }\n  #pile-results td.people,\n  #pile-results.comfy td.people {\n    position: absolute;\n    top: 0px;\n    left: 54px;\n    display: inline-block;\n    padding-right: 3px;\n    width: auto;\n    max-width: 55%;\n  }\n  #pile-results td.subject,\n  #pile-results.comfy td.subject {\n    left: 54px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n  #pile-results td.full-message,\n  #pile-results.comfy td.full-message {\n    width: 100%;\n    max-width: 100%;\n  }\n  #pile-results td.full-message .pile-message-content,\n  #pile-results.comfy td.full-message .pile-message-content {\n    margin: 2px;\n  }\n  #pile-results td.draggable,\n  #pile-results.comfy td.draggable {\n    display: block;\n    visibility: hidden;\n  }\n  #pile-results td.message-nav,\n  #pile-results.comfy td.message-nav {\n    position: absolute;\n    display: block;\n    width: 15px;\n    top: 0px;\n    left: 1px;\n  }\n  #pile-results tr.full-message,\n  #pile-results.comfy tr.full-message {\n    /* When a full message is expanded in the results */\n    background-color: inherit;\n  }\n  #pile-results tr.full-message .thread-container,\n  #pile-results.comfy tr.full-message .thread-container,\n  #pile-results tr.full-message .thread-message,\n  #pile-results.comfy tr.full-message .thread-message {\n    width: auto;\n  }\n  #pile-results tr.full-message .form-compose,\n  #pile-results.comfy tr.full-message .form-compose {\n    padding: 0;\n    padding-left: 20px;\n  }\n  #pile-results tr.full-message h3,\n  #pile-results.comfy tr.full-message h3 {\n    font-size: 13px;\n  }\n  #pile-results tr.full-message .message-subject,\n  #pile-results.comfy tr.full-message .message-subject {\n    font-size: 20px;\n    margin: 5px;\n    margin-left: 20px;\n    width: auto;\n    text-overflow: ellipsis;\n  }\n  #pile-results tr.full-message td.people,\n  #pile-results.comfy tr.full-message td.people {\n    left: 25px;\n    display: block;\n    position: relative;\n    padding: 0px;\n    top: -20px;\n    width: 100%;\n  }\n  #pile-results tr.full-message td.subject,\n  #pile-results.comfy tr.full-message td.subject {\n    left: 0px;\n    display: block;\n    position: relative;\n    padding: 0px;\n    top: -20px;\n  }\n  .pile-results-drag .drag-info {\n    display: none;\n  }\n  div.footer-nav {\n    display: none;\n  }\n  #pile-more {\n    display: none;\n  }\n  #pile-bottom .button-primary {\n    width: 49.3%;\n    float: none;\n    text-align: center;\n    margin-left: auto;\n    margin-right: auto;\n  }\n  /* Single  footer button case */\n  #pile-previous.button-primary:last-of-type,\n  #pile-next.button-primary:nth-of-type(2) {\n    width: 100%;\n  }\n  table.account-list {\n    margin: 0 0 !important;\n    display: block;\n  }\n  div.motd {\n    background: #ffffff;\n  }\n  .account-list {\n    display: block;\n  }\n  .account-list tr,\n  .account-list td,\n  .account-list th,\n  .account-list tbody,\n  .account-list thead {\n    display: block;\n    border: none;\n  }\n}\n@media only screen and (max-width: 480px) and (min-width: 470px), only screen and (max-width: 480px) and (orientation: landscape) {\n  div.notification-bubble br {\n    display: none;\n  }\n}\n@media only screen and (max-width: 480px) and (min-width: 340px) {\n  .bulk-actions li {\n    padding: 0px 9px;\n  }\n}\n@media only screen and (max-width: 480px) and (min-width: 360px) {\n  .bulk-actions li {\n    padding: 0px 10px;\n  }\n}\n@media only screen and (max-width: 480px) and (min-width: 380px) {\n  .bulk-actions li {\n    padding: 0px 11px;\n  }\n}\n@media only screen and (max-width: 480px) and (min-width: 400px) {\n  .bulk-actions li {\n    padding: 0px 12px;\n  }\n}\n@media only screen and (max-width: 480px) and (min-width: 360px) {\n  .form-search input {\n    max-width: 130px;\n  }\n}\n@media only screen and (max-width: 480px) and (min-width: 380px) {\n  .form-search input {\n    max-width: 140px;\n  }\n}\n@media only screen and (max-width: 480px) and (min-width: 400px) {\n  .form-search input {\n    max-width: 150px;\n  }\n}\n/* Terminal */\n#terminal_blanket {\n  position: fixed;\n  z-index: 500;\n  width: 100%;\n  height: 100%;\n  display: none;\n  background: none;\n  opacity: 0.0;\n}\n#terminal {\n  position: fixed;\n  z-index: 501;\n  display: none;\n  background: #000;\n  font-family: monospace;\n  font-size: 14px;\n  color: #eee;\n  width: 100%;\n}\n#terminal div.log,\n#terminal div.output {\n  white-space: pre-wrap;\n  word-wrap: break-word;\n  position: relative;\n  bottom: 0px;\n  vertical-align: bottom;\n  background: none;\n  padding: 0px;\n  margin: 0px;\n  padding-bottom: 10px;\n  color: #eee;\n  border: 0px;\n}\n#terminal div.html_blob {\n  position: relative;\n  white-space: normal;\n  background: #ddd;\n  padding: 0;\n  margin: 5px 50px 15px 50px;\n  border: 2px solid #fff;\n}\n#terminal div.html_blob #content-view {\n  position: inherit;\n  color: #111;\n}\n#terminal div.html_blob #pile-speed {\n  display: none;\n}\n#terminal #console {\n  position: absolute;\n  top: 0px;\n  bottom: 0px;\n  left: 0px;\n  width: 100%;\n}\n#terminal #console #terminal_output {\n  display: block;\n  position: absolute;\n  top: 0px;\n  bottom: 25px;\n  left: 0px;\n  right: 0px;\n  padding: 6px;\n  overflow-y: scroll;\n  overflow-x: hidden;\n}\n#terminal #console #terminal_input {\n  height: 20px;\n  background: #000;\n  position: absolute;\n  border-top: 1px solid #ccc;\n  width: 100%;\n  padding: 3px;\n  padding-left: 6px;\n  padding-right: 6px;\n  bottom: 0px;\n}\n#terminal #console #terminal_input #terminal_halfsize_button {\n  display: none;\n}\n#terminal #console #terminal_input form {\n  display: inline-block;\n  width: 90%;\n}\n#terminal #console #terminal_input #prompt {\n  color: #8f9;\n}\n#terminal #console #terminal_input input {\n  outline: none;\n  display: inline-block;\n  width: 100%;\n  background: none;\n  border: none;\n  font-family: monospace;\n  color: #eee;\n  font-size: 15px;\n}\n#terminal div.log {\n  line-height: 16px;\n  color: #77e;\n  padding: 0 0 2px 0;\n  margin: 0;\n}\n#terminal div.log .ts {\n  color: #55c;\n}\n"
  },
  {
    "path": "shared-data/default-theme/css/guide.css",
    "content": "/* Rebar V0.1\n*  Copyright 2013, Brennan Novak\n*/\n\n\n/* Documentation Styles\n================================================== */\n\t\t\n\n\tdiv.doc-section { margin: 30px 0; }\n\n\t.hidden-code a { font-size: 12px; color: #999; }\n\t.hidden-code > div { display: none; }\n\n\n\t/* Grid */\n\t#grid .column,\n\t#grid .columns {\n\t\tbackground: #ddd;\n\t\theight: 25px;\n\t\tline-height: 25px;\n\t\tmargin-bottom: 10px;\n\t\ttext-align: center;\n\t\ttext-transform: uppercase;\n\t\tcolor: #555;\n\t\tfont-size: 12px;\n\t\tfont-weight: bold;\n\t\t-moz-border-radius: 2px;\n\t\t-webkit-border-radius: 2px;\n\t\tborder-radius: 2px;\n\t}\n\t\n\t#grid .column:hover,\n\t#grid .columns:hover {\n\t\tbackground: #bbb;\n\t\tcolor: #333;\n\t}\n\n\t#grid .example-grid { overflow: hidden; }\n\n\n\t\n\t/* Boxes */\n\t#boxes div.boxes {\n\t\tbackground: #d9d9d9;\n\t}\n\n\n\t.post-button-note,\n\t.post-button-note a {\n\t\tfont-size: 11px;\n\t\tcolor: #999;\n\t}\n\n\t/* Buttons */\n\t#buttons a { margin-right: 20px; }\n\n\t#icons li { margin-bottom: 20px; }\n\t#icons span { font-size: 30px; line-height: 30px; position: relative; top: 7px; left: 0px; margin-right: 10px; }\n\n\n\t/* Color Palate */\n\t.color-palate-item { \n\t\twidth: 120px;\n\t\theight: 150px;\n\t\tmargin: 0px 20px 20px 0px;\n\t\tfloat: left;\n\t\tfont-size: 12px;\n\t\tfont-weight: bold;\n\t\ttext-align: center;\n\t\tpadding-top: 25px;\n\t\tcursor: pointer;\n\t}\n\n\n\t/* Gist */\n\t.gist-meta { display: none !important;}\n\n\n\n\n"
  },
  {
    "path": "shared-data/default-theme/css/print.css",
    "content": "body{font-family:Georgia, serif;background:none;color:black}#sidebar,#header{display:none}#content{margin:0;padding:0;width:100%;bacckground:none}.thread-item-text{clear:both}\n"
  },
  {
    "path": "shared-data/default-theme/html/abortabortabort/index.html",
    "content": "<h1>Immediate shutdown initiated ...</h1>\n"
  },
  {
    "path": "shared-data/default-theme/html/auth/login/index.html",
    "content": "{% extends \"layouts/auth.html\" %}\n{% block title %}{{_(\"Please log in\")}}{% endblock %}\n{% block content %}\n{% set status_message = status %}\n{% include(\"auth/shared.html\") %}\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/auth/logout/index.html",
    "content": "{% extends \"layouts/auth.html\" %}\n{% block title %}{{_(\"Please log in\")}}{% endblock %}\n{% block content %}\n{% set status_message = \"logout\" %}\n{% include(\"auth/shared.html\") %}\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/auth/shared.html",
    "content": "<div id=\"login\">\n  <div id=\"login-left\" class=\"animated\"></div>\n  <div id=\"login-right\" class=\"animated\"></div>\n</div>\n\n<div id=\"login-logo\" class=\"animated\">\n{% include(\"../img/logo-color.svg\") %}\n{% include(\"../img/logo-name.svg\") %}\n</div>\n\n<div id=\"login-vault-lock\" class=\"vault-lock-outer animated\">\n  <div class=\"vault-lock-inner animated\">\n    <div class=\"vault-lock icon-lock-closed animated\"></div>\n  </div>\n</div>\n\n<div id=\"login-details\" class=\"animated\">\n  <div class=\"form-text\">{{_(\"Log In With Your\")}}</div>\n  <form method=\"POST\"\n        {# Avoid adding the action if it is not needed, as it strips off\n           the query string which tells us where to redirect back to. #}\n        {% if state.command_url != \"/auth/login/\" %}action=\"{{config.sys.http_path}}/auth/login/\"{% endif %}\n        id=\"form-login\" class=\"form-login clearfix animated\">\n    <input id=\"login-passphrase\" type=\"password\" name=\"pass\" autocomplete=\"off\" tabindex=1 alt=\"{{_(\"Mailpile Password\")}}\" placeholder=\"{{_(\"Mailpile Password\")}}\">\n    <button type=\"submit\" class=\"submit\"><span class=\"icon-key\"></span></button>\n  </form>\n  {% if status_message == 'error' %}\n  <div class=\"login-wrong-passphrase animated bounceIn\">{{_(\"Oops, wrong password. Try again?\")}}</div>\n  {% elif result.login_failures|length %}\n  <div class=\"logged-out-message animated bounceIn\">{{_(\"Last failed login:\")}}\n    {{ result.login_failures[-1]|friendly_time }}\n    {{ result.login_failures[-1]|friendly_datetime }}\n    ({{ result.login_failures|length }})\n  </div>\n  {% elif result.login_banner %}\n  <div class=\"logged-out-message animated bounceIn\">{{ result.login_banner|safe }}</div>\n  {% endif %}\n  {% if status_message == 'logout' %}\n  <div class=\"logged-out-message animated bounceIn\">\n    {{_(\"You Have Been Logged Out!\")}}\n  </div>\n  <div class=\"still-running\">\n    <span class=\"icon icon-settings bigger\"></span>\n    <span class=\"icon icon-settings\"></span>\n    <span class=\"icon icon-settings smaller\"></span>\n    <i><b>{{_(\"Note\")}}:</b>\n      {{_(\"Mailpile is still running and processing your e-mail in the background.\")}}\n      {{_(\"To disable Mailpile completely, press the Shutdown button on the settings page or in the desktop application window.\")}}\n    </i>\n  </div>\n  {% endif %}\n</div>\n\n<script>\n$(document).ready(function() {\n\n  var height = $(window).height() - 16;\n  $('#content-wide').css({'height': height, 'margin-top': '0px'});\n  $('#login').height(height);\n  $('#login-left').height(height);\n  $('#login-right').height(height);\n\n  $('#login-passphrase').focus();\n});\n\n// Login Form is submitted\n$(document).on('submit', '#form-login', function(e) {\n\n  // Details\n  $('#login-details').addClass('bounceOutDown');\n\n  // Lock\n  setTimeout(function() {\n    $('#login-vault-lock').find('div.vault-lock').addClass('fadeOut');\n  }, 250);\n\n  setTimeout(function() {\n    $('#login-vault-lock').addClass('bounceOutUp');\n  }, 500);\n\n  // Panels\n  setTimeout(function() {\n    $('#login-left').addClass('bounceOutLeft');\n    $('#login-right').addClass('bounceOutRight');\n  }, 850);\n\n  // Loading\n  setTimeout(function() {\n    $('#login-logo .logo-name').addClass('fadeOut');\n    $('#login-logo').css({'left': '40%'});\n  }, 1000);\n\n  setInterval(function() {\n    $(\"#logo-bluemail\").fadeOut(2000);\n    $(\"#logo-redmail\").hide(2000);\n    $(\"#logo-greenmail\").hide(3000);\n    $(\"#logo-bluemail\").fadeIn(2000);\n    $(\"#logo-greenmail\").fadeIn(4000);\n    $(\"#logo-redmail\").fadeIn(6000);\n  }, 1000);\n\n});\n</script>\n"
  },
  {
    "path": "shared-data/default-theme/html/backup/restore/index.html",
    "content": "{% extends \"layouts/auth.html\" %}\n{% block title %}{{_(\"Restore from a Backup\")}} | {{_('Setup')}}{% endblock %}\n{% block content %}\n{% if result.configured or result.restored %}\n  {{ mailpile('http/redirect', U('/')) }}\n{% else %}\n\n<div class=\"setup-box setup-box-medium add-top half-bottom animated fadeIn\">\n  <div{% if result.metadata %} style=\"transform: scale(0.66); margin: -3em 0; color: #070;\"{% endif %}> \n    <div id=\"identity-vault-lock\" class=\"vault-lock-outer\">\n      <div class=\"vault-lock-inner\">\n        <div class=\"vault-lock icon-help animated\"></div>\n      </div>\n    </div>\n  </div>\n\n  <div class=\"text-center half-top\">\n    <form method=\"POST\" action=\"\" enctype=\"multipart/form-data\">\n      {{ csrf_field|safe }}\n      <h3 class=\"text-center\" style=\"margin-bottom: 0.75em;\">\n      {% if result.metadata %}\n        {{_(\"Backup Archive is OK\")}}\n      {% else %}\n        {{_(\"Restore from a Backup\")}}\n      {% endif %}\n      </h3>\n\n    {% if result.metadata %}\n      <div id=\"restore-possible\">\n        <p><i>\n          {{_(\"This backup was created by Mailpile version {ver} on {date}.\"\n              ).format(ver=result.metadata.mailpile_version,\n                       date=result.metadata.backup_date) }}\n        </i></p>\n        <p style=\"text-align: left; margin: -5px 0 0 9em; font-size: 0.9em; line-height: 0.9em;\">\n          <input type=\"radio\" name=\"keychain\" value=\"shared\" checked>\n          <span class=\"checkbox\">{{_(\"Restore encryption keys to shared GnuPG keychain\")}}</span>\n          <br>\n          <input type=\"radio\" name=\"keychain\" value=\"mailpile\">\n          <span class=\"checkbox\">{{_(\"Restore encryption keys to Mailpile-only keychain\")}}</span>\n          <br>\n          <input type=\"radio\" name=\"keychain\" value=\"none\">\n          <span class=\"checkbox\">{{_(\"Do not restore GnuPG/PGP encryption keys\")}}</span>\n          <br style=\"margin-bottom: 0.5em;\">\n          <input type=\"checkbox\" name=\"os_settings\" value=\"keep\" checked>\n          <span class=\"checkbox\">{{_(\"Keep default Operating System settings (override backup)\")}}</span>\n          <br style=\"margin-bottom: 1.25em;\">\n        </p>\n        <p>\n          {{_(\"Enter your Mailpile Password to restore this configuration.\")}}\n          <br style=\"margin-bottom: 0.5em;\">\n          <input type=\"password\" name=\"password\" autocomplete=\"off\"\n                 tabindex=1 alt=\"{{_(\"Mailpile Password\")}}\"\n                 placeholder=\"{{_(\"Mailpile Password\")}}\">\n          <input type=\"hidden\" name=\"restore\"\n                 value=\"{{ result.metadata.backup_date }}\">\n        </p>\n        <button class=\"button-primary\" type=\"submit\">{{_(\"Restore\")}}</button>\n      </div>\n    {% else %}\n      <div id=\"restore-how\" style=\"text-align: left; margin: 0 3em\">\n        <p>\n          {{_(\"You can restore a previous Mailpile configuration (keys, tags, etc.), provided you have a Mailpile Backup Archive available.\")}}\n{# FIXME: Make this work!\n          {{_(\"Backup archives can be uploaded manually, or automatically over WiFi if you have been using the Mailpile mobile web-app on another device.\")}}\n#}\n        </p>\n        <p style=\"text-align: center\">\n          <button id=\"show-from-file\">{{_(\"Upload a Backup Archive\")}}</button>\n{# FIXME: Make this work!\n          &nbsp; &nbsp; &nbsp;\n          <button id=\"show-over-wifi\">{{_(\"Restore Over WiFi\")}}</button>\n#}\n        </p>\n      </div>\n\n      <div id=\"restore-from-file\" class=\"hide\">\n        <p>\n          {{_(\"Please upload your Mailpile Backup Archive to continue.\")}}<br>\n          ({{_(\"It should have a name similar to: \")}}\n          <i>Mailpile_Backup_2017-01-01.zip</i>)\n        </p>\n        <p>\n          <input type=\"file\" name=\"file-data\">\n        </p>\n        <button class=\"button-primary\" type=\"submit\">{{_(\"Upload and Verify\")}}</button>\n      </div>\n\n{# FIXME: Make this work!\n      <div id=\"restore-over-wifi\" class=\"hide\">\n        <div style=\"text-align: left; margin: 0 6em\">\n          <ol style=\"list-style: decimal;\">\n            <li>Connect your laptop and mobile device to the same WiFi network</li>\n            <li>Choose RESTORE from your Mobile Mailpile App</li>\n            <li>Enter the following details into the Mobile Restortion form</li>\n          </ol>\n        </div>\n        <p id=\"address\" style=\"font-size: 1.75em; font-family: monospace;\">\n          <i>... {{_(\"loading\")}} ...</i>\n        </p>\n        <button class=\"button-primary\" type=\"submit\">{{_(\"Next\")}}</button>\n      </div>\n#}\n    {% endif %}\n    </form>\n  </div>\n</div>\n<a id='rfab' style=\"position: absolute; bottom: 10px; right: 20px;\"\n   href=\"{{ U('/setup/welcome/') }}\"\n  ><span class=\"icon icon-logo\"></span> {{_(\"Back to Setup\")}}</a>\n\n<script type=\"text/javascript\">\n  $('#show-from-file').click(function() {\n    $('#restore-how').slideUp();\n    $('#restore-from-file').slideDown();\n    return false;\n  });\n  $('#show-over-wifi').click(function() {\n    $('#restore-how').slideUp();\n    $('#restore-over-wifi').slideDown();\n    // Launch httpd on 0.0.0.0, get IP:PORT, inject into page.\n    return false;\n  });\n</script>\n{% endif %}\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/browse/index.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{%- if not result %}\n  {% set page_title = message %}\n{%- elif result.path == '/' %}\n  {% set page_title = _(\"Browsing folders and mailboxes\") %}\n{%- else %}\n  {% set page_title = _(\"Browsing\") + \": \" + result.name  %}\n{%- endif %}\n{% block title %}{{page_title}}{% endblock %}\n\n{% block contenttools %}\n{%- set activities = [{\n  'name': 'parent-folder',\n  'url': '../',\n  'text': _(\"Parent\"),\n  'description': _(\"Open Parent Folder\"),\n  'icon': 'upload'\n}] -%}\n{# }, {\n  'name': 'folder_add',\n  'url': '#folder-add',\n  'text': _(\"Add Folder\"),\n  'description': _(\"Create a new folder\"),\n  'icon': 'plus'\n}, {\n  'name': 'mailbox_add',\n  'url': '#mailbox-add',\n  'text': _(\"Add Mailbox\"),\n  'description': _(\"Create a new mailbox\"),\n  'icon': 'plus'\n}] #}\n{%- set tools_disable_checkbox = True -%}\n{%- set selection_initial_prompt = page_title -%}\n{%- include(\"partials/tools_default.html\") -%}\n{% endblock %}\n\n{%- macro help(attr) -%}\n  {% if attr == 'flag_mailpile' %}\n    <p class='help help-{{ attr }}'>\n      These are Mailpile-specific data.\n    </p>\n  {% elif attr == 'flag_directory' %}\n    <p class='help help-{{ attr }}'>\n      {{_(\"Browse local or remote folders for mailboxes to add to your pile.\")}}\n    </p>\n  {% elif attr == 'flag_mailbox' %}\n    <p class='help help-{{ attr }}'>\n      {{_(\"These are mailboxes recognized by Mailpile.\")}}\n      {{_(\"Click to view mailbox contents or change settings.\")}}\n    </p>\n  {% elif attr == 'flag_files' %}\n  {% endif %}\n{%- endmacro -%}\n\n{%- macro browse_url(attr, info) -%}\n  {% if attr == 'flag_mailbox' %}\n    {{- U('/search/?q=index:', info.encoded,\n          '&parent=', U('/browse', result.path, '/')) -}}\n  {% else %}\n    {{- U('/browse', info.path, '/') -}}\n  {% endif %}\n{%- endmacro -%}\n\n{%- macro box_icon(attr, info, icn) -%}\n  {%- if info.icon -%}\n    {%- if '-' in info.icon -%}\n      <span class='icon {{info.icon}}'></span>\n    {%- else %}\n      <img src=\"{{ U(info.icon) }}\">\n    {%- endif %}\n  {%- else -%}\n    <span class='icon-{{ icn }}'></span>\n  {%- endif -%}\n{%- endmacro -%}\n\n{%- macro settings(attr, info, icn) -%}\n  {%- set _icon = 'icon-settings' %}\n  {%- if info.settings  -%}\n    {%- set _url = info.settings -%}\n  {%- elif attr == 'flag_mailbox' -%}\n    {%- if info.tag %}\n      {%- set _url = U('/tags/edit.html?only=', info.tag) -%}\n    {%- else %}\n      {%- set _url = U('/settings/mailbox/?path=', info.encoded) -%}\n    {%- endif %}\n  {%- elif attr == 'flag_directory' -%}\n    {%- set _url = U('/settings/mailbox/?recurse=y&path=', info.encoded) -%}\n    {%- set _icon = 'icon-search' %}\n  {%- elif info.flag_mailsource and info.source -%}\n    {%- set _url = U('/profiles/edit/?rid=', config.sources[info.source].profile, '&ui_open=sources') -%}\n  {%- else %}\n    {%- set _url = '' %}\n  {%- endif -%}\n  {%- if _url -%}\n    <a class='browse-item-settings link-detail auto-modal auto-modal-reload'\n         data-icon=\"icon-settings\"\n         href=\"{{ U(_url) }}\" title='{{_(\"Settings\")}}: {{info.display_name}}'>\n        <span class='{{_icon}}'></span>\n    </a>\n  {%- endif -%}\n{%- endmacro -%}\n\n{%- macro details(attr, info) -%}\n  {%- if info.source %}\n    {%- set src = config.sources[info.source] %}\n  {%- else %}\n    {%- set src = False %}\n  {%- endif %}\n  {%- if info.display_info -%}\n    {{ info.display_info }}\n  {%- else %}\n    {%- if attr == 'flag_mailbox' -%}\n      {% if info.bytes %}{{ info.bytes|friendly_bytes }}{%- endif %}\n      {% if src %}{{ src.protocol }}{%- endif %}\n    {%- elif info.flag_mailsource and src.host %}\n      {{ src.protocol|upper }} {{ src.host }}\n    {%- endif %}\n  {%- endif -%}\n{%- endmacro -%}\n\n\n{% block content %}\n{% if result %}\n  {%- set loops = [] %}\n  {%- for attr, titl, icn, link in (\n      ('flag_mailpile',  'Mailpile',  'star',       True),\n      ('flag_mailbox',   'Mailboxes', 'inbox',      True),\n      ('flag_directory', 'Folders',   'news',       True),\n      ('flag_files',     'Files',     'document',   True)) %}\n    {%- set oloop_first = loop.first %}\n    {%- for info in result.entries|selectattr(attr) %}\n      {%- if loop.first %}\n        {%- do loops.append(info.display_name) %}\n        <div class='{{ attr }} container center'>\n          <div style=\"display: inline-block\">\n            <h2><span class='icon-{{ icn }}'></span> {{ titl }}</h2>\n          </div>\n          <div style=\"display: inline-block; margin: 25px;\">{{ help(attr) }}</div>\n          <div class=\"clearfix\"></div>\n          <table width='100%'>\n      {%- endif %}\n            <tr>\n              {%- set _url = browse_url(attr, info) %}\n              <td>\n                <a class=\"browse-item-icon\" {%- if link %} href=\"{{ _url }}\"{% endif %}>\n                  {{ box_icon(attr, info, icn) }}\n                </a>\n  {#          <input type=\"checkbox\" class=\"browse-item-checkbox right\"> #}\n                {{ settings(attr, info) }}\n              </td>\n              <td>\n                <a class=\"browse-item-name\" title='{{ info.display_path }}'\n                   {%- if link %} href=\"{{ _url }}\"{% endif %}>\n                  {{ info.display_name }}\n                </a>\n              </td>\n              <td>\n                {{ details(attr, info) }}\n              </td>\n            </tr>\n      {%- if loop.last %}\n          </table>\n        </div>\n      {%- endif %}\n    {%- endfor %}\n  {%- endfor %}\n{%- else %}\n  <div class='container rectangles-container'>\n    <h2>{{ page_title }}</h2>\n    {# FIXME: Suggest troubleshooting tips for networking-related errors #}\n  </div>\n{%- endif %}\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/contacts/add/index.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block title %}{{_(\"Add Contact\")}}{% endblock %}\n{% block content %}\n{% if result.form %}\n<div id=\"contact-add\" class=\"content-normal\">\n  <form id=\"form-contact-add\" class=\"standard\">{{ csrf_field|safe }}\n    <fieldset class=\"contact-add-fields\">\n      <label>{{_(\"Name\")}}</label>\n      <input type=\"text\" name=\"name\" data-type=\"name\" class=\"contact-add-name\" value=\"\" placeholder=\"Chelsea Manning\">\n      <label>{{_(\"E-mail\")}}</label>\n      <input type=\"text\" name=\"email\" data-type=\"email\" class=\"contact-add-email\" value=\"\" placeholder=\"chelsea@manning.com\">\n      <div class=\"contact-add-signature\"></div>\n    </fieldset>\n    <div id=\"contact-add-key\"></div>\n    <button type=\"submit\" class=\"button-primary\"><span class=\"icon-plus\"></span> {{_(\"Add\")}}</button>\n  </form>\n</div>\n{% else %}\n  {{ mailpile('http/redirect', config.sys.http_path + '/contacts/view/' + result.contacts.0.email.0.email + '/') }}\n{% endif %}\n<script>\n$(document).ready(function() {\n  Mailpile.Contacts.init();\n});\n</script>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/contacts/import/index.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block title %}{{_(\"Import Contacts\")}}{% endblock %}\n{% block content %}\n<div id=\"contact-add\" class=\"content-normal\">\n\n  <h1>Import Contacts</h1>\n\n  <div id=\"tab-container\" class=\"tab-container\">\n  <ul>\n    {% for importer in mailpile(\"contacts/importers\").result %}\n    <li class=\"tab\"><a href=\"#tabs-{{importer.short_name}}\">{{importer.format_name}}</a></li>\n    {% endfor %}\n  </ul>\n\n  {% for importer in mailpile(\"contacts/importers\").result %}\n  <div class=\"tab\" id=\"tabs-{{importer.short_name}}\">\n    <h2>{{importer.format_name}}</h2>\n    <p>{{importer.format_description}}</p>\n\n    <form id=\"form-contact-import-{{importer.short_name}}\" action=\"{{ config.sys.http_path }}/api/0/contacts/import/\" method=\"POST\">\n      {{ csrf_field|safe }}\n      <input type=\"hidden\" name=\"format\" value=\"{{importer.short_name}}\"/>\n\n      {% for parm in importer.required_parameters %}\n        <div>\n          <label><b>{{parm}}</b></label>\n          <input type=\"text\" name=\"@{{parm}}\" value=\"\"/>\n        </div>\n      {% endfor %}\n      {% for parm in importer.optional_parameters %}\n        <div>\n          <label>{{parm}}</label>\n          <input type=\"text\" name=\"@{{parm}}\" value=\"\"/>\n        </div>\n      {% endfor %}\n\n      <input type=\"submit\" value=\"Import\"/>\n    </form>\n  </div>\n  {% endfor %}\n\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/contacts/index.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block title %}{{_(\"Contacts\")}}{% endblock %}\n{% block contenttools %}\n<div id=\"content-tools\">\n  {% include(\"partials/tools_contacts.html\") %}\n</div>\n{% endblock %}\n{% block content %}\n{% if 0 and is_dev_version() %}\n  <h2>\n    IMPORTANT: This page is unfinished. Please don't be surprised by bugs.\n  </h2>\n  {% if result.contacts %}\n  <div id=\"contacts-list\" class=\"container rectangles-container center\">\n    <div class=\"clearfix\"></div>\n    {% for contact in result.contacts %}\n    {% if contact.email %}\n    <div class=\"rectangles-outer\" id=\"contact-card-{{contact.email.0.email}}\">\n      <div class=\"rectangles-inner\">\n        <a class=\"contact-card-avatar left\"\n{%- if is_dev_version() %}\n           href=\"{{ U('/contacts/view/', contact.email.0.email, '/') }}\"\n{%- endif %}\n           >\n          {% if contact.photo %}\n          <img src=\"{{ U(contact.photo.0.photo) }}\">\n          {% elif contact.email %}\n          <img src=\"{{ U('/static/img/avatar-default.png') }}\">\n          {% endif %}\n        </a>\n        <a class=\"contact-card-name\"\n{%- if is_dev_version() %}\n           href=\"{{ U('/contacts/view/', contact.email.0.email, '/') }}\"\n{%- endif %}\n           >\n          {% if contact.fn|length > 24 %}\n          {{contact.fn[:24]}}\n          {% else %}\n          {{contact.fn}}\n          {% endif %}\n        </a>\n        <input type=\"checkbox\" class=\"contact-card-checkbox right\">\n      </div>\n    </div>\n    {% else %}\n    {{_(\"No Contact Info\")}}\n    {% endif %}\n    {% endfor %}\n  </div>\n  {% else %}\n  <div class=\"add-top text-center\">\n    <h3>{{_(\"No Contacts Found\")}} :)</h3>\n    <p>{{_(\"Don't worry, it's ok. Mailpile will start to automatically create contacts whenever you send or reply to messages.\")}}</p>\n    <!-- <button><span class=\"icon-upload\"></span> Import Contacts</button> -->\n  </div>\n  {% endif %}\n\n<div id=\"pile-bottom\">\n  {% if result.offset > 0 %}\n  <a href=\"{{ U('/contacts/?offset=', result.offset-result.count, '&count=', result.count) }}\" class=\"button-primary\">{{_(\"Previous\")}}</a>\n  {% endif %}\n  {% if result.offset + result.count < result.total %}\n  <a href=\"{{ U('/contacts/?offset=', result.offset+result.count, '&count=', result.count) }}\" class=\"button-primary\">{{_(\"Next\")}}</a>\n  {% endif %}\n  <h5>{% if result.total > 1 %}{{result.start}} - {{result.end}} {{_(\"of\")}} {{result.total}} {{_(\"Contacts\")}} {% elif result.total == 1 %} {{_(\"1 Conversation\")}} {% else %} {{_(\"No results found\")}} {% endif %}</h5>\n</div>\n\n<script>\n$(document).ready(function() {\n  Mailpile.Contacts.init();\n});\n</script>\n{% else %}\n  {{ mailpile('http/redirect', U('/')) }}\n{% endif %}\n{% endblock %}\n\n"
  },
  {
    "path": "shared-data/default-theme/html/contacts/view/index.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block title %}{% if result.contact.fn %}{{ result.contact.fn }}{% else %}{{_(\"No Name\")}}{% endif %}{% endblock %}\n{% block content %}\n<div id=\"contact-view\" class=\"content-normal\">\n{% if result.contact %}\n  {% if result.contact.fn %}\n  {% set name = result.contact.fn %}\n  {% else %}\n  {% set name = _(\"No Name\") %}\n  {% endif %}\n  <ul class=\"items grouped\">\n    <li class=\"grouped\">\n      <div class=\"clearfix\">\n    {% if result.contact.photo %}\n      <img class=\"contact-avatar left\" width=\"80\" src=\"{{ show_avatar(result.contact.photo.0) }}\">\n    {% else %}\n      <img class=\"contact-avatar left\" width=\"80\" src=\"{{ show_avatar(\"\") }}\">\n    {% endif %}\n    <h2  class=\"contact-name \">{{name}}</h2>\n    {% if result.contact.kind == 'profile' %}\n      <h5 class=\"contact-subname\">{{_(\"Hey that's you\")}} :)</h5>\n    {% endif %}\n      </div>\n    </li>\n  {% if result.contact.email %}\n    {% for email in result.contact.email %}\n    <li class=\"grouped clearfix\">\n      <div class=\"left\">\n        <strong>{{email.email}}</strong>\n      </div>\n      <div class=\"right\">\n        <a class=\"compose-to-email\" href=\"mailto:{{ email.email }}\"><span class=\"icon-compose\"></span> {{_(\"Compose\")}}</a> &nbsp;&nbsp;\n        <a class=\"search-by-email\" href=\"{{ config.sys.http_path }}/search/?q=from:{{ email.email }}\"><span class=\"icon-search\"></span> {{_(\"Search\")}}</a>\n      </div>\n    </li>\n    {% endfor %}\n  {% endif %}\n  </ul>\n\n  {% set keys = mailpile(\"crypto/gpg/keylist\", result.contact.email.0.email).result %}\n  <h4>{{_(\"Security & Keys\")}}</h4>\n  <ul class=\"items grouped\">\n    {% if keys %}\n    {% for id, key in keys.iteritems() %}\n    <li class=\"grouped\">\n      <select class=\"crypto-key-policy right\" data-fingerprint=\"{{key.fingerprint}}\">\n        <option value=\"true\">Use This Key</option>\n        <option value=\"false\">Don't Use This Key</option>\n      </select>\n      <span class=\"icon-fingerprint left\"></span>\n      <h5 class=\"contact-key\">{{ nice_fingerprint(key.fingerprint) }}</h5>\n      <a href=\"#\" class=\"show-key-details\" data-keyid=\"{{id[8:]}}\">See Details</a>\n      <div id=\"contact-key-details-{{id[8:]}}\" class=\"contact-key-details clearfix\">\n        <div class=\"left add-right\">\n        Created: <strong>{{key['creation-date']}}</strong><br>\n        {% if key['revocation-date'] %}Expires: <strong>{{key['revocation-date']}}</strong>{% else %}Does not expire{% endif %}<br>\n        {% if key.capabilities %}Capabilities: <strong>{{key.capabilities}}</strong><br>{% endif %}\n        Length: <strong>{{key.keysize}}</strong><br>\n        Type: <strong>{{key.keytype}}</strong><br>\n        {% if key.trust %}Trust: <strong>{{key.trust}}</strong><br><br>{% endif %}\n        </div>\n        <div class=\"left\">\n        {% for uid in key.uids %}\n          {% if uid.name or uid.email or uid.comment %}\n          {{uid.name}}<br>\n          {{uid.email}}<br>\n          {% if uid.comment %}{{uid.comment}}<br>{% endif %}\n          {{uid['creation-date']}}\n          {% endif %}\n        {% endfor %}\n        </div>\n      </div>\n    </li>\n    {% endfor %}\n    <li class=\"grouped\">\n      <select id=\"crypto-policy\">\n        {% for policy in ['default', 'none', 'sign', 'encrypt', 'sign-encrypt'] %}\n        {% set crypto_policy = show_crypto_policy(policy) %}\n        <option value=\"{{ policy }}\"\n          {% if result.contact['crypto-policy'] and result.contact['crypto-policy'] == policy %} selected=\"selected\"{% endif %}\n          data-message=\"{{crypto_policy.message}}\">{{crypto_policy.text}}</option>\n        {% endfor %}\n      </select>\n      {{_(\"messages when communicating with\")}} {{name}}\n    </li>\n    {% else %}\n    <li class=\"grouped\">\n      {{_(\"You have no encryption keys for this contact. You need encryption keys in order to communicate securely.\")}}\n    </li>\n    {% endif %}\n  </ul>\n\n    <a data-email=\"{{result.contact.email.0.email}}\"\n       class=\"button-secondary message-action-find-keys add-bottom\">\n      <span class=\"icon-key\"></span> {{_(\"Find Encryption Keys\")}}\n    </a>\n\n  {# FIXME: Commenting out for Beta\n  <h4>{{_(\"Stats\")}}</h4>\n  <ul class=\"contact-detail\">\n    <li>\n      Messages Sent To: {{result.sent_messages}}<br>\n      Messages Received From: {{result.received_messages}}<br>\n      {% if result.last_contact_to %}\n      Last Contacted: <a href=\"{{result.last_contact_to_msg_url}}\">{{ friendly_datetime(result.last_contact_to) }}</a><br>\n      {% else %}\n      You've Never Contacted {{ result.contact.fn }}<br>\n      {% endif %}\n      {% if result.last_contact_from %}\n      Last Contacted You: <a href=\"{{result.last_contact_from_msg_url}}\">{{ friendly_datetime(result.last_contact_from) }}</a><br>\n      {% else %}\n      You've Never Been Contacted By {{ result.contact.fn }}\n      {% endif %}\n    </li>\n  </ul>\n  #}\n\n  {% set contact_search = \"email:\" + result.contact.email.0.email %}\n  {% set conversations = mailpile(\"search\", contact_search).result %}\n  {% if conversations and conversations.data %}\n  <h4>{{_(\"Conversations\")}}</h4>\n  <ul class=\"contact-tag-filter\">\n    <!-- FIXME: Commented out for Beta\n    <li><select><option>Most Recent</option><option>Longest Conversations</option><option>Oldest</option></select></li>\n    -->\n    {% if conversations.data.tags %}\n    {% for tid, tag in conversations.data.tags.iteritems() %}\n    {% if tag.display in (\"priority\", \"tag\", \"archive\") %}\n    <li><a href=\"{{ config.sys.http_path }}/search/?q=from:{{result.contact.email.0.email}}&tag:{{tag.slug}}\" style=\"color: {{theme_settings().colors[tag.label_color]}}\" data-tid=\"{{tid}}\"><span class=\"{{tag.icon}}\"></span> {{tag.name}}</a></li>\n    {% endif %}\n    {% endfor %}\n    {% endif %}\n  </ul>\n  <ul class=\"items grouped\">\n    {% for mid in conversations.thread_ids %}\n      {% set thread_mid = conversations.data.metadata[mid].thread_mid %}\n      {% set conversation = conversations.data.metadata[mid] %}\n      {% set thread_count = conversations.data.threads[thread_mid]|length + 1 %}\n      <li class=\"grouped\">\n        <a href=\"{{ config.sys.http_path }}/thread/={{ mid }}/\">{{ nice_subject(conversation.subject) }}&nbsp;&nbsp;\n          <span class=\"text-detail\"><span class=\"icon-inbox\"></span> {{ thread_count }} Messages &nbsp; <span class=\"icon-social\"></span> {{ conversation.to_aids|length + conversation.cc_aids|length }} People</span>\n        </a>\n      </li>\n    {% endfor %}\n  </ul>\n  {% else %}\n  <h4>{{_(\"No Conversations\")}} :(</h4>\n  {% endif %}\n{% else %}\n    {% set error_title = \"contact_missing\" %}\n    {% include(\"partials/errors_content.html\") %}\n{% endif %}\n</div>\n<script>\n$(document).ready(function() {\n  Mailpile.Contacts.init();\n});\n</script>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/crypto/gpg/key/index.html",
    "content": "{% extends \"logs/layout.html\" %}\n\n{% block title %}\n  {{ _(\"Download Public Key\")}}\n{% endblock %}\n{% block content %}\n<div class=\"content-normal\" style=\"max-width: 46em;\">\n\n  <div>\n    {% set rows = 1 + result.public_key.splitlines()|length %}\n    <textarea rows={{ (rows > 20) and 20 or rows }} style=\"width: 100%\">\n      {{- result.public_key -}}\n    </textarea>\n  </div>\n\n  <p>\n    <a class=\"auto-modal auto-modal-sticky\" href=\"{{ U('/settings/set/password/keys.html') }}\">\n      <button class=\"button button-primary\" aria-hidden=\"true\">\n        {{_(\"Back\")}}\n      </button>\n    </a>\n    <a target=_blank href=\"{{ U('/crypto/gpg/key/', result.key_id, '/?download=1') }}\">\n      <button class=\"button button-secondary right\">\n        <span class=\"icon icon-download\"></span> {{_(\"Download\")}}\n      </button>\n    </a>\n  </p>\n\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/crypto/tls/getcert/index.html",
    "content": "{% extends \"logs/layout.html\" %}\n\n{% block title %}\n {%- if ui_tls_failed %}\n  {{ _(\"Invalid TLS Certificate\")}}\n {%- else %}\n  {{- _(\"Examine TLS certificates\") }}\n {%- endif %}\n{% endblock %}\n{% block content %}\n<div class=\"content-normal\" style=\"max-width: 46em;\">\n\n{%- if ui_tls_failed %}\n <h1>\n   <span class=\"icon icon-signature-invalid color-12-red\"></span>\n   {{ _(\"Invalid TLS Certificate\")}}\n </h1>\n{%- else %}\n <p id=\"showdbg\" style=\"float: right; opacity: 0.5;\">\n   <a href=\"javascript:$('#debuginfo').show() && $('#showdbg').hide() && False;\"\n      title=\"{{_('Show debug information')}}\">{::}</a>\n </p>\n{%- endif %}\n\n <form action=\"{{ U(\"/crypto/tls/getcert/\") }}\" method=\"POST\">\n  {{ csrf_field|safe }}\n\n{%- for host in state.query_args.host %}\n {%- if result and result != True and host in result %}\n  <input type=\"hidden\" name=\"host\" value=\"{{ host }}\">\n  {%- set r = result[host] %}\n  {%- if r[0] %}\n   {%- set cert_validated = r[2].cert_validated %}\n   {%- set hostname_matches = r[2].hostname_matches %}\n   {%- set tofu_invalid = r[2].tofu_invalid %}\n   {%- set valid = cert_validated and hostname_matches and not tofu_invalid %}\n\n {%- if ui_tls_failed and not valid %}\n  <p>\n    {{ _(\"The identity of the remote server ({H}) could not be verified.\").format(H=host)}}\n  </p>\n  <p>\n  </p>\n {%- else %}\n  <h1 title=\"{{host}}\">\n    <span class=\"icon icon-links\"></span>\n    {{ host|replace(':443', '')|replace(':', ' : ')|truncate(25) }}\n  </h1>\n  <br>\n {%- endif %}\n\n  <div style=\"display: block; float: right; margin: 0 0 1em 1em;\">\n    <p style=\"font-family: monospace; margin-bottom: 0px;\">\n      {% for grp in r[2].fingerprint %}\n        <span style=\"background: #a{{grp[0]}}a{{grp[1:]}}; color: #000000\">{{ grp }}</span>\n        {% if not (loop.index % 4) %}<br>{% endif %}\n      {% endfor %}\n    </p>\n    <p style=\"text-align: center\">{{_(\"SHA-256 Fingerprint\")}}</p>\n  </div>\n\n  <h4>{{_(\"Certificate Vitals\")}}:</h4>\n  {% set green = (valid or r[2].using_tofu) and 'color-08-green' or 'color-02-gray' %}\n  <ul class='list' style=\"margin: 1em 0 1.5em 1em; list-style: none\">\n   {% if tofu_invalid %}\n    <li><span class=\"icon icon-signature-invalid color-12-red\"></span> &nbsp;\n        {{_(\"TOFU failed; certificate hasn't been seen before.\")}}\n   {% elif r[2].using_tofu %}\n    <li><span class=\"icon icon-signature-verified color-06-blue\"></span> &nbsp;\n        {{_(\"TOFU is active; further validation is not required.\")}}\n   {% elif r[2].tofu_seen %}\n    <li><span class=\"icon icon-star {{ green }}\"></span> &nbsp;\n        {{_(\"Has been seen on this server before.\")}}\n   {% endif %}\n   {% if not hostname_matches %}\n    <li {% if r[2].using_tofu %}style=\"opacity: 0.5\"{% endif -%}\n        ><span class=\"icon icon-signature-invalid color-12-red\"></span> &nbsp;\n        {{_(\"Not valid for this server, issued to {S}.\")\n            .format(S=r[2].subject.commonName)}}\n   {% elif hostname_matches != 'unknown' %}\n    <li {% if r[2].using_tofu %}style=\"opacity: 0.5\"{% endif -%}\n        ><span class=\"icon icon-signature-verified {{ green }}\"></span> &nbsp;\n        {{_(\"Is valid for this server.\")}}\n   {% endif %}\n   {% if cert_validated %}\n    <li {% if r[2].using_tofu %}style=\"opacity: 0.5\"{% endif -%}\n        ><span class=\"icon icon-signature-verified {{ green }}\"></span> &nbsp;\n        {{_(\"Issued and signed by {CA}.\").format(CA=r[2].issuer.commonName|truncate(38))}}\n   {% else %}\n    <li {% if r[2].using_tofu %}style=\"opacity: 0.5\"{% endif -%}\n        ><span class=\"icon icon-signature-invalid color-12-red\"></span> &nbsp;\n        {{_(\"Could not be validated against known Certificate Authorities.\")}}\n   {% endif %}\n    <li {% if r[2].using_tofu %}style=\"opacity: 0.5\"{% endif -%}\n        ><span class=\"icon {% if r[2].date_matches %}icon-clock {{ green }}{% else %}icon-signature-invalid color-12-red{% endif %}\"></span> &nbsp;\n        {{_(\"Valid from {DATE1} until {DATE2}.\")\n            .format(DATE1=r[2].not_valid_before|friendly_datetime,\n                    DATE2=r[2].not_valid_after|friendly_datetime) }}\n  </ul>\n  <br clear='both'>\n\n{% if ui_tls_failed and not valid %}\n  <p>\n    {{ _(\"If the certificate cannot be verified, then there is no guarantee you are communicating with the right server.\") }}\n    {{ _(\"Your account details or e-mail may be at risk if you proceed.\") }}\n  </p>\n  <p>\n    {{ _(\"Some servers deliberately use certificates that cannot be verified; in such cases adding a security exception should be safe.\") }}\n    {{ _(\"Ask your e-mail server administrator to be sure.\") }}\n  </p>\n\n{% else %}\n  <div {% if not valid %}style=\"opacity: 0.5;\"{% endif %}>\n    {% if not cert_validated %}\n    <p style=\"text-align: left; margin-left: -10px; padding: 0px 5px; font-size: 0.8em; border-bottom: 1px dashed #777;\">\n      <span class=\"icon icon-signature-unknown\"></span>\n      {{_(\"The information below could not be validated. It may be incorrect or forged.\")}}\n    </p>\n    {% endif %}\n\n    <h5>{{_(\"Issued To\")}}: {{ r[2].subject.commonName }}</h5>\n    <p style=\"margin-left: 1em;\">\n      {%- if r[2].subject.organizationalUnitName %}{{ r[2].subject.organizationalUnitName }}<br>{% endif %}\n      {%- if r[2].subject.organizationName %}{{ r[2].subject.organizationName }}<br>{% endif %}\n      {%- if r[2].subject.localityName %}{{ r[2].subject.localityName }}<br>{% endif %}\n      {%- if r[2].subject.stateOrProvinceName %}{{ r[2].subject.stateOrProvinceName }}<br>{% endif %}\n      {%- if r[2].subject.countryName %}{{ r[2].subject.countryName }}<br>{% endif %}\n    </p>\n\n    {% if cert_validated %}\n    <h5>{{_(\"Issued By\")}}: {{ r[2].issuer.commonName or _(\"unknown\") }} </h5>\n    {% else %}\n    <h5>{{_(\"Apparently Issued By\")}}: {{ r[2].issuer.commonName or _(\"unknown\") }}</h5>\n    {% endif %}\n    <p style=\"margin-left: 1em;\">\n      {%- if r[2].issuer.organizationalUnitName %}{{ r[2].issuer.organizationalUnitName }}<br>{% endif %}\n      {%- if r[2].issuer.organizationName %}{{ r[2].issuer.organizationName }}<br>{% endif %}\n      {%- if r[2].issuer.countryName %}{{ r[2].issuer.countryName }}<br>{% endif %}\n    </p>\n  </div>\n\n  <div id=\"pemdata\" class=\"hide\">\n    <h4>{{_(\"Raw PEM Certificate\")}}:</h4>\n    <pre>{{ r[2].pem }}</pre>\n  </div>\n{% endif %}\n\n{% if not valid %}\n  {% if not r[2].date_matches %}<p style='text-align: center'><i>\n    {{_(\"Current date appears to be {D}. Is the system clock correct?\")\n      .format(D=r[2].current_time|friendly_datetime) }}\n  </i></p>{% endif %}\n\n  <p style=\"text-align: center;\"><i>\n    {{_(\"If this certificate error is unsual, then adding a security exception is not recommended.\")}}\n  </i></p>\n{% endif %}\n\n  <p>\n   {%- if ui_tls_failed %}\n    {%- if not valid %}\n    <button class=\"button button-secondary\" data-dismiss=\"modal\" aria-hidden=\"true\">{{_(\"Try Again Later\")}}</button>\n    {%- else %}\n    <button class=\"button button-secondary\" data-dismiss=\"modal\" aria-hidden=\"true\">{{_(\"OK\")}}</button>\n    {%- endif %}\n   {%- else %}\n    <input type=\"submit\" class=\"button button-primary\" value=\"Refresh\"> &nbsp;\n   {%- endif %}\n    <span style=\"float: right;\">\n   {%- if not ui_tls_failed %}\n      <button id=\"showpem\" class=\"button button-info\"\n              onclick=\"return ($('#pemdata').show() && $('#showpem').hide() && false)\"\n              >{{_(\"Show PEM\")}}</button>\n   {%- endif %}\n   {%- if r[2].using_tofu %}\n      <input type=\"submit\" name=\"tofu-clear\" class=\"button button-info\"\n             value=\"Remove Security Exception\">\n   {%- else %}\n      <input type=\"submit\" name=\"tofu-save\" class=\"button button-info\"\n        {%- if valid %} value=\"Use TOFU for this server\"\n        {%- else %} value=\"Add Security Exception\"\n        {%- endif %}>\n   {%- endif %}\n    </span>\n  </p>\n\n  {%- else %}\n  <h1>\n    <span class=\"icon icon-signature-unknown\"></span>\n    {{ host|replace(':443', '')|replace(':', ' : ') }}\n  </h1><br>\n  <h5>{{ r[1] }}</h5>\n  <p style=\"margin-left: 1em;\">{{ r[2] }}</p>\n  <hr>\n  <p>\n    {{_(\"Server\")}}:\n    <input type=\"text\" name=\"host\" value=\"{{ host }}\" placeholder=\"imap.example.com:993\">\n    <input type=\"submit\" class=\"button button-primary\" value=\"Refresh\">\n  </p>\n  {%- endif %}\n {%- endif %}\n{%- endfor %}\n\n{%- if not state.query_args.host or result == True %}\n  <h1>\n    <span class=\"icon icon-links\"></span>\n    {{_(\"TLS Certificates\")}}\n  </h1><br>\n\n  <p>\n    {{_(\"You can use this tool to examine the TLS certificates of remote servers.\")}}\n  </p>\n  <p>\n    {{_(\"TLS certificates are a form of digital identification, used to ensure you are communicating with the intended server and not an imposter.\")}}\n  </p>\n  <p>\n    {{_(\"If necessary, you can add security exceptions (TOFU: Trust on First Use) which will allow you to connect to a server even if it does not present a valid certificate.\")}}\n    {{_(\"When TOFU is in use, if the remote certificate ever changes the new one will be rejected until you add another exception.\")}}\n  </p>\n\n  <p>\n    {{_(\"Server\")}}:\n    <input id=\"checkhost\" type=\"text\" name=\"host\" value=\"{{ state.query_args.host and state.query_args.host[0] or '' }}\" placeholder=\"imap.example.com:993\">\n    <input id=\"checkit\" type=\"submit\" class=\"button button-primary\" value=\"Check\">\n  </p>\n\n  <script>\n    function examine(host) {\n      $('#checkhost').val(host);\n      $('#checkit').trigger('click');\n    }\n  </script>\n {% if config.tls|length > 0 %}\n  <h4>{{_(\"Known Certificates\")}}</h4>\n  <ul class='list' style=\"margin: 0.5em 0 1.5em 2em; list-style: disc\">\n  {%- for hkey in config.tls %}\n    {%- set host = config.tls[hkey].server %}\n    <li><a onclick=\"examine('{{host}}');\" href=\"#\">\n          {{- host|replace(':443', '')|replace(':', ' : ') }}</a>\n        <small>{% if not config.tls[hkey].use_web_ca %}(TOFU){% else %}(CA){% endif %}</small>\n  {%- endfor %}\n  </ul>\n {%- endif %}\n{%- endif %}\n\n  <pre id=\"debuginfo\" class=\"hide\">\n\"result\": {{ result|json }},\n\"state\": {{ state|json }}\n  </pre>\n\n </form>\n\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/filter/list/index.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block title %}{{_(\"Filters\")}}{% endblock %}\n{% block content %}\n<div id=\"filters-list\" class=\"content-normal clearfix\">\n  {% set tags = mailpile(\"tags\", \"display=*\", \"mode=flat\").result.tags %}\n  {% if result %}\n  <ul class=\"items\">\n    {% for filter in result %}\n    {% set tag_groups = make_filter_groups(filter.tags) %}\n    <li class=\"separate\">\n      <h3>{{ filter.comment }}</h3>\n      <p><small>Type: <strong>{{ filter.type }}</strong></small></p>\n\n      <div id=\"filter-tags-add-{{filter.fid}}\" class=\"filter-tags-add\">\n        <em><span class=\"icon-plus\"></span> Add</em>\n        {% for tid in tag_groups.add %}\n        <a href=\"\">{{tid}}</a>\n        {% endfor %}\n      </div>\n      <div id=\"filter-tags-remove-{{filter.fid}}\" class=\"filter-tags-remove\">\n        <em><span class=\"icon-minus\"></span> Remove</em>\n        {% for tid in tag_groups.remove %}\n        <a href=\"\">{{tid}}</a>\n        {% endfor %}\n      </div>\n      <em>Search Terms:</em><br>\n      <span class=\"icon-search\"></span> <strong>{{ filter.terms }}</strong><br>\n      <label><input type=\"checkbox\"> Autotag</label>\n      <ul class=\"horizontal right\">\n        <li><a href=\"\"><span class=\"icon-settings\"></span> {{_(\"Edit\")}}</a></li>\n        <li><a href=\"\"><span class=\"icon-circle-x\"></span> {{_(\"Delete\")}}</a></li>\n      </ul>\n    </li>\n    {% endfor %}\n  </ul>\n  {% else %}\n  <h3>{{_(\"No Filters Exist\")}} :)</h3>\n  <p>{{_(\"Filters are clever rules that automatically apply tags, sort and otherwise handle your messages.\")}}</p>\n  <p>Current filters are only supported in the CLI (command line interface). You can <a href=\"{{ config.sys.http_path }}/help/filters/command-line-interface/\">read the documentation</a>.</p>\n  {% endif %}\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/group/add/index.html",
    "content": "aksjdklajslkdj"
  },
  {
    "path": "shared-data/default-theme/html/group/index.html",
    "content": "{% extends \"layouts/base.html\" %}\n\n{% block content %}\n{% include(\"partials/sub_nav_contacts.html\") %}\n\n\npooooooopp\n\n\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/help/bottom.html",
    "content": "<div class=\"add-top\">\n  <p>{{_(\"Hopefully this tip helped.\")}}\n     <a href=\"mailto:team@mailpile.is\">{{_(\"Let us know if something is missing or incorrect.\")}}</a>\n     (<a href=\"https://github.com/pagekite/mailpile/issues\" target=\"_blank\">{{_(\"Or file an issue\")}}</a>)\n     :)\n  </p>\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/help/index.html",
    "content": "{% extends \"layouts/full.html\" %}\n{% block title %}{{_(\"Help\")}}{% endblock %}\n{% block content %}\n\n<div class=\"content-normal\">\n\n  <h1><span class=\"icon-help\"></span> {{_(\"Help\")}}</h1>\n\n  <h3>{{_(\"Custom Searches\")}}</h3>\n  <p>{{_(\"Underneath the hood Mailpile is a high performance search engine that can answer complex questions.\")}}</p>\n\n  <h3>{{_(\"Keyboard Shortcuts\")}}</h3>\n  <p>{{_(\"Here will be a list of keyboard shortcuts / keybindings for all you cool cat power users out there!\")}}</p>\n\n  <h3>{{_(\"Encryption & Security\")}}</h3>\n  <p>{{_(\"Are you new to all these terms like encryption and keys? Don't fret. We will do our best to help explain and educate you how to use your Mailpile in the most secure way possible.\")}}</p>\n\n  <h3>{{_(\"Setup\")}}</h3>\n  <ul class=\"circle\">\n    <li><a href=\"{{ config.sys.http_path }}/page/gmail-2-step-verification/\">{{_(\"Gmail's 2 Step App Passwords\")}}</a></li>\n    <li><a href=\"{{ config.sys.http_path }}/page/gmail-access-non-google-accounts/\">{{_(\"Enable Access for Non-Google Apps\")}}</a></li>\n  </ul>\n\n  {% include(\"help/bottom.html\") %}\n\n</div>\n\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/app.js",
    "content": "/* JS - Crypto */\n{% include(\"jsapi/crypto/init.js\") %}\n{% include(\"jsapi/crypto/events.js\") %}\n{% include(\"jsapi/crypto/find.js\") %}\n{% include(\"jsapi/crypto/import.js\") %}\n{% include(\"jsapi/crypto/modals.js\") %}\n{% include(\"jsapi/crypto/tooltips.js\") %}\n{% include(\"jsapi/crypto/ui.js\") %}\n\n/* JS - Compose */\n{% include(\"jsapi/compose/init.js\") %}\n{% include(\"jsapi/compose/crypto.js\") %}\n{% include(\"jsapi/compose/autosave.js\") %}\n{% include(\"jsapi/compose/attachments.js\") %}\n{% include(\"jsapi/compose/recipients.js\") %}\n{% include(\"jsapi/compose/modals.js\") %}\n{% include(\"jsapi/compose/tooltips.js\") %}\n{% include(\"jsapi/compose/events.js\") %}\n{% include(\"jsapi/compose/complete.js\") %}\n{% include(\"jsapi/compose/body.js\") %}\n\n/* JS - Contacts */\n{% if 0 and is_dev_version() %}\n{% include(\"jsapi/contacts/init.js\") %}\n{% include(\"jsapi/contacts/display_modes.js\") %}\n{% include(\"jsapi/contacts/events.js\") %}\n{% include(\"jsapi/contacts/modals.js\") %}\n{% endif %}\n\n/* JS - Search */\n{% include(\"jsapi/search/init.js\") %}\n{% include(\"jsapi/search/bulk_actions.js\") %}\n{% include(\"jsapi/search/events.js\") %}\n{% include(\"jsapi/search/display_modes.js\") %}\n{% include(\"jsapi/search/selection_actions.js\") %}\n{% include(\"jsapi/search/tooltips.js\") %}\n{% include(\"jsapi/search/ui.js\") %}\n\n/* JS - Settings */\n{% include(\"jsapi/settings/content.js\") %}\n\n/* JS - Tags */\n{% include(\"jsapi/tags/init.js\") %}\n{% include(\"jsapi/tags/modals.js\") %}\n{% include(\"jsapi/tags/events.js\") %}\n\n/* JS - Message */\n{% include(\"jsapi/message/init.js\") %}\n{% include(\"jsapi/message/events.js\") %}\n{% include(\"jsapi/message/message.js\") %}\n{% include(\"jsapi/message/html-sandbox.js\") %}\n{% include(\"jsapi/message/tooltips.js\") %}\n{% include(\"jsapi/message/ui.js\") %}\n\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/compose/attachments.js",
    "content": "/* Compose - Attachments */\n\n// Prompts the user if a full-page-refresh happens while uploading attachments.\nwindow.addEventListener(\"beforeunload\", function (event) {\n  if (Mailpile.Composer.Attachments.Uploader.hasPendingUploads()) {\n    var confirmationMessage = '{{_(\"There is still an attachment upload in progress. Are you sure you want to cancel the upload?\")|escapejs}}';\n    event.returnValue = confirmationMessage;     // Gecko, Trident, Chrome 34+\n    return confirmationMessage;                  // Gecko, WebKit, Chrome <34\n  } else {\n    return null;\n  }\n});\n\nMailpile.Composer.Attachments.UploaderImagePreview = function(attachment, file) {\n\n  // Create an instance of the mOxie Image object. This\n  // utility object provides several means of reading in\n  // and loading image data from various sources.\n  // Wiki: https://github.com/moxiecode/moxie/wiki/Image\n  var preloader = new mOxie.Image();\n\n  // Define the onload BEFORE you execute the load()\n  // command as load() does not execute async.\n  preloader.onload = function() {\n\n    // Scale the image (in memory) before rendering it\n    preloader.downsize(150, 150);\n\n    // Grab preloaded the Base64 encoded image data\n    attachment['attachment_data'] = preloader.getAsDataURL();\n\n    var attachment_image_template = Mailpile.safe_template($('#template-composer-attachment-image').html());\n    var attachment_image_html = attachment_image_template(attachment);\n\n    // Append template to view\n    $('#compose-attachments-files-' + attachment.mid).append(attachment_image_html);\n  };\n\n  // Calling the .getSource() returns instance of mOxie.File\n  // Wiki: https://github.com/moxiecode/plupload/wiki/File\n  preloader.load(file.getSource());\n};\n\n\nMailpile.Composer.Attachments.ExistingImagePreview = function(attachment, file) {\n\n  // Load static preview\n  attachment['attachment_data'] = '/message/download/preview/=' + attachment.mid + '/' + attachment.aid + '/';\n\n  var attachment_image_template = Mailpile.safe_template($('#template-composer-attachment-image').html());\n  var attachment_image_html = attachment_image_template(attachment);\n\n  // Append template to view\n  $('#compose-attachments-files-' + attachment.mid).append(attachment_image_html);\n};\n\n\nMailpile.Composer.Attachments.UpdatePreviews = function(attachments, mid, file) {\n\n  // Loop through attachments\n  _.each(attachments, function(attachment, key) {\n\n    if (!$('#compose-attachment-' + mid + '-' + attachment.aid).length) {\n\n      attachment['previewable'] = _.indexOf(['image/bmp',\n                                      'image/gif',\n                                      'image/jpg',\n                                      'image/jpeg',\n                                      'image/pjpeg',\n                                      'image/x-png',\n                                      'image/png',\n                                      'application/vnd.google-apps.photo'], attachment.mimetype);\n\n      if (file && file.name === attachment.filename) {\n        attachment['is_file'] = true;\n      } else {\n        attachment['is_file'] = false;\n      }\n\n      // More UI friendly values\n      var file_parts = attachment.filename.split('.');\n      var file_parts_length = file_parts.length\n\n      if (file_parts.length > 2 || attachment.filename.length > 20) {\n        attachment['name_fixed'] = attachment.filename.substring(0, 16);\n      } else {\n        attachment['name_fixed'] = file_parts[0];\n      }\n\n      attachment['mid'] = mid;\n      attachment['size'] = plupload.formatSize(attachment.length);\n      attachment['extension'] = file_parts[file_parts.length - 1];\n\n      // Determine Preview Type (live image, req image, graphic)\n      if (attachment.previewable > -1 && attachment.is_file) {\n        Mailpile.Composer.Attachments.UploaderImagePreview(attachment, file);\n      }\n      else if (attachment.previewable > -1 && !attachment.is_file) {\n        Mailpile.Composer.Attachments.ExistingImagePreview(attachment);\n      }\n      else {\n        var attachment_template = Mailpile.safe_template($('#template-composer-attachment').html());\n        var attachment_html = attachment_template(attachment);\n        $('#compose-attachments-files-' + mid).append(attachment_html);\n      }\n    } else {\n      console.log('attachment exists ' + attachment.aid);\n    }\n  });\n};\n\n\nMailpile.Composer.Attachments.Uploader = {\n  instance: false,\n  hasPendingUploads: function() {\n    if (this.instance !== false) {\n      return this.instance.total.queued > 0;\n    } else {\n      return false;\n    }\n  }\n};\n\nMailpile.Composer.Attachments.Uploader.uploading = 0;\nMailpile.Composer.Attachments.Uploader.init = function(settings) {\n  var uploader = new plupload.Uploader({\n    runtimes : 'html5',\n    browse_button : settings.browse_button, // you can pass in id...\n    container: settings.container, // ... or DOM Element itself\n    drop_element: settings.container,\n    url : Mailpile.API.U('/api/0/message/attach/'),\n    multipart : true,\n    multipart_params : {\n      'mid': settings.mid,\n      'csrf': Mailpile.csrf_token\n    },\n    file_data_name : 'file-data',\n    filters : {\n      max_file_size : '50mb'\n    },\n    views: {\n      list: true,\n      thumbs: true,\n      active: 'thumbs'\n    },\n    init: {\n      PostInit: function() {\n        $('#compose-attachments-' + settings.mid).find('.compose-attachment-pick').removeClass('hide');\n        $('#compose-attachments-' + settings.mid).find('.attachment-browswer-unsupported').addClass('hide');\n        uploader.refresh();\n      },\n      FilesAdded: function(up, files) {\n        // Loop through added files\n        plupload.each(files, function(file) {\n\n          // Show warning for ~20MB or larger\n          if ((file.size < 200000000) ||\n              confirm(file.name + ' {{_(\"is\")|escapejs}} ' + plupload.formatSize(file.size) + '.\\n' +\n                      '\\n' +\n                      '{{_(\"Some people cannot receive such large e-mails.\")|escapejs}}\\n' +\n                      '{{_(\"Send it anyway?\")}}'))\n          {\n            uploader.start();\n            $('#form-compose-' + settings.mid + ' button.compose-action'\n              ).prop(\"disabled\", true).css({'opacity': 0.25});\n            Mailpile.Composer.Attachments.Uploader.uploading += 1;\n          } else {\n            start_upload = false;\n          }\n        });\n      },\n      UploadProgress: function(up, file) {\n        $('#' + file.id).find('b').html('<span>' + file.percent + '%</span>');\n\tif (file.percent < 100) {\n          var progress = \"<progress value=\"+file.percent+\" max='100'></progress> \"+file.percent+\"%\";\n\t}\n\telse {\n\t  var progress = \"Processing Upload...\";\n\t}\n\tMailpile.notification({status: 'info', message: progress, event_id: \"Upload-\" + file.id });\n      },\n      FileUploaded: function(up, file, response) {\n        if (response.status == 200) {\n\n          var response_json = $.parseJSON(response.response);\n          var new_mid = response_json.result.message_ids[0];\n\n          //console.log(file);\n\t  Mailpile.notification({status: 'info', message: \"Finished uploading \" + file.name, event_id: \"Upload-\" + file.id, flags: \"c\" });\n          Mailpile.Composer.Attachments.UpdatePreviews(response_json.result.data.messages[new_mid].attachments, settings.mid, file);\n\n        } else {\n          Mailpile.notification({status: 'error', message: '{{_(\"Attachment upload failed: status\")|escapejs}}: ' + response.status });\n        }\n        Mailpile.Composer.Attachments.Uploader.uploading -= 1;\n        if (Mailpile.Composer.Attachments.Uploader.uploading < 1) {\n          $('#form-compose-' + settings.mid + ' button.compose-action'\n            ).prop(\"disabled\", false).css({'opacity': 1.0});\n          Mailpile.Composer.Attachments.Uploader.uploading = 0;\n        }\n      },\n      Error: function(up, err) {\n        Mailpile.notification({status: 'error', message: '{{_(\"Could not upload attachment because\")|escapejs}}: ' + err.message + ' ' + err.code });\n        $('#' + err.file.id).find('b').html('Failed ' + err.code);\n        uploader.refresh();\n\tMailpile.notification({status: 'error', message: \"Failed to upload \" + file.name, event_id: \"Upload-\" + file.id });\n        Mailpile.Composer.Attachments.Uploader.uploading -= 1;\n        if (Mailpile.Composer.Attachments.Uploader.uploading < 1) {\n          $('#form-compose-' + settings.mid + ' button.compose-action'\n            ).prop(\"disabled\", false).css({'opacity': 1.0});\n          Mailpile.Composer.Attachments.Uploader.uploading = 0;\n        }\n      }\n    }\n  });\n\n  this.instance = uploader;\n  return uploader.init();\n};\n\nMailpile.Composer.Attachments.Remove = function(mid, aid) {\n  Mailpile.API.message_unattach_post({ mid: mid, att: aid }, function(result) {\n    if (result.status == 'success') {\n      $('#compose-attachment-' + mid + '-' + aid).fadeOut(function() {\n        $(this).remove();\n        Mailpile.Composer.Attachments.UpdatePreviews(result.result.data.messages[mid].attachments, mid, false);\n      });\n    } else {\n      Mailpile.notification(result);\n    }\n  });\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/compose/autosave.js",
    "content": "/* Compose - Autosave */\n\nMailpile.Composer.Autosave = function(mid, form_data, callback) {\n\n  if (Mailpile.Composer.Drafts[mid] === undefined) {\n    Mailpile.Composer.Drafts[mid] = Mailpile.Composer.Model({}, {});\n    Mailpile.Composer.Body.Setup(mid);\n  }\n\n  // Has text changed, or is new?  If so, run autosave.\n  if ($('#compose-text-' + mid).val() != Mailpile.Composer.Drafts[mid].body) {\n\n    // UI Feedback\n    var autosave_msg = $('#compose-message-autosaving-' + mid).data('autosave_msg');\n    $('#compose-message-autosaving-' + mid).html(autosave_msg).fadeIn();\n\n    // Autosave It\n    $.ajax({\n      url      : Mailpile.api.compose_save,\n      type     : 'POST',\n      timeout  : 15000,\n      data     : form_data,\n      dataType : 'json',\n\n      success: function(response) {\n        // Update Message (data model)\n        Mailpile.Composer.Drafts[mid].body = $('#compose-text-' + mid).val();\n\n        // Fadeout autosave UI msg\n        setTimeout(function() {\n          $('#compose-message-autosaving-' + mid).fadeOut();\n        }, 2000);\n        callback();\n      },\n      error: function() {\n        var autosave_error_msg = $('#compose-message-autosaving-' + mid).data('autosave_error_msg');\n        $('#compose-message-autosaving-' + mid).html('<span class=\"icon-x\"></span>' + autosave_error_msg).fadeIn();\n        callback();\n      }\n    });\n  }\n  // Not Autosaving\n  else {\n    callback();\n  }\n};\n\n\nMailpile.Composer.AutosaveAll = function(delay, callback) {\n  var save_chain = [];\n  $('.form-compose').each(function(key, form) {\n    var $form = $(form);\n    save_chain.push(function(chain) {\n      Mailpile.Composer.Autosave($form.data('mid'), Mailpile.Composer.SerializeForm($form),\n                                 function() {\n        if (chain && chain.length) {\n          var nxt = chain.shift();\n          if (delay) {\n            setTimeout(function() { nxt(chain); }, delay)\n          }\n          else {\n            nxt(chain);\n          }\n        }\n      });\n    });\n  });\n  if (callback) save_chain.push(callback);\n  if (save_chain.length) save_chain.shift()(save_chain);\n}\n\n\n/* Compose Autosave - finds each compose form and performs action */\nMailpile.Composer.AutosaveTimer = $.timer(function() {\n  Mailpile.Composer.AutosaveAll(250);\n});\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/compose/body.js",
    "content": "/* Composer - Body */\n\nMailpile.Composer.Model = function(strings, addresses) {\n  var model = strings;\n  model[\"addresses\"] = addresses;\n  return model;\n};\n\n\nMailpile.Composer.Body.Setup = function(mid) {\n\n  // Add Autosize\n  autosize($('#compose-text-' + mid));\n\n  // Is Ephemeral (means .compose-text has quoted_reply)\n  if (/\\breply-all\\b/g.test(mid)) {\n\n    console.log('Is Ephemeral');\n\n    // Add Quoted to Model\n    Mailpile.Composer.Drafts[mid].quoted_reply = $('#compose-text-' + mid).val();\n\n    // If Quoted Reply disabled, remove from field\n    if ($('#compose-quoted-reply-' + mid).parent().data('quoted_reply') === 'disabled') {\n      $('#compose-text-' + mid).val('').trigger('autosize:update');\n    }\n    // Not disabled, add to model\n    else {\n      Mailpile.Composer.Drafts[mid].body = $('#compose-text-' + mid).val();\n    }\n  }\n  // Is Draft add to model\n  else {\n\n    console.log('Is Not Ephemeral');\n\n    Mailpile.Composer.Drafts[mid].body = $('#compose-text-' + mid).val();\n  }\n};\n\n\nMailpile.Composer.Body.QuotedReply = function(mid, state) {\n\n  $checkbox = $('#compose-quoted-reply-' + mid);\n\n  if ($checkbox.is(':checked')) {\n    $checkbox.val('yes');\n  }\n  else {\n    $checkbox.val('no');\n\n    // Check Quoted Setting State\n    if (state === 'unset' && Mailpile.Composer.Drafts[mid].quoted_reply) {\n      Mailpile.Composer.Body.QuotedReplySetup();\n      $('#compose-text-' + mid).val('').trigger('autosize:update');\n    }\n    // Empty body & .compose-text as it's just a quoted reply\n    else if (Mailpile.Composer.Drafts[mid].body === Mailpile.Composer.Drafts[mid].quoted_reply) {\n      Mailpile.Composer.Drafts[mid].body = '';\n      $('#compose-text-' + mid).val('').trigger('autosize:update');\n    }\n    // Replace composer with quoted reply\n    else if (Mailpile.Composer.Drafts[mid].quoted_reply) {\n      Mailpile.Composer.Drafts[mid].body = Mailpile.Composer.Drafts[mid].quoted_reply;\n      $('#compose-text-' + mid).val(Mailpile.Composer.Drafts[mid].quoted_reply).trigger('autosize:update');\n    }\n  }\n};\n\n\nMailpile.Composer.Body.QuotedReplySetup = function() {\n  Mailpile.API.with_template('modal-compose-quoted-reply', function(modal) {\n    Mailpile.UI.show_modal(modal());\n  });\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/compose/complete.js",
    "content": "/* Compose - Complete */\n\nMailpile.Composer.Complete = function(mid) {\n  Mailpile.go(Mailpile.urls.message_sent + mid + \"/\");\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/compose/crypto.js",
    "content": "/* Composer - Crypto */\n\nMailpile.Composer.Crypto.UpdateEncryptionState = function(mid, chain, initial) {\n  // Assemble all the recipient addresses, as well as our sending address\n  var emails = [$('#compose-from-selected-' + mid).find('.address').html()];\n  Mailpile.Composer.Recipients.GetAll(mid, function(rcpt) {\n    emails.push(rcpt.address);\n  });\n  // Ask the back-end for updated address-book info and an aggregate\n  // recommended crypto policy.\n  var cp_args = {email: emails};\n  if ($('form#form-compose-' + mid).data('should-encrypt') == 'Y') {\n    cp_args['should-encrypt'] = 'Y';\n  };\n  Mailpile.API.crypto_policy_get(cp_args, function(response) {\n    var r = response.result;\n\n    // Update attach key (or not) state\n    if (r['can-sign']) {\n      $('.compose-attach-key').show();\n    }\n    else {\n      $('.compose-attach-key').hide();\n    }\n    $('#compose-attach-key-' + mid).prop('checked',\n                                         r['can-sign'] && r['send-keys']);\n    Mailpile.Composer.Crypto.AttachKey(mid);\n\n    // Record our capabilities: can we encrypt? Sign?\n    $('#compose-crypto-encryption-' + mid).data('can', r['can-encrypt']);\n    $('#compose-crypto-signature-' + mid).data('can', r['can-sign']);\n\n    var policy = undefined;\n    var changes = 0;\n    if (emails.length > 1) {\n\n      if (!initial) {\n        // Record reason for current policy (I could not find a better place\n        // than the send button) but there sure is :)\n        Mailpile.Composer.Crypto.SendButton().data('policy-reason', r['reason']);\n        policy = r['crypto-policy'];\n      }\n\n      // Embellish our recipients with data from the backend; in particular\n      // this adds the keys and avatars to things manually typed.\n      Mailpile.Composer.Recipients.WithToCcBcc(mid, function(field, rcpts) {\n        var changed = false;\n        for (var i in rcpts) {\n          var updated = r.addresses[rcpts[i].address];\n          if (updated) {\n            if (rcpts[i].fn.indexOf(' ') < 0) rcpts[i].fn = updated.fn;\n            rcpts[i].keys = updated.keys;\n            rcpts[i].flags = updated.flags;\n            rcpts[i].photo = updated.photo;\n            changed = true;\n            changes += 1;\n          }\n        };\n        if (changed) Mailpile.Composer.Recipients.Update(mid, field, rcpts);\n      });\n    }\n\n    if (changes || initial) {\n      Mailpile.API.async_crypto_keytofu_post(cp_args, function(data, ev) {\n        if (data.result && data.result.imported_keys)\n        {\n          for (key in data.result.imported_keys) {\n            Mailpile.Composer.Crypto.UpdateEncryptionState(mid);\n            return;\n          }\n        }\n      });\n    }\n\n    // Update encrypt/sign icons\n    Mailpile.Composer.Crypto.LoadStates(mid, policy);\n    Mailpile.Composer.Crypto.SetState(mid);\n    if (chain) chain(mid);\n  });\n};\n\n\nMailpile.Composer.Crypto.Unencryptables = function(mid) {\n  var unencryptable = [];\n  Mailpile.Composer.Recipients.GetAll(mid, function(rcpt) {\n    if (!rcpt.flags.secure) unencryptable.push(rcpt);\n  });\n  return unencryptable;\n};\n\n\nMailpile.Composer.Crypto.LoadStates = function(mid, state) {\n  state = state || $('#compose-crypto-' + mid).val();\n\n  var signature = 'none';\n  if (state.match(/sign/)) {\n    signature = 'sign';\n  }\n  Mailpile.Composer.Crypto.SignatureToggle(signature, mid);\n\n  var encryption = 'none';\n  if (state.match(/encrypt/)) {\n    encryption = 'encrypt';\n  }\n  Mailpile.Composer.Crypto.EncryptionToggle(encryption, mid);\n\n  Mailpile.Composer.Crypto.UpdateSendButton(mid, state);\n};\n\n/* Compose - Retrieve a jQuery instance of the Send button */\nMailpile.Composer.Crypto.SendButton = function(mid) {\n  return $('#form-compose-' + mid + ' button[name=send]');\n};\n\n/* Compose - Update the display properties for the send button */\nMailpile.Composer.Crypto.UpdateSendButton = function(mid, state) {\n  // FIXME: We need to know if encryption or signing is REQUIRED, and\n  // disable or enable the send button based on that. The conflict state\n  // doesn't cover for when the user does illegal things manually.\n  var cryptoStateMessage = Mailpile.Composer.Crypto.GetCryptoStateMessage(mid);\n\n  Mailpile.Composer.Crypto.SendButton(mid)\n    .data('crypto-state', state)\n    .data('crypto-reason', cryptoStateMessage)\n    .attr('title', cryptoStateMessage)\n    .css({ 'opacity': (state.match(/conflict/)) ? 0.25 : 1.0 });\n};\n\n\n/* Compose - Build crypto state message depending on current crypto selection */\nMailpile.Composer.Crypto.GetCryptoStateMessage = function(mid) {\n  var currentState = Mailpile.Composer.Crypto.GetState(mid);\n  var policyReason = Mailpile.Composer.Crypto.SendButton(mid).data('policy-reason') || '';\n  var message = {\n    \"none\":                 '{{_(\"Neither signing nor encrypting.\")|escapejs}}',\n    \"openpgp-sign\":         '{{_(\"Signing but not encrypting.\")|escapejs}}',\n    \"openpgp-encrypt\":      '{{_(\"Encrypting but not signing.\")|escapejs}}',\n    \"openpgp-sign-encrypt\": '{{_(\"Signing and encrypting.\")|escapejs}}',\n  }[currentState] || ('{{_(\"Undefined state: \")|escapejs}}' + currentState);\n  return [message, policyReason].join(\" \").trim();\n};\n\n\n/* Compose - Set crypto state of message */\nMailpile.Composer.Crypto.SetState = function(mid) {\n  var newState = Mailpile.Composer.Crypto.GetState(mid);\n  $('#compose-crypto-' + mid).val(newState);\n  return newState;\n};\n\n\n/* Compose - Determine and return current crypto state of message */\nMailpile.Composer.Crypto.GetState = function(mid) {\n  // Returns: none, openpgp-sign, openpgp-encrypt and openpgp-sign-encrypt\n  var state = 'none';\n  var signature = $('#compose-signature-' + mid).val();\n  var encryption = $('#compose-encryption-' + mid).val();\n\n  if (signature == 'sign' && encryption == 'encrypt') {\n    state = 'openpgp-sign-encrypt';\n  }\n  else if (signature == 'sign') {\n    state = 'openpgp-sign';\n  }\n  else if (encryption == 'encrypt') {\n    state = 'openpgp-encrypt';\n  }\n  else {\n    state = 'none';\n  }\n  return state;\n};\n\n\n/* Compose - Render crypto \"signature\" of a message */\nMailpile.Composer.Crypto.SignatureToggle = function(status, mid, manual) {\n  $elem = $('#compose-crypto-signature-' + mid);\n\n  // If manually set, store new preference for signing.\n  if (manual === 'manual') {\n    $elem.data('should', (status === 'sign'));\n  }\n\n  // If no preference for signing is stored, enable it.\n  var shouldSign = $elem.data('should');\n  shouldSign = (undefined === shouldSign) ? (status === 'sign') : shouldSign;\n\n  // If signin capibilities can not be detected, default to false.\n  var canSign = $elem.data('can');\n  canSign = (undefined === canSign) ? false : canSign;\n\n  // FIXME: Could/Should we store that on the element instead of passing it\n  // via `status` object.\n  if (status == 'cannot') {\n    canSign = false;\n  }\n\n  var willSign = canSign && shouldSign;\n  var newState = willSign ? 'sign' : status;\n\n  if (status === 'cannot') {\n    $elem.data('crypto_color', 'crypto-color-red');\n    $elem.attr('title', '{{_(\"Verification Error\")|escapejs}}');\n    $elem.find('span.icon').removeClass('icon-signature-none icon-signature-verified').addClass('icon-signature-error');\n    $elem.find('span.text').html('{{_(\"Error accessing your encryption key\")|escapejs}}');\n    $elem.removeClass('none').addClass('error bounce');\n  }\n  else if (willSign) {\n    $elem.data('crypto_color', 'crypto-color-green');\n    $elem.attr('title', '{{_(\"This message will be signed and verifiable to recipients who have your encryption key\")|escapejs}}');\n    $elem.find('span.icon').removeClass('icon-signature-none').addClass('icon-signature-verified');\n    $elem.find('span.text').html('{{_(\"Signed\")|escapejs}}');\n    $elem.removeClass('none').addClass('signed bounce');\n\n  } else {\n    $elem.data('crypto_color', 'crypto-color-gray');\n    $elem.attr('title', '{{_(\"This message will not be verifiable, recipients will have no way of knowing it actually came from you\")|escapejs}}');\n    $elem.find('span.icon').removeClass('icon-signature-verified').addClass('icon-signature-none');\n    $elem.find('span.text').html('{{_(\"Unsigned\")|escapejs}}');\n    $elem.removeClass('signed').addClass('none bounce');\n  }\n\n  // Set Form Value\n  if ($('#compose-signature-' + mid).val() !== newState) {\n    $elem.addClass('bounce');\n    $('#compose-signature-' + mid).val(newState);\n\n    // Remove Animation\n    setTimeout(function() {\n      $elem.removeClass('bounce');\n    }, 1000);\n\n    Mailpile.Composer.Crypto.UpdateSendButton(mid, newState);\n    if (manual) Mailpile.Composer.Crypto.SetState(mid);\n  }\n};\n\n\n/* Compose - Render crypto \"encryption\" of a message */\nMailpile.Composer.Crypto.EncryptionToggle = function(status, mid, manual) {\n  var encryption = $('#compose-encryption-' + mid).val();\n  var can = $('#compose-crypto-encryption-' + mid).data('can');\n  if (!manual && (status === 'none') && (encryption === 'encrypt') && !can) {\n    // If we were encrypting, but as a side-effect are no longer capable of\n    // doing so, then we go to the \"cannot\" state instead of \"none\".\n    status = 'cannot';\n  }\n  else if (!can) {\n    status = (status == 'cannot') ? status : 'none';\n  }\n\n  if (status === 'encrypt') {\n    $('#compose-crypto-encryption-' + mid).data('crypto_color', 'crypto-color-green');\n    $('#compose-crypto-encryption-' + mid).attr('title', '{{_(\"This message and attachments will be encrypted, unreadable to all but the intended recipients\")|escapejs}}');\n    $('#compose-crypto-encryption-' + mid).find('span.icon').removeClass('icon-lock-open').addClass('icon-lock-closed');\n    $('#compose-crypto-encryption-' + mid).find('span.text').html('{{_(\"Encrypted\")|escapejs}}');\n    $('#compose-crypto-encryption-' + mid).removeClass('none error cannot').addClass('encrypted');\n\n  } else if (status === 'cannot') {\n    $('#compose-crypto-encryption-' + mid).data('crypto_color', 'crypto-color-orange');\n    $('#compose-crypto-encryption-' + mid).attr('title', '{{_(\"This message cannot be encrypted because you do not have keys for one or more recipients\")|escapejs}}');\n    $('#compose-crypto-encryption-' + mid).find('span.icon').removeClass('icon-lock-closed').addClass('icon-lock-open');\n    $('#compose-crypto-encryption-' + mid).find('span.text').html('{{_(\"Can Not Encrypt\")|escapejs}}');\n    $('#compose-crypto-encryption-' + mid).removeClass('none encrypted error').addClass('cannot');\n\n  } else if (status === 'none' || status == '') {\n    $('#compose-crypto-encryption-' + mid).data('crypto_color', 'crypto-color-gray');\n    $('#compose-crypto-encryption-' + mid).attr('title', '{{_(\"This message and metadata will not be encrypted, if intercepted anyone can read it\")|escapejs}}');\n    $('#compose-crypto-encryption-' + mid).find('span.icon').removeClass('icon-lock-closed').addClass('icon-lock-open');\n    $('#compose-crypto-encryption-' + mid).find('span.text').html('{{_(\"None\")|escapejs}}');\n    $('#compose-crypto-encryption-' + mid).removeClass('encrypted cannot error').addClass('none');\n\n  } else {\n    $('#compose-crypto-encryption-' + mid).data('crypto_color', 'crypto-color-red');\n    $('#compose-crypto-encryption-' + mid).attr('title', '{{_(\"There was an error prepping this message for encryption\")|escapejs}}');\n    $('#compose-crypto-encryption-' + mid).find('span.icon').removeClass('icon-lock-open icon-lock-closed').addClass('icon-lock-error');\n    $('#compose-crypto-encryption-' + mid).find('span.text').html('{{_(\"Error Encrypting\")|escapejs}}');\n    $('#compose-crypto-encryption-' + mid).removeClass('encrypted cannot none').addClass('error');\n  }\n\n  // Set Form Value\n  if ($('#compose-encryption-' + mid).val() !== status) {\n    $('#compose-crypto-encryption-' + mid).addClass('bounce');\n    $('#compose-encryption-' + mid).val(status);\n\n    // Remove Animation\n    setTimeout(function() {\n      $('#compose-crypto-encryption-' + mid).removeClass('bounce');\n    }, 1000);\n\n    if (manual) Mailpile.Composer.Crypto.SetState(mid);\n  }\n};\n\n\nMailpile.Composer.Crypto.AttachKey = function(mid) {\n  var checkbox = $('#compose-attach-key-' + mid);\n  var hiddenak = $('#compose-hidden-attach-key-' + mid);\n  if (checkbox.is(':checked')) {\n    hiddenak.val('yes');\n  } else {\n    hiddenak.val('no');\n  }\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/compose/events.js",
    "content": "/* Composer - Events */\n\n$(document).on('click', '.compose-contact-find-keys', function() {\n  var $elem = $(this);\n  var mid = $elem.data('mid');\n  var email = $elem.data('email');\n  Mailpile.UI.Modals.CryptoFindKeys({\n    query: email,\n    strict: true,\n    imported: function() {\n      if (mid) Mailpile.Composer.Crypto.UpdateEncryptionState(mid);\n      $('#modal-full').modal('toggle');\n    }\n  });\n});\n\n\nMailpile.Composer.ToggleEncryption = function(mid, want) {\n  var status = $('#compose-encryption-' + mid).val();\n  var can = $('#compose-crypto-encryption-' + mid).data('can');\n  var change = '';\n\n  if (status === 'encrypt') {\n    change = want || 'none';\n  }\n  else if (status === 'cannot' && can) {\n    change = want || 'encrypt';\n  }\n  else if (status === 'cannot' || !can) {\n    change = 'cannot';\n    Mailpile.UI.Modals.ComposerEncryptionHelper(mid, {\n      state: 'cannot',\n      unencryptables: Mailpile.Composer.Crypto.Unencryptables(mid)\n    });\n  }\n  else {\n    change = want || 'encrypt';\n  }\n\n  Mailpile.Composer.Crypto.EncryptionToggle(change, mid, 'manual');\n  if (change == 'encrypt') {\n    // Encrypting without signing makes little sense; yes this makes\n    // things annoying for those who disagree.\n    Mailpile.Composer.Crypto.SignatureToggle('sign', mid, 'manual');\n  }\n  Mailpile.Composer.Tooltips.Encryption();\n  return false;\n};\n$(document).on('click', '.compose-crypto-encryption', function() {\n  Mailpile.Composer.ToggleEncryption($(this).data('mid'));\n});\n\n\n$(document).on('click', '.compose-crypto-signature', function() {\n\n  var mid = $(this).data('mid');\n  var status = $('#compose-signature-' + mid).val();\n  var change = '';\n\n  if (status === 'sign') {\n    change = 'none';\n  } else {\n    change = 'sign';\n  }\n\n  Mailpile.Composer.Crypto.SignatureToggle(change, mid, 'manual');\n  Mailpile.Composer.Tooltips.Signature();\n  return false;\n});\n\n\n/* Compose - Show Cc, Bcc */\n$(document).on('click', '.compose-show-field', function(e) {\n  $(this).hide();\n  var field = $(this).text().toLowerCase();\n  var mid = $(this).data('mid');\n  $('#compose-' + field + '-html').show().removeClass('hide');\n\n  // Configure select2\n  Mailpile.Composer.Recipients.AddressField('compose-' + field + '-' + mid);\n});\n\n\n$(document).on('click', '.compose-hide-field', function(e) {\n  var field = $(this).attr('href').substr(1);\n  var mid = $(this).data('mid');\n  $('#compose-' + field + '-html').hide().addClass('hide');\n  $('#compose-' + field + '-show').fadeIn('fast');\n\n  // Destroy select2\n  $('#compose-' + field + '-' + mid).select2('destroy');\n});\n\n\n/* Compose - Send, Save, Reply */\n$(document).on('click', '.compose-action', function(e) {\n  e.preventDefault();\n  return Mailpile.Composer.SendMessage(this);\n});\n\nMailpile.Composer.SerializeForm = function($form) {\n  var serialized = $form.serialize();\n  // This is a Horrible Hack to undo the very silly separator we use to\n  // hack around select2 being unable to fully parse e-mail addresses.\n  var re = /((^|&)(to|cc|bcc)=[^&]*)%25%26%40%25/gi;\n  while (serialized.search(re) != -1) {\n    serialized = serialized.replace(re, '$1,+');\n  }\n  return serialized\n};\n\nMailpile.Composer.SendMessage = function(send_btn) {\n  var $send_btn = $(send_btn);\n  var action = $send_btn.val();\n  var mid = $send_btn.parent().data('mid');\n  var post_send_url = $send_btn.closest('.has-url').data('url');\n  var form_data = Mailpile.Composer.SerializeForm($('#form-compose-' + mid));\n\n  // Warn the user if he's trying to go against his own security policies,\n  // let him abort... or not.\n  if ((action != 'save') && $send_btn.data('crypto-state').match(/conflict/)) {\n    if (!confirm($send_btn.data('crypto-reason') + '\\n\\n' +\n                 '{{_(\"Click OK to send the message anyway.\")|escapejs}}')) return;\n  }\n\n  if (action === 'send') {\n    var action_url     = Mailpile.api.compose_send;\n    var action_status  = 'success';\n    var action_message = 'Your message was sent <a id=\"status-undo-link\" data-action=\"undo-send\" href=\"#\">undo</a>';\n    var done_working = Mailpile.notify_working(\"{{_('Preparing to send...')|escapejs}}\", 100, 'blank');\n  }\n  else if (action == 'save') {\n    var action_url     = Mailpile.api.compose_save;\n    var action_status  = 'info';\n    var action_message = 'Your message was saved';\n    var done_working = Mailpile.notify_working(\"{{_('Saving...')|escapejs}}\", 500);\n  }\n  else if (action == 'reply') {\n    var action_url     = Mailpile.api.compose_send;\n    var action_status  = 'success';\n    var action_message = 'Your reply was sent';\n    var done_working = Mailpile.notify_working(\"{{_('Preparing to send...')|escapejs}}\", 100, 'blank');\n  }\n\n  // FIXME: Use Mailpile.API instead of this.\n  $.ajax({\n    url      : action_url,\n    type     : 'POST',\n    data     : form_data,\n    dataType : 'json',\n\n    success: function(response) {\n      // Is A New Message (or Forward)\n      done_working();\n      if (action === 'send' && response.status === 'success') {\n        if (post_send_url) {\n          if (post_send_url.indexOf('#') > -1) {\n            post_send_url = post_send_url.substring(0, post_send_url.indexOf('#'));\n          }\n          Mailpile.go(post_send_url + \"/\" + mid + '#pile-message-' + mid);\n        }\n        else {\n          Mailpile.go(Mailpile.urls.message_sent + response.result.thread_ids[0] + \"/\");\n        }\n      }\n      // Is Thread Reply\n      else if (action === 'reply' && response.status === 'success') {\n        Mailpile.Composer.Complete(response.result.thread_ids[0]);\n      }\n      else if (response.status === 'error' &&\n               response.password_needed &&\n               response.password_needed[0].id) {\n        Mailpile.auto_modal({\n          url: ('{{ U(\"/settings/set/password/keys.html?is_locked=yes&id=\") }}'\n                + response.password_needed[0].id),\n          header: 'off',\n          callback: function(result) {\n            // Let's try that again!\n            Mailpile.Composer.SendMessage(send_btn);\n          }\n        });\n      }\n      else {\n        Mailpile.notification(response);\n      }\n    },\n\n    error: function() {\n      done_working();\n      Mailpile.notification({\n        status: 'error',\n        message: 'Could not ' + action + ' your message'\n      });\n    }\n  });\n};\n\n\n/* Compose - Pick Send Date */\n$(document).on('click', '.pick-send-datetime', function(e) {\n\n  if ($(this).data('datetime') == 'immediately') {\n    $('#reply-datetime-display').html($(this).html());\n  }\n  else {\n    $('#reply-datetime-display').html('in ' + $(this).html());\n  }\n\n  $('#reply-datetime span.icon').removeClass('icon-arrow-down').addClass('icon-arrow-right');\n});\n\n\n/* Compose - Details */\n$(document).on('click', '.compose-show-details', function(e) {\n\n  e.preventDefault();\n  var mid = $(this).data('mid');\n  var new_message = $(this).data('message');\n\n  if ($('#compose-details-' + mid).hasClass('hide')) {\n    var old_message = $(this).html();\n\n    // Instatiate select2\n    if ($('#compose-to-' + mid).val()) {\n      Mailpile.Composer.Recipients.AddressField('compose-to-' + mid);\n    }\n    if ($('#compose-cc-' + mid).val()) {\n      Mailpile.Composer.Recipients.AddressField('compose-cc-' + mid);\n    }\n    if ($('#compose-bcc-' + mid).val()) {\n      Mailpile.Composer.Recipients.AddressField('compose-bcc-' + mid);\n    }\n\n    Mailpile.Composer.Tooltips.ContactDetails();\n\n    $('#compose-details-' + mid).slideDown('fast').removeClass('hide');\n    $('#compose-to-summary-' + mid).hide();\n    $(this).html('<span class=\"icon-eye\"></span> <span class=\"text\">' + new_message + '</span>');\n    $(this).data('message', old_message).attr('data-message', old_message);\n  }\n  else {\n    var old_message = $(this).find('.text').html();\n    $('#compose-details-' + mid).slideUp('fast').addClass('hide');\n    $('#compose-to-summary-' + mid).show();\n    $(this).html(new_message);\n    $(this).data('message', old_message).attr('data-message', old_message);\n  }\n});\n\n\n/* Compose - Delete message that's in a composer */\n$(document).on('click', '.compose-message-trash', function() {\n  var mid = $(this).data('mid');\n  Mailpile.API.message_unthread_post({ mid: mid }, function(response) {\n    Mailpile.API.tag_post({\n      mid: mid,\n      add: 'trash',\n      del: ['drafts', 'blank']\n    }, function(response_trash) {\n      if (response_trash.status === 'success') {\n        // FIXME: Make this more intelligent\n        Mailpile.go('/in/inbox/');\n      }\n      else if (response_trash.status === 'success' &&\n               Mailpile.instance.state.command_url === '/message/') {\n        // FIXME: NOT REACHED\n        $('#form-compose-' + mid).removeClass('form-compose clearfix')\n                            .addClass('thread-notification')\n                            .html($('#template-thread-notification-draft-trash').html());\n      } else {\n        Mailpile.notification(response_trash.status, response_trash.message);\n      }\n    });\n  });\n});\n\n\n$(document).on('click', '.compose-from', function(e) {\n  e.preventDefault();\n  var mid = $(this).data('mid');\n  var sig = $(this).data('sig');\n  var avatar = $(this).find('.avatar img').attr('src');\n  var name = $(this).find('.name').html();\n  var address = $(this).find('.address').html();\n  $('#compose-from-selected-' + mid).find('.avatar img').attr('src', avatar);\n  $('#compose-from-selected-' + mid).find('.name').html(name);\n  $('#compose-from-selected-' + mid).find('.address').html(address);\n  $('#compose-from-' + mid).val(name + ' <' + address + '>');\n  $('#compose-send-' + mid).show();\n  if (sig) {\n    $('#compose-signature-' + mid).html(\n      '-- <br>' + sig.replace(/\\n/g, '<br>') + '<br><br>');\n  }\n  else {\n    $('#compose-signature-' + mid).html('');\n  }\n  Mailpile.Composer.Crypto.UpdateEncryptionState(mid, function() {});\n});\n\n\n$(document).on('click', '.compose-attachment-remove', function(e) {\n  Mailpile.Composer.Attachments.Remove($(this).data('mid'), $(this).data('aid'));\n});\n\n\n$(document).on('focus', '.compose-text', function() {\n  autosize($(this));\n});\n\n\n$(document).on('click', '.compose-text', function() {\n  return false;\n});\n\n\n$(document).on('click', '.compose-attach-key', function(e) {\n  var mid = $(this).data('mid');\n  Mailpile.Composer.Crypto.AttachKey(mid);\n});\n\n\n/* Compose - Quoted Reply */\n$(document).on('click', '.compose-apply-quote', function(e) {\n  var mid = $(this).data('mid');\n  var state = $(this).data('quoted_reply');\n  Mailpile.Composer.Body.QuotedReply(mid, state);\n});\n\n\n$(document).on('submit', '#form-compose-quoted-reply', function(e) {\n  e.preventDefault();\n  var quoted_reply = 'enabled';\n  if ($(this).find('input[type=checkbox]').is(':checked')) {\n    quoted_reply = 'disabled';\n  }\n  Mailpile.API.settings_set_post({ 'web.quoted_reply': quoted_reply }, function(result) {\n    Mailpile.notification(result);\n    Mailpile.UI.hide_modal();\n  });\n});\n\n\n$(document).on('click', '.encryption-helper-find-key', function(e) {\n  var mid = $(this).data('mid');\n  var address = $(this).data('email');\n  Mailpile.crypto_keylookup = [];  // Reset Model\n\n  e.preventDefault();\n\n  // Reset and show progress area...\n  //$('#encryption-helper-find-keys').find('ul.result').html('');\n  $('#encryption-helper-find-keys').find('.loading').fadeIn();\n  $('#encryption-helper-find-keys').find('.color-01-gray-mid').html(address);\n\n  // Hide the list of missing keys, since we don't really handle\n  // multiple searches at once.\n  $('#encryption-helper-missing-keys').slideUp('slow');\n  $('li[address=\"' + address + '\"]').hide();\n  //$('#encryption-helper-missing-keys li.searchkey-result-item').show();\n\n  // Go Get Keys\n  var find_options = {\n    query: address,\n    strict: true,\n    container: '#encryption-helper-found-keys',\n    action: 'hide-item',\n    searched: function(status) {\n      // Hide loading animation\n      $('#encryption-helper-find-keys').find('.loading').slideUp('fast');\n\n      // If nothing was found, bring back the missing key list.\n      if (status === 'none' || status === 'error') {\n        $('#encryption-helper-missing-keys').slideDown();\n        $('li[address=\"' + address + '\"]').show();\n      }\n    },\n    imported: function() {\n      Mailpile.Composer.Crypto.UpdateEncryptionState(mid, function() {\n        // If the updated state says we can encrypt, then we should make\n        // the interface all happy like!\n      });\n\n      if (false) {\n        // Tally Total Missing Keys\n        var count_missing = [];\n        _.each($('#encryption-helper-missing-keys li.searchkey-result-item'), function(elem, key) {\n          count_missing.push($(elem).css('display'));\n        });\n        count_missing = _.indexOf(count_missing, 'list-item');\n\n        // Show \"Now Able To Encrypt\" Message\n        console.log('Missing: ' + count_missing);\n        if (count_missing < 1) {\n          console.log('yay, all have been searched & imported');\n\n          // Positive Feedback\n          $('#modal-full').find('span.icon-lock-open')\n            .removeClass('icon-lock-open color-10-orange')\n            .addClass('icon-lock-closed color-08-green')\n            .html('{{_(\"Yay, Can Now Encrypt\")|escapejs}}');\n\n          var success_template = Mailpile.safe_template($('#template-encryption-helper-complete-message').html());\n          var success_html = success_template({ mid: mid });\n\n          $('#modal-full').find('div.modal-body').html(success_html);\n\n          // Hide Missing\n          $('#encryption-helper-missing-keys').fadeOut();\n        }\n        else {\n          $('#encryption-helper-missing-keys').show();\n        }\n      }\n    },\n    error: function() {\n      $('#encryption-helper-missing-keys').slideDown();\n      $('li[address=\"' + address + '\"]').show();\n    }\n  };\n  Mailpile.Crypto.Find.Keys(find_options);\n});\n\n\n$(document).on('click', '.modal-retry-encryption', function(e) {\n  var mid = $(this).data('mid');\n  Mailpile.Composer.Crypto.UpdateEncryptionState(mid, function() {\n    Mailpile.Composer.ToggleEncryption(mid, 'encrypt');\n  });\n});\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/compose/init.js",
    "content": "/* Composer */\nMailpile.Composer = {};\nMailpile.Composer.Drafts = {};\nMailpile.Composer.Crypto = {};\nMailpile.Composer.Recipients = {};\nMailpile.Composer.Tooltips = {};\nMailpile.Composer.Body = {};\nMailpile.Composer.Attachments = {};\n\n/* Composer - Create new instance of composer */\nMailpile.Composer.init = function(mid, strings, addresses) {\n\n  // Reset tabindex for To: field\n  $('#search-query').attr('tabindex', '-1');\n\n  // Save Text Composing Objects (move to data model)\n  //\n  // FIXME: This is a bad pattern; here we're about to duplicate in JS\n  //        land information that comes from the DOM. Duplication is bad.\n  //\n  Mailpile.Composer.Drafts[mid] = Mailpile.Composer.Model(strings, addresses);\n\n  // Load Crypto States\n  // FIXME: needs dynamic support for multi composers on a page\n  Mailpile.Composer.Crypto.LoadStates(mid);\n\n  // Initialize select2\n  Mailpile.Composer.Recipients.AddressField('compose-to-' + mid);\n\n  if ($('#compose-cc-' + mid).val()) {\n    Mailpile.Composer.Recipients.AddressField('compose-cc-' + mid);\n  }\n\n  if ($('#compose-bcc-' + mid).val()) {\n    Mailpile.Composer.Recipients.AddressField('compose-bcc-' + mid);\n  }\n\n  // Show Crypto Tooltips\n  Mailpile.Composer.Crypto.UpdateEncryptionState(mid, function() {\n    Mailpile.Composer.Tooltips.Signature();\n    Mailpile.Composer.Tooltips.Encryption();\n    Mailpile.Composer.Tooltips.ContactDetails();\n  }, 'initial');\n\n  // Initialize signature; use setTimeout to isolate faults.\n  setTimeout(function() {\n    var email = $('#compose-from-selected-' + mid + ' .address').html();\n    $('a.compose-from').each(function(i, elem) {\n      if ($(elem).data('email') == email) $(elem).trigger('click');\n    });\n  }, 50);\n\n  // Initialize Attachments; use setTimeout to isolate faults.\n  setTimeout(function() {\n    // FIXME: needs to be bound to unique ID that can be destroyed\n    Mailpile.Composer.Attachments.Uploader.init({\n      browse_button: 'compose-attachment-pick-' + mid,\n      container: 'compose-attachments-' + mid,\n      mid: mid\n    });\n  }, 100);\n\n  // Body\n  Mailpile.Composer.Body.Setup(mid);\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/compose/modals.js",
    "content": "/* Modals - Composer */\n\nMailpile.UI.Modals.ComposerEncryptionHelper = function(mid, determine) {\n  Mailpile.API.with_template('modal-composer-encryption-helper', function(modal) {\n    determine['mid'] = mid;\n    Mailpile.UI.show_modal(modal(determine));\n  });\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/compose/recipients.js",
    "content": "/* Composer - Recipients */\n\nMailpile.Composer.Recipients.Get = function(mid, which) {\n  var $elem = $('#compose-' + which + '-' + mid);\n  return Mailpile.Composer.Recipients.Analyze($elem.val());\n};\n\n\nMailpile.Composer.Recipients.WithToCcBcc = function(mid, callback) {\n  var fields = ['to', 'cc', 'bcc'];\n  for (var i in fields) {\n    var rcpts = Mailpile.Composer.Recipients.Get(mid, fields[i]);\n    callback(fields[i], rcpts);\n  }\n};\n\n\nMailpile.Composer.Recipients.GetAll = function(mid, filter) {\n  var recipients = [];\n  Mailpile.Composer.Recipients.WithToCcBcc(mid, function(field, rcpts) {\n    for (var j in rcpts) {\n      recipients.push(filter ? filter(rcpts[j]) : rcpts[j]);\n    }\n  });\n  return recipients;\n};\n\n\nMailpile.Composer.Recipients.Fixup = function(r) {\n  if (r.fn.indexOf('\"') == -1) r.fn = '\"' + r.fn + '\"';\n  r.fn = r.fn.replace(/%@&%/g, '_');  // Horrible Hack\n  return r;\n};\n\n\nMailpile.Composer.Recipients.AnalyzeAddress = function(address, preset) {\n  /* The Mailpile API guarantees consistent formatting of addresses, so\n     a simple regexp should generally work juuuust fine. However, we also\n     want to handle pasted or typed input. So we try the simple parse first,\n     and then get a bit more creative. */\n  var check = address.match(/^\\s*([^<]*?)\\s*<?([^\\s<>]+@[^\\s<>#]+)(#[a-zA-Z0-9]+)?>?\\s*$/);\n  if (!check) {\n    var check2 = address.match(/^\\s*([^\\s]+)(@[^\\s]+)\\s*$/);\n    if (check2) {\n      check = [check2[0], check2[1], check2[1] + check2[2], false];\n    }\n    else {\n      check = [address, address, address, false];\n    }\n  }\n  var fn = $.trim(check[1] || check[2].substring(0, check[2].indexOf('@')));\n  if (fn.substring(0, 1) == '<') fn = fn.substring(1);\n  var parsed = {\n    \"id\": check[2],\n    \"fn\": fn,\n    \"address\": check[2],\n    \"keys\": [],\n    \"flags\": {\"secure\" : false, \"manual\": !preset}\n  };\n  if (check[3]) {\n    parsed[\"keys\"] = [{\"fingerprint\": check[3].substring(1)}];\n    parsed[\"flags\"] = {\"secure\" : true};\n  };\n  return Mailpile.Composer.Recipients.Fixup(parsed);\n};\n\n\n/* Composer - tokenize input field (to: cc: bcc:) */\nMailpile.Composer.Recipients.Analyze = function(addresses) {\n  var existing = [];\n\n  // Is Valid & Has Multiple\n  if (addresses) {\n    // We know this simple strategy works, because the backend formats the\n    // address lines in a conisistent way.\n    var multiple = addresses.split(/>, */);\n    // But we have select2 configured to do this Horrible Hack instead.\n    if (addresses.indexOf('%@&%') != -1) {\n      multiple = addresses.split(/%@&%/);\n    }\n\n    var seen = [];\n    $.each(multiple, function(key, value) {\n      if (value.indexOf('@') > -1) {\n        if (value.indexOf('<') == -1) value = value + ' <' + value;\n        if (value.indexOf('>') == -1) value = value + '>';\n      }\n      var analyzed = Mailpile.Composer.Recipients.AnalyzeAddress(value, true)\n      if (seen.indexOf(analyzed.address) == -1) {\n        existing.push(analyzed);\n        seen.push(analyzed.address);\n      }\n    });\n  }\n  return existing;\n};\n\n\nMailpile.Composer.Recipients.RecipientToAddress = function(object) {\n  if (object.flags.secure) {\n    address = object.address + '#' + object.keys[0].fingerprint;\n  } else {\n    address = object.address;\n  }\n  if (object.fn !== \"\" && object.address !== object.fn) {\n    return object.fn + ' <' + address + '>';\n  } else {\n    return '<' + address + '>';\n  }\n};\n\n\n/* Composer - instance of select2 */\nMailpile.Composer.Recipients.AddressField = function(id) {\n\n  // Get MID\n  var $elem = $('#' + id);\n  var mid = $elem.data('mid');\n\n  $elem.select2({\n    id: Mailpile.Composer.Recipients.RecipientToAddress,\n    ajax: {\n      // instead of writing the function to execute the request we use\n      // Select2's convenient helper\n      url: Mailpile.api.contacts,\n      quietMillis: 1,\n      cache: true,\n      dataType: 'json',\n      data: function(term, page) {\n        return {\n          q: term\n        };\n      },\n      results: function(response, page) {\n        // Convert the results into the format expected by Select2\n        var result_list = [];\n        for (i in response.result.addresses) {\n          var r = response.result.addresses[i];\n          result_list.push(Mailpile.Composer.Recipients.Fixup(r));\n        }\n        return {results: result_list};\n      }\n    },\n    multiple: true,\n    allowClear: true,\n    width: '100%',\n    minimumInputLength: 1,\n    minimumResultsForSearch: -1,\n    placeholder: \"{{_('Type to add contacts')}}\",\n    maximumSelectionSize: 100,\n    separator: \"%&@%\",  // Horrible hack, see fixup code in events.js\n    tokenSeparators: [\", \", \";\"],\n    createSearchChoice: Mailpile.Composer.Recipients.AnalyzeAddress,\n    formatResult: function(state) {\n      var avatar = '<span class=\"icon-user\"></span>';\n      var secure = '';\n      if (state.photo) {\n        avatar = '<img src=\"' + _.escape(state.photo) + '\">';\n      }\n      if (state.flags.secure) {\n        secure = '<span class=\"icon-lock-closed\"></span>';\n      }\n      return ('<span class=\"compose-select-avatar\">' + avatar + '</span>' +\n              '<span class=\"compose-select-name\">' + \n              _.escape(state.fn.replace(/(^\\\"|\\\"$)/g, '')) + secure + '<br>' +\n              '<span class=\"compose-select-address\">' + state.address +\n              '</span></span>');\n    },\n    formatSelection: function(state, elem) {\n      // Update Model\n      var found = false;\n      var model = Mailpile.Composer.Drafts[mid];\n      for (i in model.addresses) {\n        var contact_data = model.addresses[i];\n        if (contact_data.address == state.address) {\n          if (state.flags.manual) {\n            state.photo = contact_data.photo;\n            model.addresses[i] = state;\n          }\n          else {\n            state = model.addresses[i];\n          }\n          found = i;\n        }\n      }\n      if (!found) {\n        found = Math.random().toString(16).substring(6);\n        model.addresses[found] = state;\n      }\n\n      // Create HTML\n      var avatar = '<span class=\"avatar icon-user\" data-address=\"' + _.escape(state.address) + '\"></span>';\n      var name   = state.fn.replace(/(^\\\"|\\\"$)/g, '');\n      var secure = '';\n\n      if (state.photo) {\n        avatar = '<span class=\"avatar\"><img src=\"' + _.escape(state.photo) + '\" data-address=\"' + state.address + '\"></span>';\n      }\n      if (state.flags.secure) {\n        secure = '<span class=\"icon-lock-closed\" data-address=\"' + _.escape(state.address) + '\"></span>';\n      }\n      if (!state.fn){\n        return avatar + ' <span class=\"compose-choice-name\" data-address=\"' + _.escape(state.address) + '\">' + _.escape(state.address) + secure + '</span>';\n      } else {\n        return avatar + ' <span class=\"compose-choice-name\" data-address=\"' + _.escape(state.address) + '\">' + _.escape(name) + secure + '</span>';\n      }\n    },\n    formatSelectionTooBig: function() {\n      return 'You\\'ve added the maximum contacts allowed, to increase this go to <a href=\"#\">settings</a>';\n    },\n    selectOnBlur: true\n\n  }).on('select2-selecting', function(e) {\n    setTimeout(function() {\n      Mailpile.Composer.Crypto.UpdateEncryptionState(mid);\n      Mailpile.Composer.Tooltips.ContactDetails();\n    }, 50);\n  }).on('select2-removed', function(e) {\n    setTimeout(function() {\n      Mailpile.Composer.Crypto.UpdateEncryptionState(mid);\n    }, 50);\n  });\n\n  /* Check encryption state */\n  $('#'+id).select2('data', Mailpile.Composer.Recipients.Analyze($('#' + id).val()));\n};\n\nMailpile.Composer.Recipients.Update = function(mid, which, rcpts) {\n  var $elem = $('#compose-' + which + '-' + mid);\n  $elem.select2('data', rcpts);\n  Mailpile.Composer.Tooltips.ContactDetails();\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/compose/tooltips.js",
    "content": "/* Composer - Tooltips */\n\nMailpile.Composer.Tooltips.Signature = function() {\n  $('.compose-crypto-signature').qtip({\n    content: {\n      title: false,\n      text: function(event, api) {\n        $(this).find('text').removeClass('hide');\n        var html = ('<div><h4 class=\"'\n          + _.escape($(this).data('crypto_color')) + '\">'\n          + $(this).html().replace(' hide', '') + '</h4><p>'\n          + _.escape($(this).attr('title')) + '</p></div>');\n        return html;\n      }\n    },  \n    style: {\n     tip: {\n        corner: 'bottom center',\n        mimic: 'bottom center',\n        border: 0,\n        width: 10,\n        height: 10\n      },\n      classes: 'qtip-thread-crypto'\n    },\n    position: {\n      my: 'bottom center',\n      at: 'top center',\n\t\t\tviewport: $(window),\n\t\t\tadjust: {\n\t\t\t\tx: -5,  y: 0\n\t\t\t}\n    },\n    events: {\n      show: function(event, api) {}\n    },\n    show: { delay: 100 },\n    hide: { fixed: true, delay: 350 }\n  });\n};\n\n\nMailpile.Composer.Tooltips.Encryption = function() {\n  $('.compose-crypto-encryption').qtip({\n    content: {\n      title: false,\n      text: function(event, api) {\n        var html = ('<div><h4 class=\"'\n          + _.escape($(this).data('crypto_color')) + '\">'\n          + $(this).html().replace(' hide', '') + '</h4><p>'\n          + _.escape($(this).attr('title')) + '</p></div>');\n        return html;\n      }\n    },\n    style: {\n     tip: {\n        corner: 'bottom center',\n        mimic: 'bottom center',\n        border: 0,\n        width: 10,\n        height: 10\n      },\n      classes: 'qtip-thread-crypto'\n    },\n    position: {\n      my: 'bottom center',\n      at: 'top center',\n\t\t\tviewport: $(window),\n\t\t\tadjust: {\n\t\t\t\tx: -5,  y: 0\n\t\t\t}\n    },\n    events: {\n      show: function(event, api) {\n        // FIXME: Replace colors with dynamic JSAPI values\n        $('.select2-choices').css('border-color', '#fbb03b');\n        $('.compose-from').css('border-color', '#fbb03b');\n        $('.compose-subject input[type=text]').css('border-color', '#fbb03b');\n\n        if ($('#compose-encryption').val() === 'encrypt') {\n          var encrypt_color = '#a2d699';\n        } else {\n          var encrypt_color = '#fbb03b';\n        }\n\n        $('.compose-options-crypto').css('border-color', encrypt_color);\n        $('.compose-body').css('border-color', encrypt_color);\n        $('.compose-attachments').css('border-color', encrypt_color);\n      },\n      hide: function(event, api) {\n        $('.select2-choices').css('border-color', '#CCCCCC');\n        $('.compose-from').css('border-color', '#CCCCCC');\n        $('.compose-subject input[type=text]').css('border-color', '#CCCCCC');\n\n        $('.compose-options-crypto').css('border-color', '#CCCCCC');\n        $('.compose-body').css('border-color', '#CCCCCC');\n        $('.compose-attachments').css('border-color', '#CCCCCC');\n      }\n    },\n    show: { delay: 100 },\n    hide: { fixed: true, delay: 350 }\n  });\n};\n\n\nMailpile.Composer.Tooltips.ContactDetails = function() {\n  $('.select2-search-choice').qtip({\n    content: {\n      title: true,\n      text: function(e, api) {\n        $target = $(e.target);\n        var address = $target.data('address');\n        var mid = $target.closest('form.form-compose').data('mid');\n        var model = Mailpile.Composer.Drafts[mid];\n\n        if ($target.hasClass('select2-search-choice')) {\n          address = $target.find('.compose-choice-name').data('address');\n        }\n        if ($target.hasClass('select2-search-choice-close')) {\n          address = $target.parent().find('.compose-choice-name').data('address');\n        }\n        if ($target.is('img')) {\n          address = $target.parent().parent().find('.compose-choice-name').data('address');\n        } \n\n        var contact_data = _.findWhere(model.addresses, { address: address });\n        if (contact_data) {\n          contact_data['mid'] = mid;\n\n          if (contact_data.photo === undefined) {\n            contact_data.photo = '';\n          }\n\n          var contact_template = Mailpile.safe_template($('#tooltip-contact-details').html());\n          return contact_template(contact_data);\n        }\n      }\n    },\n    style: {\n     tip: {\n        corner: 'bottom center',\n        mimic: 'bottom center',\n        border: 0,\n        width: 10,\n        height: 10\n      },\n      classes: 'qtip-contact-details'\n    },\n    position: {\n      my: 'bottom center',\n      at: 'bottom center',\n\t\t\tviewport: $(window),\n\t\t\tadjust: {\n\t\t\t\tx: 5,  y: -25\n\t\t\t}\n    },\n    show: { delay: 100 },\n    hide: { fixed: true, delay: 350 }\n  });\n};\n\nMailpile.Composer.Tooltips.AttachKey = function() {\n  $('.compose-attach-key').qtip({\n    position: {\n      my: 'bottom center',\n      at: 'bottom center',\n\t\t\tviewport: $(window),\n\t\t\tadjust: {\n\t\t\t\tx: 5,  y: -25\n\t\t\t}\n    },\n    show: { delay: 100 },\n    hide: { fixed: true, delay: 350 }\n  });\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/contacts/display_modes.js",
    "content": ""
  },
  {
    "path": "shared-data/default-theme/html/jsapi/contacts/events.js",
    "content": "/* Contacts - Show contact add form */\n$(document).on('click', '.btn-activity-contact_add', function(e) {\n  e.preventDefault();\n  Mailpile.UI.Modals.ContactAdd();\n});\n\n\n/* Contact - Form  */\n$(document).on('submit', '#form-contact-add', function(e) {\n  e.preventDefault();\n  Mailpile.UI.Modals.ContactAddProcess();\n});\n\n\n/* Contacts - Show details of a given key */\n$(document).on('click', '.show-key-details', function(e) {\n  e.preventDefault();\n  $(this).hide();\n  var keyid = $(this).data('keyid');\n  $('#contact-key-details-' + keyid).fadeIn();\n});\n\n\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/contacts/init.js",
    "content": "/* Contacts */\n\nMailpile.Contacts = {};\nMailpile.Contacts.UI = {};\n\n\nMailpile.Contacts.init = function() {\n\n  // Search Bar\n  $('#search-query').val('contacts: ');\n\n  // Hide Key Details\n  $('.contact-key-details').hide();\n\n};"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/contacts/modals.js",
    "content": "/* Modals - Contacts */\n\nMailpile.UI.Modals.ContactAdd = function() {\n  $('.sub-navigation ul li').removeClass('navigation-on');\n  $(this).addClass('navigation-on');\n\n  Mailpile.API.with_template('modal-contact-add', function(modal) {\n    var modal_data = { name: '', address: '', extras: '' };\n    Mailpile.UI.show_modal(modal(modal_data));\n  });\n};\n\n\nMailpile.UI.Modals.ContactAddProcess = function() {\n  Mailpile.API.contacts_add_post($('#form-contact-add').serialize(), function(result) {\n    if (result.status == 'success') {\n      Mailpile.UI.hide_modal();\n\n      // If Contacts List\n      var $clist = $('#contacts-list');\n      if ($clist.length > 0) {\n        var contact_template = Mailpile.safe_template($('#template-contact-list-item').html());\n        var contact_html = contact_template(result.result.contact);\n        $clist.append(contact_html);\n      }\n    }\n  });\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/crypto/events.js",
    "content": "/* Crypto - Events */\n\n$(document).on('click', '.btn-crypto-search-key', function(e) {\n  e.preventDefault();\n  Mailpile.UI.Modals.CryptoFindKeys({ query: '' });\n});\n\n\n$(document).on('click', '.btn-crypto-upload-key', function(e) {\n  e.preventDefault();\n  Mailpile.UI.Modals.CryptoUploadKey({});\n});\n\n\n/* Crypto - show / hide details */\n$(document).on('click', '.searchkey-result-score', function(e) {\n  var fingerprint = $(this).data('fingerprint');\n  var $details = $('#item-encryption-key-' + fingerprint\n                   ).find('.searchkey-result-details');\n  if ($details.css('display') == 'none') {\n    $details.fadeIn();\n  }\n  else {\n    $details.fadeOut();\n  }\n});\n\n\n$(document).on('submit', '#form-search-keyservers', function(e) {\n  e.preventDefault();\n\n  // Hide Form\n  $('#form-search-keyservers').removeClass('fadeIn').addClass('hide');\n\n  // Query\n  var query = $(this).find('input[type=text]').val();\n  Mailpile.Crypto.Find.Keys({\n    container: '#search-keyservers',\n    action: 'hide-modal',\n    query: query,\n    complete: function() {\n      $('#search-keyservers-again').removeClass('hide').addClass('fadeIn');\n    }\n  });\n});\n\n\n$(document).on('click', '#btn-search-keyservers-again', function(e) {\n  e.preventDefault();\n  $('#search-keyservers-again').removeClass('fadeIn').addClass('hide');\n  $('#search-keyservers').fadeOut().find('ul.result').html('');\n  $('#form-search-keyservers').removeClass('hide').addClass('fadeIn')\n    .find('input[type=text]').val('');\n});\n\n\n$(document).on('click', '.crypto-show-hidden-keys', function(e) {\n  e.preventDefault();\n  $(this).parent().fadeOut().remove();\n  $('#search-keyservers').find('ul.result-hidden-keys').removeClass('hide');\n});\n\n\n/* Crypto - import key */\n$(document).on('click', '.crypto-key-import', function(e) {\n  e.preventDefault();\n  Mailpile.Crypto.Import.Key({\n    pinned: false,\n    action: $(this).data('action'),\n    fingerprint: $(this).data('fingerprint')\n  });\n});\n$(document).on('click', '.crypto-key-import-pinned', function(e) {\n  e.preventDefault();\n  Mailpile.Crypto.Import.Key({\n    pinned: true,\n    action: $(this).data('action'),\n    fingerprint: $(this).data('fingerprint')\n  });\n});\n\n\n/* Crypto - key use */\n$(document).on('change', '.crypto-key-policy', function() {\n  Mailpile.Crypto.Import.SetKeyPolicy({\n    action: $(this).data('action'),\n    email: $(this).data('email'),\n    fingerprint: $(this).data('fingerprint'),\n    policy: $(this).val()\n  });\n});\n\n\n/* Crypto - looks up keys based on a given e-mail address */\n$(document).on('click', '.crypto-searchkey-address', function(e) {\n  e.preventDefault();\n  var address = $(this).data('address');\n  var target = $(this).data('target');\n});\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/crypto/find.js",
    "content": "/* Crypto - Find */\n\n\nMailpile.Crypto.FixupKeyStructure = function(key) {\n    // Make sure these attribute exists, default to False\n    if (!key.on_keychain) key.on_keychain = false;\n    if (!key.in_vcards) key.in_vcards = false;\n\n    // Readable creation date\n    if (key.created && key.created > 0) {\n      cdate = new Date(key.created * 1000);\n      key.creation_date = (\n        cdate.getFullYear() + '-' +\n        (cdate.getMonth()+1) + '-' +\n        cdate.getDate());\n    }\n    else key.creation_date = '{{_(\"unknown\")|escapejs}}';\n    return key;\n};\n\n\nMailpile.Crypto.Find.KeysResult = function(data, options) {\n  var items_hidden = '';\n\n  _.each(data.result, function(key) {\n    // Loop through UIDs for match to Query\n    var uid = _.findWhere(key.uids, {email: options.query});\n    var avatar   = '{{ U(\"/static/img/avatar-default.png\") }}';\n\n    // Try to find Avatar\n    if (uid) {\n      var contact  = _.findWhere(key.vcards, {address: uid.email});\n      if (contact) {\n        if (contact.photo) {\n          avatar = contact.photo;\n        }\n      }\n    } else {\n      // UID Featured Item\n      var uid = {\n        name: '{{_(\"No Name\")|escapejs}}',\n        email: '{{_(\"No Email\")|escapejs}}'\n      };\n\n      if (key.uids[0].name) {\n        uid.name = key.uids[0].name;\n      }\n      if (key.uids[0].email) {\n        uid.email = key.uids[0].email;\n      }\n      if (key.uids[0].comment) {\n        uid.comment = key.uids[0].comment;\n      }\n    }\n\n    Mailpile.Crypto.FixupKeyStructure(key);\n\n    // Key Score\n    var score_color = Mailpile.UI.Crypto.ScoreColor(key.score_stars);\n\n    // Show View\n    var item_data = _.extend({ score_color: score_color,\n                               avatar: avatar,\n                               uid: uid,\n                               address: options.query,\n                               action: options.action,\n                               pinned: false }, key);\n    var item_template = Mailpile.safe_template($('#template-crypto-encryption-key').html());\n    var item_html = item_template(item_data);\n\n    // Only show results with positive score (hide others)\n    var $elem = $('#item-encryption-key-' + key.fingerprint);\n    if ($elem.length) {\n      $elem.replaceWith(item_html);\n    }\n    else {\n      if (key.score_stars > 0) {\n        $(options.container).find('.result').append(item_html);\n      }\n      else {\n        $(options.container).find('.result-hidden-keys').append(item_html);\n      }\n    }\n\n    // Set Lookup State (data model)\n    var key_data = {fingerprints: key.fingerprint,\n                    address: options.query,\n                    origins: key.origins };\n    Mailpile.crypto_keylookup.push(key_data);\n  });\n\n  var $container = $(options.container);\n  if ($container.find('.result-hidden-keys li').length > 0) {\n    $container.find('.result')\n              .append($('#template-search-keyserver-show-hidden').html());\n  }\n\n  // Tooltips\n  Mailpile.Crypto.Tooltips.KeyScore();\n};\n\n\nMailpile.Crypto.Find.KeysDone = function(options) {\n  var status = 'none';\n  var $container = $(options.container);\n  $container.find('.loading').fadeOut();\n\n  // FIXME: doesn't work for 2nd and third lookups returning empty results\n  if (!Mailpile.crypto_keylookup.length) {\n    var message_template = Mailpile.safe_template($('#template-find-keys-none').html());\n    var message_html = message_template(options);\n    $container.find('.message')\n      .html(message_html)\n      .removeClass('paragraph-important paragraph-success')\n      .addClass('paragraph-alert');\n  }\n  else {\n    status = 'success';\n    $(options.container).find('.message')\n      .removeClass('paragraph-important paragraph-alert')\n      .addClass('paragraph-success');\n  }\n\n  if (options.searched) options.searched(status);\n};\n\n\nMailpile.Crypto.Find.KeysError = function(options) {\n  var $container = $(options.container);\n  $container.find('.loading').fadeOut();\n  setTimeout(function() {\n    var message_template = Mailpile.safe_template($('#template-find-keys-error').html());\n    var message_html = message_template(options);\n    $container.find('.message')\n      .html(message_html)\n      .removeClass('paragraph-success paragraph-important paragraph-alert')\n      .addClass('paragraph-warning');\n    if (options.error) options.error();\n  }, 250);\n};\n\n\n/**\n * Performs a lookup for encryption keys and renders UI elements\n * @param {string} options.container - container element of all UI elements\n * @param {string} options.action -  \n * @param {string} options.query - the term to lookup\n * @param {function} options.complete \n */\nMailpile.Crypto.Find.Keys = function(options) {\n\n  var $container = $(options.container);\n  if ($container.hasClass('hide')) $container.fadeIn();\n\n  // Register events on the container so it is possible to invoke them\n  // using $(this).closest('.has-keylookup-events').trigger('foo');\n  $container.addClass('has-keylookup-events');\n  if (options.imported) $container.on('keylookup:imported', options.imported);\n  if (options.searched) $container.on('keylookup:searched', options.searched);\n  if (options.error) $container.on('keylookup:error', options.error);\n\n  var args = {}\n  args[(options.strict ? \"email\" : \"address\")] = options.query;\n  if ($('#keylookup_check_all').is(':checked')) args['origins'] = '*';\n  $('span.keylookup_check_all').hide();\n  Mailpile.API.async_crypto_keylookup_get(args, function(data, ev) {\n    // Render each result found\n    if (data.result) Mailpile.Crypto.Find.KeysResult(data, options);\n    if (data.progress) data.progress = ('{{_(\"Searching\")|escapejs}}: ' +\n                                        data.progress.join(', ') +\n                                        ' ...');\n    $(options.container).find('.progress').show().html(data.message || data.progress || '');\n\n    // Report progress...\n    if (ev.flags != 'c') {\n      if (data.result && data.message) {\n        $(options.container).find('.message')\n          .html('<span class=\"icon-key\"></span> ' + data.message)\n          .removeClass('paragraph-success paragraph-alert')\n          .addClass('paragraph-important');\n      }\n      else {\n        var searching_template = Mailpile.safe_template($('#template-find-keys-running').html());\n        var searching_html     = searching_template(options);\n        $(options.container).find('.message').html(searching_html);\n      }\n    }\n    // FIXME: Detecting errors does not work well, the backend needs to\n    //        report better here.\n    else if (data.result !== undefined) {\n      if (data.result && data.result.length) $(options.container).find('.progress').hide();\n      console.log('Search done, no error');\n      Mailpile.Crypto.Find.KeysDone(options);\n    }\n    else {\n      console.log('Search done, error state');\n      Mailpile.Crypto.Find.KeysError(options);\n    }\n  });\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/crypto/import.js",
    "content": "/* Crypto - Import */\n\n\nMailpile.Crypto.Import.Key = function(import_data) {\n  // Set Null\n  if (import_data.file === undefined) import_data.file = false;\n  import_data.failed = false;\n\n  // Show Processing UI feedback\n  var importing_template = Mailpile.safe_template($('#template-crypto-encryption-key-importing').html());\n  var importing_html     = importing_template(import_data);\n  $('#item-encryption-key-' + import_data.fingerprint).replaceWith(importing_html);\n\n  // Lookup\n  var import_args = _.findWhere(Mailpile.crypto_keylookup,\n                                {fingerprints: import_data.fingerprint});\n  import_args.pinned = import_data.pinned;\n  Mailpile.API.async_crypto_keyimport_post(import_args, function(result) {\n    if (result.status === 'success') {\n      var key_result;\n      for (var key in result.result) {\n        if (result.result[key].fingerprint === import_data.fingerprint) {\n          key_result = Mailpile.Crypto.FixupKeyStructure(result.result[key]);\n          key_result['avatar'] = '{{ U(\"/static/img/avatar-default.png\") }}';\n          key_result['uid'] = key_result.uids[0];\n          key_result['action'] = 'hide-modal';\n          key_result['on_keychain'] = true;\n          key_result['score_color'] = Mailpile.UI.Crypto.ScoreColor(key.score_stars);\n        }\n      }\n\n      var $key_elem = $('#item-encryption-key-' + import_data.fingerprint);\n      var $events = $key_elem.closest('.has-keylookup-events');\n      var key_template_html;\n      if (key_result) {\n        var key_template = Mailpile.safe_template($('#template-crypto-encryption-key').html());\n        key_template_html = key_template(key_result);\n      }\n      else {\n        import_data.failed = true;\n        key_template_html = importing_template(import_data);\n      }\n      $key_elem.replaceWith(key_template_html);\n      $events.trigger('keylookup:imported');\n    }\n  });\n};\n\n\nMailpile.Crypto.Import.SetKeyPolicy = function(args) {\n  fpl = args.fingerprint.length\n  var key_id = ('... ' +\n     args.fingerprint.substring(fpl - 8, fpl - 4) + ' ' +\n     args.fingerprint.substring(fpl - 4));\n  if (args.policy == 'false') {\n    Mailpile.API.vcards_rmlines_post({\n      email: args.email,\n      name: ['key', 'x-mailpile-pgpkey-pinned']\n    }, function(result) {\n      if (result.status === 'success') {\n        result.icon = 'icon-x';\n        result.message = \"{{_('Disabled Encryption Key: ')|escapejs}}\" + key_id;\n      }\n      Mailpile.notification(result);\n    });\n  }\n  else {\n    Mailpile.API.vcards_addlines_post({\n      replace_all: true,\n      email: args.email,\n      name: 'key',\n      value: 'data:application/x-pgp-fingerprint,' + args.fingerprint \n    }, function(result) {\n      if (result.status === 'success') {\n        if (args.policy == 'pin') {\n          Mailpile.API.vcards_addlines_post({\n            replace_all: true,\n            email: args.email,\n            name: 'x-mailpile-pgpkey-pinned',\n            value: true\n          }, function(result) {\n            if (result.status === 'success') {\n              result.icon = 'icon-star';\n              result.message = \"{{_('Pinned Encryption Key: ')|escapejs}}\" + key_id;\n            }\n            Mailpile.notification(result);\n          });\n        }\n        else Mailpile.API.vcards_rmlines_post({\n          email: args.email,\n          name: 'x-mailpile-pgpkey-pinned'\n        }, function(result) {\n          if (result.status === 'success') {\n            result.icon = 'icon-key';\n            result.message = \"{{_('Using Encryption Key: ')|escapejs}}\" + key_id;\n          }\n          Mailpile.notification(result);\n        });\n      }\n      else Mailpile.notification(result);\n    });\n  }\n};\n\n\nMailpile.Crypto.Import.Uploader = function() {\n\n  var uploader = new plupload.Uploader({\n  \truntimes : 'html5',\n  \tbrowse_button : 'upload-key-pick', // you can pass in id...\n  \tcontainer: 'upload-key-container', // ... or DOM Element itself\n    drop_element: 'upload-key-container',\n  \turl : '{{ config.sys.http_path }}/api/0/crypto/gpg/importkey/',\n//    multipart : true,\n//    multipart_params : {'key_file': 'upload'},\n    file_data_name : 'key_data',\n  \tfilters : {\n  \t\tmax_file_size : '5mb'\n  \t},\n  \tinit: {\n      PostInit: function() {\n        $('#upload-key-pick').removeClass('hide');\n        $('#upload-key-browswer-unsupported').addClass('hide');\n        uploader.refresh();\n      },\n      FilesAdded: function(up, files) {\n  \n        // Hide Message (in case)\n        $('#upload-key-container').find('p.message').fadeOut();\n        $('#form-upload-key').addClass('hide');\n\n        // Loop through added files\n      \tplupload.each(files, function(file) {\n  \n          // Show Warning for 50 mb or larger\n          if (file.size > 5242880) {\n            start_upload = false;\n            alert(file.name + ' {{_(\"is too large:\")|escapejs}} ' + plupload.formatSize(file.size) + '. {{_(\"You can not upload a key larger than 5 Megabytes.\")|escapejs}}');\n          } else {\n\n            var importing_template = Mailpile.safe_template($('#template-crypto-encryption-key-importing').html());\n            var importing_html = importing_template({fingerprint: 'UPLOADING', file: file });            \n            $('#upload-key-list').removeClass('hide').html(importing_html);\n            $('#form-upload-key').addClass('hide');\n\n            // Start (with slight delay)\n            setTimeout(function() {\n              uploader.start();\n            }, 500);\n          }\n      \t});\n      },\n      UploadProgress: function(up, file) {\n      \t$('#item-encryption-key-UPLOADING').find('em.text-detail').html('Uploading ' + file.name + ' ' + file.percent + '% complete');\n      },\n      FileUploaded: function(up, file, response) {\n\n        // Delay UI feedback (for local installs)\n        setTimeout(function() {\n          if (response.status === 200) {\n            var response_data = $.parseJSON(response.response);\n\n            // Show UI feedback\n            if (response_data.status === 'success') {\n              $('#item-encryption-key-UPLOADING').html('Yay. Encrypted Key Successfully Uploaded. Someday this will look nicer :)');\n            } else {\n              $('#upload-key-container').find('p.message').fadeIn();\n              $('#item-encryption-key-UPLOADING').fadeOut().remove();\n              $('#form-upload-key').fadeIn().removeClass('hide');\n            }\n          } else {\n            Mailpile.notification({status: 'error', message: '{{_(\"Could not upload encryption key. Status:\")|escapejs}}: ' + response.status });\n          }\n        }, 1000);\n      },\n      Error: function(up, err) {\n        Mailpile.notification({status: 'error', message: '{{_(\"Could not upload encryption key because\")|escapejs}}: ' + err.message });\n        $('#' + err.file.id).find('b').html('Failed ' + err.code);\n        uploader.refresh();\n      }\n    }\n  });\n\n  return uploader.init();\n\n};\n\n\nMailpile.Crypto.Import.UploaderProcessing = function() {\n\n    \n\n};\n\nMailpile.Crypto.Import.UploaderComplete = function(key) {\n\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/crypto/init.js",
    "content": "/* Crypto */\n\nMailpile.Crypto = {};\nMailpile.Crypto.Find = {};\nMailpile.Crypto.Import = {};\nMailpile.Crypto.Tooltips = {};"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/crypto/modals.js",
    "content": "/* Modals - Crypto */\n\n/* Modals - Crypto - try to find keys locally & remotely */\nMailpile.UI.Modals.CryptoFindKeys = function(options) {\n  options.container = '#search-keyservers';\n  options.action = 'hide-modal';\n  options.searched = function() {\n    $('#search-keyservers').find('.loading').slideUp('fast');\n  };\n\n  Mailpile.API.with_template('modal-search-keyservers', function(modal) {\n    Mailpile.UI.show_modal(modal(options));\n    if (options.query) {\n      Mailpile.Crypto.Find.Keys(options);\n    } else {\n      $('#form-search-keyservers').removeClass('hide').addClass('fadeIn');\n      $('#form-search-keyservers').find('input[name=query]').focus();\n    }\n  });\n};\n\nMailpile.UI.Modals.CryptoUploadKey = function(options) {\n  Mailpile.API.with_template('modal-upload-key', function(modal) {\n    Mailpile.UI.show_modal(modal(options));\n    Mailpile.Crypto.Import.Uploader();\n  });\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/crypto/tooltips.js",
    "content": "/* Crypto - Tooltips */\n\nMailpile.Crypto.Tooltips.KeyScore = function() {\n  $('.searchkey-result-score').qtip({\n    content: {\n      title: false,\n      text: function(event, api) {\n        return (_.escape($(this).data('score_reason'))\n          + '<small>{{_(\"Click For Details\")|escapejs}}</small>');\n      }\n    },\n    style: {\n      classes: 'qtip-tipped',\n      tip: {\n        corner: 'left middle',\n        mimic: 'left middle',\n        border: 0,\n        width: 10,\n        height: 10\n      }\n    },\n    position: {\n      my: 'left center',\n      at: 'right center',\n      viewport: $(window),\n      adjust: {x: 5, y: 2}\n    },\n    show: {event: 'mouseenter', delay: 0},\n    hide: {event: 'click', inactive: 2000}\n  });\n\n  $('.crypto-key-import-pinned').qtip({\n    content: {\n      title: false,\n      text: \"{{_('Use this key with automatic updates disabled')|escapejs}}\",\n    },\n    style: {\n      classes: 'qtip-tipped',\n      tip: {\n        corner: 'bottom middle',\n        mimic: 'bottom middle',\n        border: 0,\n        width: 10,\n        height: 10\n      }\n    },\n    position: {\n      my: 'bottom center',\n      at: 'top center',\n      viewport: $(window),\n      adjust: {x: 2, y: -5}\n    },\n    show: {event: 'mouseenter', delay: 0},\n    hide: {event: 'click', inactive: 2000}\n  });\n\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/crypto/ui.js",
    "content": "/* Crypto - UI */\n\nMailpile.UI.Crypto.ScoreColor = function(score) {\n\n  var score_color = 'color-01-gray-mid';\n\n  if (score >= 5) {\n    score_color = 'color-08-green';\n  } else if (score < 5 && score > 2) {\n    score_color = 'color-06-blue';\n  } else if (score <= 2 && score >= 0) {\n    score_color = 'color-09-yellow';\n  } else if (score < 2 && score > -3) {\n    score_color = 'color-10-orange';\n  } else if (score < -3) {\n    score_color = 'color-12-red';\n  }\n\n  return score_color;\n}"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/global/activities.js",
    "content": "/* Activities */\nMailpile.activities.compose = function(to, from) {\n  var compose_data = {};\n  if (to) compose_data.to = to;\n  if (from) compose_data.from = from;\n\tMailpile.API.message_compose_post(compose_data, function(response) {\n    if (response.status === 'success') {\n      Mailpile.go(Mailpile.urls.message_draft + response.result.message_ids[0] + '/');\n    } else {\n      Mailpile.notification(response);\n    }\n  });\n};\n\n\nMailpile.activities.render_typeahead = function() {\n\n  var baseMatcher = function(strs) {\n    return function findMatches(q, cb) {\n      var matches, substrRegex;\n      matches = [];\n      substrRegex = new RegExp(q, 'i');\n      $.each(strs, function(i, str) {\n        if (substrRegex.test(str.term)) {\n          matches.push(str);\n        }\n      });\n      cb(matches);\n    };\n  };\n\n  var tagMatcher = function(strs) {\n    return function findMatches(q, cb) {\n      var matches, substrRegex;\n      matches = [];\n      substrRegex = new RegExp(q, 'i');\n      $.each(strs, function(i, str) {\n        if (substrRegex.test(str.name)) {\n          matches.push(str);\n        }\n      });\n      cb(matches);\n    };\n  };\n\n  var peopleMatcher = function(strs) {\n    return function findMatches(q, cb) {\n      var matches, substrRegex;\n      matches = [];\n      substrRegex = new RegExp(q, 'i');\n      $.each(strs, function(i, str) {\n        if (substrRegex.test(str.fn) || substrRegex.test(str.address)) {\n          str.term =  'from:';\n          matches.push(str);\n        }\n      });\n      cb(matches);\n    };\n  };\n\n  // List of basic suggestions for search helpers\n  var helpers = [\n    { term: 'dates:', helper: '2011-12..2012-04-15' },\n    { term: 'date:', helper: 'date:2013-8-3 +date:2013-9-10' },\n    { term: 'year:', helper: '2013' },\n    { term: 'month:', helper: '8' },\n    { term: 'subject:', helper: 'any normal words' },\n    { term: 'att:', helper: 'jpg' },\n    { term: 'has:', helper: 'attachment' },\n    { term: 'contacts: ', helper: 'name@email.com' },\n    { term: 'to:', helper: 'name@email.com' }\n  ];\n\n  // Create Typeahead\n  $('#form-search .typeahead').typeahead({\n    hint: true,\n    highlight: true,\n    minLength: 0\n  },{\n    name: 'search',\n    displayKey: 'term',\n    source: baseMatcher(helpers),\n    templates: {\n      suggestion: function(data) {\n        var template = Mailpile.safe_template('<div class=\"tt-suggestion\"><p><span class=\"icon-search\"></span> <%= term %> <span class=\"helper\"><%= helper %></span></p></div>');\n        return template(data);\n      }\n    }\n  },{\n    name: 'tags',\n    displayKey: function(value) {\n      return 'in:' + value.slug\n    },\n    source: tagMatcher(Mailpile.instance.tags),\n    templates: {\n      empty: '<div class=\"tt-suggestion\"><p><span class=\"icon-tag\"></span> No tags match your search</p></div>',\n      suggestion: function(data) {\n        if (data.display !== 'invisible') {\n          var template = Mailile.safe_template('<div class=\"tt-suggestion\"><p><span class=\"color-<%= label_color %> <%= icon %>\"></span> <%= name %></p></div>');\n          return template(data);\n        }\n      }\n    }\n  },{\n    name: 'people',\n    displayKey: function(value) {\n      return value.term + value.address;\n    },\n    source: peopleMatcher(Mailpile.instance.addresses),\n    templates: {\n      header: '<span class=\"separator\"></span>',\n      empty: '<div class=\"tt-suggestion\"><p><span class=\"icon-user\"></span> No people match your search</p></div>',\n      suggestion: function(data) {\n        if (data.photo === undefined) { data.photo = '{{ config.sys.http_path }}/static/img/avatar-default.png'; }\n        var template = Mailpile.safe_template('<div class=\"tt-suggestion\"><p><img class=\"avatar\" src=\"<%= photo %>\"> <%= term %> <%= fn %></p></div>');\n        return template(data);\n      }\n    }\n  },{\n    name: 'keys',\n    displayKey: 'term',\n    source: baseMatcher([{ term: 'keys: team@mailpile.is' },{ term: 'keys: 707775F9' }]),\n    templates: {\n      suggestion: function(data) {\n        var template = Mailpile.safe_template('<div class=\"tt-suggestion\"><p><span class=\"icon-key\"></span> <%= term %></p></div>');\n        return template(data);\n      }\n    }\n  });\n\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/global/eventlog.js",
    "content": "var EventLog = {\n  eventBindings: [],  // All the subscriptions\n  last_ts: Mailpile.local_storage['eventlog_last_ts'] || -1800,\n  first_load: true,\n  other_tab: 0,\n  timeOut: null,\n  timer: null\n};\n\nEventLog.last_result = function(new_result) {\n  if (new_result !== undefined) {\n    Mailpile.local_storage['eventlog_last_result'] = JSON.stringify(new_result);\n  }\n  else {\n    return JSON.parse(Mailpile.local_storage['eventlog_last_result'] || '{}');\n  }\n}\n\nEventLog.pause = function() {\n  return EventLog.timer.pause();\n};\n\n\nEventLog.play = function() {\n  return EventLog.timer.play();\n};\n\n\nEventLog.request = function(conditions, callback) {\n  if (EventLog.first_load) {\n    Mailpile.API.logs_events_get({incomplete: true}, EventLog.invoke_callbacks);\n    EventLog.first_load = false;\n  }\n  // We check localStorage here, to see if any other tab has a poll in\n  // flight. If it does, we just don't do anything as our localStorage\n  // subscription will get the data that way and we don't want too many\n  // requests in-flight to the backend at once, both for performance\n  // reasons and because of browser simultaneous connection limits.\n  var now = new Date().getTime();\n  if (now - EventLog.other_tab > 30000) {\n    var conditions = conditions || {};\n    conditions._error_callback = EventLog.process_error;\n    Mailpile.API.logs_events_get(conditions, callback || EventLog.process_result);\n  }\n  else {\n    // Keep checking every 5 seconds, in case the other tab gets closed.\n    EventLog.timeOut = setTimeout(function() {EventLog.poll();}, 5000);\n  }\n};\n\n\nEventLog.poll = function() {\n  //\n  // Note: This is unfiltered for these reasons:\n  //\n  // 1) The eventlog filter language is not flexible enough to watch for all\n  //    the different events we need in one call.\n  // 2) It also has the issue that if subscriptions change then we need the\n  //    ability to immediately terminate the outstanding request and fire off\n  //    a new one.\n  // 3) Other tabs may rely on us putting things in localStorage that we\n  //    ourselves don't care about.\n  //\n  // FIXME: The Mailpile.Terminal.settings.enabled check does cross tabs.\n\n  // Time for server to wait for new events\n  var waittime = Mailpile.Terminal.settings.enabled ? 2 : 30;\n  // Extra time to account for processing time on backend\n  var buffer = 5;\n  EventLog.request({\n    since: EventLog.last_ts,\n    gather: (EventLog.last_ts < 0) ? 0.2 : 1.0,\n    debuglog: (Mailpile.Terminal.settings.enabled ? 'yes' : 'no'),\n    wait: waittime,\n    _timeout: (waittime+buffer)*1000\n  });\n};\n\n\nEventLog.invoke_callbacks = function(response) {\n  // Update the API CSRF token\n  Mailpile.csrf_token = response.state.csrf_token;\n  // DEBUGGING: console.log('Update CSRF: ' + Mailpile.csrf_token);\n\n  var event_template = $('#template-event').html();\n  if (event_template) {\n    event_template = Mailpile.safe_jinjaish_template(event_template);\n  }\n\n  if (Mailpile.Terminal.settings.enabled) {\n    Mailpile.Terminal.updateDebugLog(response.result.debuglog);\n  }\n\n  // Iterate through the events, calling callbacks...\n  var last_ts = response.result.ts;\n  for (event in response.result.events) {\n    var ev = response.result.events[event];\n\n    for (i in EventLog.eventBindings) {\n      var eventBinding = EventLog.eventBindings[i];\n      if (eventBinding !== undefined) {\n        var binding = eventBinding.event;\n        var sourceMatched = !binding.source || ev.source.match(new RegExp(binding.source));\n        var eventIdMatched = !binding.event_id || (ev.event_id == binding.event_id);\n        var flagsMatched = !binding.flags || ev.flags.match(new RegExp(binding.flags));\n        if (sourceMatched && eventIdMatched && flagsMatched) {\n          eventBinding.callback(ev);\n        }\n      }\n    }\n\n    // This will update any event-log viewer\n    if (event_template) {\n      var d = new Date(ev.ts * 1000);\n      ev.ts_hhmm = (('0' + d.getHours()).substr(-2) + ':' +\n                    ('0' + d.getMinutes()).substr(-2));\n      var $existing = $('#'+ ev.event_id +'.event-summary');\n      if ($existing.data('flags') != ev.flags) {\n        $existing.remove();\n        $existing = [];\n      }\n      if ($existing.length > 0) {\n        $existing.replaceWith($(event_template(ev)));\n      }\n      else {\n        $('.events-' + ev.flags + ' p:gt(49)').remove();\n        $('.events-' + ev.flags).prepend($(event_template(ev)).slideDown());\n      }\n    }\n    last_ts = response.result.ts;\n  }\n  return last_ts;\n};\n\n\nEventLog.process_error = function(result, textstatus) {\n  EventLog.timeOut = setTimeout(function() {EventLog.poll();}, 5000);\n};\n\n\nEventLog.process_result = function(result, textstatus) {\n  EventLog.last_ts = EventLog.invoke_callbacks(result);\n  EventLog.poll();\n  Mailpile.local_storage['eventlog_last_ts'] = EventLog.last_ts;\n  EventLog.last_result(result);\n};\n\n\nEventLog.subscribe = function(ev, func, id) {\n  // Subscribe a function to an event.\n  // Returns a subscription ID.\n  if (!$.isFunction(func)) {\n    console.log(\"Can only subscribe functions\");\n    return false;\n  }\n\n  // generate a random id if not specified\n  if (typeof id === 'undefined' || id === null) {\n    id = Math.random().toString(24).substring(5);\n  }\n\n  // Check if event is already subscribed\n  var existingEventBinding = this.eventBindings.filter(function (eventBinding) {\n    return eventBinding.id === id;\n  });\n\n  if (typeof(ev) == \"string\") {\n    ev = {source: ev, event_id: null};\n  }\n\n  if (existingEventBinding.length) {\n    console.log('Overriding already subscribed event with id: ' + id);\n    existingEventBinding[0].event = ev;\n    existingEventBinding[0].callback = func;\n    return existingEventBinding[0].id;\n  }\n  else {\n    this.eventBindings.push({id: id, event: ev, callback: func});\n    return id;\n  }\n};\n\n\nEventLog.unsubscribe = function(ev, func_or_id) {\n  // Given an event class and a subscription id\n  // or a function, will unsubscribe from the event.\n  // Returns true if successfully unsubscribed.\n  var initialLength = this.eventBindings.length;\n  this.eventBindings = this.eventBindings.filter(function (eventBinding) {\n    return !(eventBinding.id === func_or_id || eventBinding.callback === func_or_id);\n  });\n\n  return this.eventBindings.length < initialLength;\n};\n\n\nEventLog.get_popup_events_cache = function() {\n    if (Mailpile.local_storage[\"seen_popup_events\"] === undefined ||\n        Mailpile.local_storage[\"seen_popup_events\"] === \"\") {\n        Mailpile.local_storage.setItem(\"seen_popup_events\", JSON.stringify(new Array()));\n    }\n    return JSON.parse(Mailpile.local_storage[\"seen_popup_events\"]);\n}\n\n\n// The following functions control intermittent popups, mostly\n//  for logging into stuff.\n\nEventLog.TIMEOUT_EXPIRE_OLD_EVENTS = 604800; // Once per week\nEventLog.TIMEOUT_CHECK_OLD_EVENTS = 3600; // Once per hour\n\nEventLog.seen_event_recently = function(ev) {\n    var events = EventLog.get_popup_events_cache();\n    for (e in events) {\n        if (events[e][0] == ev) {\n            return true;\n        }\n    }\n    return false;\n};\n\n\nEventLog.clear_old_events = function(ev) {\n    var events = EventLog.get_popup_events_cache();\n    var curTime = Math.floor(Date.now() / 1000);\n    for (e in events) {\n        if (curTime - events[e][1] > EventLog.TIMEOUT_EXPIRE_OLD_EVENTS) {\n            events.splice(e, 1);\n            Mailpile.local_storage[\"seen_popup_events\"] = JSON.stringify(events);\n        }\n    }\n    return true;\n};\n\n\nEventLog.forget_about_event = function(ev) {\n    var events = EventLog.get_popup_events_cache();\n    for (e in events) {\n        if (events[e][0] == ev) {\n            events.splice(e, 1);\n            Mailpile.local_storage[\"seen_popup_events\"] = JSON.stringify(events);\n            return true;\n        }\n    }\n    return true;\n};\n\n\nEventLog.just_saw_event = function(ev) {\n    if (EventLog.seen_event_recently(ev)) {\n        return false;\n    }\n    var events = EventLog.get_popup_events_cache();\n    var curTime = Math.floor(Date.now() / 1000);\n    events.push([ev, curTime]);\n    Mailpile.local_storage[\"seen_popup_events\"] = JSON.stringify(events);\n    return true;\n};\n\n\n\n$(document).ready(function () {\n  window.addEventListener('storage', function(evt) {\n    // When the localStorage result sharing object gets updated, we parse\n    // as if we'd run the API call ourselves.\n    if (evt.key == 'eventlog_last_result') {\n      EventLog.other_tab = new Date().getTime();\n      EventLog.last_ts = EventLog.invoke_callbacks(JSON.parse(evt.newValue));\n    }\n  }, false);\n  window.setTimeout(EventLog.clear_old_events, EventLog.TIMEOUT_CHECK_OLD_EVENTS * 1000);\n});\n\n/* Notification - Undo */\n$(document).on('click', '.eventlog-undo', function() {\n  var event_id = $(this).data('event_id');\n  Mailpile.API.logs_events_undo_post({ event_id: event_id }, function(result) {\n    if (result.status === 'success') {\n      window.location.reload(true);\n    }\n    else {\n      alert(\"{{ _('Oops. Mailpile failed to complete your task.') }}\");\n    }\n  });\n});\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/global/global.js",
    "content": "Mailpile.fix_url = function(url) {\n  if (url.indexOf(\"{{ config.sys.http_path }}\") != 0) {\n    return \"{{ config.sys.http_path }}\" + url;\n  }\n  return url;\n}\n\nMailpile.go = function(url) {\n  // FIXME: This check is lame; a workaround for the fact that download\n  // URLs never end up triggering the event that cancels the notification.\n  if (url.indexOf('/download/') < 0) {\n    Mailpile.notify_working(undefined, 1000, 'blank');\n  }\n  window.location.href = Mailpile.fix_url(url);\n};\n\n\n/* Compose - Create a new email to an address */\n$(document).on('click', 'a', function(e) {\n  if ($(this).attr('href') && ($(this).attr('href').indexOf('mailto:') == 0)) {\n    e.preventDefault();\n    Mailpile.activities.compose($(this).attr('href').replace('mailto:', ''));\n  }\n});\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/global/helpers.js",
    "content": "/* Helpers */\n\nMailpile.Helpers = [{\n    slug: 'what-is-encryption-key',\n    title: '{{_(\"What is an Encryption Key\")|escapejs}}',\n    actions: false\n},{\n    slug: 'what-is-missing-key',\n    title: '{{_(\"What is Missing Encryption Key\")|escapejs}}',\n    actions: ['hideable']\n}];\n\n\n$(document).on('click', '.btn-helper', function(e) {\n  e.preventDefault();\n  var helper = $(this).data('helper');\n  var helper_data = _.findWhere(Mailpile.Helpers, {slug: helper});\n\n  helper_data['content'] = $('#template-helper-' + helper).html();\n\n  console.log(helper);\n  console.log(helper_data);\n\n  // FIXME: Unsafe template, this should be audited\n  var modal_template = Mailpile.unsafe_template($('#template-modal-helper').html());\n  Mailpile.UI.show_modal(modal_template(helper_data));\n});\n\n\n$(document).on('click', '.btn-helper-action', function(e) {\n  e.preventDefault();\n  var action = $(this).data('action');\n\n  console.log(action);\n\n  Mailpile.UI.hide_modal();\n\n  // Show New\n  setTimeout(function() {\n    Mailpile.UI.Modals[action]();\n  }, 500);\n\n});\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/global/silly.js",
    "content": "/* Setup - Complete - Model */\nMailpile.silly_strings = {\n  copying: [\n    '{{_(\"Mail is being imported into your Mailpile\")|escapejs}}',\n    '{{_(\"Copying your mail, please do not be alarmed!\")|escapejs}}',\n    '{{_(\"Copying mail. This could take a while\")|escapejs}}',\n    '{{_(\"Please be patient, we are copying mail as fast as possible\")|escapejs}}',\n    '{{_(\"Wow, you have a lot of mail\")|escapejs}}',\n    '{{_(\"I hope you dont have a plane to catch or anything\")|escapejs}}',\n    '{{_(\"Copying mail. Put your arms above your head and whistle La Cucaracha\")|escapejs}}',\n    '{{_(\"Making a copy of your mail. Putting it in your inbox, all comfy like\")|escapejs}}'\n  ],\n  nerds: [\n    '{{_(\"Damn kids.\")|escapejs}}',\n    '{{_(\"RMS approves!\")|escapejs}}',\n    '{{_(\"Formatting your C:\\\\ drive... (just kidding)\")|escapejs}}',\n    '{{_(\"Fortifying encryption shields\")|escapejs}}',\n    '{{_(\"Increasing entropy & scrambling bits\")|escapejs}}',\n    '{{_(\"Patching bugs...\")|escapejs}}',\n    '{{_(\"Indexing kittens...\")|escapejs}}',\n    '{{_(\"Indexing lovenotes\")|escapejs}}',\n    '{{_(\"Reticulating Splines\")|escapejs}}',\n    '{{_(\"Syntax error in line 45 of this e-mail\")|escapejs}}',\n    '{{_(\"Shoveling more coal into the server\")|escapejs}}',\n    '{{_(\"Calibrating flux capacitors\")|escapejs}}',\n    '{{_(\"A few bytes tried to escape, but we caught them\")|escapejs}}',\n    '{{_(\"Deterministically simulating future state\")|escapejs}}',\n    '{{_(\"Embiggening prototypes\")|escapejs}}',\n    '{{_(\"Resolving interdependence\")|escapejs}}',\n    '{{_(\"Spinning violently around the y-axis\")|escapejs}}',\n    '{{_(\"Locating additional gigapixels\")|escapejs}}',\n    '{{_(\"Initializing hamsters\")|escapejs}}',\n    '{{_(\"This is our world now... the world of the electron and the switch\")|escapejs}}',\n    '{{_(\"Fedexing all your spam to online advertisers...\")|escapejs}}',\n    '{{_(\"Becoming self-aware\")|escapejs}}',\n    '{{_(\"Looking for heretofore unknown prime numbers\")|escapejs}}',\n    '{{_(\"Re-aligning satellite grid\")|escapejs}}',\n    '{{_(\"Re-routing bitstream\")|escapejs}}',\n    '{{_(\"Warming up particle accelerator\")|escapejs}}',\n    '{{_(\"Time is an illusion. Loading time doubly so\")|escapejs}}',\n    '{{_(\"Verifying local gravitational constant\")|escapejs}}',\n    '{{_(\"This server is powered by a lemon and two electrodes\")|escapejs}}'\n  ],\n  email: [\n    '{{_(\"Wow, you have a lot of mail\")|escapejs}}',\n    '{{_(\"Mailpile has an advanced tagging system & search engine at its core\")|escapejs}}',\n    '{{_(\"Do you really need to keep all these Amazon Prime shipping confirmations?\")|escapejs}}',\n    '{{_(\"I am pretty sure e-mail from ThinkGeeks 2011 X-mas sale can be deleted.\")|escapejs}}',\n    '{{_(\"Do you really need an e-mail to know when you have been retweeted?\")|escapejs}}',\n    '{{_(\"E-mail is the largest internet based social network on the planet\")|escapejs}}',\n    '{{_(\"Which other technology do you use that is 40 years old?\")|escapejs}}',\n    '{{_(\"There are 2.5 billion e-mail users worldwide, that is double the amount of Facebook users!\")|escapejs}}',\n    '{{_(\"Use Mailpile Tags to better organize and search your mail\")|escapejs}}',\n    '{{_(\"Remember getting your first e-mail address? Remember how it felt like something private?\")|escapejs}}',\n    '{{_(\"E-mail is decentralized by design, this means no one company or government owns it!\")|escapejs}}',\n    '{{_(\"Over 100 trillion e-mails are sent per year, wow!\")|escapejs}}',\n    '{{_(\"E-mail is the most widely used communication protocol ever created by humans\")|escapejs}}',\n    '{{_(\"E-mail uses an open standard agreed upon by the entire world & owned by no one\")|escapejs}}'\n  ],\n  security: [\n    '{{_(\"BCC-ing ALL THE SPIES!\")|escapejs}}',\n    '{{_(\"Many powerful governments are conducting mass dragnet surveillance\")|escapejs}}',\n    '{{_(\"Most e-mail can be read by network operators as it travels through the internet\")|escapejs}}',\n    '{{_(\"Encryption ensures that your e-mails are only read by the intended recipient\")|escapejs}}',\n    '{{_(\"Unencrypted e-mail is more like sending a postcard than sending a letter\")|escapejs}}',\n    '{{_(\"Mailpile uses OpenPGP to encrypt and decrypt your messages securely\")|escapejs}}',\n    '{{_(\"All of your config settings & passwords are encrypted with AES-256\")|escapejs}}',\n    '{{_(\"Encrypting e-mails means your communication actually stays private\")|escapejs}}',\n    '{{_(\"The more encrypted e-mail you send, the better!\")|escapejs}}',\n    '{{_(\"Make sure you print or save your keys & passwords somewhere secure\")|escapejs}}',\n    '{{_(\"Mailpile by default encrypts your search index!\")|escapejs}}',\n    '{{_(\"The most common e-mail password is 123456, hopefully yours is different\")|escapejs}}'\n  ],\n  misc: [\n    '{{_(\"Good things come to those who wait\")|escapejs}}',\n    '{{_(\"Make Free Software and be happy\")|escapejs}}',\n    '{{_(\"Much of Mailpile was built in cafes in Reykjavík, Iceland\")|escapejs}}',\n    '{{_(\"Many Icelanders believe in elves and magical hidden people\")|escapejs}}',\n    '{{_(\"The founders of Mailpile first met in a public hot tub in Reykjavík\")|escapejs}}',\n    '{{_(\"We like volcanos, do you like volcanos?\")|escapejs}}',\n    '{{_(\"A million hamsters are spinning their wheels right now\")|escapejs}}',\n    '{{_(\"Tapping earth for more geothermal energy\")|escapejs}}',\n    '{{_(\"Digging moat. Filling with alligators. Fortifying walls\")|escapejs}}',\n    '{{_(\"Crossing out swear words...\")|escapejs}}',\n    '{{_(\"Compiling bullshit bingo grid...\")|escapejs}}',\n    '{{_(\"Abandon all hope, ye who enter here\")|escapejs}}',\n    '{{_(\"Welcome to the nine circles of suffering\")|escapejs}}',\n    '{{_(\"Informing David Cameron of suspicious ac^H^H^H ... naaah :)\")|escapejs}}',\n    '{{_(\"Letting you wait for no apparent reason\")|escapejs}}',\n    '{{_(\"What are you wearing?\")|escapejs}}',\n    '{{_(\"Go put the kettle on, this could be a while\")|escapejs}}',\n    '{{_(\"Go get a cup of tea and some biscuits. This will take approximately 4 custard creams worth of time\")|escapejs}}',\n    '{{_(\"Did you know humans share 50 percent of their DNA with a banana? Freaky\")|escapejs}}',\n    '{{_(\"Estimating chance of astroid hitting Earth\")|escapejs}}',\n    '{{_(\"Reading Terms of Service documents\")|escapejs}}',\n    '{{_(\"Catching up on shows on Netflix\")|escapejs}}',\n    '{{_(\"Oh, you have some very interesting old e-mails\")|escapejs}}',\n    '{{_(\"I think I better understand you now\")|escapejs}}',\n    '{{_(\"Your past is just a story you tell yourself\")|escapejs}}',\n    '{{_(\"Checking e-mails for stolen Winklevoss ideas\")|escapejs}}',\n    '{{_(\"Applying coupons...\")|escapejs}}',\n    '{{_(\"Licking stamps...\")|escapejs}}',\n    '{{_(\"Self potato\")|escapejs}}',\n    '{{_(\"Yum yum, that one was tasty\")|escapejs}}',\n    '{{_(\"Hey, there is some Nigerian prince here who wants to give you twenty million dollars...\")|escapejs}}',\n    '{{_(\"How rude!\")|escapejs}}',\n    '{{_(\"Now enhancing photos\")|escapejs}}',\n    '{{_(\"Backing up the entire Internet...\")|escapejs}}',\n    '{{_(\"Really? You are still waiting?\")|escapejs}}',\n    '{{_(\"Well... it sure is a beautiful day\")|escapejs}}',\n    '{{_(\"You should probably go outside or something\")|escapejs}}',\n    '{{_(\"Slacking off over here\")|escapejs}}',\n    '{{_(\"Doing nothing\")|escapejs}}',\n    '{{_(\"Making you wait for no reason\")|escapejs}}',\n    '{{_(\"Testing your patience\")|escapejs}}',\n    '{{_(\"Pay no attention to the man behind the curtain\")|escapejs}}',\n    '{{_(\"You are great just the way you are\")|escapejs}}',\n    '{{_(\"Warning: do not think of purple hippos\")|escapejs}}',\n    '{{_(\"Follow the white rabbit\")|escapejs}}',\n    '{{_(\"Wanna see how deep the rabbit hole goes?\")|escapejs}}',\n    '{{_(\"Supplying monkeys with typewriters\")|escapejs}}',\n    '{{_(\"Waiting for Godot.\")|escapejs}}',\n    '{{_(\"Swapping time and space\")|escapejs}}'\n  ]\n};\n\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/index.css",
    "content": "/* Colors */\n\n  {% for name, color in theme_settings().colors.iteritems() %}\n  .color-{{name}} { color: {{color}}; }\n  {% endfor %}\n\n/* Sidebar & Navigation On */\n\n  a.sidebar-tag,\n  a.sidebar-tag:active,\n  a.sidebar-tag:hover  { color: {{ theme_settings().navigation_link }};  }\n\n  li.navigation-on a.sidebar-tag:hover,\n  li.navigation-on a.sidebar-tag:hover span.sidebar-name,\n  li.navigation-on a.sidebar-tag:hover span.sidebar-notification { color: {{ theme_settings().navigation_link }}; }\n\n/* Sidebar Labels (dynamic) */\n  {% for name, color in theme_settings().colors.iteritems() %}\n\n  a.sidebar-tag.color-{{ name }}:active span.sidebar-name,\n  a.sidebar-tag.color-{{ name }}:hover span.sidebar-name,\n  li.navigation-on a.sidebar-tag.color-{{ name }}:active span.sidebar-name,\n  li.navigation-on a.sidebar-tag.color-{{ name }}:hover span.sidebar-name { color: {{ color }}; }\n\n  {% endfor %}\n\n{% for css_file in result.css_files %}\n  /* CSS for {{ css_file.classname }} */\n  {{ css_file.css|safe }}\n{% endfor %}\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/index.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block content %}\n\n<h1>JSAPI test page</h1>\n<p>\n  This page just tries to load <b><tt>/jsapi/as.js</tt></b> and parse,\n  for triggering nice Javascript errors.  In the future, it might be nice\n  if it printed out documentation or something for developers to look at.\n</p>\n<p>Status: <b><script>\n  if (Mailpile && Mailpile.API) {\n    document.write('Yay, the Mailpile JS API appears to exist!');\n  } else {\n    document.write('The Mailpile JS API failed to load, boo!');\n  }\n</script><noscript>The Mailpile JS API failed to load, boo!</noscript></b></p>\n\n<p>Use the source, Luke:</p>\n<pre>{{ mailpile_render('as.js', 'jsapi')[1] }}</pre>\n\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/index.js",
    "content": "// Make console.log not crash JS browsers that do not support it...\nif (!window.console) window.console = {\n  log: $.noop,\n  group: $.noop,\n  groupEnd: $.noop,\n  info: $.noop,\n  error: $.noop\n};\n\n// Common typos.\nTrue = true;\nFalse = false;\n\n\n// Mailpile global Javascript state and configuration /========================\nMailpile = {\n  csrf_token:         \"{{ csrf_token }}\",\n  ui_in_action:       0,\n  instance:           {},\n  search_target:      'none',\n  messages_composing: {},\n  crypto_keylookup:   [],\n  keybindings:        [ {#\n    // Note to hackers:\n    //\n    // We avoid binding TAB, ENTER, UP, DOWN and SPACE because those all have\n    // meaning in common browser defaults. We aim to play nice with browser\n    // defaults, not end up in a preventDefault war.\n    //\n    // See also: https://github.com/mailpile/Mailpile/issues/1814\n    //\n    // #}\n    { title: '{{_(\"Search mail\")|escapejs}}',         keys: \"/\",       callback: function(e) { $(\"#search-query\").focus(); return false; } },\n    { title: '{{_(\"Compose e-mail\")|escapejs}}',      keys: \"c\",       callback: function(e) { Mailpile.activities.compose(); }},\n    { title: '{{_(\"Move selection down\")|escapejs}}', keys: \"j\",       callback: function(e) { Mailpile.keybinding_selection_down(); }},\n    { title: '{{_(\"Extend selection down\")|escapejs}}', keys: \"x\",     callback: function(e) { Mailpile.keybinding_selection_extend(); }},\n    { title: '{{_(\"Move selection up\")|escapejs}}',   keys: \"k\",       callback: function(e) { Mailpile.keybinding_selection_up(); }},\n    { title: '{{_(\"Previous page of results\")|escapejs}}', keys: \"h\",  callback: function(e) { $('#pile-previous').eq(0).trigger('click'); }},\n    { title: '{{_(\"Next page of results\")|escapejs}}', keys: \"l\",      callback: function(e) { $('#pile-next').eq(0).trigger('click'); }},\n    { title: '{{_(\"More results\")|escapejs}}',         keys: \"m\",      callback: function(e) { $('#pile-more').eq(0).trigger('click'); }},\n    { title: '{{_(\"Open e-mail for reading\")|escapejs}}', keys: \"o\",   callback: function(e) { Mailpile.open_or_close_selected_thread(); }},\n    { title: '{{_(\"Go to Drafts\")|escapejs}}',        keys: \"g d\",     callback: function(e) { Mailpile.go(\"/in/drafts/\"); }},\n    { title: '{{_(\"Go to Inbox\")|escapejs}}',         keys: \"g i\",     callback: function(e) { Mailpile.go(\"/in/inbox/\"); }},\n    { title: '{{_(\"Go to Outbox\")|escapejs}}',        keys: \"g o\",     callback: function(e) { Mailpile.go(\"/in/outbox/\"); }},\n    { title: '{{_(\"Go to Sent\")|escapejs}}',          keys: \"g s\",     callback: function(e) { Mailpile.go(\"/in/sent/\"); }},\n    { title: '{{_(\"Go to Spam\")|escapejs}}',          keys: \"g j\",     callback: function(e) { Mailpile.go(\"/in/spam/\"); }},\n    { title: '{{_(\"Go to Trash\")|escapejs}}',         keys: \"g t\",     callback: function(e) { Mailpile.go(\"/in/trash/\"); }},\n    { title: '{{_(\"Go to All Mail\")|escapejs}}',      keys: \"g a\",     callback: function(e) { Mailpile.go(\"/in/all-mail/\"); }},\n    { title: '{{_(\"Follow search hint\")|escapejs}}',  keys: \"g h\",     callback: function(e) { Mailpile.go($('.bulk-actions-hints a').eq(0).attr('href')); }},\n    { title: '{{_(\"Reply to e-mail\")|escapejs}}',     keys: \"r\",       callback: function(e) { Mailpile.keybinding_reply(); }},\n    { title: '{{_(\"Reply to many e-mails at once\")|escapejs}}',\n                                                      keys: \"shift+r\", callback: function(e) { Mailpile.keybinding_reply('many'); }},\n    { title: '{{_(\"Forward one or more e-mails\")|escapejs}}',\n                                                      keys: \"shift+f\", callback: function(e) { Mailpile.keybinding_forward(); }},\n    { title: '{{_(\"Mark as read\")|escapejs}}',        keys: \"shift+i\", callback: function(e) { Mailpile.keybinding_mark_read(); }},\n    { title: '{{_(\"Mark as unread\")|escapejs}}',      keys: \"shift+u\", callback: function(e) { Mailpile.keybinding_mark_unread(); }},\n    { title: '{{_(\"Move to spam\")|escapejs}}',        keys: \"!\",       callback: function(e) { Mailpile.keybinding_move_messages('spam'); }},\n    { title: '{{_(\"Archive e-mail\")|escapejs}}',      keys: \"e\",       callback: function(e) { Mailpile.keybinding_move_messages('!archive'); }},\n    { title: '{{_(\"Untag, remove e-mail from view\")|escapejs}}',\n                                                      keys: \"shift+e\", callback: function(e) { Mailpile.keybinding_move_messages('!untag', 'keep_new'); }},\n    { title: '{{_(\"Delete e-mail\")|escapejs}}',       keys: \"#\",       callback: function(e) { Mailpile.keybinding_move_messages('trash'); }},\n    { title: '{{_(\"Undo last action\")|escapejs}}',    keys: \"z\",       callback: function(e) { Mailpile.keybinding_undo_last(); }},\n    { title: '{{_(\"Select all visible\")|escapejs}}',  keys: \"* a\",     callback: function(e) { Mailpile.bulk_action_select_all(); }},\n    { title: '{{_(\"Select all matching search\")|escapejs}}',\n                                                      keys: \"* s\",     callback: function(e) { Mailpile.keybinding_select_all_matches(); }},\n    { title: '{{_(\"Deselect all\")|escapejs}}',        keys: \"* n\",     callback: function(e) { Mailpile.bulk_action_select_none(); }},\n    { title: '{{_(\"Dismiss all notifications\")|escapejs}}',\n                                                      keys: \"_\",       callback: function(e) { $('a.notifications-close-all').eq(0).trigger('click'); }},\n    { title: '{{_(\"Account List\")|escapejs}}',        keys: \"g h\",     callback: function(e) { Mailpile.go(\"/profiles/\"); }},\n    { title: '{{_(\"Security and Privacy Settings\")|escapejs}}',\n                                                      keys: \"g p\",     callback: function(e) { Mailpile.go(\"/settings/privacy.html\"); }},\n    // Assign hot-keys to the contextual actions (Edit, New, Attachments, ...)\n    {                                                 keys: \"1\",       callback: function(e) { $('#content-tools a').not('.hide').eq(0).trigger('click'); }},\n    {                                                 keys: \"2\",       callback: function(e) { $('#content-tools a').not('.hide').eq(1).trigger('click'); }},\n    {                                                 keys: \"3\",       callback: function(e) { $('#content-tools a').not('.hide').eq(2).trigger('click'); }},\n    {                                                 keys: \"4\",       callback: function(e) { $('#content-tools a').not('.hide').eq(3).trigger('click'); }},\n    {                                                 keys: \"5\",       callback: function(e) { $('#content-tools a').not('.hide').eq(4).trigger('click'); }},\n    {                                                 keys: \"6\",       callback: function(e) { $('#content-tools a').not('.hide').eq(5).trigger('click'); }},\n    {                                                 keys: \"7\",       callback: function(e) { $('#content-tools a').not('.hide').eq(6).trigger('click'); }},\n    {                                                 keys: \"8\",       callback: function(e) { $('#content-tools a').not('.hide').eq(7).trigger('click'); }},\n    {                                                 keys: \"9\",       callback: function(e) { $('#content-tools a').not('.hide').eq(8).trigger('click'); }},\n    // Assign hot-keys to the contextual bulk actions (toggle read/...)\n    {                                                 keys: \"* 1\",       callback: function(e) { $('.bulk-actions ul a').not('.hide').eq(0).trigger('click'); }},\n    {                                                 keys: \"* 2\",       callback: function(e) { $('.bulk-actions ul a').not('.hide').eq(1).trigger('click'); }},\n    {                                                 keys: \"* 3\",       callback: function(e) { $('.bulk-actions ul a').not('.hide').eq(2).trigger('click'); }},\n    {                                                 keys: \"* 4\",       callback: function(e) { $('.bulk-actions ul a').not('.hide').eq(3).trigger('click'); }},\n    {                                                 keys: \"* 5\",       callback: function(e) { $('.bulk-actions ul a').not('.hide').eq(4).trigger('click'); }},\n    {                                                 keys: \"* 6\",       callback: function(e) { $('.bulk-actions ul a').not('.hide').eq(5).trigger('click'); }},\n    {                                                 keys: \"* 7\",       callback: function(e) { $('.bulk-actions ul a').not('.hide').eq(6).trigger('click'); }},\n    {                                                 keys: \"* 8\",       callback: function(e) { $('.bulk-actions ul a').not('.hide').eq(7).trigger('click'); }},\n    {                                                 keys: \"* 9\",       callback: function(e) { $('.bulk-actions ul a').not('.hide').eq(8).trigger('click'); }},\n    // Assign hot-keys to the visible user-generated tags...\n    {                                                 keys: \"g 1\",     callback: function(e) { $('#sidebar-lists #sidebar-tag li').not('.hide').find('a.sidebar-tag').eq(0).trigger('click'); }},\n    {                                                 keys: \"g 2\",     callback: function(e) { $('#sidebar-lists #sidebar-tag li').not('.hide').find('a.sidebar-tag').eq(1).trigger('click'); }},\n    {                                                 keys: \"g 3\",     callback: function(e) { $('#sidebar-lists #sidebar-tag li').not('.hide').find('a.sidebar-tag').eq(2).trigger('click'); }},\n    {                                                 keys: \"g 4\",     callback: function(e) { $('#sidebar-lists #sidebar-tag li').not('.hide').find('a.sidebar-tag').eq(3).trigger('click'); }},\n    {                                                 keys: \"g 5\",     callback: function(e) { $('#sidebar-lists #sidebar-tag li').not('.hide').find('a.sidebar-tag').eq(4).trigger('click'); }},\n    {                                                 keys: \"g 6\",     callback: function(e) { $('#sidebar-lists #sidebar-tag li').not('.hide').find('a.sidebar-tag').eq(5).trigger('click'); }},\n    {                                                 keys: \"g 7\",     callback: function(e) { $('#sidebar-lists #sidebar-tag li').not('.hide').find('a.sidebar-tag').eq(6).trigger('click'); }},\n    {                                                 keys: \"g 8\",     callback: function(e) { $('#sidebar-lists #sidebar-tag li').not('.hide').find('a.sidebar-tag').eq(7).trigger('click'); }},\n    {                                                 keys: \"g 9\",     callback: function(e) { $('#sidebar-lists #sidebar-tag li').not('.hide').find('a.sidebar-tag').eq(8).trigger('click'); }},\n    {\n      keys: \"t\",\n      callback: function(e) { Mailpile.Terminal.toggle(\"small\"); return false; },\n      title: \"{{_(\"Show terminal (small).\")|escapejs}}\"\n    },\n    {\n      keys: \"shift+t\",\n      callback: function(e) { Mailpile.Terminal.toggle(\"full\"); return false; },\n      title: \"{{_(\"Show terminal (full).\")|escapejs}}\"\n    }\n  ],\n  nagify: 1000 * 60 * 60 * 24 * 7, // Default nag is 1 per week\n  ajax_timeout: {{config.sys.ajax_timeout}},\n  commands:      [],\n  graphselected: [],\n  defaults: {\n    view_size: \"comfy\",\n  },\n  config: {\n    web: {{config.web|json|safe}}\n  },\n  api: {\n    compose      : \"{{ config.sys.http_path }}/api/0/message/compose/\",\n    compose_send : \"{{ config.sys.http_path }}/api/0/message/update/send/\",\n    compose_save : \"{{ config.sys.http_path }}/api/0/message/update/\",\n    contacts     : \"{{ config.sys.http_path }}/api/0/search/address/\",\n    message      : \"{{ config.sys.http_path }}/api/0/message/=\",\n    tag          : \"{{ config.sys.http_path }}/api/0/tag/\",\n    tag_list     : \"{{ config.sys.http_path }}/api/0/tags/\",\n    tag_add      : \"{{ config.sys.http_path }}/api/0/tags/add/\",\n    tag_update   : \"{{ config.sys.http_path }}/api/0/settings/set/\",\n    search_new   : \"{{ config.sys.http_path }}/api/0/search/?q=in%3Anew\",\n    search       : \"{{ config.sys.http_path }}/api/0/search/\",\n    settings_add : \"{{ config.sys.http_path }}/api/0/settings/add/\"\n  },\n  urls: {\n    message_draft : \"{{ config.sys.http_path }}/message/draft/=\",\n    message_sent  : \"{{ config.sys.http_path }}/thread/=\",\n    thread        : \"{{ config.sys.http_path }}/thread/=\",\n    tags          : \"{{ config.sys.http_path }}/tags/\"\n  },\n  plugins: [],\n  theme: {},\n  activities: {},\n  local_storage: localStorage || {},\n  unsafe_template: function(tpl) { return _.template(tpl); },\n  safe_template: function(tpl) {\n    return _.template(tpl, undefined, {\n      evaluate: /<%([\\s\\S]+?)%>/g,\n      // interpolate: /<%=([\\s\\S]+?)%>/g, <- DISABLED FOR SAFETY :)\n      escape: /<%[-=]([\\s\\S]+?)%>/g\n    });\n  },\n  safe_jinjaish_template: function(tpl) {\n    return _.template(tpl, undefined, {\n      evaluate: /[{]%([\\s\\S]+?)%[}]/g,\n      // interpolate: /<%=([\\s\\S]+?)%>/g, <- DISABLED FOR SAFETY :)\n      escape: /{[{]([\\s\\S]+?)[}]}/g\n    });\n  }\n};\n{% set theme_settings = theme_settings() %}\nMailpile.theme = {{ theme_settings|json|safe }};\n\n\n// AJAX Wappers - This is the core Mailpile JS API /==========================\n{#\n##\n## This autogenerates JS methods which fire GET & POST calls to Mailpile\n## API/command endpoints.\n##\n## It also name-spaces and wraps any and all plugin javascript code.\n##\n#}\nMailpile.API = {\n  _endpoints: { {%- for command in result.api_methods %}\n    {{command.url|replace(\"/\", \"_\")}}_{{command.method|lower}}: \"/0/{{command.url}}/\"{% if not loop.last %},{% endif %}\n  {% endfor -%} },\n  _sync_url: \"{{ config.sys.http_path }}/api\",\n  _async_url: \"{{ config.sys.http_path }}/async\",\n\n  _dead_notification: undefined,\n  _notify_dead: function(status, message, fullscreen) {\n    var advice = ((document.location.href.indexOf('/localhost:') == -1)\n                  ? '{{_(\"Check your network?\")|escapejs}}'\n                  : '{{_(\"Restart the app?\")|escapejs}}');\n    if (status == \"user\") advice = \"\";\n    if (fullscreen) {\n      if (!$('#connection-down').length) {\n        var template = Mailpile.safe_template($('#template-connection-down').html());\n        $('body').append(template({\n          message: message,\n          advice: advice\n        }));\n      }\n    }\n    else {\n      Mailpile.API._dead_notification = Mailpile.notification({\n        status: status,\n        message: message,\n        message2: advice,\n        event_id: Mailpile.API._dead_notification,\n        icon: 'icon-signature-unknown'\n      });\n    }\n  },\n  _ajax_dead_count: 0,\n  _ajax_is_alive: function() {\n    Mailpile.API._ajax_dead_count = 0;\n    if (Mailpile.API._dead_notification) {\n      Mailpile.cancel_notification(Mailpile.API._dead_notification);\n      Mailpile.API._dead_notification = undefined;\n      if ($('#connection-down').length) {\n        $('#connection-down').fadeOut().remove();\n      }\n    }\n  },\n  _ajax_error: function(base_url, command, data, method, response, status) {\n    console.log('Oops, an AJAX call returned as error :(');\n    console.log('status: ' + status + ' method: ' + method + ' base_url: ' + base_url + ' command: ' + command);\n    console.log(response);\n\n    if (response.status == 403) {\n      // We have been logged out or the backend restated, go to login\n      var href = document.location.href;\n      if (href.indexOf('#') != -1) href = href.substring(0, href.indexOf('#'));\n      href += (href.indexOf('?') == -1) ? '?' : '&';\n      document.location.href = href + 'ui_relogin=1';\n      return;\n    }\n    else if (response.status == 500) {\n      // 500 internal errors and timeouts...\n      if (command != '/0/cached/' && command != '/0/logs/events/') {\n        Mailpile.notification({\n          status: 'error',\n          message: '{{_(\"Oops. Mailpile failed to complete your task.\")|escapejs}}',\n          icon: 'icon-signature-unknown'\n        });\n      }\n      Mailpile.API._ajax_dead_count = 0;\n      return;\n    }\n    if (response.status == 0 ||    // Some internal error state\n        response.status == 503 ||  // PageKite or reverse proxy down\n        status == 'parsererror' || // Server replaced with sth. else?\n        response.status == 302) {  // Server gone somewhere else?\n      // Tell the user to check the network, but could probably provide clearer\n      // feedback.\n      Mailpile.API._ajax_dead_count += 1;\n    }\n\n    if (Mailpile.API._ajax_dead_count > 3) {\n      Mailpile.API._notify_dead('error', '{{_(\"Mailpile is unreachable.\")|escapejs}}',\n                                (Mailpile.API._ajax_dead_count > 10));\n    }\n    else if (status == \"timeout\") {\n      Mailpile.API._notify_dead('warning', '{{_(\"Mailpile timed out...\")|escapejs}}');\n    }\n  },\n\n  _action: function(base_url, command, data, method, callback) {\n    // Output format, timeout...\n    var output = '';\n    var timeout = Mailpile.ajax_timeout;\n    var error_callback = undefined;\n    if (data._output) {\n      output = data._output;\n      delete data['_output'];\n    }\n    if (data._timeout) {\n      timeout = data._timeout;\n      delete data['_timeout'];\n    }\n    if (data._error_callback) {\n      error_callback = data._error_callback;\n      delete data['_error_callback'];\n    }\n    if (data._args) {\n      for (var i in data._args) {\n        command = command + '/' + data._args[i];\n      }\n      delete data['_args'];\n    }\n\n    // Get search context; should be overridden by methods that know better\n    var context = (data['context'] || $('#search-query').data('context'));\n\n    // Force method to GET if not POST\n    if (method !== 'GET' && method !== 'POST') method = 'GET';\n\n    if (method === 'GET') {\n      // Make Querystring\n      var params = data._serialized;\n      if (!params) {\n        for (var k in data) {\n          if (!data[k] || data[k] == undefined) {\n            delete data[k];\n          }\n        }\n        params = $.param(data);\n      }\n      if (context && (-1 == params.indexOf('&context=')) &&\n                     (0 != params.indexOf('context='))) {\n        params += '&context=' + context;\n      }\n\n      $.ajax({\n        url: base_url + command + output + \"?\" + params,\n        type: 'GET',\n        timeout: timeout,\n        dataType: 'json',\n        success: function(response, status) {\n          if (response.result) {\n            Mailpile.API._ajax_is_alive();\n            if (callback) return callback(response, status);\n          }\n          else {\n            Mailpile.API._ajax_error(base_url, command, data,\n                                     method, response, status);\n            if (error_callback) error_callback(response, status);\n          }\n        },\n        error: function(response, status) {\n          Mailpile.API._ajax_error(base_url, command, data,\n                                   method, response, status);\n          if (error_callback) error_callback(response, status);\n        }\n      });\n    }\n    else if (method === 'POST') {\n      if (data._serialized) {\n        data = data._serialized + '&csrf=' + Mailpile.csrf_token;\n        if (context) data = data + '&context=' + context;\n      }\n      else {\n        if (context) data['context'] = context;\n        if (data['csrf']) {\n          Mailpile.csrf_token = data['csrf'];\n        }\n        else {\n          data['csrf'] = Mailpile.csrf_token;\n        }\n      }\n      $.ajax({\n        url: base_url + command + output,\n        type: 'POST',\n        data: data,\n        timeout: timeout,\n        dataType: 'json',\n        success: function(response, status) {\n          Mailpile.API._ajax_is_alive();\n          if (callback) return callback(response, status);\n        },\n        error: function(response, status) {\n          Mailpile.API._ajax_error(base_url, command, data,\n                                   method, response, status);\n          if (error_callback) error_callback(response, status);\n        }\n      });\n    }\n    return true;\n  },\n\n  U: function(original_url) {\n    var prefix = \"{{ config.sys.http_path }}\";\n    if (original_url.indexOf(prefix+'/') != 0) {\n      return prefix + original_url;\n    }\n    return original_url;\n  },\n\n  jhtml_url: function(original_url, rendering) {\n    var new_url = original_url;\n    var html = new_url.indexOf('.html');\n    rendering = rendering || 'minimal';\n    if (html != -1) {\n      new_url = (new_url.slice(0, html+1) + 'jhtml!' + rendering +\n                 new_url.slice(html+5));\n    }\n    else {\n      var qs = new_url.indexOf('?');\n      if (qs != -1) {\n        new_url = (new_url.slice(0, qs) + 'as.jhtml!' + rendering +\n                   new_url.slice(qs));\n      }\n      else {\n        var anch = new_url.indexOf('#');\n        if (anch != -1) {\n          new_url = (new_url.slice(0, anch) + 'as.jhtml!' + rendering +\n                     new_url.slice(anch));\n        }\n        else {\n          new_url += 'as.jhtml!' + rendering;\n        }\n      }\n    }\n    return new_url;\n  },\n\n  with_template: function(name, action, error, flags, unsafe) {\n      var url = \"{{ config.sys.http_path }}/jsapi/templates/\" + name + \".html\";\n      if (flags) {\n        url += '?ui_flags=' + flags.replace(' ', '+');\n      }\n      $.ajax({\n        url: url,\n        type: 'GET',\n        success: function(data) {\n          action((unsafe) ? Mailpile.unsafe_template(data)\n                          : Mailpile.safe_template(data));\n        },\n        error: error\n      });\n  },\n\n  _sync_action: function(command, data, method, callback) {\n    return Mailpile.API._action(Mailpile.API._sync_url, command, data, method, callback);\n  },\n\n  _async_action: function(command, data, method, callback, flags) {\n    function handle_event(data) {\n      if (data.result.resultid) {\n        subreq = {event_id: data.result.resultid, flags: flags};\n        var subid = EventLog.subscribe(subreq, function(ev) {\n          callback(ev.private_data, ev);\n          if (ev.flags == \"c\") {\n            EventLog.unsubscribe(data.result.resultid, subid);\n          }\n        });\n      }\n    }\n    return Mailpile.API._action(Mailpile.API._async_url, command, data, method, handle_event, flags);\n  },\n\n  _method: function(method, methods) {\n    if (!method || methods.indexOf(method) == -1) return methods[0];\n    return method;\n  },\n{#- Loop through all commands, creating both sync and async API methods #}\n  {%- for command in result.api_methods -%}\n    {%- set n = command.url|replace(\"/\", \"_\") %}\n    {%- set m = command.method|lower %}\n    {%- set u = command.url %}\n    {%- set cm = command.method %}\n\n  {{n}}_{{m}}: function(d,c,m){return Mailpile.API._sync_action(\"/0/{{u}}/\",d,Mailpile.API._method(m,[\"{{cm}}\"]),c);},\n  async_{{n}}_{{m}}: function(d,c,m){return Mailpile.API._async_action(\"/0/{{u}}/\",d,Mailpile.API._method(m,[\"{{cm}}\"]),c);}{% if not loop.last %},{% endif %}\n  {% endfor %}\n\n};\n\n\n// JS App Files /=============================================================\n{% include(\"jsapi/global/eventlog.js\") %}\n{% include(\"jsapi/global/activities.js\") %}\n{% include(\"jsapi/global/global.js\") %}\n{% include(\"jsapi/global/helpers.js\") %}\n{% include(\"jsapi/global/silly.js\") %}\n\n\n// JS - UI /==================================================================\n{% include(\"jsapi/ui/init.js\") %}\n{% include(\"jsapi/ui/notifications.js\") %}\n{% include(\"jsapi/ui/selection.js\") %}\n{% include(\"jsapi/ui/tagging.js\") %}\n{% include(\"jsapi/ui/content.js\") %}\n{% include(\"jsapi/ui/global.js\") %}\n{% include(\"jsapi/ui/topbar.js\") %}\n{% include(\"jsapi/ui/sidebar.js\") %}\n{% include(\"jsapi/ui/tooltips.js\") %}\n{% include(\"jsapi/ui/keybindings.js\") %}\n{% include(\"jsapi/ui/events.js\") %}\n{% include(\"jsapi/ui/terminal.js\") %}\n\n\n// Plugin Javascript /========================================================\n{#\n## Note: we do this in multiple commands instead of one big dict, so plugin\n## setup code can reference other plugins. Plugins are expected to return a\n## dictionary of values they want to make globally accessible.\n##\n## FIXME: Make sure the order is somehow sane given dependenies.\n#}\n{% for js_class in result.javascript_classes %}\n{% set js_classname = js_class.classname.capitalize() -%}\n{% if js_class.code -%}\n{{ js_classname }} = (function(){\n{{ js_class.code|safe }}\n})(); // End of {{ js_classname }} /----------- ---- --- -- -\n\n{% else -%}\n{{ js_classname }} = {};\n{% endif %}\n{% endfor %}\n\n// EOF\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/index.txt",
    "content": "This is a test!\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/message/events.js",
    "content": "/* Thread - Show People In Conversation */\n$(document).on('click', '.show-thread-people', function() {\n  // FIXME: Old/unreliable modal code\n  $('#modal-full .modal-title').html($('#thread-people').data('modal_title'));\n  $('#modal-full .modal-body').html($('#thread-people').html());\n  $('#modal-full').modal(Mailpile.UI.modal_options);\n});\n\n\n/* Thread - Show Tags In Converstation */\n$(document).on('click', '.show-thread-tags', function() {\n  // FIXME: Old/unreliable modal code\n  $('#modal-full .modal-title').html($('#thread-tags').data('modal_title'));\n  $('#modal-full .modal-body').html($('#thread-tags').html());\n  $('#modal-full').modal(Mailpile.UI.modal_options);\n});\n\n\n/* Thread - Show Metadata Info */\n$(document).on('click', '.message-metadata-details-toggle', function() {\n  var mid = $(this).data('mid');\n  var target = '#metadata-details-' + mid;\n  if ($(target).css('display') === 'none') {\n    $(target).show('fast').addClass('border-bottom');\n    $(this).css('color', '#4d4d4d');\n  }\n  else {\n    $(target).hide('fast').removeClass('border-bottom');\n    $(this).css('color', '#ccc');\n  }\n});\n\n\n/* Thread - Expand Snippet */\n$(document).on('click', 'div.thread-snippet', function(e) {  \n  var mid = $(this).data('mid');\n  if (e.target.href === undefined && $(e.target).data('expand') !== 'no' && $(e.target).hasClass('show-message-metadata-details') === false) {\n    Mailpile.UI.Message.ShowMessage(mid);\n  }\n});\n\n\n/* Thread - Message Quote */\n$(document).on('click', '.message-actions-quote', function() {\n  var mid = $(this).parent().parent().data('mid');\n  $('#message-' + mid).find('.message-part-quote').removeClass('hide');\n  $('#message-' + mid).find('.message-part-signature').removeClass('hide');\n  $(this).parent().hide();\n});\n\n\n/* Thread - Might Move to Global Location / Abstraction */\n$(document).on('click', '.dropdown-toggle', function() {\n  $(this).find('.icon-arrow-right').removeClass('icon-arrow-right').addClass('icon-arrow-down');\n});\n\n\n$(document).on('click', '.message-toggle-html', function(e) {\n  var state = $(this).data('state');\n  var mid = $(this).data('mid');\n  if (state === 'plain') {\n    $(this).data('state', 'html');\n    $(this).html('{{_(\"Plain Text\")|escapejs}}');\n    Mailpile.Message.ShowHTML(mid);\n  } else {\n    $(this).data('state', 'plain');\n    $(this).html('HTML');\n    Mailpile.Message.ShowPlain(mid);\n  }\n});\n\n\n$(document).on('click', '.message-show-html', function(e) {\n  var mid = $(this).closest('.has-mid').data('mid');\n  console.log('Should show message HTML parts for ' + mid);\n  Mailpile.Message.ShowHTML(mid);\n  return false;\n});\n\n\n$(document).on('click', '.message-show-text', function(e) {\n  var mid = $(this).closest('.has-mid').data('mid');\n  console.log('Should show message text parts ' + mid);\n  Mailpile.Message.ShowPlain(mid);\n  return false;\n});\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/message/html-sandbox.js",
    "content": "// HTML mail display & sandboxing\n\n\nMailpile.Message.ShowHTMLLegacy = function(mid, $old_message_body, html_data) {\n  // Inject iframe\n  $old_message_body.append(\n    '<iframe id=\"message-iframe-' + mid + '\" class=\"message-part-html\"' +\n    ' sandbox=\"allow-top-navigation allow-popups allow-popups-to-escape-sandbox\"' +\n    ' seamless target=\"_blank\" srcdoc=\"\"></iframe>');\n\n  // Add html parts\n  var html_parts = '';\n  _.each(html_data, function(part, key) {\n    html_parts += part.data;\n  });\n  $('#message-iframe-' + mid).attr('srcdoc', DOMPurify.sanitize(html_parts));\n\n  // Resize & Style\n  setTimeout(function() {\n    var iframe_height = $('#message-iframe-' + mid).contents().height();\n    $('#message-iframe-' + mid).height(iframe_height);\n    $('#message-iframe-' + mid).contents().find('body').addClass('message-part-html-text');\n  }, 100);\n};\n\n\nMailpile.Message.AddDOMPurifyHooks = function(with_image_proxy,\n                                              changes_callback) {\n  // The following code is based on dompurify/demos/hooks-proxy-demo.html and\n  // the whiteout.io sandbox implementation.\n  function proxy() {\n    if (changes_callback) changes_callback();\n    if (with_image_proxy) {\n      return (\n        Mailpile.API._sync_url +\n        Mailpile.API._endpoints['http_proxy_get'] +\n        '?csrf=' +\n        Mailpile.csrf_token +\n        '&url='\n      );\n    }\n    else {\n      // Replace all images with a 1x100 transparent PNG, yay!\n      // Note: using a 1x1 square results in large square empty\n      //       spaces in many e-mails, because only the width is\n      //       defined in the HTML; and the hight gets scaled\n      //       proportionally. Thus the 1x100 ratio instead.\n      return Mailpile.API.U('/static/img/1x100.png#old=');\n    }\n  }\n\n  // Specify attributes to proxy\n  var attributes = ['background', 'href', 'src'];  // not: 'action', 'poster'\n\n  // specify the regex to detect external content\n  var url_regex = /(url\\(\"?)(?!data:)/gim;\n\n  /**\n   *  Take CSS property-value pairs and proxy URLs in values,\n   *  then add the styles to an array of property-value pairs\n   */\n  function addStyles(output, styles) {\n    for (var prop = styles.length-1; prop >= 0; prop--) {\n      if (styles[styles[prop]]) {\n        var url = styles[styles[prop]].replace(url_regex, '$1' + proxy());\n        styles[styles[prop]] = url;\n      }\n      if (styles[styles[prop]]) {\n        output.push(styles[prop] + ':' + styles[styles[prop]] + ';');\n      }\n    }\n  }\n\n  /**\n   * Take CSS rules and analyze them, proxy URLs via addStyles(),\n   * then create matching CSS text for later application to the DOM\n   */\n  function addCSSRules(output, cssRules) {\n    for (var index=cssRules.length-1; index>=0; index--) {\n      var rule = cssRules[index];\n      // check for rules with selector\n      if (rule.type == 1 && rule.selectorText) {\n        output.push(rule.selectorText + '{')\n        if (rule.style) {\n          addStyles(output, rule.style)\n        }\n        output.push('}');\n      // check for @media rules\n      } else if (rule.type === rule.MEDIA_RULE) {\n        output.push('@media ' + rule.media.mediaText + '{');\n        addCSSRules(output, rule.cssRules)\n        output.push('}');\n      // check for @font-face rules\n      } else if (rule.type === rule.FONT_FACE_RULE) {\n        output.push('@font-face {');\n        if (rule.style) {\n          addStyles(output, rule.style)\n        }\n        output.push('}');\n      // check for @keyframes rules\n      } else if (rule.type === rule.KEYFRAMES_RULE) {\n        output.push('@keyframes ' + rule.name + '{');\n        for (var i=rule.cssRules.length-1;i>=0;i--) {\n          var frame = rule.cssRules[i];\n          if (frame.type === 8 && frame.keyText) {\n            output.push(frame.keyText + '{');\n            if (frame.style) {\n              addStyles(output, frame.style);\n            }\n            output.push('}');\n          }\n        }\n        output.push('}');\n      }\n    }\n  }\n\n  /**\n   * Proxy a URL in case it's not a Data URI\n   */\n  function proxyAttribute(url) {\n    if (/^data:image\\//.test(url)) {\n      return url;\n    } else {\n      return proxy() + encodeURIComponent(url)\n    }\n  }\n\n  // Add a hook to enforce proxy for leaky CSS rules\n  DOMPurify.removeHooks('uponSanitizeElement');\n  DOMPurify.addHook('uponSanitizeElement', function (node, data) {\n    if (data.tagName === 'style') {\n      var output  = [];\n      addCSSRules(output, node.sheet.cssRules);\n      node.textContent = output.join(\"\\n\");\n    }\n  });\n\n  DOMPurify.removeHooks('afterSanitizeAttributes');\n  DOMPurify.addHook('afterSanitizeAttributes', function(node) {\n    if ('target' in node) {\n      // This is a belt-and-suspenders thing, in case the load()\n      // event below does not fire.\n      node.setAttribute('target', '_blank');\n    }\n    else if (node.hasAttribute('xlink:href') ||\n             node.hasAttribute('href')) {\n      node.setAttribute('xlink:show', 'new');\n    }\n\n    // Check all src attributes and proxy them\n    if (node.nodeName != 'A') {\n      for(var i = 0; i <= attributes.length-1; i++) {\n        if (node.hasAttribute(attributes[i])) {\n          node.setAttribute(attributes[i], proxyAttribute(\n            node.getAttribute(attributes[i]))\n          );\n        }\n      }\n    }\n\n    // Check all style attribute values and proxy them\n    if (node.hasAttribute('style')) {\n      var styles = node.style;\n      var output = [];\n      for (var prop = styles.length-1; prop >= 0; prop--) {\n        // we re-write each property-value pair to remove invalid CSS\n        if (node.style[styles[prop]] && url_regex.test(node.style[styles[prop]])) {\n          var url = node.style[styles[prop]].replace(url_regex, '$1' + proxy())\n          node.style[styles[prop]] = url;\n        }\n        output.push(styles[prop] + ':' + node.style[styles[prop]] + ';');\n      }\n      // re-add styles in case any are left\n      if (output.length) {\n        node.setAttribute('style', output.join(\"\"));\n      } else {\n        node.removeAttribute('style');\n      }\n    }\n  });\n};\n\n\nMailpile.Message.SetHTMLPolicy = function(mid, old_policy, new_policy) {\n  // Record as preference on VCard\n  var $from = $('.pile-message-' + mid).find('.from');\n  if ($from.length > 0 && (!old_policy || old_policy != new_policy)) {\n    Mailpile.API.contacts_set_post({\n      'fn': $from.data('fn'),\n      'email': $from.data('address'),\n      'name': 'x-mailpile-html-policy',\n      'value': new_policy\n    });\n  }\n};\n\n\nMailpile.Message.SandboxHTML = function(part_id, $part, html_data, policy, allow_images) {\n\n  var $iframe_html = (\n    '<iframe id=\"message-iframe-' + part_id + '\" seamless');\n{% if config.prefs.html5_sandbox %}\n  // This is the sandbox. It has issues, the browsers are still developing\n  // this feature at their end!  We could disable it and rely on DOMPurify\n  // entirely...\n  $iframe_html += (                        // IMPORTANT: Do not allow-scripts!\n    ' sandbox=\"allow-same-origin' +        // Let us manipulate contents\n    '          allow-top-navigation' +     // For mailto:\n    '          allow-popups' +             // Allow target=_blank links\n    '          allow-popups-to-escape-sandbox\"'); // Back to the normal web\n{% endif %}\n  $iframe_html += (\n    ' class=\"message-part-html\" target=\"_blank\" srcdoc=\"\"></iframe>');\n\n  var $wrapper = $('<div/>');\n  var $iframe = $($iframe_html);\n  $iframe.load(function() {\n    var $contents = $iframe.contents();\n\n    // Make clicked links open in new window\n    $contents.find('a').each(function(i, elem) {\n        if (elem.href.indexOf(\"mailto:\") == 0) {\n            elem.href = Mailpile.API.U('/message/compose/?to=' +\n                                       elem.href.substring(7));\n        }\n        else {\n            $(elem).attr('target', '_blank');\n        }\n    });\n\n    // Copy some defaults from our CSS...\n    $contents.find('body').css('color', $part.css('color'))\n                          .css('background', $part.css('background'));\n\n    // Adjust size - the trick here is we let the $iframe load, and\n    // once it is ready, we ask how big it is. We then resize the IFrame\n    // to have the same width and height (to get rid of scroll bars) and\n    // finally use a CSS transform to scale the whole thing down so it\n    // fits again. The $wrapper keeps all this from affecting the rest\n    // of the page.\n    var cheight = $contents.outerHeight();\n    var cwidth = $contents.outerWidth();\n    var iwidth = $part.width();\n    if (cwidth > iwidth) {\n      var scale = iwidth / cwidth;\n      if (scale < 0.6) scale = 0.6; // FIXME: Breaks ratios below\n      $iframe.css({\n        'width': cwidth + 'px',\n        'height': (15 / scale + cheight) + 'px',\n        'margin': 0, 'padding': 0,\n        'transform': 'scale(' + scale + ')',\n        'transform-origin': '0 0'\n      });\n      $wrapper.height(15 + cheight * scale);\n    }\n    else {\n      $iframe.height(15 + cheight);\n      $wrapper.height(15 + cheight);\n    }\n  });\n\n  // Sanitize the HTML: the sandbox keeps Javascript from running\n  // and blocks any external content-loads; we're mainly using\n  // DOMPurify to rewrite image references to go through our proxy,\n  // as per the code above.\n  var changes = 0;\n  Mailpile.Message.AddDOMPurifyHooks(allow_images && (policy == 'images'),\n                                     function() { changes += 1 });\n  $iframe.attr('srcdoc', DOMPurify.sanitize(html_data, {\n    WHOLE_DOCUMENT: true,\n  }));\n\n  if (allow_images && (changes > 0) && (policy != 'images')) {\n    $msg_details = $part.closest('.has-mid').find('.message-details');\n    $msg_details.find('.html-image-question').remove();\n    $msg_details.append($(\n      '<div class=\"message-app-note html-image-question\"><p>' +\n      '  <span class=\"icon icon-eye\"></span>' +\n      '  {{_(\"This message references images or other content from the web. Downloading and displaying these images may notify the sender that you have read the mail.\")|escapejs}}' +\n      '</p><ul>' +\n      '  <li><a class=\"display-now\">{{_(\"Okay, display the images\")|escapejs}}</a>' +\n      '  <li><a class=\"display-always\">{{_(\"Always display images from this sender\")|escapejs}}</a>' +\n      '  <li><a class=\"dismiss\">{{_(\"No, thanks!\")|escapejs}}</a>' +\n      '</ull></div>'\n    ));\n    $msg_details.find('.display-now').click(function() {\n      $wrapper.remove();\n      Mailpile.Message.SandboxHTML(part_id, $part, html_data, 'images');\n    });\n    $msg_details.find('.display-always').click(function() {\n      $wrapper.remove();\n      $msg_details.find('.html-image-question').remove();\n      Mailpile.Message.SandboxHTML(part_id, $part, html_data, 'images');\n      Mailpile.Message.SetHTMLPolicy($part.closest('.has-mid').data('mid'),\n                                     policy, 'images');\n    });\n    $msg_details.find('.dismiss').click(function() {\n      $msg_details.find('.html-image-question').remove();\n    });\n  }\n\n  // More size adjusting hackery. See comment above.\n  $wrapper.css({\n    'width': $part.width(),\n    'margin': 0,\n    'padding': 0,\n    'display': 'block',\n    'position': 'relative',\n    'overflow': 'hidden'\n  });\n  $wrapper.append($iframe);\n  $part.append($wrapper);\n  $wrapper.width($part.width());\n};\n\n\nMailpile.Message.ShowHTML = function(mid, policy, allow_images) {\n  // HTML Parts Exist\n  var $msg = $('#message-' + mid);\n  var html_data = $msg.data('html');\n  if (html_data) {\n    $msg = $msg.closest('.has-mid');\n    $msg.find('.pile-message-html-part').show();\n    $msg.find('.html-image-question').show();\n    $msg.find('.html-display-hint').hide();\n    $msg.find('.message-part-text, .pile-message-text-part').hide();\n\n    // FIXME: Legacy code, kill kill kill\n    var $old_message_body = $msg.find('.thread-message-body');\n    if ($old_message_body.length > 0) {\n      Mailpile.Message.ShowHTMLLegacy(mid, $old_message_body, html_data);\n    }\n    else {\n      for (var i in html_data) {\n        var part_id = mid + '-' + (1 + parseInt(i));\n        var $part = $('#html-part-' + part_id);\n        if ($part.find('.noframe').length > 0) {\n          Mailpile.Message.SandboxHTML(\n            part_id, $part, html_data[i].data, policy, allow_images);\n          $part.find('.noframe').remove();\n        }\n      }\n      if (!policy || policy == 'none') {\n        Mailpile.Message.SetHTMLPolicy(mid, policy, 'display');\n      }\n    }\n  } else {\n    // FIXME: Hardcoded untranslated stuff here, ick ick.\n    $msg.find('.thread-message-body').append('<em>Message does not have any HTML parts</em>');\n  }\n};\n\n\nMailpile.Message.ShowPlain = function(mid, policy) {\n  var $msg = $('#message-' + mid).closest('.has-mid');\n  $msg.find('#message-iframe-' + mid).remove();\n  $msg.find('.html-image-question').hide();\n  $msg.find('.pile-message-html-part').hide();\n  $msg.find('.message-part-text, .pile-message-text-part').show();\n  Mailpile.Message.SetHTMLPolicy(mid, policy, 'none');\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/message/init.js",
    "content": "/* Message - Create new instance of composer */\n\nMailpile.Message = {};\nMailpile.Message.Tooltips = {};\n\nMailpile.Message.setup = function($content) {\n  /* Drag & Drop */\n  Mailpile.UI.Message.Draggable($content.find('div.thread-draggable'));\n\n  /* Tooltips */\n  Mailpile.Message.Tooltips.Crypto($content);\n  Mailpile.Message.Tooltips.Attachments($content);\n};\n\nMailpile.Message.init = function() {\n  /* Scroll To */\n  Mailpile.UI.Message.ScrollToMessage();\n};\n\nMailpile.UI.content_setup.push(Mailpile.Message.setup);\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/message/message.js",
    "content": "/* Message */\n\nMailpile.Message.AnalyzeMessageInline = function(mid) {\n  // Iterate through all plain-text parts of the e-mail\n  $('#message-' + mid).find('.message-part-text').each(function(i, text_part) {\n    var content = $(text_part).html();\n\n    // Check & Extract Inline PGP Key\n    var pgp_begin = '-----BEGIN PGP PUBLIC KEY BLOCK-----';\n    var pgp_end = '-----END PGP PUBLIC KEY BLOCK-----';\n    var check_inline_pgp_key = content.split(pgp_begin);\n    if (check_inline_pgp_key.length > 1) {\n      var pgp_key = check_inline_pgp_key.slice(1).join().split(pgp_end)[0];\n      pgp_key = pgp_begin + pgp_key + pgp_end;\n\n      // Make HTML5 download href\n      var pgp_href = 'data:application/pgp-keys;charset=ascii,' + encodeURIComponent(pgp_key.replace(/<\\/?[^>]+(>|$)/g, ''));\n\n      // Replace Text\n      // FIXME: Unsafe template, please audit\n      var key_template = Mailpile.unsafe_template($('#template-messsage-inline-pgp-key-import').html());\n      var name = Mailpile.instance.metadata[mid].from.fn;\n      var import_key_html = key_template({ pgp_key: pgp_key, pgp_href: pgp_href, mid: mid, name: name });\n      var new_content = content.replace(pgp_key, import_key_html);\n      $(text_part).html(new_content);\n    }\n  });\n};\n\n\n/* Message - Replies  */\nMailpile.Message.DoReply = function(mids, reply_all) {\n  var mid = undefined;\n  $.each(mids, function() {\n    if ($('.pile-message-' + this + ' .message-container').length) mid = this;\n  });\n\n  var args = {\n    mid: mids,\n    reply_all: (reply_all == 'all') ? 'True' : 'False',\n  };\n  if (mid) args['_output'] = 'composer.jhtml!minimal'\n\n  Mailpile.API.message_reply_post(args, function(response) {\n    if (mid) {\n      var $msg = $('#message-' + mid);\n      if ($msg.hasClass('pile-message')) {\n        $msg.closest('td').html(response.result);\n      }\n      else {\n        $msg.append(response.result);\n        var new_mid = $msg.find('.form-compose').data('mid');\n        $('#compose-details-' + new_mid).hide();\n        $('#compose-to-summary-' + new_mid).show();\n        $('#compose-show-details-' + new_mid).show();\n      }\n    }\n    else {\n      Mailpile.go(Mailpile.urls.message_draft + response.result.created + '/');\n    }\n  });\n};\n$(document).on('click', '.message-action-reply-all', function(e) {\n  e.preventDefault();\n  Mailpile.Message.DoReply([$(this).closest('.has-mid').data('mid')], 'all');\n});\n$(document).on('click', '.message-action-reply', function(e) {\n  e.preventDefault();\n  Mailpile.Message.DoReply([$(this).closest('.has-mid').data('mid')]);\n});\n\n\n/* Message - Create forward and go to composer */\nMailpile.Message.DoForward = function(mids) {\n  Mailpile.API.message_forward_post({\n    mid: mids,\n    atts: true\n  }, function(response) {\n    Mailpile.go(Mailpile.urls.message_draft + response.result.created + '/');\n  });\n};\n$(document).on('click', '.message-action-forward', function(e) {\n  e.preventDefault();\n  Mailpile.Message.DoForward([$(this).closest('.has-mid').data('mid')]);\n});\n\n\n/* Message - Move message to inbox */\n$(document).on('click', '.message-action-inbox', function(e) {\n  e.preventDefault();\n  var mid = $(this).closest('.has-mid').data('mid');\n  Mailpile.API.tag_post({ add: ['inbox'],  del: ['spam', 'trash'], mid: mid}, function() {\n    Mailpile.go('/in/inbox/');\n  });\n});\n\n\n/* Message - Move message to archive */\n$(document).on('click', '.message-action-archive', function() {\n  var mid = $(this).closest('.has-mid').data('mid');\n  Mailpile.API.tag_post({ add: '', del: ['inbox'], mid: mid}, function(response) {\n    Mailpile.go('/in/inbox/');\n  });\n});\n\n\n/* Message - Mark message as spam */\n$(document).on('click', '.message-action-spam', function() {\n  var mid = $(this).closest('.has-mid').data('mid');\n  Mailpile.API.tag_post({ add: ['spam'], del: ['trash', 'inbox'], mid: mid}, function() {\n    Mailpile.go('/in/inbox/');\n  });\n});\n\n\n/* Message - Move a message to trash */\n$(document).on('click', '.message-action-trash', function() {\n  var mid = $(this).closest('.has-mid').data('mid');\n  Mailpile.API.tag_post({ add: ['trash'], del: ['spam', 'inbox'], mid: mid},\n                        function() {\n    Mailpile.go('/in/inbox/');\n  });\n});\n\n\n/* Message - Add Contact */\n$(document).on('click', '.message-action-add-contact', function(e) {\n\n  // FIXME: Does not work from Dropdown\n  e.preventDefault();\n  var mid = $(this).closest('.has-mid').data('mid');\n  var modal_data = {\n    name: $(this).data('name'),\n    address: $(this).data('address'),\n    signature: 'FIXME: ' + $('#message-' + mid).find('.message-part-signature').html(),\n    mid: mid\n  };\n\n  Mailpile.API.with_template('modal-contact-add', function(modal) {\n    Mailpile.UI.show_modal(modal(modal_data));\n  });\n});\n\n\n/* Message - Unsubscribe */\n$(document).on('click', '.message-action-unsubscribe', function(e) {\n  e.preventDefault();\n  alert('FIXME: this should compose an e-mail to: ' + $(this).data('unsubscribe'));\n  //Mailpile.activities.compose($(this).data('unsubscribe'));\n});\n\n\n/* Message - Discover keys */\n$(document).on('click', '.message-action-find-keys', function(e) {\n  e.preventDefault();\n  Mailpile.UI.Modals.CryptoFindKeys({\n    query: $(this).data('email')\n  });\n});\n\n\n/* Message - Crypto Feedback Actions */\n$(document).on('click', '.message-crypto-action', function() {\n  Mailpile.API.crypto_gpg_keylist_secret_get({}, function(result) {\n    var mid = $(this).data('mid');\n    var modal_data = { name: 'User Name', address: 'name@address.org' };\n    Mailpile.API.with_template('modal-send-public-key', function(modal) {\n      var key_html = '';\n      _.each(result.result, function(key) {\n        var key_template = Mailpile.safe_template($('#template-modal-private-key-item').html());\n        key_html += key_template(key);\n      });\n\n      $('#modal-full').html(modal(modal_data));\n      $('#crypto-private-key-list').html(key_html);\n      Mailpile.UI.show_modal();\n    });\n  });\n});\n\n\n/* Message - Investigate a message with error or missing crypto state */\n$(document).on('click', '.message-crypto-investigate', function() {\n\n  var mid = $(this).data('mid');\n  var part = $(this).data('part');\n  var message = Mailpile.instance.messages[mid];\n  var missing_keys = message.text_parts[part].crypto.encryption.missing_keys;\n\n  // Search Keyservers Missing Keys\n  if (missing_keys.length) {\n    // FIXME: this needs to search all \"missing_key\" values\n    // this is tricky as searching multiple calls to keyservers\n    // can have much latency and slowness\n    Mailpile.API.crypto_gpg_searchkey_get(missing_keys[0], function(data) {\n      // FIXME: Unsafe template, please audit\n      var modal_template = Mailpile.unsafe_template($(\"#modal-search-keyservers\").html());\n      Mailpile.UI.show_modal(modal_template({\n        keys: '<li>Key of User #1</li>'\n      }));\n    });\n  }\n});\n\n\n$(document).on('click', '.message-crypto-show-inline-key', function() {\n  $(this).hide();\n  $('#message-crypto-inline-key-' + $(this).data('mid')).fadeIn();\n});\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/message/tooltips.js",
    "content": "Mailpile.Message.Tooltips.Crypto = function($content) {\n  $content.find('.message-part-crypto-info, ' +\n                '.message-inline-crypto-info, ' +\n                '.message-metadata-crypto-info').qtip({\n    content: {\n      title: false,\n      text: function(event, api) {\n        var keyinfo = $(this).data('crypto_keyinfo');\n        var html = ('<div><h4 class=\"'\n          + $(this).data('crypto_color') + '\">'\n          + '  <span class=\"'\n          +      _.escape($(this).data('crypto_icon')) + '\"></span>'\n          + _.escape($(this).attr('title'))\n          + '</h4>');\n        html += '<p>' + $(this).data('crypto_message') + '</p>';\n        if (keyinfo) {\n          keyinfo = keyinfo.substring(keyinfo.length - 16);\n          html += ('<p>'\n            + '<a href=\"javascript:Mailpile.UI.Modals.CryptoFindKeys({query: \\''\n            + _.escape(keyinfo) + '\\'})\"><small>KEY ID: '\n            + _.escape(keyinfo) + '</small></a></p>');\n        }\n        html += '</div>';\n        return html;\n      }\n    },\n    style: {\n      classes: 'qtip-thread-crypto',\n      tip: {\n        corner: 'bottom center',\n        mimic: 'bottom center',\n        border: 1,\n        width: 12,\n        height: 12,\n        corner: true\n      }\n    },\n    position: {\n      my: 'bottom center',\n      at: 'top center',\n\t\t\tviewport: $(window),\n\t\t\tadjust: {\n\t\t\t\ty: -5\n\t\t\t}\n    },\n    show: { delay: 100 },\n    hide: { fixed: true, delay: 250 }\n  });\n};\n\n\nMailpile.Message.Tooltips.Attachments = function($content) {\n  $content.find('a.attachment, a.attachment-image').qtip({\n    content: {\n      title: false,\n      text: function(event, api) {\n        var $e = $(this);\n        console.log($e);\n        var html = _.escape($e.attr('title'));\n        if ($e.data('description')) html += ('<small>'\n          + _.escape($e.data('description'))\n          + '</small>');\n        html += ('<small>{{_(\"Download\")|escapejs}} '\n          + _.escape($e.data('size'))\n          + '</small>');\n        return html;\n      }\n    },\n    style: {\n      classes: 'qtip-tipped',\n      tip: {\n        corner: 'bottom center',\n        mimic: 'bottom center',\n        border: 1,\n        width: 12,\n        height: 12,\n        corner: true\n      }\n    },\n    position: {\n      my: 'bottom center',\n      at: 'top center',\n\t\t\tviewport: $(window),\n\t\t\tadjust: {\n\t\t\t\tx: 0, y: -5\n\t\t\t}\n    },\n    show: { delay: 100 },\n    hide: { fixed: true, delay: 250 }\n  });\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/message/ui.js",
    "content": "/* UI - Message */\n\nMailpile.UI.Message.ShowMessage = function(mid) {\n  var done_loading = Mailpile.notify_working(\"{{_('Loading message...')|escapejs}}\",\n                                             500);\n  var failed = function() {\n    done_loading();\n    Mailpile.notification({\n      status: 'error',\n      message: '{{_(\"Could not retrieve message\")|escapejs}}'\n    });\n  }\n  Mailpile.API.message_get({\n    mid: mid,\n    _output: 'single.jhtml!minimal',\n    _error_callback: failed\n  }, function(response) {\n    if (response.result) {\n      done_loading();\n      $('#message-' + mid).replaceWith(response.result);\n      Mailpile.UI.prepare_new_content($('#message-' + mid));\n    }\n    else failed();\n  });\n};\n\n\nMailpile.UI.Message.ScrollToMessage = function() {\n\n  var msg_top_pos = 0;\n  var check_new = $('#content-view, #content-tall-view').find('div.new');\n\n  if (check_new.length) {\n    var unread_thread_id = $(check_new[0]).data('mid');\n    msg_top_pos = $('#message-' + unread_thread_id).position().top + 1;\n  } \n  else {\n    var full_message = $('#content-view, #content-tall-view').find('div.thread-message');\n    if (full_message.length) {\n      var thread_id = $(full_message[0]).data('mid');\n      msg_top_pos = $('#message-' + thread_id).position().top + 1;\n    }\n  }\n\n  // Scroll To\n  setTimeout(function(){\n    $('#content-view, #content-tall-view').animate({ scrollTop: msg_top_pos }, 450);\n  }, 50);\n};\n\n\nMailpile.UI.Message.Draggable = function(element) {\n  $(element).draggable({\n    containment: 'window',\n    appendTo: 'body',\n    cursor: 'move',\n    scroll: false,\n    revert: false,\n    opacity: 1,\n    helper: function(event) {\n      return $('<div class=\"pile-results-drag ui-widget-header\"><span class=\"icon-inbox\"></span> Moving Thread</div>');\n    },\n    start: function(event, ui) {\n      Mailpile.ui_in_action += 1;\n    },\n    stop: function(event, ui) {\n      setTimeout(function() { Mailpile.ui_in_action -= 1; }, 250);\n    }\n  });\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/pwa/manifest.json",
    "content": "{\n    \"name\": \"Mailpile\",\n    \"short_name\": \"Mailpile\",\n    \"icons\": [{\n        \"src\": \"{{ U('/static/img/favicon.svg') }}\",\n        \"sizes\": \"32x32\",\n        \"type\": \"image/png\"\n    },{\n        \"src\": \"{{ U('/static/img/logo-color.png') }}\",\n        \"sizes\": \"100x100\",\n        \"type\": \"image/png\"\n    },{\n        \"src\": \"{{ U('/static/img/logo-color.svg') }}\",\n        \"sizes\": \"101x101 192x192 256x256\",\n        \"type\": \"image/svg+xml\"\n    }],\n    \"start_url\": \"{{ U('/?ui_from_homescreen=1') }}\",\n    \"background_color\": \"#ffffff\",\n    \"theme_color\": \"#e9e9e9\",\n    \"display\": \"standalone\",\n    \"serviceworker\": {\n        \"src\": \"{{ U('/jsapi/pwa/service-worker.js?ts=', version_identifier()) }}\",\n        \"scope\": \"{{ U('/') }}\",\n        \"use_cache\": false\n    }\n}\n\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/pwa/service-worker.js",
    "content": "// Placeholder logic, copied from here: \n// https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers\n\n{# Magic #}\n{% do http_response_headers.append(('Service-Worker-Allowed', U(\"/\"))) %}\n\nself.addEventListener('install', function(event) {\n  event.waitUntil(\n    caches.open('v1').then(function(cache) {\n      return cache.addAll([\n        '{{ U(\"/static/img/logo.svg\") }}',\n      ]);\n    })\n  );\n});\n\n//self.addEventListener('fetch', function(event) {\n//  event.respondWith(caches.match(event.request).then(function(response) {\n//    // caches.match() always resolves\n//    // but in case of success response will have value\n//    if (response !== undefined) {\n//      return response;\n//    } else {\n//      return fetch(event.request).then(function (response) {\n//        // response may be used only once\n//        // we need to save clone to put one copy in cache\n//        // and serve second one\n//        let responseClone = response.clone();\n//        caches.open('v1').then(function (cache) {\n//          cache.put(event.request, responseClone);\n//        });\n//        return response;\n//// Disabled fallback, for now.\n////    }).catch(function () {\n////      return caches.match('/sw-test/gallery/myLittleVader.jpg');\n//      });\n//    }\n//  }));\n//});\n\nconsole.log('Service worker loaded, hooray!');\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/search/bulk_actions.js",
    "content": "Mailpile.bulk_actions_update_ui = function() {\n  $('.selection-context').each(function (i, context) {\n    var $context = $(context);\n    var selected = Mailpile.UI.Selection.selected($context);\n    var checkboxes = $context.find('.pile-results input[type=checkbox]');\n\n    // This is a hack to make sure the length check below fails\n    if (checkboxes.length == 0) checkboxes = ['fake', 'fake'];\n\n    // Reset state...\n    Mailpile.hide_message_hints($context);\n    if ((selected.length < 1) || (selected[0] !== \"!all\")) {\n      $context.find('#pile-select-all-action').val('');\n    }\n    if ((selected.length == checkboxes.length) ||\n        (selected[0] == '!all')) {\n      $context.find('#pile-select-all-action').prop('checked', true);\n    }\n    else {\n      $context.find('#pile-select-all-action').prop('checked', false);\n    }\n\n    Mailpile.update_selection_classes($context.find('td.checkbox'));\n\n    if (selected.length > 0) {\n      var message = ('<span id=\"bulk-actions-selected-count\">' +\n                       Mailpile.UI.Selection.human_length(selected) +\n                     '</span> ');\n      if (selected[0] == \"!all\") {\n        message = ('<a onclick=\"javascript:Mailpile.unselect_all_matches(this);\">' +\n                     '<span class=\"icon-star\"></span> ' +\n                     $context.find('#bulk-actions-message').data('unselect_all') +\n                   '</a>');\n      }\n      else if ($context.find(\"#pile-select-all-action\").is(':checked')) {\n        message = ('<a onclick=\"javascript:Mailpile.select_all_matches(this);\">' +\n                     message + ' ' +\n                     $context.find('#bulk-actions-message').data('select_all') +\n                   '</a>');\n      }\n      else {\n        message += $context.find('#bulk-actions-message').data('bulk_selected');\n        Mailpile.show_related_search_link($context, selected);\n      }\n      $context.find('#bulk-actions-message').addClass('mobile-hide').html(message);\n      $context.find('.sub-navigation').addClass('mobile-hide');\n      $context.find('.bulk-actions').removeClass('mobile-hide');\n\n      var have_tags = {};\n      for (var i = 0; i < selected.length; i++) {\n        var search = '.pile-message-' + selected[i];\n        if (selected[i] == \"!all\") search = '.pile-message';\n        var tids = $context.find(search).data('tids').split(/,/);\n        for (var j = 0; j < tids.length; j++) have_tags[tids[j]] = true;\n      }\n\n      Mailpile.show_bulk_actions($context.find('.bulk-actions').find('li'),\n                                 have_tags);\n    }\n    else {\n      var message = $context.find('#bulk-actions-message').data('bulk_selected_none');\n      $context.find('#bulk-actions-message').removeClass('mobile-hide').html(message);\n      Mailpile.hide_bulk_actions($context.find('.bulk-actions').find('li.hide'));\n      $context.find('.sub-navigation').removeClass('mobile-hide');\n      $context.find('.bulk-actions').addClass('mobile-hide');\n    }\n  });\n};\n\n\nMailpile.hide_message_hints = function($context) {\n  $context.find('div.bulk-actions-hints').html('').on('click', undefined);\n};\n\n\nMailpile.show_related_search_link = function($context, selected) {\n  if (selected && (selected.length < 4) && (selected[0] != '!all')) {\n    $context.find('div.bulk-actions-hints').html(\n      '<a><span class=\"icon-search\"></span>' +\n      ' {{_(\"Search for Similar E-mail\")}}</a>').off('click').on('click',\n    function() {\n      var stoplist = {{ ('%s' % stoplist|list)|safe }};\n      var data = {\n        qq: function(p, t, cutoff) {\n          // Warning: This is the same as mailpile.utils.WORD_REGEXP\n          //          and they need to be kept in sync.\n          var qt = t.replace(\n            /[\\s\\!@#$%^&*\\(\\)_+=\\{\\}\\[\\]:\\\"|;`\\'\\\\\\<\\>\\?,\\.\\/\\-]/g, ' '\n            ).split(/ +/\n            ).filter(function(w) {\n              return ((stoplist.indexOf(w.toLowerCase()) < 0) &&\n                      (w.length > 1))}\n            ).sort(function(a, b) {\n              return (a.length < b.length)}\n            ).slice(0, (cutoff || 3));\n          if (p && p.length) return p + ':' + qt.join(' ' + p + ':');\n          return qt.join(' ');\n        },\n        q: function(p, t, cutoff) {\n          t = t.split(/ +/\n            ).sort(function(a, b) {\n              return (a.length < b.length)}\n            ).slice(0, (cutoff || 3));\n          return p + ':' + t.join(' ' + p + ':');\n        },\n        trunc: function(s, len) {\n          if (s.length > (len-3)) return s.substring(0, (len-3)) + ' ...';\n          return s;\n        },\n        subjects: [],\n        lists: [],\n        emails: [],\n        froms: [],\n        muas: [],\n        hpts: [],\n        hpss: [],\n        extras: ''};\n\n      var date_start = '';\n      var date_end = '';\n\n      $.each(selected, function(i, mid) {\n        var $msg = $('tr.pile-message-' + mid);\n        var from = $msg.find('td.from').data('address');\n        var subj = $msg.find('.message-subject').html();\n        if (!subj) subj = $msg.find('.subject a').html();\n        data.subjects.push(subj);\n        if (data.emails.indexOf(from) < 0) data.emails.push(from);\n        $.each($msg.data('to-cc').split(/ /), function(i, email) {\n          if (email && (data.emails.indexOf(email) < 0)) {\n            data.emails.push(email);\n          }\n        });\n        if (data.froms.indexOf(from) < 0) data.froms.push(from);\n        data.lists.push($msg.data('list'));\n        data.muas.push($msg.data('mua'));\n        data.hpts.push($msg.data('mua-fingerprint'));\n        data.hpss.push($msg.data('sender-fingerprint'));\n        var ts = $msg.find('td.date').data('ts');\n        if (!date_start || (ts < date_start)) date_start = ts;\n        if (!date_end || (ts > date_end)) date_end = ts;\n\n        // FIXME: Add more special cases\n        if (from == 'notifications@github.com') {\n          data.extras = $msg.find('td.from').data('fn');\n        }\n      });\n\n      var d4s = new Date((date_start - (24 * 3600 * 14)) * 1000);\n      var d4e = new Date((date_end + (24 * 3600 * 14)) * 1000);\n      data.date_range_4wks = (\n        d4s.getFullYear() + '-' +\n        (d4s.getMonth()+1) + '-' +\n        d4s.getDate() + '..' +\n        d4e.getFullYear() + '-' +\n        (d4e.getMonth() + 1) + '-' +\n        d4e.getDate());\n\n      var d2s = new Date((date_start - (24 * 3600 * 7)) * 1000);\n      var d2e = new Date((date_end + (24 * 3600 * 7)) * 1000);\n      data.date_range_2wks = (\n        d2s.getFullYear() + '-' +\n        (d2s.getMonth()+1) + '-' +\n        d2s.getDate() + '..' +\n        d2e.getFullYear() + '-' +\n        (d2e.getMonth() + 1) + '-' +\n        d2e.getDate());\n\n      data.froms.sort();\n      data.lists.sort();\n      data.emails.sort();\n      data.subjects.sort();\n\n      Mailpile.API.with_template('modal-related-search', function(modal) {\n        Mailpile.UI.show_modal(modal(data));\n      });\n    });\n  }\n};\n\n\nMailpile.DELETEMEshow_message_hints = function($context, selected) {\n  $.each(selected, function(key, mid) {\n    if (mid != '!all') {\n      var $elem = $context.find('.pile-message-' + mid);\n      var hint = $elem.data('context-hint');\n      if (hint) {\n        var icon = $elem.data('context-icon');\n        var url = $elem.data('context-url');\n        var html = '';\n        if (icon) html += '<span class=\"icon icon-' + icon + '\"></span> ';\n        html += hint;\n        if (url) html = '<a href=\"' + url + '\">' + html + '</a>';\n        $('div.bulk-actions-hints').html(html);\n      }\n    }\n  });\n};\n\n\nMailpile.select_all_matches = function(elem) {\n  if (!elem) elem = '.pile-results a';\n  $(elem).closest('.selection-context')\n         .find('#pile-select-all-action').val('!all');\n  Mailpile.bulk_actions_update_ui();\n  return false;\n};\n\n\nMailpile.unselect_all_matches = function(elem) {\n  if (!elem) elem = '.pile-results a';\n  $(elem).closest('.selection-context')\n         .find('#pile-select-all-action').val('');\n  Mailpile.bulk_actions_update_ui();\n  return false;\n};\n\n\nMailpile.bulk_action_read = function(elem, callback) {\n  var $context = Mailpile.UI.Selection.context(elem || '.pile-results');\n  Mailpile.UI.Tagging.tag_and_update_ui({\n    del: 'new',\n    mid: Mailpile.UI.Selection.selected($context),\n    context: $context.find('.search-context').data('context')\n  }, 'read', callback);\n};\n\n\nMailpile.bulk_action_unread = function(elem, callback) {\n  var $context = Mailpile.UI.Selection.context(elem || '.pile-results');\n  Mailpile.UI.Tagging.tag_and_update_ui({\n    add: 'new',\n    mid: Mailpile.UI.Selection.selected($context),\n    context: $context.find('.search-context').data('context')\n  }, 'unread', callback);\n};\n\n\nMailpile.bulk_action_select_target = function() {\n  var target = this.search_target;\n  var $tr = $('.pile-message').eq(target);\n  $tr.addClass('result-on').find('input[type=checkbox]').prop('checked', true);\n  this.bulk_actions_update_ui();\n  return true;\n};\n\n\nMailpile.bulk_action_deselect_target = function() {\n  var target = this.search_target;\n  var $tr = $('.pile-message').eq(target);\n  $tr.removeClass('result-on')\n     .find('input[type=checkbox]').prop('checked', false);\n  this.bulk_actions_update_ui();\n  return true;\n};\n\nMailpile.bulk_action_select_all = function() {\n  var checkboxes = $('.pile-results input[type=checkbox]');\n  $.each(checkboxes, function() {\n    Mailpile.pile_action_select($(this).parent().parent());\n  });\n  $(\"#pile-select-all-action\").prop('checked', true);\n  Mailpile.bulk_actions_update_ui();\n};\n\n\nMailpile.bulk_action_select_none = function() {\n  var checkboxes = $('.pile-results input[type=checkbox]');\n  $.each(checkboxes, function() {\n    Mailpile.pile_action_unselect($(this).parent().parent());\n  });\n  $(\"#pile-select-all-action\").prop('checked', false).val('');\n  Mailpile.bulk_actions_update_ui();\n};\n\n\nMailpile.bulk_action_select_invert = function() {\n  var checkboxes = $('.pile-results input[type=checkbox]');\n  $.each(checkboxes, function() {\n    if ($(this).is(\":checked\")) {\n      Mailpile.pile_action_unselect($(this).parent().parent(), 'partial');\n    } else {\n      Mailpile.pile_action_select($(this).parent().parent(), 'partial');\n    }\n  });\n  Mailpile.bulk_actions_update_ui();\n};\n\nMailpile.bulk_action_move_selection = function(keep, mover) {\n  var checkboxes = $('.pile-results input[type=checkbox]');\n  var selected = Mailpile.UI.Selection.selected(checkboxes.eq(0));\n  if (selected.length == 0) {\n    var $elem = $(checkboxes[0]).parent().parent();\n    Mailpile.pile_action_select($elem);\n    return $elem;\n  }\n\n  var $last = [];\n  $.each(checkboxes, function() {\n    var $e = $(this);\n    if ($e.is(\":checked\")) $last = $e.parent().parent();\n  });\n  if ($last.length > 0) {\n    var $next = $(mover($last));\n    if (keep !== 'keep') Mailpile.pile_action_unselect($last);\n    if ($next) Mailpile.pile_action_select($next);\n    Mailpile.bulk_actions_update_ui();\n    return $next;\n  }\n  return $last;\n};\n\nMailpile.bulk_action_selection_up = function(keep) {\n  return Mailpile.bulk_action_move_selection(keep, function($elem) {\n    return $elem.prev();\n  });\n};\n\nMailpile.bulk_action_selection_down = function(keep) {\n  return Mailpile.bulk_action_move_selection(keep, function($elem) {\n    return $elem.next();\n  });\n};\n\nMailpile.open_or_close_selected_thread = function() {\n  var selected = Mailpile.UI.Selection.selected('.pile-results');\n  var msg = [];\n  if (selected.length === 1 && selected[0] == '!all') {\n    msg = $(\".pile-results .pile-message\");\n  }\n  else {\n    if (selected.length < 1) {\n      Mailpile.pile_action_select($('.pile-results .pile-message').eq(0));\n      selected = Mailpile.UI.Selection.selected('.pile-results');\n    }\n    if ((selected.length > 0) && (selected[0] != '!all')) {\n      msg = $(\".pile-results .pile-message-\" + selected[selected.length - 1]);\n    }\n  }\n  if (msg.length) {\n    var $close = msg.eq(0).find('#close-message');\n    if ($close.length == 0) {\n      msg.eq(0).find(\".subject a\").trigger('click');\n    }\n    else {\n      $('#close-message').trigger('click');\n    }\n  }\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/search/display_modes.js",
    "content": "/* Search - Display Mode */\nMailpile.pile_display = function(current, change) {\n\n  if (change) {\n    $('#sidebar').removeClass(current).addClass(change);\n    $('.pile-results').removeClass(current).addClass(change);\n  } else {\n    $('#sidebar').addClass(current);\n    $('.pile-results').addClass(current);\n  }\n\n  setTimeout(function() {\n    $('#sidebar').fadeIn('fast');\n    $('.pile-results').fadeIn('fast');\n  }, 250);\n};\n\n\n/* Search - Change Display Size */\n$(document).on('click', 'a.change-view-size', function(e) {\n\n  e.preventDefault();\n  var current_size = Mailpile.local_storage['view_size'];\n  var change_size = $(this).data('view_size');\n\n  // Update Link Selected\n  $('a.change-view-size').removeClass('selected');\n  $(this).addClass('selected');\n\n  // Update View Sizes\n  Mailpile.pile_display(current_size, change_size);\n\n  // Data\n  Mailpile.local_storage['view_size'] = change_size;\n\n  // Update Config & Model\n  Mailpile.API.settings_set_post({ 'web.display_density': change_size }, function(result) {});\n  Mailpile.config.web.display_density = change_size;\n\n});\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/search/events.js",
    "content": "/* Search - select item via clicking */\n$(document).on('click', '.pile-results tr.result', function(e) {\n  if ($(e.target).attr('type') === 'checkbox') {\n    $(e.target).blur();\n    Mailpile.pile_action_select($(this));\n  }\n  else if (e.target.href === undefined &&\n    $(this).data('state') !== 'selected' &&\n    $(e.target).hasClass('pile-message-tag-name') == false) {\n    Mailpile.pile_action_select($(this));    \n  }\n});\n\n\n/* Search - unselect search item via clicking */\n$(document).on('click', '.pile-results tr.result-on', function(e) {\n  if ($(e.target).attr('type') === 'checkbox') {\n    $(e.target).prop('checked', false).blur();\n    Mailpile.pile_action_unselect($(this));\n  }\n  else if (e.target.href === undefined &&\n    $(this).data('state') === 'selected' && \n    $(e.target).hasClass('pile-message-tag-name') == false) {\n    Mailpile.pile_action_unselect($(this));\n  }\n});\n\n\n/* Search - Delete Tag via Tooltip */\n$(document).on('click', '.pile-tag-delete', function(e) {\n  e.preventDefault();\n  var tid = $(this).data('tid');\n  var mid = $(this).data('mid');\n  Mailpile.UI.Tagging.tag_and_update_ui({\n    del: tid,\n    mid: mid\n  }, 'untag', function() {\n    $('.pile-message-' + mid + ' .pile-message-tag-' + tid).qtip('hide');\n  });\n});\n\n\n/* Search - Searches web for people (currently keyservers only) */\n$(document).on('click', '#btn-pile-empty-search-web', function(e) {\n  e.preventDefault();\n  Mailpile.UI.Modals.CryptoFindKeys({\n    query: $('#pile-empty-search-terms').html()\n  });\n});\n\n\n/* Save Search & Edit Tag */\n(function() {\n  $(document).on('click', '.bulk-action-save_search', function(e) {\n    Mailpile.API.with_template('modal-save-search', function(modal) {\n      var searchq = ($('#search-query').val() + ' ');\n      var mf = Mailpile.UI.show_modal(modal({ terms: searchq }));\n      mf.find('#ss-search-terms').attr('value', searchq);\n    });\n  });\n  $(document).on('click', '#modal-save-search .ss-save', function() {\n    if ($('#modal-save-search #ss-comment').val() != '') {\n      Mailpile.API.filter_post({\n        _serialized: $('#modal-save-search').serialize()\n      }, function(data) {\n        Mailpile.go('/in/saved-search-' + data['result']['id'] + '/');\n      });\n    }\n    else {\n      alert('Please name your search!');\n    }\n    return false;\n  });\n  function show_settings() {\n    $('#modal-full .modal-basic-settings').show();\n    $('#modal-full .modal-choose-tag-icon').hide();\n    $('#modal-full .modal-choose-tag-color').hide();\n    $('#modal-full .tag-edit-technical').hide();\n    $('#modal-full .tag-edit-automation').hide();\n    $('#modal-full .modal-save-group').show();\n  };\n  $(document).on('click', '#modal-full .modal-basic-settings-title', show_settings);\n  $(document).on('click', '#modal-full .modal-open-choose-tag-color', function() {\n    $('#modal-full ul.tag-colors').html(Mailpile.UI.tag_colors_as_lis());\n    $('#modal-full .modal-save-group').hide();\n    $('#modal-full .modal-basic-settings').hide();\n    $('#modal-full .modal-choose-tag-color').show();\n  }); \n  $(document).on('click', '#modal-full .modal-open-choose-tag-icon', function() {\n    $('#modal-full ul.tag-icons').html(Mailpile.UI.tag_icons_as_lis());\n    $('#modal-full .modal-save-group').hide();\n    $('#modal-full .modal-basic-settings').hide();\n    $('#modal-full .modal-choose-tag-icon').show();\n  }); \n  $(document).on('click', '#modal-full .modal-tag-icon-option', function() {\n    var icon = $(this).data('icon');\n    $('#modal-full input.choose-tag-icon').val(icon);\n    $('#modal-full .modal-open-choose-tag-icon'\n      ).removeClass().addClass('modal-open-choose-tag-icon').addClass(icon);\n    show_settings();\n  });\n  $(document).on('click', '#modal-full .modal-tag-color-option', function() {\n    var name = $(this).data('name');\n    var hex = $(this).data('hex');\n    $('#modal-full input.choose-tag-color').val(name);\n    $('#modal-full .modal-open-choose-tag-icon').css('color', hex);\n    $('#modal-full .modal-open-choose-tag-color').css('color', hex);\n    show_settings();\n  });\n  $(document).on('click', '#modal-full .modal-tag-edit-automation', function() {\n    $('#modal-full .modal-basic-settings').hide();\n    $('#modal-full .modal-choose-tag-icon').hide();\n    $('#modal-full .modal-choose-tag-color').hide();\n    $('#modal-full .tag-edit-technical').hide();\n    $('#modal-full .tag-edit-automation').show();\n    $('#modal-full .modal-save-group').show();\n  });\n  $(document).on('click', '#modal-full .modal-tag-technical', function() {\n    $('#modal-full .modal-basic-settings').hide();\n    $('#modal-full .modal-choose-tag-icon').hide();\n    $('#modal-full .modal-choose-tag-color').hide();\n    $('#modal-full .tag-edit-technical').show();\n    $('#modal-full .tag-edit-automation').hide();\n    $('#modal-full .modal-save-group').show();\n  });\n})();\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/search/init.js",
    "content": "/* Search */\n\nMailpile.Search = {};\nMailpile.Search.Tooltips = {};\n\nMailpile.Search.init = function() {\n\n  // Drag Items\n  var index_capabilities = $('.pile-results').data('index-capabilities');\n  if (index_capabilities.indexOf('has_tags') >= 0) {\n    Mailpile.UI.Search.Draggable('td.draggable, td.avatar');\n    Mailpile.UI.Search.Droppable('.pile-results tr', 'a.sidebar-tag');\n  };\n\n  // Render Display Size\n  if (!Mailpile.local_storage['view_size']) {\n    Mailpile.local_storage['view_size'] = Mailpile.config.web.display_density;\n  }\n\n  Mailpile.pile_display(Mailpile.local_storage['view_size']);\n\n  // Display Select\n  $.each($('a.change-view-size'), function() {\n    if ($(this).data('view_size') == Mailpile.local_storage['view_size']) {\n      $(this).addClass('selected');\n    }\n  });\n\n  // Navigation highlights\n  $.each($('.display-refiner'), function() {\n    if (document.location.href.endsWith($(this).find('a').attr('href'))) {\n      $(this).addClass('navigation-on');\n    }\n    else {\n      $(this).removeClass('navigation-on');\n    };\n  });\n\n  // Tooltips\n  Mailpile.Search.Tooltips.MessageTags();\n\n  EventLog.subscribe(\".mail_source\", function(ev) {\n    // Cutesy animation, just for fun\n    if ((ev.data && ev.data.copying && ev.data.copying.running) ||\n        (ev.data && ev.data.rescan && ev.data.rescan.running)) {\n      $(\"#logo-bluemail\").fadeOut(2000);\n      $(\"#logo-redmail\").hide(2000);\n      $(\"#logo-greenmail\").hide(3000);\n      $(\"#logo-bluemail\").fadeIn(2000);\n      $(\"#logo-greenmail\").fadeIn(4000);\n      $(\"#logo-redmail\").fadeIn(6000);\n    }\n  }, 'mail-source-subscription');\n\n  // Focus and scroll...\n  $('.pile-results .pile-message .subject a').eq(0).focus();\n  var hashIndex = document.location.href.indexOf('#');\n  if (hashIndex != -1) {\n    var target = document.location.href.substring(hashIndex+1);\n    if (target.indexOf('/') != -1) {\n      target = target.substring(0, target.indexOf('/'));\n    }\n    if (target) {\n      var $elem = $('#' + target + ', .' + target);\n      if ($elem.length > 0) {\n        var top_pos = $elem.eq(0).position().top;\n        $elem.eq(0).focus();\n        setTimeout(function() {\n          $('#content-view, #content-tall-view').animate({ scrollTop: top_pos }, 150);\n        }, 50);\n      }\n    }\n  }\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/search/selection_actions.js",
    "content": "/* Search - Bulk Select / Unselect All */\n$(document).on('click', '#pile-select-all-action', function(e) {\n  var $checkbox = $(this);\n  var $results = $checkbox.closest('.selection-context')\n                          .find('.pile-results .pile-message');\n  if ($checkbox.is(':checked')) {\n    // Going from unchecked -> checked\n    $results.each(function(i, result) {\n      Mailpile.pile_action_select($(result), \"partial\");\n    });\n  }\n  else {\n    // Going from checked -> unchecked\n    $checkbox.val('');\n    $results.each(function(i, result) {\n      Mailpile.pile_action_unselect($(result), \"partial\");\n    });\n  }\n  Mailpile.bulk_actions_update_ui();\n  return true;\n});\n\n\n/* Search - Bulk Action - Tag */\n$(document).on('click', '.bulk-action-tag', function() {\n  Mailpile.render_modal_tags(this);\n});\n\n/* Search - Bulk Action - Toggle attribute (Tag) */\n$(document).on('click', '.bulk-action-tag-op', function() {\n  var $elem = $(this);\n  var tag = (($elem.data('tag') || \"\") + \"\").split(/\\s+/);\n  var op = $elem.data('op');\n  var desc = $elem.data('ui');\n\n  var $context = Mailpile.UI.Selection.context($elem);\n  var args = {\n    mid: Mailpile.UI.Selection.selected($context),\n    context: $context.find('.search-context').data('context')\n  };\n\n  if (op == \"toggle\") {\n    if ($elem.data('mode') != 'untag') {\n      args.add = tag;\n      desc = desc || 'tag';\n    } else {\n      args.del = tag;\n      desc = desc || 'untag';\n    }\n  }\n  else if (op == \"move\") {\n    args.add = tag;\n    args.del = (($context.find('.pile-results').data(\"tids\") || \"\"\n                 ) + \"\").split(/\\s+/);\n  }\n  else if (op == \"tag\") {\n    args.add = tag;\n    if ($elem.data('untag')) args.del = $elem.data('untag').split(/\\s+/);\n  }\n  else if (op == \"untag\") {\n    args.del = (($context.find('.pile-results').data(\"tids\") || \"\"\n                 ) + \"\").split(/\\s+/);\n    desc = desc || 'untag';\n  }\n  else if (op == \"archive\") {\n    args.del = ['type:inbox', 'type:tag', 'type:attribute', 'type:sent'];\n    desc = desc || 'archive';\n  }\n\n  if (!desc) desc = (args.del) ? 'move' : 'tag';\n  Mailpile.UI.Tagging.tag_and_update_ui(args, desc);\n});\n\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/search/tooltips.js",
    "content": "/* Search - Tooltips */\n\nMailpile.Search.Tooltips.MessageTags = function() {\n  $('.pile-message-tag').qtip({\n    content: {\n      title: false,\n      text: function(event, api) {\n        var mid = $(this).data('mid').toString();\n        var tid = $(this).data('tid').toString();\n        Mailpile.API.tags_get({ tid: tid }, function(response) {\n          var tooltip_template = Mailpile.safe_template($('#tooltip-pile-tag-details').html());\n          var tooltip_data = response.result.tags[0];\n          tooltip_data['mid'] = mid;\n          api.set('content.text', tooltip_template(tooltip_data));\n        });\n        return \"...\";\n      }\n    },\n    style: {\n      classes: 'qtip-thread-crypto',\n      tip: {\n        corner: 'bottom center',\n        mimic: 'bottom center',\n        border: 0,\n        width: 10,\n        height: 10\n      }\n    },\n    position: {\n      my: 'bottom center',\n      at: 'top left',\n      viewport: $(window),\n      adjust: {x: 7, y: 2},\n      effect: false\n    },\n    show: { delay: 100 },\n    hide: { fixed: true, delay: 350 }\n  });\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/search/ui.js",
    "content": "Mailpile.focus_search = function() {\n    $(\"#search-query\").focus(); return false;\n};\n\n\nMailpile.update_selection_classes = function($items) {\n  $.each($items.find('input[type=checkbox]'), function() {\n    var $t = $(this);\n    var $e = $t.closest('.result, .result-on');\n    if ($t.prop('checked')) {\n      $e.removeClass('result').addClass('result-on').data('state', 'selected');\n    }\n    else {\n      $e.removeClass('result-on').addClass('result').data('state', 'normal');\n    }\n  });\n};\n\n\n/* Search - Action Select */\nMailpile.pile_action_select = function($item, partial) {\n    // Style & Select Checkbox\n    $item.find('td.checkbox input[type=checkbox]').prop('checked', true);\n\n    // Update UI\n    if (partial) {\n      Mailpile.update_selection_classes($item);\n    }\n    else {\n      Mailpile.bulk_actions_update_ui();\n    }\n};\n\n\n/* Search - Action Unselect */\nMailpile.pile_action_unselect = function($item, partial) {\n    // Style & Unselect Checkbox\n    $item.find('td.checkbox input[type=checkbox]').prop('checked', false);\n\n    // Update UI\n    if (partial) {\n      Mailpile.update_selection_classes($item);\n    }\n    else {\n      // If something has been unselected, then not selecting all anymore!\n      $item.closest('.selection-context')\n           .find('#pile-select-all-action').val('');\n      Mailpile.bulk_actions_update_ui();\n    }\n};\n\n\n/* Search - Result List */\nMailpile.results_list = function() {\n    // Navigation\n    $('#btn-display-list').addClass('navigation-on');\n    $('#btn-display-graph').removeClass('navigation-on');\n\n    // Show & Hide View\n    $('#pile-graph').hide('fast', function() {\n        $('#form-pile-results').show('normal');\n        $('.pile-results').show('fast');\n        $('.pile-speed').show('normal');\n        $('#footer').show('normal');\n    });\n};\n\n\nMailpile.display_keybindings = function(elem) {\n  Mailpile.API.with_template(\"modal-display-keybindings\", function(modal) {\n    Mailpile.UI.show_modal(modal({\n      keybindings: Mailpile.keybindings\n    }));\n  });\n};\n\nMailpile.render_modal_tags = function(elem) {\n  var selected = Mailpile.UI.Selection.selected(elem || '.pile-results');\n  if (selected.length) {\n\n    // Open Modal with selection options\n    Mailpile.API.tags_get({}, function(data) {\n      Mailpile.API.with_template('template-modal-tag-picker-item',\n                                 function(tag_template) {\n        var priority_html = '';\n        var tags_html     = '';\n        var archive_html  = '';\n\n        /// Show tags in selected messages\n        var selected_tids = {};\n        _.each(selected, function(mid, key) {\n          if (mid != '!all') {\n            var metadata = _.findWhere(Mailpile.instance.metadata, { mid: mid });\n            if (metadata && metadata.tag_tids) {\n              _.each(metadata.tag_tids, function(tid, key) {\n                if (selected_tids[tid] === undefined) {\n                  selected_tids[tid] = 1;\n                } else {\n                  selected_tids[tid]++;\n                }\n              });\n            }\n          }\n        });\n\n        // Build Tags List\n        _.each(data.result.tags, function(tag, key) {\n          if (tag.display === 'priority' && tag.type === 'tag') {\n            priority_data  = _.extend(tag, { selected: selected_tids });\n            priority_html += tag_template(priority_data);\n          }\n          else if (tag.display === 'tag' && tag.type === 'tag') {\n            tag_data   = _.extend(tag, { selected: selected_tids });\n            tags_html += tag_template(tag_data);\n          }\n          else if (tag.display === 'archive' && tag.type === 'tag') {\n            archive_data  = _.extend(tag, { selected: selected_tids });\n            archive_html += tag_template(archive_data);\n          }\n        });\n\n        Mailpile.API.with_template('modal-tag-picker', function(modal) {\n          Mailpile.UI.show_modal(modal({\n            count: selected.length,\n            priority: priority_html,\n            tags: tags_html,\n            archive: archive_html\n          }));\n        });\n      });\n    });\n\n  } else {\n    Mailpile.notification({ status: 'info', message: '{{_(\"No Messages Selected\")|escapejs}}' });\n  }\n};\n\n\nMailpile.UI.Search.Draggable = function(element) {\n  var $element = $(element);\n  var initializing = true;\n  $element.draggable({\n    containment: 'window',\n    appendTo: 'body',\n    cursor: 'move',\n    cursorAt: { left: 15, bottom: -10 },\n    scroll: false,\n    revert: false,\n    refreshPositions: true,\n    opacity: 1,\n    helper: function(event) {\n      var selected = Mailpile.UI.Selection.selected(element);\n      if ((selected.length < 2) && (selected[0] != '!all')) {\n        // Note: Dragging w/o selecting first may mean the length is zero\n        drag_count = '{{_(\"1 conversation\")|escapejs}}';\n        icon = 'icon-inbox';\n      }\n      else {\n        human_count = Mailpile.UI.Selection.human_length(selected);\n        drag_count = human_count + ' {{_(\"conversations\")|escapejs}}';\n        icon = 'icon-logo';\n      }\n      return $('<div class=\"pile-results-drag ui-widget-header\"><span class=\"' + icon + '\"></span> <span class=\"drag-info\">{{_(\"Moving\")|escapejs}} ' + drag_count + '</span></div>');\n    },\n    drag: function() {\n      var $e = $(this);\n      if ((!initializing) && $e.draggable('option', 'refreshPositions')) {\n        setTimeout(function() {\n          $e.draggable('option', 'refreshPositions', initializing);\n        }, 10);\n      }\n    },\n    start: function(event, ui) {\n      Mailpile.ui_in_action += 1;\n\n      // Style & Select Checkbox\n      $(event.target).parent().removeClass('result')\n                              .addClass('result-on')\n                              .data('state', 'selected')\n                              .find('td.checkbox input[type=checkbox]')\n                              .prop('checked', true);\n\n      // Update Bulk UI\n      Mailpile.bulk_actions_update_ui();\n\n      // Unveil the magical untagger!\n      $('.sidebar-untag-dropzone').slideDown(1000, function() {\n        initializing = false;\n      });\n    },\n    stop: function(event, ui) {\n      setTimeout(function() {\n        Mailpile.ui_in_action -= 1;\n        console.log(\"Decremented ui_in_action: \" + Mailpile.ui_in_action);\n      }, 250);\n      initializing = true;\n      $('.sidebar-untag-dropzone').slideUp();\n      $(this).draggable('option', 'refreshPositions', initializing);\n    }\n  });\n};\n\n\nMailpile.UI.Search.Droppable = function(element, accept) {\n  $(element).droppable({\n    accept: accept,\n    hoverClass: 'result-hover',\n    tolerance: 'pointer',\n    drop: function(event, ui) {\n      // Suppress spurious drops that happened outside the content window\n      var $box = $('#content');\n      var ofs = $box.offset();\n      if ((ui.position.top > ofs.top + 20) &&\n          (ui.position.top < (ofs.top + $box.height() - 20)) &&\n          (ui.position.left > ofs.left + 40) &&\n          (ui.position.left < (ofs.left + $box.width() - 40)))\n      {\n        var $context = Mailpile.UI.Selection.context(event.target);\n        var selected = Mailpile.UI.Selection.selected($context);\n        selected.push($(event.target).data('mid'));\n        Mailpile.UI.Tagging.tag_and_update_ui({\n          add: ui.draggable.data('tid'),\n          mid: selected,\n          context: $context.find('.search-context').data('context')\n        }, 'tag');\n      }\n      else {\n        console.log('Dropped outside content area!');\n        return false;\n      }\n    }\n  });\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/settings/content.js",
    "content": "/* Settings - Shows profile add modal */\n$(document).on('click', '#btn-settings-profile-add', function(e) {\n  Mailpile.UI.show_modal($(\"#modal-settings-profile-add\").html());\n});\n\n\n\n/* Settings - Submit profile add form */\n$(document).on('submit', '#form-settings-profile-add', function(e) {\n\n  e.preventDefault();\n  var profile_data = {\n    name : $('#profile-add-name').val(),\n    email: $('#profile-add-email').val()\n  };\n\n  var smtp_route = $('#profile-add-username').val() + ':' + $('#profile-add-password').val() + '@' + $('#profile-add-server').val() + ':' + $('#profile-add-port').val();\n\n  if (smtp_route !== ':@:25') {\n    profile_data.route = 'smtp://' + smtp_route;\n  }\n\n  // FIXME: this is currently g'borked\n  // {profiles: JSON.stringify(profile_data)}\n});\n\n\n/* Settings - Shows route add modal */\n$(document).on('click', '#btn-settings-route-add', function(e) {\n  Mailpile.UI.show_modal($(\"#modal-settings-route-add\").html());\n});\n\n\n$(document).on('submit', '#form-settings-route-add', function(e) {\n\n  alert('This is not quite implemented yet :)');\n\n});\n\n\n/* Settings - Submit route edit form */\n$(document).on('submit', '.form-settings-route-edit', function() {\n\n\n});\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/tags/events.js",
    "content": "/* Tag - Tag Add */\nMailpile.tag_add = function(tag_add, mids, complete) {\n  $.ajax({\n\t  url\t\t\t : Mailpile.api.tag,\n\t  type\t\t : 'POST',\n\t  data     : {\n      csrf: Mailpile.csrf_token,\n      add: tag_add,\n      mid: mids\n    },\n\t  dataType : 'json',\n    success  : function(response) {\n      if (response.status == 'success') {\n        complete(response.result);\n      } else {\n        Mailpile.notification(response);\n      }\n    }\n  });\n};\n\n\n/* Tag - Make Setting Data */\nMailpile.tag_setting = function(tid, setting, value) {\n  var key = 'tags.' + tid + '.' + setting;\n  var setting = {};\n  setting[key] = value;\n  return setting;\n};\n\n\n$(document).on('click', '#button-tag-change-icon', function() {\n  var modal_template = Mailpile.unsafe_template($(\"#modal-tag-icon-picker\").html());\n  Mailpile.UI.show_modal(modal_template({\n    icons: Mailpile.UI.tag_icons_as_lis()\n  }));\n});\n\n\n$(document).on('click', '#tag-edit-icon-picker .modal-tag-icon-option', function() {\n\n  var tid  = $('#data-tag-tid').val();\n  var old  = $('#data-tag-icon').val();\n  var icon = $(this).data('icon');\n\n  var setting = Mailpile.tag_setting(tid, 'icon', icon);\n  Mailpile.API.settings_set_post(setting, function(result) {\n\n    Mailpile.notification(result);\n\n    // Update Sidebar\n    $('#sidebar-tag-' + tid).find('span.icon').removeClass(old).addClass(icon);\n\n    // Update Tag Editor\n    $('#data-tag-icon').val(icon);\n    $('#tag-editor-icon').removeClass().addClass(icon);\n    Mailpile.UI.hide_modal();\n  });\n});\n\n\n$(document).on('click', '#button-tag-change-label-color', function(e) {\n  var modal_html = $(\"#modal-tag-color-picker\").html();\n  var modal_template = Mailpile.unsafe_template(modal_html);\n  Mailpile.UI.show_modal(modal_template({\n    colors: Mailpile.UI.tag_colors_as_lis()\n  }));\n});\n\n\n$(document).on('click', '#tag-edit-color-picker .modal-tag-color-option', function(e) {\n\n  var tid   = $('#data-tag-tid').val();\n  var old   = $('#data-tag-label-color').val();\n  var name = $(this).data('name');\n  var hex = $(this).data('hex');\n\n  var setting = Mailpile.tag_setting(tid, 'label_color', name);\n  Mailpile.API.settings_set_post(setting, function(result) {\n\n    Mailpile.notification(result);\n\n    // Update Sidebar\n    $('#sidebar-tag-' + tid).find('span.icon').css('color', hex);\n\n    // Update Tag Editor\n    $('#data-tag-label-color').val(name);\n    $('#tag-editor-icon').css('color', hex);\n    Mailpile.UI.hide_modal();\n  });\n});\n\n\n/* API - Tag Add */\n$(document).on('submit', '#form-tag-add', function(e) {\n  e.preventDefault();\n  var tag_data = $('#form-tag-add').serialize();\n  Mailpile.API.tags_add_post(tag_data, function() {\n    Mailpile.go('/tags/edit.html?only=' + $('#data-tag-add-slug').val());\n  });\n});\n\n\n/* Tag - Delete Tag */\n$(document).on('click', '#button-tag-delete', function(e) {\n  var tag_slug = $(this).data('slug');\n  if (confirm(\"{{_('Are you sure you want to delete this tag?')|escapejs}}\\n\"\n              + \"{{_('This action cannot be undone.')|escapejs}}\")) {\n    Mailpile.API.tags_delete_post({ tag: tag_slug }, function(response) {\n      Mailpile.go('/in/inbox/');\n    }, 'POST');\n  }\n});\n\n\n/* Tag - Toggle Archive */\n$(document).on('click', '#button-tag-toggle-archive', function(e) {\n  var new_message = $(this).data('message');\n  var old_message = $(this).html();\n  $(this).data('message', old_message);\n  $(this).html(new_message);\n  if ($('#tags-archived-list').hasClass('hide')) {\n    $('#tags-archived-list').removeClass('hide');\n  } else {\n    $('#tags-archived-list').addClass('hide');\n  }\n});\n\n\n/* Tag - Update the Name & Slug */\n$(document).on('blur', '#data-tag-add-tag', function(e) {\n  var settings = {};\n  settings['tags.' + $('#data-tag-tid').val() + '.name'] = $(this).val();\n  settings['tags.' + $('#data-tag-tid').val() + '.slug'] = $('#data-tag-add-slug').val();\n  Mailpile.API.settings_set_post(settings, function(result) {\n    Mailpile.notification(result);\n  });\n});\n\n\n/* Tag - Update the Slug */\n$(document).on('blur', '#data-tag-add-slug', function(e) {\n  var setting = Mailpile.tag_setting($('#data-tag-tid').val(), 'slug', $('#data-tag-add-slug').val());\n  Mailpile.API.settings_set_post(setting, function(result) {\n    Mailpile.notification(result);\n  });\n});\n\n\n/* Tag - Update (multiple attribute events) */\n$(document).on('change', '#data-tag-display', function(e) {\n  var setting = Mailpile.tag_setting($('#data-tag-tid').val(), 'display', $(this).val());\n  Mailpile.API.settings_set_post(setting, function(result) {\n    Mailpile.notification(result);\n  });\n});\n\n\n/* Tag - Update parent */\n$(document).on('change', '#data-tag-parent', function(e) {\n  var setting = Mailpile.tag_setting($('#data-tag-tid').val(), 'parent', $(this).val());\n  Mailpile.API.settings_set_post(setting, function(result) {\n    Mailpile.notification(result);\n  });\n});\n\n\n/* Tag - Update Label */\n$(document).on('change', '#data-tag-label', function(e) {\n  var label = 'false';\n  if ($(this).is(':checked')) {\n    label = 'true';\n  }\n  var setting = Mailpile.tag_setting($('#data-tag-tid').val(), 'label', label);\n  Mailpile.API.settings_set_post(setting, function(result) {\n    Mailpile.notification(result);\n  });\n});\n\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/tags/init.js",
    "content": "/* Tags */\n\nMailpile.Tags = {};\nMailpile.Tags.UI = {};\nMailpile.Tags.Tooltips = {};\n\nMailpile.Tags.init = function() {\n\n  var tids = ($('#pile-results').data('tids') || \"\") + \"\";\n  $(\"#sidebar li\").removeClass('navigation-on');\n  if (tids) {\n    $.each(tids.split(/ /), function() {\n      $(\"#sidebar li#sidebar-tag-\" + this).addClass('navigation-on');\n    });\n  }\n\n};\n\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/tags/modals.js",
    "content": "/* Modals - Tags */\n\nMailpile.UI.Modals.TagAdd = function(add_tag_data) {\n  var $elem = $('#add-tag');\n  if ($elem.length > 0) {\n    $elem.eq(0).trigger('click');\n  }\n  else {\n    Mailpile.API.with_template('modal-add-tag', function(modal) {\n      Mailpile.UI.show_modal(modal(add_tag_data));\n    });\n  }\n};\n\nMailpile.UI.Modals.TagAddProcess = function(location) {\n  var tag_data = $('#modal-form-tag-add').serialize();\n  Mailpile.API.tags_add_post(tag_data, function(result) {\n    var tag_template = Mailpile.safe_template($('#template-sidebar-item').html());\n    if (result.status == 'success' && location == 'sidebar') {\n      var tag_html = tag_template(result.result.added[0]);\n      $('#sidebar-tag').prepend(tag_html);\n      Mailpile.UI.prepare_new_content('#sidebar-tag');\n\n      // FIXME: these drag & drops probably break on non search views\n\n      Mailpile.UI.hide_modal();\n    } else {\n      Mailpile.notification(result);\n    }\n  });\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/templates/modal-add-tag.html",
    "content": "<div class=\"modal-dialog\">\n<form id=\"modal-form-tag-add\" class=\"standard\">{{ csrf_field|safe }}\n  <div class=\"modal-content\">\n    <div class=\"modal-header\">\n      <button type=\"button\" class=\"close button-info\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n      <h4 class=\"modal-title\"><span class=\"icon-tag\"></span> {{_(\"Add Tag\")}}</h4>\n    </div>\n    <div class=\"modal-body clearfix\">\n      <label>{{_(\"Name\")}}</label>\n      <input type=\"text\" name=\"name\" value=\"\" placeholder=\"{{_(\"Friends & Family\")}}\" class=\"input-medium\">\n      <button id=\"button-modal-add-tag\" title=\"{{_(\"Add\")}}\" alt=\"{{_(\"Add\")}}\" class=\"button-primary add-bottom\" data-action=\"add\" data-location=\"<%= location %>\">\n        <span class=\"icon-plus\"></span> {{_(\"Add\")}}\n      </button>\n    </div>\n    <!-- <div class=\"modal-footer\"></div> -->\n  </div>\n</form>\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/templates/modal-auto.html",
    "content": "<div data-flags=\"<%= flags %>\" class=\"modal-dialog\">\n <div class=\"modal-content\">\n  <% if (header != \"off\") { %>\n   <div class=\"modal-header\">\n     <button type=\"button\" class=\"close button-info\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n     <h4 class=\"modal-title\"><span class=\"<%= icon %>\"></span>\n       <% print((title) ? title : data.message) %>\n     </h4>\n   </div>\n  <% } %>\n   <div class=\"modal-body clearfix\"><%= data.result %></div>\n </div>\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/templates/modal-compose-quoted-reply.html",
    "content": "<div class=\"modal-dialog\">\n  <div class=\"modal-content\">\n    <div class=\"modal-header\">\n      <button type=\"button\" class=\"close button-info\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n      <h4 class=\"modal-title\">\n        <span class=\"icon-compose\"></span>\n        <span class=\"title\">{{_(\"Quoted Replies\")}}</span>\n      </h4>\n    </div>\n    <div class=\"modal-body clearfix\">\n      <form id=\"form-compose-quoted-reply\" class=\"standard\">{{ csrf_field|safe }}\n        <p>{{_(\"Would you like to disable quoted replies in all the messages you compose?\")}}</p>\n        <label class=\"add-bottom\"><input type=\"checkbox\" name=\"web.quoted_reply\" checked=\"checked\"> {{_(\"Disable Quoted Replies\")}}</label>\n        <button type=\"submit\"><span class=\"icon-checkmark\"></span> {{_(\"Save\")}}</button>\n      </form>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/templates/modal-composer-encryption-helper.html",
    "content": "<div class=\"modal-dialog\">\n  <div class=\"modal-content\">\n    <div class=\"modal-header\">\n      <button type=\"button\" class=\"close button-info\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n      <h4 class=\"modal-title\">\n        <span class=\"icon-lock-open color-10-orange\"></span>\n        <span class=\"title\">{{_(\"Cannot Encrypt\")}}</span>\n      </h4>\n    </div>\n    <div class=\"modal-body clearfix\">\n\n      <div id=\"encryption-helper-missing-keys\">\n        <p class=\"paragraph-alert\">\n          <span class=\"icon-signature-unknown\"></span>\n          {{_(\"You are missing encryption keys for the following contacts\")}}\n        </p>\n        <% if (unencryptables && unencryptables.length) { %>\n        <ul>\n          <% _.each(unencryptables, function(recipient, key) { %>\n          <li id=\"item-encryption-key-\" address=\"<%= recipient.address %>\"\n              class=\"searchkey-result-item animated fadeIn\">\n            <div class=\"clearfix\">\n              <div class=\"avatar\"\n{%- if 0 and is_dev_version() %}\n                   href=\"{{ U('/contacts/view/') }}<%= recipient.address %>/\"\n{%- endif %}\n                   target=\"_blank\">\n                <img src=\"{{ U('/static/img/avatar-default.png') }}\">\n              </div>\n              <div class=\"name\">\n                <%= recipient.fn %><br>\n                <span><%= recipient.address %></span>\n              </div>\n              <div class=\"right text-right\">\n              <ul class=\"none\">\n                <li class=\"half-bottom\">\n                  <a data-email=\"<%= recipient.address %>\" data-mid=\"<%= mid %>\"\n                     class=\"encryption-helper-find-key\"\n                     ><span class=\"icon-search\"></span>\n                      {{_(\"Find Encryption Keys\")}}</a>\n                </li>\n              </ul>\n              </div>\n            </div>\n          </li>\n          <% }); %>\n        </ul>\n        <% } else { %>\n        <h2 class=\"text-center\">\n          <b>{{_(\"Yourself!\")}}</b>\n        </h2>\n        <p>\n          {{_(\"You cannot send encrypted or signed mail without an encryption key of your own.\")}}\n          {{_(\"Check your account settings or send using a different profile.\")}}\n        </p>\n        <% } %>\n      </div>\n\n      <div id=\"encryption-helper-find-keys\" class=\"hide\">\n        <p class=\"message paragraph-important animated\">\n          <span class=\"icon-search\"></span>\n          {{_(\"Searching for encryption keys for:\")}}\n          <span class=\"color-01-gray-mid\"></span>\n        </p>\n        <p class=\"loading text-center\">\n        {% include(\"../img/loading-ellipsis.svg\") %}\n        </p>\n        <p class=\"progress\" style=\"text-align: center;\"></p>\n      </div>\n\n      <div id=\"encryption-helper-found-keys\" class=\"hide\">\n        <p class=\"message paragraph-important animated\">\n          <span class=\"icon-search\"></span>\n          {{_(\"Searching for encryption keys...\")}}\n          <span class=\"color-01-gray-mid\"></span>\n        </p>\n        <p class=\"loading text-center\">\n        {% include(\"../img/loading-ellipsis.svg\") %}\n        </p>\n        <p class=\"progress\" style=\"text-align: center;\"></p>\n        <ul class=\"result\"></ul>\n      </div>\n\n    </div>\n    <div class=\"modal-footer\">\n      <button onclick=\"javascript:Mailpile.UI.hide_modal();Mailpile.Composer.Crypto.EncryptionToggle('none', '<%= mid %>');\"\n              class=\"modal-send-unencrypted button-info left\">\n        <span class=\"icon-lock-open\"></span> {{_(\"Send Unencrypted\")}}\n      </button>\n      <span class=\"keylookup_check_all\"\n            style=\"display: inline-block; float: left; margin: 7px 0 0 15px;\">\n        <input type=\"checkbox\" id=\"keylookup_check_all\">\n        <span title=\"{{_('Tick this box to keep searching, even after keys have been found in preferred locations.')}}\"\n              class=\"checkbox\">{{_(\"Search All Sources\")}}</span>\n      </span>\n      <button onclick=\"javascript:Mailpile.UI.hide_modal();\"\n              class=\"modal-retry-encryption button-primary right\"\n              data-mid=\"<%= mid %>\">\n        {{_(\"Try Again\")}}\n      </button>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/templates/modal-contact-add.html",
    "content": "<div class=\"modal-dialog\">\n<form id=\"form-contact-add\" class=\"standard\">{{ csrf_field|safe }}\n  <div class=\"modal-content\">\n    <div class=\"modal-header\">\n      <button type=\"button\" class=\"close button-info\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n      <h4 class=\"modal-title\"><span class=\"icon-user\"></span> {{_(\"Add Contact\")}}</h4>\n    </div>\n    <div class=\"modal-body clearfix\">\n      <input type=\"hidden\" class=\"contact-add-mid\">\n        <label>{{_(\"Name\")}}</label>\n        <input type=\"text\" name=\"name\" data-type=\"name\" class=\"contact-add-name\" value=\"<%= name %>\" placeholder=\"Chelsea Manning\">\n        <label>{{_(\"E-mail\")}}</label>\n        <input type=\"text\" name=\"email\" data-type=\"email\" class=\"contact-add-email\" value=\"<%= address %>\" placeholder=\"chelsea@manning.com\">\n      <button title=\"{{_(\"Add\")}}\" alt=\"{{_(\"Add\")}}\" class=\"button-primary add-bottom\" data-action=\"add\">\n        <span class=\"icon-plus\"></span> {{_(\"Add\")}}\n      </button>\n    </div>\n    <!-- <div class=\"modal-footer\"></div> -->\n  </div>\n</form>\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/templates/modal-display-keybindings.html",
    "content": "<div class=\"modal-dialog\">\n  <div class=\"modal-content\">\n    <div class=\"modal-header\">\n      <button type=\"button\" class=\"close button-info\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n      <h4 class=\"modal-title\">{{ _(\"Keyboard Shortcuts\") }}</h4>\n    </div>\n    <div class=\"modal-body modal-body-light-gray\">\n      <table class=\"default-table\">\n        <thead>\n          <th>{{ _(\"Keys\") }}</th>\n          <th>{{ _(\"Action\") }}</th>\n        </thead>\n        <tbody>\n          <% _.each(keybindings, function(binding) { if (binding.title) { %>\n            <tr>\n              <td>\n                  <% _.each(binding.keys.split(' '), function(keystroke) { %>\n                    <% var keys = keystroke.split('+') %>\n                    <% _.forEach(keys, function(key, idx) { %>\n                      <kbd><%= key %></kbd>\n                      <%= (idx < (keys.length - 1)) ? '+' : '' %>\n                    <% }); %>\n                  <% }); %>\n              </td>\n              <td><%= binding.title %></td>\n            </tr>\n          <% } }); %>\n        </tbody>\n      </table>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/templates/modal-related-search.html",
    "content": "<div class=\"modal-dialog\">\n<form id=\"modal-related-search\" class=\"standard\"\n      method=\"GET\" action=\"{{ U('/search/') }}\">\n  <script type=\"text/javascript\">\n    function q(prefix, val) {\n      return prefix + ':' + val.split(/ +/).join(' ' + prefix + ':');\n    }\n  </script>\n  <div class=\"modal-content\">\n    <div class=\"modal-header\">\n      <button type=\"button\" class=\"close button-info\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n      <h4 class=\"modal-title\"><span class=\"icon-search\"></span>\n        {{_(\"Search for Similar E-mail\")}}\n      </h4>\n    </div>\n    <div class=\"modal-body clearfix\">\n      <div class=\"modal-basic-settings\">\n        <p>\n          {{_(\"Select which common message attributes you would like to search for.\")}}\n          {{_(\"The more attributes you select, the narrower the search.\")}}\n        </p>\n        <ul class=\"left\" style=\"margin-left: 1em; min-width: 40%;\">\n          <% _.each(subjects, function(subject) { if (subject) { %><li>\n            <input type=\"checkbox\" name=\"q\" value=\"<%= qq('', subject, 3) %>\">\n            <span class=\"checkbox\">{{_(\"Subject\")}}: <%= trunc(subject, 30) %></span>\n          </li><% }}); %>\n          <% _.each(froms, function(email, i) { if (email) { %><li>\n            <input type=\"checkbox\" name=\"q\" value=\"<%= q('from', email) %>\"<% if (i==0) { %> checked<% } %>>\n            <span class=\"checkbox\">{{_(\"From\")}}: <%= email %></span>\n          </li><% }}); %>\n          <% _.each(emails, function(email) { if (email) { %><li>\n            <input type=\"checkbox\" name=\"q\" value=\"<%= q('email', email) %>\">\n            <span class=\"checkbox\">{{_(\"E-mail\")}}: <%= email %></span>\n          </li><% }}); %>\n          <% _.each(lists, function(lst) { if (lst) { %><li>\n            <input type=\"checkbox\" name=\"q\" value=\"<%= q('list', lst) %>\">\n            <span class=\"checkbox\">{{_(\"Mailing list\")}}: <%= lst %></span>\n          </li><% }}); %>\n{% if config.web.developer_mode %}\n          <% _.each(muas, function(mua) { if (mua) { %><li>\n            <input type=\"checkbox\" name=\"q\" value=\"<%= q('mua', mua, 1) %>\">\n            <span class=\"checkbox\">{{_(\"E-mail client\")}}: <%= mua %></span>\n          </li><% }}); %>\n          <% _.each(hpts, function(hp) { if (hp) { %><li>\n            <input type=\"checkbox\" name=\"q\" value=\"<%= q('hpt', hp) %>\">\n            <span class=\"checkbox\" title=\"<%= hp %>\">{{_(\"Message structure\")}}</span>\n          </li><% }}); %>\n          <% _.each(hpss, function(hp) { if (hp) { %><li>\n            <input type=\"checkbox\" name=\"q\" value=\"<%= q('hps', hp) %>\">\n            <span class=\"checkbox\" title=\"<%= hp %>\">{{_(\"Sender fingerprint\")}}</span>\n          </li><% }}); %>\n{% endif %}\n        </ul>\n        <ul class=\"left\" style=\"margin-left: 1em; color: #777;\">\n          <li title=\"<%= date_range_2wks %>\">\n            <input type=\"checkbox\" name=\"q\" value=\"dates:<%= date_range_2wks %>\">\n            <span class=\"checkbox\">\n              <span class=\"icon-clock\"></span>\n              {{_(\"Similar dates\")}}: &plusmn;1 {{_(\"week\")}}\n            </span>\n          </li>\n          <li title=\"<%= date_range_4wks %>\">\n            <input type=\"checkbox\" name=\"q\" value=\"dates:<%= date_range_4wks %>\">\n            <span class=\"checkbox\">\n              <span class=\"icon-clock\"></span>\n              {{_(\"Similar dates\")}}: &plusmn;2 {{_(\"weeks\")}}\n            </span>\n          </li>\n          <li>\n            <input type=\"checkbox\" name=\"q\" value=\"has:image\">\n            <span class=\"checkbox\">\n              <span class=\"icon-photos\"></span> {{_(\"Has an image\")}}\n            </span>\n          </li>\n          <li>\n            <input type=\"checkbox\" name=\"q\" value=\"has:attachment\">\n            <span class=\"checkbox\">\n              <span class=\"icon-attachment\"></span> {{_(\"Has an attachment\")}}\n            </span>\n          </li>\n{% if config.web.developer_mode %}\n          <li>\n            <input type=\"checkbox\" name=\"q\" value=\"is:signed\">\n            <span class=\"checkbox\">\n              <span class=\"icon-checkmark\"></span> {{_(\"Has a digital signature\")}}\n            </span>\n          </li>\n          <li>\n            <input type=\"checkbox\" name=\"q\" value=\"is:encrypted\">\n            <span class=\"checkbox\">\n              <span class=\"icon-lock-closed\"></span> {{_(\"Is encrypted\")}}\n            </span>\n          </li>\n{% endif %}\n        </ul>\n        <br clear=\"both\">\n      </div>\n    </div>\n    <div class=\"modal-footer\">\n      <div class=\"right\">\n        <button class=\"button-secondary right\">\n          <span class=\"icon-search\"></span>\n          {{_(\"Search\")}}\n        </button>\n      </div>\n      <input class=\"right\" style=\"margin: 0 1em;\"\n             type=text name=q value=\"<%= extras %>\"\n             placeholder=\"{{_(\"Additional search terms\")}}\">\n    </div>\n  </div>\n</form>\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/templates/modal-save-search.html",
    "content": "<div class=\"modal-dialog\">\n<form id=\"modal-save-search\" class=\"standard\"\n      method=\"POST\" action=\"{{ U('/filter/') }}\">{{ csrf_field|safe }}\n  <div class=\"modal-content\">\n    <div class=\"modal-header\">\n      <button type=\"button\" class=\"close button-info\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n      <h4 class=\"modal-title\"><span class=\"icon-star\"></span>\n        {{_(\"Save Search\")}}: <%= terms %>\n      </h4>\n    </div>\n    <div class=\"modal-body clearfix\">\n      <p>\n        {{_(\"Saved Searches appear as Tags in the sidebar.\")}}\n        {{_(\"New matching messages will be tagged automatically as they arrive, but you can also add or remove messages by hand.\")}}\n      </p>\n      <p class=\"message paragraph-important modal-basic-settings-title\">\n        <span class=\"icon-settings\"></span> {{_(\"Configure new Saved Search\")}}</h4>\n      </p>\n      <div class=\"modal-basic-settings\">\n        <div class=\"left\">\n          <div class=\"right\">\n            <a class=\"icon-star modal-open-choose-tag-icon\" title='{{_(\"Change Icon\")}}'></a>\n            <input type=\"hidden\" class=\"choose-tag-icon\" name=\"tag-icon\" value=\"icon-star\">\n            <a class=\"modal-open-choose-tag-color\" title='{{_(\"Change Color\")}}'>{{_(\"Color\")}}</a>\n            <input type=\"hidden\" class=\"choose-tag-color\" name=\"tag-color\" value=\"black\">\n          </div>\n          <label>{{_(\"Save as\")}} ...</label>\n          <input type=\"text\" name=\"comment\" id='ss-comment' value=\"\" placeholder=\"{{_(\"Tag Name\")}}\" class=\"input-medium\">\n          <input type=\"hidden\" name=\"terms\" id='ss-search-terms'>\n          <input type=\"hidden\" name=\"create-tag\" value='yes'>\n          <input type=\"hidden\" name=\"add-tag\" value=\"!PRIMARY\">\n        </div>\n        <ul class=\"right\">\n          <li><input type=\"checkbox\" name=\"mark-read\" value=\"yes\">\n              <span class=\"checkbox\">{{_(\"Mark as Read\")}}</span></li>\n          <li><input type=\"checkbox\" name=\"skip-inbox\" value=\"yes\">\n              <span class=\"checkbox\">{{_(\"Remove from Inbox\")}}</span></li>\n          <li><input type=\"checkbox\" name=\"never-spam\" value=\"yes\">\n              <span class=\"checkbox\">{{_(\"Never send to Spam\")}}</span></li>\n        </ul>\n        <br clear=\"both\">\n      </div>\n      <div class=\"modal-choose-tag-icon hide\">\n        <p class=\"message paragraph-important\">\n          <span class=\"icon-settings\"></span> {{_(\"Choose an Icon\")}}</h4>\n        </p>\n        <ul class=\"horizontal tag-icons\"></ul>\n      </div>\n      <div class=\"modal-choose-tag-color hide\">\n        <p class=\"message paragraph-important\">\n          <span class=\"icon-settings\"></span> {{_(\"Choose a Color\")}}</h4>\n        </p>\n        <div class=\"text-center\">\n          <ul class=\"horizontal tag-colors\"></ul>\n        </div>\n      </div>\n    </div>\n    <div class=\"modal-footer\">\n      <div class=\"right ss-save-group\">\n        &nbsp;\n        <button class=\"button-secondary right ss-save\">\n          <span class=\"icon-star\"></span>\n          {{_(\"Save Search\")}}\n        </button>\n      </div>\n      {% if config.filters %}\n      <div class=\"right ss-save-group\">\n        <small><select name=\"replace\" style=\"padding: 8px;\">\n           <option value=''>{{_(\"Create new Saved Search\")}}</option>\n           <option value=''></option>\n           {% for f in config.filters %}\n           <option value='{{f}}'>{{_(\"Replace\")}}: {{config.filters[f].comment}}</option>\n           {% endfor %}\n        </select></small>\n      </div>\n      {% endif %}\n      <div class=\"left\">\n        <button class=\"close button-info\" type=\"button\" data-dismiss=\"modal\">\n          <span class=\"icon-close\"></span>\n          {{_(\"Cancel\")}}\n        </button>\n      </div>\n    </div>\n  </div>\n</form>\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/templates/modal-search-keyservers.html",
    "content": "<!-- Crypto - Modal with processing & results from searching keyservers -->\n<div class=\"modal-dialog\">\n  <div class=\"modal-content\">\n    <div class=\"modal-header\">\n      <button type=\"button\" class=\"close button-info\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n      <h4 class=\"modal-title\">\n        <span class=\"icon-key\"></span>\n        <span class=\"title\">{{_(\"Find Encryption Keys\")}}</span>\n      </h4>\n    </div>\n    <div class=\"modal-body clearfix\">\n      <form id=\"form-search-keyservers\" class=\"standard hide animated\">{{ csrf_field|safe }}\n        <label>{{_(\"Enter Name or Email Address\")}} </label>\n        <input type=\"text\" name=\"query\" value=\"\" placeholder=\"name@email.org\" tabindex=\"2\">\n        <button type=\"submit\" class=\"button-primary\"><span class=\"icon-search\"></span> {{_(\"Search\")}}</button>\n      </form>\n      <div id=\"search-keyservers-again\" class=\"hide\">\n        <p class=\"message paragraph-important\">\n          <span class=\"icon-search\"></span> {{_(\"Want to search for something else\")}} <a href=\"#\" id=\"btn-search-keyservers-again\">{{_(\"click here\")}}</a>\n        </p>\n      </div>\n      <div id=\"search-keyservers\" class=\"hide\">\n        <p class=\"message paragraph-important\">\n          <span class=\"icon-search\"></span> {{_(\"Searching for encryption keys for\")}}: <span class=\"color-01-gray-mid\"><%= query %></span>\n        </p>\n        <p class=\"loading text-center half-bottom\">\n          {% include(\"../img/loading-ellipsis.svg\") %}\n        </p>\n        <p class=\"progress\" style=\"text-align: center;\"></p>\n        <ul class=\"result\"></ul>\n        <ul class=\"result-hidden-keys hide\"></ul>\n      </div>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/templates/modal-send-public-key.html",
    "content": "<div class=\"modal-dialog\">\n <div class=\"modal-content\">\n  <div class=\"modal-header\">\n    <button type=\"button\" class=\"close button-info\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n    <h4 class=\"modal-title\"><span class=\"icon-key\"></span> {{_(\"Send Encryption Keys\")}}</h4>\n  </div>\n  <div class=\"modal-body clearfix\">\n    <ul id=\"crypto-private-key-list\" class=\"items grouped\"></ul>\n  </div>\n  <div class=\"modal-footer\">\n    <button class=\"button-primary\" title=\"{{_(\"Add\")}}\" alt=\"{{_(\"Add\")}}\" data-action=\"add\">\n      <span class=\"icon-sent\"></span> {{_(\"Send Selected Keys\")}}\n    </button>\n  </div>\n </div>\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/templates/modal-tag-picker.html",
    "content": "<div class=\"modal-dialog\">\n <form id=\"form-tag-picker\">{{ csrf_field|safe }}\n  <div class=\"modal-content\">\n    <div class=\"modal-header\">\n      <button type=\"button\" class=\"close button-info\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n      {# FIXME: messages_cache is GONE, this needs to be done differently #}\n      <h4 class=\"modal-title\"><span class=\"icon-tag\"></span> {{_(\"Tag\")}} <span class=\"modal-title-count\"><%= Mailpile.messages_cache.length %></span> {{_(\"Messages\")}}</h4>\n    </div>\n    <div class=\"modal-body modal-body-light-gray\">\n      <table class=\"default-table\">\n      <%= priority %>\n      <tr class=\"modal-tag-picker-header\" data-display=\"tag\">\n        <td colspan=\"2\">{{_(\"Tags\")}}</td>\n        <td class=\"text-center\"><a class=\"modal-tag-picker-expand\">&middot;&middot;&middot;&middot;</a></td>\n      </tr>\n      <%= tags %>\n      <%= archive %>\n      </table>\n    </div>\n    <div class=\"modal-footer\">\n      <button class=\"left button-warning\" title=\"{{_(\"Remove\")}}\" alt=\"{{_(\"Remove\")}}\" data-action=\"remove\">\n        <span class=\"icon-minus\"></span> {{_(\"Remove\")}}\n      </button>\n      <button class=\"button-primary\" title=\"{{_(\"Apply\")}}\" alt=\"{{_(\"Apply\")}}\" data-action=\"add\">\n        <span class=\"icon-plus\"></span> {{_(\"Apply\")}}\n      </button>\n    </div>\n  </div>\n </form>\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/templates/modal-upload-key.html",
    "content": "<div class=\"modal-dialog\">\n  <div class=\"modal-content\">\n    <div class=\"modal-header\">\n      <button type=\"button\" class=\"close button-info\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n      <h4 class=\"modal-title\"><span class=\"icon-key\"></span> {{_(\"Upload Encryption Key\")}}</h4>\n    </div>\n    <div id=\"upload-key-container\" class=\"modal-body clearfix\">\n      <p class=\"message paragraph-alert animated hide\">\n        <span class=\"icon-signature-unknown\"></span> {{_(\"Something went wrong uploading encryption key. Try again?\")}}\n      </p>\n      <ul id=\"upload-key-list\" class=\"hide\"></ul>\n      <form id=\"form-upload-key\" class=\"standard add-bottom\">{{ csrf_field|safe }}\n        <fieldset>\n          <label>{{_(\"Select or Drag Encryption Key\")}} <span class=\"icon-signature-unverified\" title=\"{{_(\"A file located on your computer usually ending in: .asc .key .pub\")}}\"></span></label>\n          <a id=\"upload-key-pick\" class=\"hide half-top button-primary clickable\" title=\"{{_(\"Add\")}}\" alt=\"{{_(\"Add\")}}\" data-action=\"add\">\n            <span class=\"icon-upload\"></span> {{_(\"Select Encryption Key\")}}\n          </a>\n          <span id=\"upload-key-browswer-unsupported\">{{_(\"Unable to create uploader\")}}, <a href=\"\">{{_(\"update your browser\")}}</a></span>\n        </fieldset>\n      </form>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/templates/template-modal-tag-picker-item.html",
    "content": "<tr class=\"modal-tag-picker-item <%= display %>\" data-tid=\"<%= tid %>\">\n  <td class=\"tag text-left color-<%= label_color %>\">\n    <% if (parent) { var parent = _.findWhere(Mailpile.instance.tags, { tid: parent}); %>\n    <span class=\"<%= parent.icon %>\"></span> <span class=\"text\"><%= parent.name %></span> &nbsp;&gt;&nbsp;\n    <% } %>\n    <span class=\"<%= icon %>\"></span> <span class=\"text\"><%= name %></span>\n  </td>\n  <td class=\"selection text-right\" id=\"tag-selected-<%= tid %>\">\n    <% if (selected[tid]) { %><%= selected[tid] %> {{_(\"Messages\")}}<% } %>\n  </td>\n  <td class=\"checkbox text-center\">\n    {# NOTE: Mailpile.tags_cache is GONE, this needs to be done differently! #}\n    <input class=\"tag-picker-checkbox\" type=\"checkbox\" name=\"tid\" value=\"<%= tid %>\" <% if (_.indexOf(Mailpile.tags_cache, tid) > -1) { %>checked<% } %>>\n  </td>\n</tr>\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/ui/content.js",
    "content": "/* UI - Configure the toggle based on current selection */\nMailpile.set_bulk_action_tag_toggle = function($elem, have_tags) {\n  var $elem = $elem.find('.bulk-action-tag-op');\n  if ($elem && $elem.data('op') == 'toggle') {\n    if (have_tags[$elem.data('tag')]) {\n      $elem.data('mode', 'untag').css('opacity', 0.5);\n    }\n    else {\n      $elem.data('mode', 'normal').css('opacity', 1.0);\n    }\n  }\n};\n\n\n/* UI - Show specific elements */\nMailpile.show_bulk_actions = function(elements, have_tags) {\n  $.each(elements, function(){    \n    var $e = $(this).css('visibility', 'visible');\n    Mailpile.set_bulk_action_tag_toggle($e, have_tags);\n  });\n};\n\n\n/* UI - Hide specific elements */\nMailpile.hide_bulk_actions = function(elements) {\n  $.each(elements, function(){    \n    $(this).css('visibility', 'hidden');\n  });\n};\n\n\n/* UI - Update sub navigation .navigation-on class state */\n$(document).on('click', '.button-sub-navigation', function() {\n\n  var filter = $(this).data('filter');\n\n  $('.sub-navigation ul li').removeClass('navigation-on');\n  $(this).parent().addClass('navigation-on');\n\n  if (filter == 'in_unread') {\n\n    $('#display-unread').addClass('navigation-on');\n    $('tr').hide('fast', function() {\n      $('tr.in_new').show('fast');\n    });\n  }\n  else if (filter == 'in_later') {\n\n    $('#display-later').addClass('navigation-on');\n    $('tr').hide('fast', function() {\n      $('tr.in_later').show('fast');\n    });\n  }\n  else {\n    $('#display-all').addClass('navigation-on');\n    $('tr.result').show('fast');\n  }\n  return false;\n});\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/ui/events.js",
    "content": "$(document).on('click', '.sidebar-tag-expand', function(e) {\n  e.preventDefault();\n  Mailpile.UI.Sidebar.SubtagsToggle($(this).data('tid'));\n});\n\n\n$(document).on('click', '.is-editing', function(e) {\n  e.preventDefault();\n});\n\n\n$(document).on('click', '#button-sidebar-organize', function(e) {\n  e.preventDefault();\n  Mailpile.UI.Sidebar.OrganizeToggle(this);\n});\n\n\n$(document).on('click', '#button-sidebar-add', function(e) {\n  e.preventDefault();\n  Mailpile.UI.Modals.TagAdd({ location: 'sidebar' });\n});\n\n\n$(document).on('click', '#button-modal-add-tag', function(e) {\n  e.preventDefault();\n  Mailpile.UI.Modals.TagAddProcess($(this).data('location'));\n});\n\n\n$(document).on('click', '.hide-donate-page', function(e) {\n  Mailpile.API.settings_set_post({ 'web.donate_visibility': 'False' }, function(e) {\n    Mailpile.go('/in/inbox/');\n  });\n});\n\n\n$(document).on('click', 'span.checkbox, div.checkbox', function(e) {\n  $(this).prev().trigger('click');\n});\n\n\n$(document).on('click', 'a.show-hide, a.do-show', function(e) {\n  var $elem = $(this);\n  if ($elem.data('done') == 'show') {\n    $($elem.data('hide')).slideDown();\n    $($elem.data('show')).slideUp();\n    if ($elem.hasClass('show-hide')) {\n      $elem.data('done', 'hide').removeClass('did-show').addClass('did-hide');\n    }\n  }\n  else {\n    $($elem.data('show')).slideDown();\n    $($elem.data('hide')).slideUp();\n    if ($elem.hasClass('show-hide')) {\n      $elem.data('done', 'show').removeClass('did-hide').addClass('did-show');\n    }\n  }\n  if ($elem.data('other')) {\n    var msg = $elem.html();\n    $elem.html($elem.data('other')).data('other', msg);\n  }\n  e.preventDefault();\n});\n\n\n// FIXME: this is in the wrong place\nMailpile.auto_modal_display = function(jhtml_url, params, modal, data) {\n  var mf = Mailpile.UI.show_modal(modal({\n    data: data,\n    icon: params.icon,\n    title: params.title,\n    header: params.header,\n    flags: params.flags\n  }));\n  if (!params.title) {\n    mf.find('.modal-title').html(mf.find('h1').html());\n  }\n  if (params.sticky && !params.callback) {\n    params.callback = function(data) {\n      Mailpile.auto_modal_display(jhtml_url, params, modal, data);\n    }\n  }\n  if (params.reload && !params.callback) {\n    params.callback = function(data) { location.reload(true); };\n  }\n  if (params.callback) {\n    // If there is a callback, we override the form's default behavior\n    // and use AJAX instead so our callback can handle the result.\n    mf.find('input[type=submit]').click(function(ev) {\n      // Set input type to hidden, so serialize picks it up.\n      if ($(this).attr('name')) {\n        $(this).closest('form').append(\n          $('<input>').attr('name', $(this).attr('name'))\n                      .attr('type', 'hidden')\n                      .attr('value', $(this).val()));\n      }\n    });\n    mf.find('form').submit(function(ev) {\n      ev.preventDefault();\n      var url = jhtml_url;\n      if (!params.sticky) {\n        url = mf.find('form').attr('action');\n        if ('{{ config.sys.http_path }}' != '') url = url.substring('{{ config.sys.http_path }}'.length);\n        url = '{{ config.sys.http_path }}/api/0' + url;\n      }\n      var post_data = mf.find('form').serialize();\n\n      var loadtimer = setTimeout(function() {\n        Mailpile.UI.show_modal(\n          Mailpile.safe_template($('#template-modal-loading').html())\n        );\n      }, 250);\n\n      $.ajax({\n        type: \"POST\",\n        url: url,\n        data: post_data,\n        success: function(data) {\n          clearTimeout(loadtimer);\n          Mailpile.UI.hide_modal();\n          return params.callback(data);\n        },\n        error: function(xhr, status, error) {\n          // FIXME: This is a bit lame...\n          clearTimeout(loadtimer);\n          Mailpile.UI.hide_modal();\n        }\n      });\n      return false;\n    });\n  }\n};\n\nMailpile.auto_modal = function(params) {\n  var loadtimer = setTimeout(function() {\n    Mailpile.UI.show_modal(\n      Mailpile.safe_template($('#template-modal-loading').html())\n    );\n  }, 250);\n\n  var jhtml_url = Mailpile.API.jhtml_url(params.url);\n  if (params.flags) {\n    jhtml_url += ((jhtml_url.indexOf('?') != -1) ? '&' : '?') +\n                  'ui_flags=' + params.flags.replace(' ', '+');\n  }\n  var post_data = params.data || {};\n  if (params.method == \"POST\") post_data.csrf = Mailpile.csrf_token;\n  return Mailpile.API.with_template('modal-auto', function(modal) {\n    $.ajax({\n      url: jhtml_url,\n      type: params.method,\n      data: post_data,\n      success: function(data) {\n        clearTimeout(loadtimer);\n        Mailpile.auto_modal_display(jhtml_url, params, modal, data);\n      },\n      error: function(xhr, status, error) {\n        // FIXME: This is a bit lame...\n        clearTimeout(loadtimer);\n        Mailpile.UI.hide_modal();\n      }\n    });\n  }, undefined, params.flags, 'Unsafe');\n};\n\n\n$(document).on('click', '.modal-backdrop', function(e) {\n  Mailpile.UI.hide_modal();\n});\n\n$(document).on('click', '.auto-modal', function(e) {\n  var elem = $(this);\n  var title = elem.data('title') || elem.attr('title');\n  Mailpile.auto_modal({\n    url: this.href,\n    method: elem.data('method') || 'GET',\n    title: title,\n    icon: elem.data('icon'),\n    flags: elem.data('flags'),\n    header: elem.data('header'),\n    reload: elem.data('reload') || elem.hasClass('auto-modal-reload'),\n    sticky: elem.data('sticky') || elem.hasClass('auto-modal-sticky')\n  });\n  return false;\n});\n\n\n$(document).on('click', 'a.ok-got-it', function(e) {\n  var $elem = $(this);\n  var cfg_variable = $elem.data('variable');\n  var dom_remove = $elem.data('remove');\n  var cleanup = function() {\n    if (dom_remove) $('.' + dom_remove).remove();\n    Mailpile.UI.hide_modal();\n  };\n  if (cfg_variable) {\n    var args = {};\n    args[cfg_variable] = false;\n    Mailpile.API.settings_set_post(args, cleanup);\n  }\n  else cleanup();\n  return false;\n});\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/ui/global.js",
    "content": "Mailpile.render = function() {\n  // DISABLED: Resize Elements Start + On Drag\n  //Mailpile.render_dynamic_sizing();\n  //window.onresize = function(event) {\n  //  Mailpile.render_dynamic_sizing();\n  //};\n\n  // Show Mailboxes\n  if ($('#sidebar-tag-outbox').find('span.sidebar-notification').html() !== undefined) {\n    $('#sidebar-tag-outbox').show();\n  }\n\n{% if config.web.keybindings %}\n  // Initialize/Configure Keybindings\n  Mailpile.initialize_keybindings();\n{% endif %}\n\n  // Update page title from content, if necessary\n  var $page_title_data = $('.page-title-data');\n  var $page_title_icon = $page_title_data.find('.page-title-icon');\n  var $page_title_text = $page_title_data.find('.page-title-text');\n  if ($page_title_icon.length == 1) {\n    $page_title_icon.clone().appendTo($('#page-title-icon').html(''));\n    $('.topbar-logo a').addClass('mobile-hide');\n    $('#page-title-icon').removeClass('mobile-hide').addClass('mobile-block');\n  }\n  else {\n    $('.topbar-logo a').removeClass('mobile-hide');\n    $('#page-title-icon').addClass('mobile-hide').removeClass('mobile-block');\n  }\n  if ($page_title_text.length == 1) {\n    Mailpile.update_title($page_title_text.html());\n    $page_title_text.clone().appendTo($('#page-title-text').html(''));\n    $('#page-title-text').removeClass('mobile-hide');\n    $('.topbar-logo-name a').addClass('mobile-hide');\n  }\n  else {\n    $('.topbar-logo-name a').removeClass('mobile-hide');\n    $('#page-title-text').addClass('mobile-hide').html('');\n  };\n\n  // This fixes some of the drag-drop misbehaviours; first we disable the\n  // native HTML5 drag-drop of <a> elements...\n  $('.pile-message a').on('dragstart', function(ev) {return false;});\n};\n\n\nMailpile.update_title = function(message) {\n  var ct = document.title;\n  suffix = ct.substring(ct.indexOf('|'));\n  document.title = message.replace(/&amp;/, '&') + ' ' + suffix;\n};\n\n\nMailpile.render_dynamic_sizing = function() {\n  var sidebar_width  = $('#sidebar').width();\n  var content_width  = $(window).width() - sidebar_width;\n  var content_height = $(window).height() - 62;\n  if (content_width < 10) {\n    /* This means we are in portrait mode */\n    sidebar_width = 0;\n    content_width = $(window).width();\n    content_height -= 80;\n  }\n  var content_tools_height    = $('#content-tools').height();\n  var new_content_width       = $(window).width() - sidebar_width;\n  var new_content_view_height = content_height - content_tools_height;\n\n  $('#content-tools').css('position', 'fixed');\n  $('.sub-navigation').width(content_width);\n  $('#thread-title').width(content_width);\n\n  // Set Content View\n  $('#content, #content-wide').css({'height': content_height});\n  $('#content-tools, .sub-navigation, .bulk-actions').width(new_content_width);\n  $('#content-view').css({'height': new_content_view_height, 'top': content_tools_height});\n};\n\n\n/* Mailpile - UI - Make fingerprints nicer */\nMailpile.nice_fingerprint = function(fingerprint) {\n  // FIXME: I'd really love to make these individual pieces color coded\n  // Pertaining to the hex value pairings & even perhaps toggle-able icons\n  return fingerprint.split(/(....)/).join(' ');\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/ui/init.js",
    "content": "/* UI */\n\nMailpile.UI = {\n  content_setup: [],\n  modal_options: { backdrop: true, keyboard: true, show: true, remote: false },\n  Favico: new Favico({animation:'none'}),\n\n  Crypto: {},\n  Sidebar: {},\n  Modals: {},\n  Tooltips: {},\n  Message: {},\n  Search: {}\n};\n\n\nMailpile.UI.prepare_new_content = function(content) {\n  var $content = $(content);\n\n  // Iterate through the list of setup callbacksk, so different parts of\n  // the JS app, plugins included, can mess with the new content.\n  for (var i in Mailpile.UI.content_setup) {\n    Mailpile.UI.content_setup[i]($content);\n  }\n\n  // Check if this update tells us new things about the Inbox unread count,\n  // update the Favico if so.\n  // FIXME: This will not update the favico if the sidebar is not visible.\n  var inbox_new = $content.find('.sidebar-tag-inbox').data('new');\n  if (inbox_new !== undefined) {\n    setTimeout(function() { Mailpile.UI.Favico.badge(inbox_new); }, 250);\n  }\n};\n\nMailpile.UI.get_modal = function() {\n  return $(\"#modal-full\");\n};\n\nMailpile.UI.is_modal_active = function() {\n  var modal = Mailpile.UI.get_modal();\n  var modalData = modal.data(\"bs.modal\");\n  if(modalData === undefined) {\n    // The modal has not yet been initialized.\n    return false;\n  } else {\n    return modalData.isShown;\n  }\n};\n\nMailpile.UI.hide_modal = function() {\n  if (Mailpile.UI.is_modal_active()) {\n    Mailpile.UI.get_modal().modal('hide');\n  }\n  $('.modal-backdrop').remove();\n};\n\nMailpile.UI.show_modal = function(html) {\n  var modal = Mailpile.UI.get_modal();\n  if (html) {\n    modal.html(html);\n  }\n  modal.modal(Mailpile.UI.modal_options);\n  Mailpile.UI.prepare_new_content(modal);\n  return modal;\n};\n\nMailpile.UI.init = function() {\n  // BRE: disabled for now, it doesn't really work\n  // Show Typeahead\n  //Mailpile.activities.render_typeahead();\n\n  // Start Eventlog\n  setTimeout(function() {\n\n    // make event log start async (e.g. for proper page load event handling)\n    EventLog.timer = $.timer();\n    EventLog.timer.set({ time : 22500, autostart : false });\n    EventLog.poll();\n\n    // Run Composer Autosave\n    Mailpile.Composer.AutosaveTimer.play();\n    Mailpile.Composer.AutosaveTimer.set({ time : 20000, autostart : true });\n\n  }, 200);\n\n  // Register callbacks etc on new content: the whole page is new!\n  Mailpile.UI.prepare_new_content(document);\n};\n\n\nMailpile.UI.tag_icons_as_lis = function() {\n  var icons_html = '';\n  $.each(Mailpile.theme.icons, function(key, icon) {\n    icons_html += ('<li class=\"modal-tag-icon-option ' + icon +\n                   '\" data-icon=\"' + icon + '\"></li>');\n  });\n  return icons_html;\n};\n\n\nMailpile.UI.tag_colors_as_lis = function() {\n  var sorted_colors =  _.keys(Mailpile.theme.colors).sort();\n  var colors_html = '';\n  $.each(sorted_colors, function(key, name) {\n    var hex = Mailpile.theme.colors[name];\n    colors_html += ('<li><a href=\"#\" class=\"modal-tag-color-option\" ' +\n                    'style=\"background-color: ' + hex + '\" data-name=\"' +\n                    name + '\" data-hex=\"' + hex + '\"></a></li>');\n  });\n  return colors_html;\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/ui/keybindings.js",
    "content": "// Providing Keybinding/Keyboard shortcuts via Mousetrap\nMailpile.initialize_keybindings = function() {\n  Mousetrap.bind(\"?\", function() { Mailpile.display_keybindings(); });\n  Mousetrap.bindGlobal(\"esc\", function() {\n    $('input[type=text]').blur();\n    $('textarea').blur();\n  });\n\n  // Map user/system configured bindings\n  for (item in Mailpile.keybindings) {\n    var keybinding = Mailpile.keybindings[item];\n    if (keybinding.global) {\n        Mousetrap.bindGlobal(keybinding.keys, keybinding.callback);\n    } else {\n        Mousetrap.bind(keybinding.keys, keybinding.callback);\n    }\n  }\n};\n\nMailpile.keybinding_move_messages = function(op, keep_new) {\n  // Has Messages\n  var $context = Mailpile.UI.Selection.context(\".selection-context\");\n  var selection = Mailpile.UI.Selection.selected($context);\n  if (selection.length < 1) {\n    console.log('FIXME: Provide helpful / unobstrusive UI feedback that tells a user they hit a keybinding, then fades away');\n    return;\n  }\n\n  // If there is a button in the UI, prefer to click that to keep behaviours\n  // consistent. If not we fall through to direct tagging etc.\n  var $button;\n  if (op.startsWith('!')) {\n    $button = $context.find(\"a.bulk-action-tag-op[data-op='\"+ op.substring(1) +\"']\");\n    op = '';\n  }\n  else {\n    $button = $context.find(\"a.bulk-action-tag-op[data-tag='\"+ op +\"']\");\n  }\n  if ($button.length) {\n    $button.eq(0).trigger('click');\n    return;\n  }\n\n  var tids = $context.find(\".pile-results\").data(\"tids\");\n  var delete_tags = ((tids || \"\") + \"\").split(/\\s+/);\n  if (!keep_new) delete_tags.push('new');\n\n  Mailpile.UI.Tagging.tag_and_update_ui({\n    add: op,\n    del: delete_tags,\n    mid: selection,\n    context: $context.find('.search-context').data('context')\n  }, 'move');\n\n  Mailpile.UI.Selection.select_none($context);\n  Mailpile.bulk_actions_update_ui();\n};\n\nMailpile.keybinding_mark_read = function() {\n  var $context = Mailpile.UI.Selection.context(\".selection-context\");\n  Mailpile.bulk_action_read(undefined, function() {\n    Mailpile.UI.Selection.select_none($context);\n  });\n};\n\nMailpile.keybinding_mark_unread = function() {\n  var $context = Mailpile.UI.Selection.context(\".selection-context\");\n  Mailpile.bulk_action_unread(undefined, function() {\n    Mailpile.UI.Selection.select_none($context);\n  });\n};\n\nMailpile.keybinding_undo_last = function() {\n  var $undo = $('#notification-bubbles').find('a.notification-undo');\n  if ($undo.length) {\n    $undo.eq(0).trigger('click'); //closest('.notification-bubble').css({'background': '#770'});\n  }\n  else {\n    // FIXME: Do this yellow thing with classes\n    $('#notifications-header').css({'background': '#770'});\n    setTimeout(function() {\n        $('#notifications-header').css({'background': ''});\n    }, 250);\n  }\n};\n\n\nMailpile.keybinding_adjust_viewport = function($last) {\n  var $container = $('#content-view, #content-tall-view').eq(0);\n  var scroll_top = $container.scrollTop();\n  var last_top = $last.position().top - 100;\n  $container.animate({ scrollTop: scroll_top + last_top }, 150);\n\n  // Ensure that browser hotkeys focus on the right message too\n  $last.find(\".subject a\").focus();\n\n  // Moving around closes viewed messages\n  $('#close-message').trigger('click');\n};\n\nMailpile.keybinding_selection_up = function() {\n  var $last = Mailpile.bulk_action_selection_up();\n  Mailpile.keybinding_adjust_viewport($last);\n};\n\nMailpile.keybinding_selection_extend = function() {\n  var $last = Mailpile.bulk_action_selection_down('keep');\n  Mailpile.keybinding_adjust_viewport($last);\n};\n\nMailpile.keybinding_selection_down = function() {\n  var $last = Mailpile.bulk_action_selection_down();\n  Mailpile.keybinding_adjust_viewport($last);\n};\n\nMailpile.keybinding_select_all_matches = function() {\n  Mailpile.bulk_action_select_all();\n  Mailpile.select_all_matches();\n};\n\nMailpile.keybinding_reply = function(many) {\n  var $context = Mailpile.UI.Selection.context(\".selection-context\");\n  if (!many) {\n    var $rbuttons = $context.find('a.message-action-reply-all');\n    if ($rbuttons.length) return $rbuttons.eq(0).trigger('click');\n  }\n\n  // Which messages are we replying to?\n  var selection = Mailpile.UI.Selection.selected($context);\n  if (selection.length < 1) {\n    Mailpile.keybinding_selection_up();\n    selection = Mailpile.UI.Selection.selected($context);\n    if (selection.length < 1) return;\n  }\n  if (!many) selection = [selection[0]];\n\n  Mailpile.Message.DoReply(selection, true);\n};\n\nMailpile.keybinding_forward = function() {\n  var $context = Mailpile.UI.Selection.context(\".selection-context\");\n\n  var $fbuttons = $context.find('a.message-action-forward');\n  if ($fbuttons.length) return $fbuttons.eq(0).trigger('click');\n\n  var selection = Mailpile.UI.Selection.selected($context);\n  if (selection.length < 1) {\n    Mailpile.keybinding_selection_up();\n    selection = Mailpile.UI.Selection.selected($context);\n    if (selection.length < 1) return;\n  }\n\n  Mailpile.Message.DoForward(selection);\n};\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/ui/notifications.js",
    "content": "/* Notifications - UI notification at top of window */\n\nMailpile.expire_canceled_notifictions = function() {\n  var expired = new Date().getTime() - (3600 * 1000 * 16);\n  for (item in Mailpile.local_storage) {\n    if (item.indexOf('canceled-') == '0'\n        && Mailpile.local_storage[item] < expired) {\n      delete Mailpile.local_storage[item];\n    }\n  }\n};\n\nMailpile.pushy_notifications_area = function() {\n  return ($('#notifications-header').css('display') == 'none');\n};\n\nMailpile.uncancel_notification = function(not_id) {\n  delete Mailpile.local_storage['canceled-' + not_id];\n};\n\nMailpile.cancel_notification = function(not_id, $existing, replace, record) {\n  // Cancel existing notification, if any\n  var $existing = $existing || $('#event-' + not_id);\n  if ($existing.length > 0) {\n    clearTimeout($existing.data('timeout_id'));\n    if (replace) {\n      return $existing;\n    }\n    else {\n      if (record) {\n        not_id = $existing.attr('id').substring(6);\n        Mailpile.local_storage['canceled-' + not_id] = new Date().getTime();\n      }\n      $existing.slideUp('normal', function() {\n        $(this).remove();\n        if ($('.notification-bubble').length < 1) {\n          $('.notifications-close-all span').hide();\n          $('#sidebar').css('bottom', '0px');\n        }\n        else if (Mailpile.pushy_notifications_area()) {\n          var $notif = $('#notifications');\n          $('#sidebar').css('bottom', $notif.height() + 'px');\n        }\n      });\n    }\n  }\n  return undefined;\n};\n\nMailpile.raise_mail_source_mailbox_limit = function(not_id, src_id, howhigh) {\n  var settings = {};\n  settings['sources.' + src_id + '.discovery.max_mailboxes'] = howhigh;\n  Mailpile.API.settings_set_post(settings, function(result) {\n    Mailpile.cancel_notification(not_id);\n  });\n};\n\nMailpile.certificate_error_details = function(server, event_id) {\n  var url = Mailpile.API.U(\n    '/crypto/tls/getcert/?host=' + server + '&ui_tls_failed=True');\n  Mailpile.auto_modal({ url: url, method: 'POST', sticky: true });\n  //Mailpile.cancel_notification(event_id);\n};\n\nMailpile.profile_edit = function(profile_id, section) {\n  var url = Mailpile.API.U(\n    '/profiles/edit/?rid=' + profile_id +\n    '&ui_open=' + section +\n    '&ui_flags=reload');\n  Mailpile.auto_modal({ url: url, method: 'GET' });\n};\n\nMailpile.mailsource_login = function(mailsource_id, event_id) {\n  var url = Mailpile.API.U(\n    '/settings/set/password/?ui_force_login=1&ui_oneshot=1&mailsource='\n    + mailsource_id);\n  Mailpile.auto_modal({\n    url: url,\n    title: '{{ _(\"Password Required\") }}',\n    method: 'POST', sticky: true });\n  //Mailpile.cancel_notification(event_id, false, false, true);\n};\n\nMailpile.mailsource_oauth2 = function(mailsource_id, event_id) {\n  var url = Mailpile.API.U('/setup/oauth2/?mailsource=' + mailsource_id);\n  Mailpile.auto_modal({ url: url, method: 'POST', sticky: true });\n  //Mailpile.cancel_notification(event_id, false, false, true);\n};\n\nMailpile.user_host_oauth2 = function(username, hostname, event_id) {\n  var url = Mailpile.API.U('/setup/oauth2/?username=' + username + '&hostname=' + hostname);\n  Mailpile.auto_modal({ url: url, method: 'POST' });\n  //Mailpile.cancel_notification(event_id, false, false, true);\n};\n\n\nMailpile.notification = function(result) {\n  Mailpile.expire_canceled_notifictions();\n\n  // Create CSS friend event_id OR fake-id\n  if (result.event_id !== undefined) {\n    result.event_id = result.event_id.split('.').join('-');\n  } else {\n    result.event_id = 'fake-id-' + Math.random().toString(24).substring(5);\n  }\n\n  // Message\n  var default_messages = {\n    \"success\" : \"Success, we did what you asked\",\n    \"info\"    : \"Here is a basic info update\",\n    \"debug\"   : \"This is a simple debug message\",\n    \"warning\" : \"This here be a warning to you\",\n    \"error\"   : \"You have discovered an error\"\n  }\n  if (result.message  === undefined) {\n    result.message = default_messages[result.status];\n  }\n\n  // Default Options\n  if (result.message2 === undefined) {\n    if (result.data && result.data.name) {\n      result.message2 = result.message;\n      result.message = result.data.name;\n    }\n    else {\n      result.message2 = '';\n    }\n  }\n  if (result.undo        === undefined) result.undo = false;\n  if (result.type        === undefined) result.type = 'notify';\n  if (result.complete    === undefined) result.complete = 'hide';\n  if (result.action      === undefined) result.action = '';\n  if (result.action_js   === undefined) result.action_js = '';\n  if (result.action_url  === undefined) result.action_url = '';\n  if (result.action_cls  === undefined) result.action_cls = '';\n  if (result.action_text === undefined) result.action_text = '';\n  if (result.icon        === undefined) result.icon = 'icon-inbox';\n  if (result.timeout     === undefined) {\n    if (result.flags == \"c\") {\n      result.timeout = 8000; // Event complete, timeout quickly\n    }\n    else {\n      result.timeout = 360000000; // 100 hours - await completion\n    }\n  }\n\n  // Undo & Icon\n  if (result.command !== 'tag' && result.type === 'nagify') {\n    result.undo = false;\n    result.icon = 'icon-signature-unknown';\n  }\n  else if (result.command === 'tag') {\n    result.undo = (result.status == \"success\");\n    result.icon = 'icon-tag';\n  }\n  else if (result.source && result.source.indexOf('.mail_source.') == 0) {\n    // Mail source specific notification logic\n    if (!result.data.enabled) return result.event_id;\n\n    if ((result.data.discovery_error == \"toomany\") &&\n        (!result.data.rescan || !result.data.rescan.running) &&\n        (!result.data.copying || !result.data.copying.running)) {\n      // Mail sources have a limit on how many mailboxes are auto-added\n      // during discovery, to prevent runaway bloat if we're pointed at\n      // an over-large directory or badly behaved IMAP server. This means\n      // users need a UI to raise the limit.\n      //\n      var lim = result.data.discovery_limit;\n\n      var msg = '{{_(\"Found over (LIMIT) mailboxes\")|escapejs}}';\n      var ri = msg.indexOf('(LIMIT)');\n      if (ri >= 0) {\n        msg = msg.substring(0, ri) + lim + msg.substring(ri+7);\n      }\n\n      if (lim < 250) {\n        lim = lim * 2;\n      } else {\n        lim = lim + 250;\n      }\n\n      result.message2 = msg;\n      result.action_text = '{{_(\"continue adding more\")|escapejs}}';\n      result.action_js = (\n          ' href=\"javascript:Mailpile.raise_mail_source_mailbox_limit('\n          + '\\'' + result.event_id + '\\', '\n          + '\\'' + result.data.id + '\\', '\n          + lim + ');\" ');\n    }\n  }\n\n  // If Undo, extend hide\n  if (result.undo && result.complete === 'hide') {\n    result.timeout = 20000;\n  }\n\n  // If user has canceled this notification, don't bug him again.\n  if (Mailpile.local_storage['canceled-' + result.event_id]) {\n    return result.event_id;\n  }\n\n  // Show Notification\n  var $elem = Mailpile.cancel_notification(result.event_id, undefined, 'keep');\n  var notification_template = Mailpile.unsafe_template($('#template-notification-bubble').html());\n  // Remove excess whitespace from notification to avoid creating TextNodes in the\n  // DOM. Fixes a subtle memory leak (https://github.com/mailpile/Mailpile/issues/1931).\n  var notification_html = notification_template(result).trim();\n  if ($elem) {\n    $elem.replaceWith(notification_html);\n  }\n  else {\n    var bubbles = $('#notification-bubbles');\n    if (bubbles.children().length < 15) {\n      $('.notifications-close-all span').show();\n      bubbles.prepend($(notification_html).slideDown('normal', function() {\n        if (Mailpile.pushy_notifications_area()) {\n          var $notif = $('#notifications');\n          $('#sidebar').css('bottom', $notif.height() + 'px');\n        }\n      }));\n    }\n  }\n\n  // If Not Nagify, default\n  if (result.complete === 'hide' && result.type !== 'nagify') {\n    var to_id = setTimeout(function() {\n      Mailpile.cancel_notification(result.event_id);\n    }, result.timeout);\n    $('#event-' + result.event_id).data('timeout_id', to_id);\n  }\n  else if (result.complete == 'redirect') {\n    setTimeout(function() {\n      Mailpile.go(result.action);\n    }, 4000);\n  }\n\n  return result['event_id'];\n};\n\n/* Use when stuff is loading in the backgrount to show progress */\nMailpile.notify_working = function(message, timeout, blank) {\n  var events = [undefined, undefined];\n  var $content = $('#content-view, #content-tall-view').parent();\n  var notify = function() {\n    var silly = Math.floor(Math.random() * Mailpile.silly_strings.misc.length);\n    events[1] = Mailpile.notification({\n      event_id: events[1],\n      message: message || \"{{_('Working...')|escapejs}}\",\n      message2: Mailpile.silly_strings.misc[silly],\n      status: 'warning',\n      icon: 'icon-robot'\n    });\n    if (blank) $content.css({\"opacity\": 0.25});\n    events[0] = setTimeout(notify, 5000);\n  };\n  events[0] = setTimeout(notify, timeout);\n  cancel = function(delay) {\n    // This cancels the event. To avoid weird flickering, if the notification\n    // has already been displayed, we leave it up for a little longer.\n    if (events[0]) clearTimeout(events[0]);\n    setTimeout(function() {\n      if (blank) $content.css({\"opacity\": \"\"});\n      if (events[1]) Mailpile.cancel_notification(events[1]);\n    }, 1250);\n  }\n  setTimeout(cancel, 120000); // After two minutes just give up...\n  return cancel;\n};\n\n/* Notification - Close all */\n$(document).on('click', '.notifications-close-all', function() {\n  $('.notification-close').click();\n});\n\n\n\n/* Notification - Close */\n$(document).on('click', '.notification-close', function() {\n  if ($(this).data('type') === 'nagify') {\n    var next_nag = new Date().getTime() + Mailpile.nagify;\n    Mailpile.API.settings_set_post({ 'web.nag_backup_key': next_nag });\n  }\n  Mailpile.cancel_notification('', $(this).parent(), undefined, true);\n});\n\n\n/* Notification - Undo */\n$(document).on('click', '.notification-undo', function() {\n  var done = Mailpile.notify_working(\"{{_('Undoing...')|escapejs}}\", 250, 'blank');\n  var event_id = $(this).data('event_id').split('.').join('-');\n  Mailpile.API.logs_events_undo_post({ event_id: event_id }, function(result) {\n    if (result.status === 'success') {\n      window.location.reload(true);\n    }\n    else {\n      done();\n      alert(\"{{ _('Oops. Mailpile failed to complete your task.') }}\");\n    }\n  });\n});\n\n\n/* Notification - Nag */\n$(document).on('click', '.notification-nag', function(e) {\n  e.preventDefault();\n  var href = $(this).attr('href');\n  var next_nag = new Date().getTime() + Mailpile.nagify;\n  Mailpile.API.settings_set_post({ 'web.nag_backup_key': next_nag }, function() {\n    Mailpile.go(href);\n  });\n});\n\n\n/* Set up some default notifications by listening to the Event log */\nEventLog.subscribe('.*(Add|Edit)Profile', function(ev) {\n  console.log('AddProfile event: ' + ev.data.keygen_started);\n  if (ev.data.keygen_started > 0) {\n      ev.icon = 'icon-lock-closed';\n      var $icon = $('.profile-' + ev.data.profile_id + '-key.icon');\n      if (ev.data.keygen_finished > 0) {\n          $icon.removeClass('unconfigured');\n          $icon.removeClass('icon-clock').removeClass('icon-lock-open');\n          $icon.addClass('configured').addClass('icon-lock-closed');\n          ev.timeout = 60000; // Keep completed notification up for 1 minute\n      }\n      else {\n          $icon.removeClass('configured');\n          $icon.removeClass('icon-lock-open').removeClass('icon-lock-closed');\n          $icon.addClass('unconfigured').addClass('icon-clock');\n          if (ev.data.keygen_gotlock > 0) {\n              ev.action_url = \"{{ U('/page/entropy/') }}\";\n              ev.action_cls = 'auto-modal';\n              ev.action_text = '{{_(\"learn more\")|escapejs}}';\n              ev.message2 = '{{_(\"This may take some time!\")|escapejs}}';\n          }\n      }\n      Mailpile.notification(ev);\n  }\n});\n\nEventLog.subscribe('.*mail_source.*', function(ev) {\n  //\n  // Mail source notifications behave differently depending on which\n  // page in the UI you are. On most pages, they behave like normal event\n  // notifications, popping up and then disappearing 20 seconds later, and\n  // can be silenced for a while by clicking the X.\n  //\n  // On the profile page however, these messages are sticky and persistent,\n  // and they can't be silenced. The rationale for this is that the profile\n  // page is the go-to place for account configuration, and the event\n  // provides critical information in that context.\n  //\n  var $src = $('.source-' + ev.data.id);\n  var conn_error = (ev.data.connection &&\n                    ev.data.connection.error &&\n                    ev.data.connection.error[0]);\n  if ($src.length > 0) {\n    var $icon = $src.find('.icon');\n    if ((conn_error &&\n         conn_error != 'tls' &&\n         conn_error != 'auth' &&\n         conn_error != 'oauth2') ||\n        (!ev.data.enabled)) {\n      $icon.removeClass('configured').removeClass('unconfigured');\n      $icon.addClass('misconfigured');\n      $src.attr('title', $src.data('title') + '\\n\\n' +\n                         '{{_(\"Error\")|escapejs}}: ' +  ev.message);\n    }\n    else {\n      $icon.removeClass('misconfigured').removeClass('unconfigured');\n      $icon.addClass('configured');\n    }\n    if (ev.data.enabled) Mailpile.uncancel_notification(ev.event_id);\n  }\n  else {\n    ev.timeout = 20000;\n  }\n  if (((conn_error && conn_error != 'tls') || (!ev.data.enabled)) &&\n      (ev.data.profile_id)) {\n    ev.action_js = (\"onclick=\\\"Mailpile.profile_edit('\"\n       + ev.data.profile_id + \"','sources');\\\"\");\n    ev.action_text = '{{_(\"edit settings\")|escapejs}}';\n  }\n  if (conn_error == 'tls') {\n    ev.action_text = '{{_(\"details\")|escapejs}}';\n    ev.action_js = (\"onclick=\\\"Mailpile.certificate_error_details('\"\n       + ev.data.connection.error[2] + \"','\" + ev.event_id + \"');\\\"\");\n  }\n  else if (conn_error == 'auth') {\n    ev.action_text = '{{_(\"please log in\")|escapejs}}';\n    ev.action_js = (\"onclick=\\\"Mailpile.mailsource_login('\"\n       + ev.data.id + \"','\" + ev.event_id + \"');\\\"\");\n    if (!EventLog.seen_event_recently(ev.data.profile_id)) {\n      if (!$('.modal-dialog').is(':visible')) {\n        EventLog.just_saw_event(ev.data.profile_id);\n        Mailpile.mailsource_login(ev.data.id, ev.event_id);\n      }\n    }\n  }\n  else if (conn_error == 'oauth2') {\n    ev.action_text = '{{_(\"grant access\")|escapejs}}';\n    ev.action_js = (\"onclick=\\\"Mailpile.mailsource_oauth2('\"\n       + ev.data.id + \"','\" + ev.event_id + \"');\\\"\");\n    console.log(ev.data);\n    if (!EventLog.seen_event_recently(ev.data.profile_id)) {\n      if (!$('.modal-dialog').is(':visible')) {\n        EventLog.just_saw_event(ev.data.profile_id);\n        Mailpile.mailsource_oauth2(ev.data.id, ev.event_id);\n      }\n    }\n  }\n  ev.icon = 'icon-mailsource';\n  Mailpile.notification(ev);\n});\nEventLog.subscribe('.*compose.Sendit', function(ev) {\n  if (ev.data.delivered == ev.data.recipients) {\n    ev.icon = 'icon-outbox';\n  }\n  else if (ev.data.last_error) {\n    ev.icon = 'icon-signature-unknown';\n    ev.message2 = ev.data.last_error\n  }\n\n  if (ev.data.last_error_details) {\n    if (ev.data.last_error_details.oauth_error) {\n      ev.action_text = '{{_(\"grant access\")|escapejs}}';\n      ev.action_js = (\"onclick=\\\"Mailpile.user_host_oauth2('\"\n         + ev.data.last_error_details.username + \"','\"\n         + ev.data.host + \"','\"\n         + ev.event_id + \"');\\\"\");\n      Mailpile.uncancel_notification(ev.event_id);\n      ev.timeout = 1200000;\n    }\n    else if (ev.data.last_error_details.tls_error) {\n      ev.action_text = '{{_(\"details\")|escapejs}}';\n      ev.action_js = (\"onclick=\\\"Mailpile.certificate_error_details('\"\n         + ev.data.last_error_details.server + \"','\" + ev.event_id + \"');\\\"\");\n    }\n  }\n\n  Mailpile.notification(ev);\n});\nEventLog.subscribe('.*HealthCheck', function(ev) {\n  if (ev.data.healthy) {\n    ev.icon = 'icon-checkmark';\n  }\n  else {\n    ev.icon = 'icon-signature-unknown';\n    ev.timeout = 1200000;\n  }\n  Mailpile.notification(ev);\n});\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/ui/selection.js",
    "content": "/* This is the shared selection code (checkboxes etc).\n**\n** Instead of a global object tracking which objects are selected, selection\n** counts are calculated on-the-fly by evaluating the DOM.\n**\n** Selection contexts are grouped using the class \"selection-context\"; any\n** checkboxes contained within the same selection context are assumed to be\n** of the same type.\n**\n** The rationale for doing this, is it allows a single screen to have\n** multiple groups of selectable items, each of which can be manipulated\n** independently of the others. This should makes selection behavior stable,\n** no matter how updates happen (AJAX, js, ...).\n*/\nMailpile.UI.Selection = (function(){\n\nvar update_callbacks = {};\n\n/* Helpers... */\nfunction _call_callbacks(context, selected) {\n  for (var i in update_callbacks) {\n    update_callbacks[i](context, selected);\n  }\n}\nfunction context(selector) {\n  return $(selector).eq(0).closest('.selection-context').eq(0);\n}\nfunction _select_all(ctx) {\n  return ctx.find('#pile-select-all-action');\n}\nfunction _each_checkbox(ctx, func) {\n  ctx.find('input[type=checkbox]').each(func);\n}\n\n\n/**\n * Register a callback function, invoked when selection state changes\n * @param {String} name - Callback name\n * @param {Function} callback - Callback function\n */\nfunction register(name, callback) {\n  update_callbacks[name] = callback;\n}\n\n/**\n * Unregister a callback function\n * @param {String} name - Callback name\n */\nfunction unregister(name) {\n  delete update_callbacks[name];\n}\n\n/**\n * Select \"these\", all visible elements on page\n * @param {String|Object} selector - A JQuery selector or DOM element\n * @param {Boolean} [no_callbacks] - Skip invoking callbacks\n * @return {Array} Array of selected values\n */\nfunction select_these(selector, no_callbacks) {\n  var ctx = context(selector);\n  var sel = [];\n  _each_checkbox(ctx, function() {\n    if (this.value == '!all') this.value = '';\n    if (this.value != '') sel.push(this.value);\n    $(this).prop('checked', true);\n  });\n  if (!no_callbacks) _call_callbacks(ctx, sel);\n  return sel;\n}\n\n/**\n * Select nothing\n * @param {String|Object} selector - A JQuery selector or DOM element\n * @param {Boolean} [no_callbacks] - Skip invoking callbacks\n * @return {Array} Array of selected values, should be []\n */\nfunction select_none(selector, no_callbacks) {\n  var ctx = context(selector);\n  _each_checkbox(ctx, function() {\n    if (this.value == '!all') this.value = '';\n    $(this).prop('checked', false);\n  });\n  if (!no_callbacks) _call_callbacks(ctx, []);\n  return [];\n}\n\n/**\n * Select \"all\", potentially including things not visible on page\n * @param {String|Object} selector - A JQuery selector or DOM element\n * @param {Boolean} [no_callbacks] - Skip invoking callbacks\n * @return {Array} Array of selected values, should be ['!all']\n */\nfunction select_all(selector, no_callbacks) {\n  var ctx = context(selector);\n  Mailpile.UI.Selection.select_these(ctx, true);\n  _select_all(ctx).val('!all');\n  if (!no_callbacks) _call_callbacks(ctx, ['!all']);\n  return ['!all'];\n}\n\n/**\n * Stop selecting \"all\"\n * @param {String|Object} selector - A JQuery selector or DOM element\n * @param {Boolean} [no_callbacks] - Skip invoking callbacks\n * @return {Array} Array of selected values\n */\nfunction select_not_all(selector, no_callbacks) {\n  var ctx = context(selector);\n  _select_all(ctx).val('');\n  var sel = Mailpile.UI.Selection.selected(ctx);\n  if (!no_callbacks) _call_callbacks(ctx, sel);\n  return sel;\n}\n\n/**\n * Returns a list of currently selected items.\n * @param {String|Object} selector - A JQuery selector or DOM element\n * @return {Array} Array of selected values or ['!all']\n */\nfunction selected(selector) {\n  var ctx = context(selector);\n  var selected = [];\n  var all = false;\n  _each_checkbox(ctx, function() {\n    var $elem = $(this);\n    if ($elem.is(':checked') && this.value) {\n      if (this.value == '!all') all = true;\n      selected.push(this.value);\n    }\n  });\n  if (all) return ['!all'];\n  return selected;\n}\n\n/**\n * Returns the number of selected items or a translation of \"all\".\n * @param {Array} Array of selected values or ['!all']\n * @return {String} Length of selection as a string\n */\nfunction human_length(selection) {\n  if (selection && selection[0] != '!all') return ('' + selection.length);\n  return '{{_(\"All\")|escapejs}}';\n}\n\nreturn {\n  'register': register,\n  'unregister': unregister,\n  'context': context,\n  'select_these': select_these,\n  'select_none': select_none,\n  'select_all': select_all,\n  // From the Zoolander school for kids who want to learn\n  // to code good and do other stuff good too!\n  'select_not_all': select_not_all,\n  'selected': selected,\n  'human_length': human_length\n}})();\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/ui/sidebar.js",
    "content": "Mailpile.UI.Sidebar.SubtagsToggle = function(tid) {\n  // Strategy: assume whatever is in the DOM is correct and take actions\n  // based on that (after all, that's what the users sees). However, we\n  // then send config updates to the backend for persistence.\n\n  var $toggle = $('.sidebar-tag-expand[data-tid='+ tid +']');\n  var $subtags = $('.subtag-of-' + tid);\n\n  var is_collapsed = $toggle.data('collapsed');\n  if (is_collapsed == 'False') is_collapsed = false;\n\n  if (is_collapsed) {\n    $toggle.find('span').removeClass('icon-arrow-right').addClass('icon-arrow-down');\n    $toggle.data('collapsed', 'False');\n    $subtags.slideDown('fast');\n  }\n  else {\n    $toggle.find('span').addClass('icon-arrow-right').removeClass('icon-arrow-down');\n    $toggle.data('collapsed', 'True');\n    $subtags.slideUp('fast');\n  }\n\n  var collapsed = [];\n  $('.sidebar-tag-expand').each(function(k, v) {\n    var $v = $(v);\n    var is_collapsed = $v.data('collapsed');\n    if (is_collapsed == 'False') is_collapsed = false;\n    if (is_collapsed) collapsed.push($v.data('tid'));\n  });\n  if (collapsed.length < 1) collapsed = ['_none_'];\n\n  Mailpile.API.settings_set_post({\n    'web.subtags_collapsed': collapsed\n  }, function(result) {\n    // Nothing\n  });\n};\n\n\nMailpile.UI.Sidebar.Sortable = function() {\n $('.sidebar-sortable').sortable({\n    placeholder: \"sidebar-tags-sortable\",\n    distance: 13,\n    scroll: false,\n    opacity: 0.8,\n    stop: function(event, ui) {\n\n      var item  = $(ui.item);\n      var tid   = item.data('tid');\n      var index = parseInt(item.index());\n\n      var get_order = function(index, base) {\n        var elem = item.parent().find('li:nth-child(' + index + ')');\n        if (elem.length > 0) {\n          var display_order = parseFloat(elem.data('display_order'));\n          if (!isNaN(display_order)) {\n            return display_order;\n          }\n        }\n        return base;\n      };\n\n      // Calculate new orders\n      var previous  = get_order(index, 0);\n      var next      = get_order(index + 2, 1000000);\n      var new_order = (parseFloat(previous) + parseFloat(next)) / 2.0;\n\n      // Save Tag Order\n      var tag_setting = Mailpile.tag_setting(tid, 'display_order', new_order);\n      Mailpile.API.settings_set_post(tag_setting, function(result) {\n        // Update Current Element\n        $(ui.item).attr('data-display_order', new_order).data('display_order', new_order);\n      });\n\t\t}\n\t}).disableSelection();\n};\n\n\nMailpile.UI.Sidebar.Draggable = function(element) {\n  $(element).draggable({\n    containment: 'window',\n    appendTo: 'body',\n    handle: '.name',\n    cursor: 'move',\n    cursorAt: { left: 15, bottom: -10 },\n    distance: 15,\n    scroll: false,\n    revert: false,\n    opacity: 1,\n    helper: function(event) {\n      // FIXME: Helper tooltip assumes only one destination for the drag,\n      //        which may not always be true!\n      var count = '';\n      var selection = Mailpile.UI.Selection.selected('#content');\n      if (selection.length >= 1) {\n        count = (' {{_(\"to\")|escapejs}} (' +\n                 Mailpile.UI.Selection.human_length(selection) + ')');\n      }\n\n      var $elem = $(this);\n      var tid = $elem.data('tid').toString();\n      var hex = Mailpile.theme.colors[$elem.data('color')];\n      var icon = $elem.data('icon');\n      var name = $elem.find('.name').html();\n      return $('<div class=\"sidebar-tag-drag ui-widget-header\" style=\"color: '\n               + hex + '\"><span class=\"' + icon + '\"></span> '\n               + name + count + '</div>');\n    },\n    start: function() {\n      Mailpile.ui_in_action += 1;\n    },\n    stop: function() {\n      setTimeout(function() {\n        Mailpile.ui_in_action -= 1;\n        console.log(\"Decremented ui_in_action: \" + Mailpile.ui_in_action);\n      }, 250);\n    }\n  });\n};\n\n\nMailpile.UI.Sidebar.Droppable = function(element, accept) {\n  $(element).droppable({\n    accept: accept,\n    activeClass: 'sidebar-tags-draggable-hover',\n    hoverClass: 'sidebar-tags-draggable-active',\n    tolerance: 'pointer',\n    greedy: true,\n    drop: function(event, ui) {\n      // This is necessary to prevent drops on elements that aren't visible.\n      var t = $(this).droppable(\"widget\")[0];\n      var e = document.elementFromPoint(event.clientX, event.clientY);\n      while (e && (t !== e)) {\n        e = e.parentNode;\n      }\n      if (t !== e) return false;\n\n      console.log(\"Dropped on sidebar!\");\n\n{#    // What should happen:\n      //    - The drop happens on a tag, this tells us which tag to *add*\n      //    - If the drop happens on something else... are we just untagging?\n      //    - For clarity, require a selection: starting a drag should select\n      //    - Can look at ui.draggable.parent() .closest()? to find container.\n      //    - Container should be annotated with whatever tags we are looking\n      //      at, to facilitate removal so the \"move\" really is a move.\n      // #}\n\n      var $context = Mailpile.UI.Selection.context(ui.draggable);\n      var tags_delete = (($context.find('.pile-results').data(\"tids\") || \"\"\n                          ) + \"\").split(/\\s+/);\n      Mailpile.UI.Tagging.tag_and_update_ui({\n        add: $(this).find('a').data('tid'),\n        del: tags_delete,\n        mid: Mailpile.UI.Selection.selected($context),\n        context: $context.find('.search-context').data('context')\n      }, 'move');\n    }\n  });\n};\n\n\nMailpile.UI.Sidebar.OrganizeToggle = function(elem) {\n  var $elem = $(elem);\n  var new_message = $elem.data('message');\n  var old_message = $elem.find('span.text').html();\n\n  // Make Editable\n  if ($elem.data('state') != 'editing') {\n    Mailpile.ui_in_action += 1;\n    Mailpile.UI.Sidebar.Sortable();\n\n    // Disable Drag & Drop\n    $('a.sidebar-tag').draggable({ disabled: true });\n\n    // Display tags that are normally hidden\n    $('li.sidebar-tag.hide').addClass('should-hide').slideDown();\n    $('li.sidebar-tag.hide a.sidebar-tag').css({'opacity': 0.5});\n\n    // Update Cursor Make Links Not Work\n    $('.sidebar-sortable li').addClass('is-editing');\n\n    // Hide Notification & Subtags\n    $('a.sidebar-tag .notification').hide();\n    $('li.sidebar-tag .sidebar-tag-expand').hide();\n    $('.sidebar-subtag').slideUp();\n\n    // Add Settings Button\n    $.each($('li.sidebar-tag'), function(key, value) {\n      var slug = $(this).data('slug');\n      $(this).append(\n        '<a class=\"sidebar-tag-settings auto-modal auto-modal-reload\"' +\n        ' title=\"Edit: ' + slug + '\"' +\n        ' href=\"' + Mailpile.API.U('/tags/edit.html?only=')+slug+'\">' +\n        '<span class=\"icon-settings\"></span></a>');\n    });\n\n    // Update Edit Button\n    $elem.data('state', 'editing');\n    $elem.find('span.icon').removeClass('icon-settings').addClass('icon-checkmark');\n\n  } else {\n\n    Mailpile.ui_in_action -= 1;\n\n    // Enable Drag & Drop\n    $('a.sidebar-tag').draggable({ disabled: false });\n\n    // Update Cursor Make Links Work\n    $('.sidebar-sortable li').removeClass('is-editing');\n\n    // Show Notification / Remove Settings Button\n    $('a.sidebar-tag .notification').show();\n    $('li.sidebar-tag .sidebar-tag-expand').show();\n    $('.sidebar-tag-settings').remove();\n\n    // Hide tags that are normally hidden\n    $('li.sidebar-tag.should-hide').slideUp();\n\n    // Update Edit Button\n    $elem.data('state', 'done');\n    $elem.find('span.icon').removeClass('icon-checkmark').addClass('icon-settings');\n  }\n\n  $elem.data('message', old_message)\n  $elem.find('span.text').html(new_message);\n};\n\n\n// Register update functions\nMailpile.UI.content_setup.push(function($content) {\n  Mailpile.UI.Sidebar.Draggable(\n    $content.find('.sidebar-tags-draggable a.sidebar-tag'));\n  Mailpile.UI.Sidebar.Droppable(\n    $content.find('.sidebar-tags-draggable'),\n   'td.draggable, td.avatar, div.thread-draggable');\n});\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/ui/tagging.js",
    "content": "/* This is shared message tagging/untagging code.\n*/\nMailpile.UI.Tagging = (function(){\n\nvar operations = {\n  'tag':     ['{{_(\"Tagged 1 message\")|escapejs}}',\n              '{{_(\"Tagged (num) messages\")|escapejs}}'],\n  'untag':   ['{{_(\"Untagged 1 message\")|escapejs}}',\n              '{{_(\"Untagged (num) messages\")|escapejs}}'],\n  'read':    ['{{_(\"Marked 1 message read\")|escapejs}}',\n              '{{_(\"Marked (num) messages read\")|escapejs}}'],\n  'unread':  ['{{_(\"Marked 1 message unread\")|escapejs}}',\n              '{{_(\"Marked (num) messages unread\")|escapejs}}'],\n  'move':    ['{{_(\"Moved 1 message\")|escapejs}}',\n              '{{_(\"Moved (num) messages\")|escapejs}}'],\n  'archive': ['{{_(\"Archived 1 message\")|escapejs}}',\n              '{{_(\"Archived (num) messages\")|escapejs}}'],\n  'trash':   ['{{_(\"Moved 1 message to trash\")|escapejs}}',\n              '{{_(\"Moved (num)  messages to trash\")|escapejs}}'],\n  'unspam':  ['{{_(\"Moved 1 message out of spam\")|escapejs}}',\n              '{{_(\"Moved (num)  messages out of spam\")|escapejs}}'],\n  'spam':    ['{{_(\"Moved 1 message to spam\")|escapejs}}',\n              '{{_(\"Moved (num) messages to spam\")|escapejs}}']\n};\n\n/**\n * Tag/untag messages and then update the visible UI to match\n * @param {String|Object} selector - A JQuery selector or DOM element\n * @param {Boolean} [no_callbacks] - Skip invoking callbacks\n * @return {Array} Array of selected values\n */\nfunction tag_and_update_ui(options, op, callback) {\n  // If there's nothing to do, don't bother the back-end.\n  if (!options.mid) return;\n  if (!options.add && !options.del) return;\n\n  var notify_done = Mailpile.notify_working(\"{{_('Tagging...')|escapejs}}\", 500);\n  options._error_callback = notify_done;\n\n  Mailpile.API.tag_post(options, function(response) {\n{#  // The output of tag_post response.result is:\n    // {\n    //   \"conversations\": true, \n    //   \"msg_ids\": [ ...(list of mids)... ],\n    //   \"tagged\": [\n    //     [ { ...(tag info)... }, [ ...(list of mids)... ] ],\n    //     ...\n    //   ],\n    //   \"untagged\": (same format as tagged),\n    //   \"state\": ...,\n    //   \"status\": \"success\"\n    // }\n    //\n    // Messages are displayed in different tag contexts and the context\n    // determines a) whether the message is displayed (inbox, new, etc),\n    // or b) whether tags are listed as labels.\n    //\n    // Untagging a message means either labels disappear, or the entire\n    // message may be gone. Tagging may add labels or change the results\n    // of a search - we handle the former here but not the latter.\n    //\n#}\n    notify_done();\n\n    if ((!response.result) || (response.status != \"success\")) {\n      // Just report errors, do nothing else.\n      Mailpile.notification(response);\n      return;\n    }\n\n    var count = response.result.msg_ids.length;\n    if (count < 1) return; // This was a no-op.\n\n    // Call callbacks, if any. We do this first, because callbacks may\n    // want to reference bits of the DOM that get changed below.\n    if (callback) callback(response);\n\n    $('.pile-results').each(function (i, context) {\n      var $context = $(context);\n\n      var tids = (($context.data(\"tids\") || \"\") + \"\").split(/\\s+/);\n      var context_tids = {};\n      for (var i in tids) { context_tids[tids[i]] = true; }\n\n      for (var i in response.result.untagged) {\n        var tag = response.result.untagged[i][0];\n        var mids = response.result.untagged[i][1];\n        var untagged = {};\n        for (var j in mids) { untagged[mids[j]] = true; }\n\n        $context.find('.pile-message').each(function(i, elem) {\n          var $elem = $(elem);\n          var mid = $elem.data('mid');\n          if (untagged[mid]) {\n            if (context_tids[tag.tid]) {\n              // Message should no longer appear in this context at all\n              $elem.slideUp(200, function() { $(this).remove(); });\n            }\n            else {\n              // Remove any tag labels\n              $elem.removeClass('in_' + tag.slug)\n                   .find('.pile-message-tag-' + tag.tid).remove();\n\n              // Remove tag ID from tids list\n              var tids = $elem.data('tids').split(/,/);\n              for (var i = tids.length-1; i >= 0; i--) {\n                if (tids[i] === tag.tid) tids.splice(i, 1);\n              }\n              $elem.data('tids', tids.join(','));\n            }\n          }\n        });\n      }\n\n      // Add tag labels to elements as necessary\n      for (var i in response.result.tagged) {\n        var tag = response.result.tagged[i][0];\n        if (!context_tids[tag.tid]) {\n          var mids = response.result.tagged[i][1];\n          var tagged = {};\n          for (var j in mids) { tagged[mids[j]] = true; }\n\n          var hex = Mailpile.theme.colors[tag.label_color];\n          var tag_html = (\n            '<span class=\"pile-message-tag pile-message-tag-' + \n                          tag.tid + '\" style=\"color: ' + hex + ';\">' +\n                          '<span class=\"pile-message-tag-icon ' + tag.icon +\n                          '\"></span></span>'\n          );\n          $context.find('.pile-message').each(function(i, elem) {\n            var $elem = $(elem);\n            var mid = $elem.data('mid');\n            if (tagged[mid]) {\n              if (tag.flag_hides) {\n                // If tag is a hiding tag, make element disappear\n                $elem.slideUp(200, function() { $(this).remove(); });\n              }\n              else {\n                $elem.addClass('in_' + tag.slug);\n                if (tag.label) {\n                  $elem.find('span.item-tags').append(tag_html);\n                }\n                // Remove tag ID from tids list\n                var tids = $elem.data('tids').split(/,/);\n                tids.push(tag.tid);\n                $elem.data('tids', tids.join(','));\n              }\n            }\n          });\n        }\n      }\n\n    });\n\n    // Update Bulk UI; do this after a delay, so the fade-outs can complete.\n    setTimeout(Mailpile.bulk_actions_update_ui, 300);\n\n    // Override message text if we think we know better\n    if (op && operations[op]) {\n      if (count == 1) {\n        response.message = operations[op][0];\n      }\n      else {\n        var msg = operations[op][1];\n        var ofs = msg.indexOf('(num)');\n        response.message = (msg.substring(0, ofs) +\n                            count +\n                            msg.substring(ofs+5));\n                        \n      }\n    }\n\n    // Show notification\n    Mailpile.notification(response);\n  });\n}\n\nreturn {\n  'tag_and_update_ui': tag_and_update_ui\n}})();\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/ui/terminal.js",
    "content": "Mailpile.Terminal = {\n    settings: {\n        prompt: 'mailpile>',\n        enabled: false,\n        fullscreen: false,\n        session: null,\n    }\n};\n\nMailpile.Terminal.atBottom = function() {\n    var d = document.getElementById(\"terminal_output\");\n    return (d.scrollHeight - d.scrollTop === d.clientHeight);\n};\n\nMailpile.Terminal.scrollDown = function() {\n    var d = document.getElementById(\"terminal_output\");\n    d.scrollTop = d.scrollHeight;\n    $(\"#terminal_input input\").focus();\n};\n\nMailpile.Terminal.updateDebugLog = function(new_lines) {\n    if (!new_lines || !new_lines.length) return;\n    var wasAtBottom = Mailpile.Terminal.atBottom();\n    for (i in new_lines) {\n        var log_line_id = new_lines[i][0];\n        var log_ts = Math.floor(new_lines[i][1] * 1000);\n        var log_time = (new Date(log_ts)).toLocaleTimeString();\n        var log_msg = new_lines[i][2].replace(/&/g, '&amp;')\n                                     .replace(/>/g, '&gt;')\n                                     .replace(/</g, '&lt;');\n        var $existing = $('#log_line_' + log_line_id);\n        if ($existing.length) {\n            $existing.find('.ts').html(log_time);\n            $existing.find('.log').html(log_msg);\n        }\n        else {\n            $(\"#terminal_output\").append(\n                '<div data-ts=\"'+ log_ts +'\" id=\"log_line_'+ log_line_id +'\" class=\"log\">'+\n                  '[<span class=\"ts\">'+ log_time +'</span>] '+\n                  '<span class=\"log\">'+ log_msg +'</span>'+\n                '</div>');\n        }\n    }\n    if (wasAtBottom) {\n        var $terminal = $(\"#terminal_output\");\n        var $elements = $terminal.children(\"div\");\n        $elements.sort(function(a, b) {\n            a = a.getAttribute('data-ts');\n            b = b.getAttribute('data-ts');\n            if (a < b) return -1;\n            if (a > b) return 1;\n            return 0;\n        });\n        $elements.detach();\n        $elements = $elements.slice(-500);\n        $elements.appendTo($terminal);\n        Mailpile.Terminal.scrollDown();\n    }\n};\n\nMailpile.Terminal.output = function(mode_out) {\n    // We're going to inject new elements timestamped 2 seconds in the future,\n    // to make any debug log lines sort *above* the new output for a short\n    // period of time, so they're less disruptive.\n    var elem_ts = (new Date().getTime()) + 2000;\n    var $output = $('<div data-ts=\"'+elem_ts+'\">').addClass(\"output\");\n\n    if (mode_out[0] == \"html\") {\n        // FIXME: The HTML mode will need a fair bit of CSS to work well.\n        var $new_elem = $(mode_out[1]);\n        $new_elem.find('div.footer-nav').remove();\n        $output.addClass('html_blob')\n               .addClass('sub-content')\n               .addClass('sub-content-view').append($new_elem);\n    }\n    else {\n        $output.html(mode_out[1].replace(/&/g, '&amp;')\n                                .replace(/>/g, '&gt;')\n                                .replace(/</g, '&lt;'));\n    }\n    $(\"#terminal_output\").append($output);\n    Mailpile.Terminal.scrollDown();\n};\n\nMailpile.Terminal.handleResponse = function(r) {\n    if (r.status == \"success\") {\n        if (r.result.result.error) {\n            Mailpile.Terminal.output([\"text\", \"Error: \" + r.result.result.error]);\n        } else {\n            Mailpile.Terminal.output(r.result.result);\n        }\n    } else if (r.status == \"error\") {\n        Mailpile.Terminal.output([\"text\", r.message]);\n    }\n}\n\nMailpile.Terminal.submitCommand = function(ev) {\n    Mailpile.Terminal.executeCommand($(\"#terminal_input input\").val());\n    return false;\n};\n\nMailpile.Terminal.executeCommand = function(cmd) {\n    $(\"#terminal_input input\").val(\"\");\n    if (cmd == \"/full\") return Mailpile.Terminal.makeFull();\n    if (cmd == \"/small\") return Mailpile.Terminal.makeSmall();\n    if (cmd == \"/clear\") return Mailpile.Terminal.clearOutput();\n    if (cmd == \"/close\") return Mailpile.Terminal.session_end();\n    if (!cmd) return Mailpile.Terminal.executeCommand('help/splash web_terminal');\n    var chars = 10 * $('#terminal #console').width() / $('#terminal #prompt').width();\n    Mailpile.Terminal.output([\"text\", \"mailpile> \" + cmd]);\n    Mailpile.API.terminal_command_post({\n            command: cmd,\n            width: chars,\n            sid: Mailpile.Terminal.settings.session},\n        Mailpile.Terminal.handleResponse);\n};\n\nMailpile.Terminal.init = function() {\n    $(\"body\").append(\n    \"<div id=\\\"terminal_blanket\\\" onclick=\\\"Mailpile.Terminal.hide();\\\">\" +\n    \"</div>\" +\n    \"<div id=\\\"terminal\\\">\" +\n    \"  <div id=\\\"console\\\">\" +\n    \"    <div id=\\\"terminal_output\\\"></div>\" +\n    \"    <div id=\\\"terminal_input\\\">\" +\n    \"        <a onclick=\\\"Mailpile.Terminal.session_end();\\\">\" +\n    \"            <span class=\\\"icon icon-x\\\">\" +\n    \"        </a>\" +\n    \"        <a id=\\\"terminal_fullsize_button\\\" onclick=\\\"Mailpile.Terminal.makeFull();\\\">\" +\n    \"            <span class=\\\"icon icon-arrow-down\\\">\" +\n    \"        </a>\" +\n    \"        <a id=\\\"terminal_halfsize_button\\\" onclick=\\\"Mailpile.Terminal.makeSmall();\\\">\" +\n    \"            <span class=\\\"icon icon-arrow-up\\\">\" +\n    \"        </a>\" +\n    \"        <span id=\\\"prompt\\\">mailpile&gt; </span>\" +\n    \"        <form>\" +\n    \"            <input>\" +\n    \"        </form>\" +\n    \"    </div>\" +\n    \"  </div>\" +\n    \"  <div id=\\\"debug_container\\\">\" +\n    \"     <div id=\\\"debug_output\\\"></div>\" +\n    \"  </div>\" +\n    \"</div>\");\n    $(\"#terminal_input form\")\n        .submit(Mailpile.Terminal.submitCommand)\n        .find('input').on('keydown', function(ev, input) {\n            var code = ev.charCode || ev.keyCode;\n            if (code == 27) {  // ESC\n                Mailpile.Terminal.toggle();\n                ev.preventDefault();\n                return false;\n            }\n            if (code == 9) {  // TAB\n                // FIXME: Command completion, please\n                ev.preventDefault();\n                return false;\n            }\n            // History on keyup/keydown, please!\n        });\n}\n\nMailpile.Terminal.toggle = function(size) {\n    if (!Mailpile.Terminal.settings.session) {\n        Mailpile.Terminal.session_start();\n    }\n    if (Mailpile.Terminal.settings.enabled) {\n        Mailpile.Terminal.settings.enabled = false;\n        $(\"#terminal_input input\").blur();\n        $(\"#terminal_blanket\").show();\n        $(\"#terminal\").slideUp('fast');\n        $(\"body\").focus();\n    } else {\n        if (size == \"full\") {\n            $(\"#terminal\").css(\"height\", \"100%\");\n        } else {\n            $(\"#terminal\").css(\"height\", \"400px\");\n        }\n        $(\"#terminal_blanket\").show();\n        $(\"#terminal\").slideDown('fast');\n        Mailpile.Terminal.settings.enabled = true;\n    }\n    Mailpile.Terminal.scrollDown();\n};\n\nMailpile.Terminal.makeFull = function() {\n    $(\"#terminal\").animate({\"height\": \"100%\"}, Mailpile.Terminal.scrollDown);\n    $(\"#terminal_fullsize_button\").hide();\n    $(\"#terminal_halfsize_button\").show();\n}\n\nMailpile.Terminal.makeSmall = function() {\n    $(\"#terminal\").animate({\"height\": \"400px\"}, Mailpile.Terminal.scrollDown);\n    $(\"#terminal_halfsize_button\").hide();\n    $(\"#terminal_fullsize_button\").show();\n}\n\nMailpile.Terminal.hide = function() {\n    $(\"#terminal_blanket\").hide();\n    $(\"#terminal\").slideUp('fast');\n    Mailpile.Terminal.settings.enabled = false;\n    $(\"#terminal_input input\").blur();\n};\n\nMailpile.Terminal.session_start = function() {\n    Mailpile.API.terminal_session_new_post({}, function(data) {\n        console.log(data);\n        console.log(\"Session started\");\n        Mailpile.Terminal.settings.session = data.result.sid;\n        Mailpile.Terminal.executeCommand('help/splash web_terminal');\n        Mailpile.Terminal.scrollDown();\n    });\n};\n\nMailpile.Terminal.session_end = function() {\n    Mailpile.API.terminal_session_end_post(\n        {sid: Mailpile.Terminal.settings.session},\n        function(data) {\n            Mailpile.Terminal.settings.session = null;\n            Mailpile.Terminal.hide();\n            setTimeout(Mailpile.Terminal.clearOutput, 500);\n        }\n    );\n};\n\nMailpile.Terminal.clearOutput = function() {\n    $(\"#terminal_output\").empty();\n};\n\n$(function() {\n    Mailpile.Terminal.init();\n})\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/ui/tooltips.js",
    "content": "/* UI - Tooltips */\nMailpile.UI.content_setup.push(function($content) {\n\n  // Topbar navigation tooltips\n  $content.find('.topbar-nav a').qtip({\n    style: {\n     tip: {\n        corner: 'top center',\n        mimic: 'top center',\n        border: 0,\n        width: 10,\n        height: 10\n      },\n      classes: 'qtip-tipped'\n    },\n    position: {\n      my: 'top center',\n      at: 'bottom center',\n\t\t\tviewport: $(window),\n\t\t\tadjust: {\n\t\t\t\tx: 0,  y: 5\n\t\t\t}\n    },\n    show: {\n      delay: 350\n    }\n  });\n\n  // Bulk action tooltips\n  $content.find('.bulk-actions ul li a').qtip({\n    style: {\n      classes: 'qtip-tipped'\n    },\n    position: {\n      my: 'top center',\n      at: 'bottom center',\n\t\t\tviewport: $(window),\n\t\t\tadjust: {\n\t\t\t\tx: 0,  y: 5\n\t\t\t}\n    }\n  });\n\n  // Email composition tooltips\n  $content.find('.compose-to-email').qtip({\n    content: {\n      title: false,\n      text: function(event, api) {\n        return '{{_(\"Compose to:\")|escapejs}} ' + $(this).attr('href').replace('mailto:', '');\n      }\n    },  \n    style: {\n      classes: 'qtip-tipped'\n    },\n    position: {\n      my: 'bottom center',\n      at: 'top center',\n\t\t\tviewport: $(window),\n\t\t\tadjust: {\n\t\t\t\tx: 0,  y: 0\n\t\t\t}\n    },\n    show: {\n      delay: 450\n    }\n  });\n\n});\n"
  },
  {
    "path": "shared-data/default-theme/html/jsapi/ui/topbar.js",
    "content": "/* Clear button */\nMailpile.UI.set_clear_state = function(queryBox){\n  var $clearButton = $(queryBox).next('.clear-search');\n  if (queryBox.value.length > 0) {\n    $clearButton.show();\n  }\n  else {\n    $clearButton.hide();\n  }\n};\n\n$(function(){\n  var queryBox = $('#search-query');\n  if (queryBox.length) {\n    Mailpile.UI.set_clear_state(queryBox[0]);\n  }\n});\n\nMailpile.UI.maybe_hide_search_box = function() {\n  $('nav.topbar-nav ul li').removeClass('hide');\n  $('div.topbar-logo-name').show().removeClass('hide');\n  $('nav.topbar-nav ul li.nav-search').show().addClass('mobile-pt-inline');\n  $('form#form-search').addClass('mobile-pt-hide');\n  $('nav.topbar-nav ul li.nav-search-hide').addClass('hide');\n};\n$(document).on('click', '#nav-search-hide', Mailpile.UI.maybe_hide_search_box);\n$(document).on('click', '#nav-search', function() {\n  $('nav.topbar-nav ul li').addClass('hide');\n  $('div.topbar-logo-name').hide().addClass('hide');\n  $('nav.topbar-nav ul li.nav-search').removeClass('mobile-pt-inline').hide();\n  $('form#form-search').removeClass('mobile-pt-hide');\n  $('nav.topbar-nav ul li.nav-search-hide').removeClass('hide');\n  $('#search-query').focus();\n});\n\n$(document).on('input change', '#search-query', function(e) {\n  Mailpile.UI.set_clear_state(e.target);\n});\n\n$(document).on('click', '#form-search .clear-search', function(e) {\n  var dflt = $('#search-query').data('q');\n  if ($('#search-query').val() == dflt) {\n    $('#search-query').val('').focus();\n    $(e.target).hide();\n  }\n  else {\n    $('#search-query').val(dflt).focus();\n  }\n});\n\n\n// {# FIXME: Disabled by Bjarni, this doesn't really work reliably\n//  #\n/* Search - Special handling of certain queries */\n$(document).on('submit', '#form-search', function(e) {\n  var search_query = $('#search-query').val();\n  if (search_query.substring(0, 3) === 'in:') {\n    var more_check = search_query.substring(3, 999).split(' ');\n    if (!more_check[1]) {\n      e.preventDefault();\n      Mailpile.go('/in/' + $.trim(search_query.substring(3, 999)) + '/');\n    }\n  }\n  else if (search_query.substring(0, 9) === 'contacts:') {\n    e.preventDefault();\n    $.getJSON(\"/contacts/\" + $.trim(search_query.substring(9, 999)) + \"/as.jhtml\", function(data) {\n  \t  $(\"#content-wide\").html(data.result);\n    });\n  }\n  else if (search_query.substring(0, 5) === 'tags:') {\n    e.preventDefault();\n    $.getJSON(\"{{ config.sys.http_path }}/tags/\" + $.trim(search_query.substring(5, 999)) + \"/as.jhtml\", function(data) {\n  \t  $(\"#content-wide\").html(data.result);\n    });\n  }\n  else if (search_query.substring(0, 5) === 'keys:') {\n    e.preventDefault();\n    Mailpile.UI.Modals.CryptoFindKeys({\n      query: $.trim(search_query.substring(5, 999))\n    });\n  }\n  else {\n    console.log('inside of else, just a normal query');\n  }\n});\n/* Activities - */\n$(document).on('click', '#button-search-options', function(key) {\n\t$('#search-params').slideDown('fast');\n});\n/* Activities - */\n$(document).on('blur', '#button-search-options', function(key) {\n\t$('#search-params').slideUp('fast');\n});\n// #\n// #}\n// #\n\nMailpile.UI.content_setup.push(function($content) {\n  // FIXME: This will do silly things if we have multiple search results\n  //        on a page at a time.\n  var $st = $content.find('#search-terms');\n  var search_terms = $st.data('q');\n  if (search_terms) {\n    var $sq = $('#search-query');\n    $sq.val(search_terms);\n    $sq.data('q', search_terms);\n    $sq.data('context', $st.data('context'));\n    Mailpile.UI.set_clear_state($sq[0]);\n  }\n});\n"
  },
  {
    "path": "shared-data/default-theme/html/layouts/auth.html",
    "content": "{% if render_mode != 'minimal' %}\n<!doctype html>\n<!--[if lt IE 7]><html class=\"no-js ie6 oldie\" lang=\"en\"> <![endif]-->\n<!--[if IE 7]><html class=\"no-js ie7 oldie\" lang=\"en\"> <![endif]-->\n<!--[if IE 8]><html class=\"no-js ie8 oldie\" lang=\"en\"> <![endif]-->\n<!--[if gt IE 8]><!--> <html class=\"no-js\" lang=\"en\"> <!--<![endif]-->\n<head>\n  <title>{% if result %}{% block title %}{{title}}{% endblock %}{% else %}Error{% endif %}</title>\n  {% include(\"partials/head.html\") %}\n  {% block head %}{% endblock %}\n</head>\n<body>\n{% endif %}\n  {%- block content %}{{results}}{% endblock %}\n{% if render_mode != 'minimal' %}\n</body>\n</html>\n{% endif %}\n"
  },
  {
    "path": "shared-data/default-theme/html/layouts/content-tall.html",
    "content": "{% extends \"layouts/content.html\" %}\n{% block content_top %}\n<div class=\"selection-context\"><div id=\"content-tall-view\">\n{% endblock %}\n\n"
  },
  {
    "path": "shared-data/default-theme/html/layouts/content-wide.html",
    "content": "{% extends \"layouts/content.html\" %}\n"
  },
  {
    "path": "shared-data/default-theme/html/layouts/content.html",
    "content": "{%- block layout_top %}{% endblock -%}\n\n{%- block content_top %}\n<div class=\"selection-context\">\n  <div id=\"content-tools\">\n  {% block contenttools %}\n    {% if result %}\n      {% if command == \"search\" %}\n        <span id='search-terms' class='hide'\n              data-context=\"{{state.context}}\"\n              data-q=\"{% for t in result.search_terms %}{{ t }} {% endfor %}\">\n        </span>\n        {% include(\"partials/tools_search.html\") %}\n      {% elif state.command_url in (\"/contacts/view/\", \"/contacts/\", \"/contacts/add/\") %}\n        {% include(\"partials/tools_contacts.html\") %}\n      {% elif state.command_url in (\"/tags/\", \"/tags/add/\", \"/filter/list/\") %}\n        {% include(\"partials/tools_tags.html\") %}\n      {% elif state.command_url in (\"/settings/\",) %}\n        {% include(\"partials/tools_settings.html\") %}\n      {% elif state.command_url in (\"/message/\",) %}\n        {% include(\"partials/tools_message.html\") %}\n      {% else %}\n        {% include(\"partials/tools_default.html\") %}\n      {% endif %}\n    {% endif %}\n  {% endblock %}\n  </div>\n  <div id=\"content-view\">\n{%- endblock %}\n\n    <div {% if state and state.cache_id -%}\n         data-template=\"{{ render_template }}\"\n         class=\"search-context cached content-{{ state.cache_id }}\"\n         {% else -%}\n         class=\"search-context\"\n         {% endif -%}\n         data-context=\"{{state.context}}\">\n{%- block content %}<h1>No Content</h1>{% endblock -%}\n{%- block footer_nav %}\n      <div class=\"footer-nav text-center clearfix\">\n        <noscript><b>\n          {{_(\"Uh oh! You have Javascript disabled. This will cause problems.\")}}\n        </b><br></noscript>\n        <a href=\"{{ U(\"/page/release-notes/license.html\") }}\"\n           title=\"{{_(\"The AGPLv3 License\")}}\"\n           id=\"copyright\">{{_(\"Copyright\")}} 2013-2018</a>,\n        <a href=\"https://www.mailpile.is/\" target=_blank\n           title=\"{{_(\"Visit www.mailpile.is\")}}\"\n           id=\"mailpile_ehf\">Mailpile ehf</a> &amp;\n        <a href=\"{{ U(\"/page/release-notes/credits.html\") }}\" class=\"auto-modal\"\n           title=\"{{_(\"Credits and Thanks\")}}\"\n           id=\"credits\">{{_(\"the community\")}}</a> &dash;\n        <a href=\"{{ U(\"/page/release-notes/\") }}\" class=\"auto-modal\"\n           title=\"{{_(\"Welcome to Mailpile %(version)s\", version=config.version)}}\"\n           id=\"release_notes\">{{_(\"About\")}}</a>&dash;\n        <a href=\"{{ U(\"/settings/privacy.html\") }}\"\n           title=\"{{_(\"Security and Privacy Settings\")}}\"\n           id=\"privacy_settings\">{{_(\"Privacy\")}}</a>\n      </div>\n{%- endblock %}\n    </div>\n\n{%- block content_bottom %}\n  </div>\n</div>\n{%- endblock %}\n\n{%- block layout_bottom %}{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/layouts/full-tall.html",
    "content": "{%- extends \"layouts/full.html\" %}\n{% block content_top %}\n<div class=\"selection-context\"><div id=\"content-tall-view\">\n{% endblock %}\n\n"
  },
  {
    "path": "shared-data/default-theme/html/layouts/full-wide.html",
    "content": "<!doctype html>\n<!--[if lt IE 7]><html class=\"no-js ie6 oldie\" lang=\"en\"> <![endif]-->\n<!--[if IE 7]><html class=\"no-js ie7 oldie\" lang=\"en\"> <![endif]-->\n<!--[if IE 8]><html class=\"no-js ie8 oldie\" lang=\"en\"> <![endif]-->\n<!--[if gt IE 8]><!--> <html class=\"no-js\" lang=\"en\"> <!--<![endif]-->\n<head>\n  <title>{% if result %}{% block title %}{{title}}{% endblock %}{% else %}Error{% endif %} | {{name}}'s mailpile</title>\n  {% include(\"partials/head.html\") %}\n  {% block head %}{% endblock %}\n</head>\n<body>\n{% include(\"partials/topbar.html\") %}\n<div id=\"content-wide\"><div{% if state and state.cache_id %} class=\"cached content-{{ state.cache_id }}\"{% endif %}>\n  {%- block contenttools %}{% endblock %}\n  {%- block content %}{{results}}{% endblock %}\n</div></div>\n{% include(\"partials/hidden.html\") %}\n{% include(\"partials/helpers.html\") %}\n{% include(\"partials/tooltips.html\") %}\n{% include(\"partials/modals.html\") %}\n{% include(\"partials/javascript.html\") %}\n</body>\n</html>\n"
  },
  {
    "path": "shared-data/default-theme/html/layouts/full.html",
    "content": "{% extends \"layouts/content.html\" %}\n\n{% block layout_top %}\n<!doctype html>\n<!--[if lt IE 7]><html class=\"no-js ie6 oldie\" lang=\"en\"> <![endif]-->\n<!--[if IE 7]><html class=\"no-js ie7 oldie\" lang=\"en\"> <![endif]-->\n<!--[if IE 8]><html class=\"no-js ie8 oldie\" lang=\"en\"> <![endif]-->\n<!--[if gt IE 8]><!--> <html class=\"no-js\" lang=\"en\"> <!--<![endif]-->\n<head>\n  <title>{% if result %}{% block title %}{{title}}{% endblock %}{% else %}Error{% endif %} | {{name or _(\"Somebody\")}}'s mailpile v{{ config.version }}</title>\n  {% include(\"partials/head.html\") %}\n  {% block head %}{% endblock %}\n</head>\n<body>\n{% include(\"partials/topbar.html\") %}\n{% include(\"partials/sidebar.html\") %}\n<div id=\"content\" class=\"clearfix\">\n{% endblock %}\n\n{# content.html, which we inherited from, places content here #}\n\n{% block layout_bottom %}\n</div>\n{% include(\"partials/hidden.html\") %}\n{% include(\"partials/helpers.html\") %}\n{% include(\"partials/tooltips.html\") %}\n{% include(\"partials/modals.html\") %}\n{% include(\"partials/javascript.html\") %}\n</body>\n</html>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/layouts/minimal-tall.html",
    "content": "{%- extends \"layouts/minimal.html\" %}\n"
  },
  {
    "path": "shared-data/default-theme/html/layouts/minimal-wide.html",
    "content": "{%- extends \"layouts/minimal.html\" %}\n"
  },
  {
    "path": "shared-data/default-theme/html/layouts/minimal.html",
    "content": "{%- extends \"layouts/content.html\" %}\n{%- block content_top %}{% endblock %}\n{%- block content_bottom %}{% endblock %}\n{%- block footer_nav %}{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/logs/events/index.html",
    "content": "{%- extends \"logs/layout.html\" %}\n{%- block title %}{{_(\"Mailpile Event Log\")}}{% endblock %}\n\n{%- macro event_flags_icon(event) %}\n      {%- if 'R' in event.flags -%}\n        <span class=\"icon icon-robot\"></span>\n      {%- elif 'c' in event.flags -%}\n        <span class=\"icon icon-checkmark\"></span>\n      {%- else -%}\n        {{- event.flags -}}\n      {%- endif -%}\n{%- endmacro %}\n\n{%- macro event_source(event) %}\n  {{ event.source }}\n{%- endmacro %}\n\n{%- macro event_details(event) %}\n      <pre>{{ event|json }}</pre>\n{%- endmacro %}\n\n{%- macro show_event(event) %}\n  <p id=\"{{ event.event_id }}\" data-flags=\"{{ event.flags }}\"\n     class=\"event-summary\" data-source=\"{{ event.source }}\">\n    <span class=\"event-time\">{{ event.ts_hhmm or event.ts|friendly_time }}</span>\n    <span class=\"event-source\">{{ event_source(event) }}</span>\n    <a title=\"{{ event.source }}\" class=\"event-message\"\n       href=\"{{ U('/logs/events/?ui_expand=', event.event_id, '#', event.event_id) }}\">\n      {{ event.message }}\n    </a>\n    {%- if event.data and event.data.undo %}\n    <a href=\"#\"\n       title=\"{{ event.data.undo }}\"\n       class=\"auto-modal eventlog-undo\"\n       data-event_id=\"{{ event.event_id }}\"\n       style=\"font-style: italic;\n              text-decoration: underline;\">{{ _('Undo') }}</a>\n    {%- endif %}\n  </p>\n  {%- if ui_expand == event.event_id %}\n  <p class=\"subsection\">\n    {{ event_details(event) }}\n  </p>\n  {%- endif %}\n{# FIXME:\n    <td class=\"checkbox\">\n      <input type=\"checkbox\" name=\"mid\" value=\"{{ event.event_id }}\">\n    </td>\n#}\n{%- endmacro %}\n\n{%- block logs_content %}\n<h1 class=\"page-title-data mobile-hide\">\n  <span class=\"page-title-icon\"><span class=\"icon icon-notifications\"></span></span>\n  <span class=\"page-title-text\">{{_(\"Event Log\")}}</span>\n</h1>\n<div class=\"setting-group\">\n  <h3>{{_(\"Ongoing Events\")}}</h3>\n  <div class=\"explanation\">\n    <p class=\"what\">\n      <span class=\"icon icon-robot\"></span>\n      {{_(\"Ongoing events represent current actions taken by Mailpile on your behalf, such as watching a Mail Source for new mail or refreshing your contact database.\")}}\n    </p>\n    <p class=\"actions\">\n      <span class=\"icon icon-work\"></span>\n      {{_(\"Browsing the event details can help with troubleshooting if Mailpile is not behaving as you expect.\")}}\n    </p>\n  </div>\n  <div class=\"settings events-R\">\n  {%- set displayed = [] %}\n  {%- for event in result.events|reverse %}\n    {%- if 'R' in event.flags %}\n      {{- show_event(event) }}\n      {% do displayed.append(1) %}\n    {% endif %}\n  {%- endfor %}\n  {%- if not displayed %}\n    <div class=\"add-top add-bottom\" style=\"text-align: center;\">\n      <p><i>{{_(\"No Events Found\")}}</i></p>\n    </div>\n  {%- endif %}\n  </div>\n  <div class=\"settings events-i\">\n  {%- set displayed = [] %}\n  {%- for event in result.events|reverse %}\n    {%- if 'i' in event.flags %}\n      {{- show_event(event) }}\n      {% do displayed.append(1) %}\n    {% endif %}\n  {%- endfor %}\n  </div>\n  <br clear=\"both\">\n</div>\n\n<div class=\"setting-group\">\n  <h3>{{_(\"Completed Events\")}}</h3>\n  <div class=\"explanation\">\n    <p class=\"what\">\n      <span class=\"icon icon-notifications\"></span>\n      {{_(\"The Event Log gives an overview over what has happened in your Mailpile recently.\")}}\n    </p>\n    <p class=\"actions\">\n      <span class=\"icon icon-work\"></span>\n      {{_(\"Some events, such as Tagging operations, can be undone.\")}}\n    </p>\n  </div>\n  <div class=\"settings events-c\">\n  {%- for event in result.events[-50:]|reverse %}\n    {%- if 'R' not in event.flags %}\n      {{- show_event(event) }}\n    {% endif %}\n  {%- endfor %}\n  </div>\n  <br clear=\"both\">\n</div>\n<script id=\"template-event\" type=\"text/template\">\n  {{- show_event({\n    'flags': '{{ flags }}',\n    'event_id': '{{ event_id }}',\n    'message': '{{ message }}',\n    'source': '{{ source }}',\n    'ts_hhmm': '{{ ts_hhmm }}',\n    'ts': '0',\n  })|safe }}\n</script>\n\n{%- endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/logs/layout.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n\n{%- block contenttools %}\n  {%- set activities = [{\n    'name': 'settings',\n    'icon': 'settings',\n    'url': U('/settings/'),\n    'text': _(\"Settings\"),\n    'description': _(\"Settings and Technical Tools\")\n  },{\n    'name': 'notifications',\n    'icon': 'notifications',\n    'url': U('/logs/events/'),\n    'text': _(\"Event Log\"),\n    'description': _(\"Mailpile Event Log\")\n  },{\n    'name': 'netlog',\n    'icon': 'force-graph',\n    'url': U('/logs/network/'),\n    'text': _(\"Network\"),\n    'description': _(\"Recent Network Activity\")\n  }] %}\n  {%- set selection_initial_prompt = _(\"Settings, logs, events, troubleshooting, ...\") %}\n  {%- include(\"partials/tools_default.html\") %}\n{%- endblock %}\n\n{% block content %}\n<div class=\"content-normal settings-page\">\n  {%- block logs_content %}{% endblock %}\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/logs/network/index.html",
    "content": "{% extends \"logs/layout.html\" %}\n\n{% block title %}{{_(\"Recent Network Activity\")}}{% endblock %}\n{% block logs_content %}\n  <h1 class=\"page-title-data mobile-hide\">\n    <span class=\"page-title-icon\"><span class=\"icon icon-force-graph\"></span></span>\n    <span class=\"page-title-text\">{{_(\"Network Activity\")}}</span>\n  </h1>\n\n  <div class=\"notices\">\n  </div>\n\n  <div class=\"setting-group\">\n    <h3>{{_(\"Recent Activity\")}}</h3>\n\n    <div class=\"explanation\">\n      <p class=\"what\">\n        <span class=\"icon icon-lightbulb\"></span>\n        {{_(\"Here you can see a log of recent network activity, both successful and failed attempts.\")}}\n      </p>\n{% set proxy_settings = config.get_proxy_settings() %}\n{% if proxy_settings.protocol not in (\"none\", \"unknown\") %}\n      <p class=\"what\">\n        {{_(\"You have a proxy (%(proxy)s) configured at %(host)s:%(port)s.\",\n            proxy=proxy_settings.protocol,\n            host=proxy_settings.host,\n            port=proxy_settings.port)}}\n      </p>\n  {% if not proxy_settings.fallback %}\n      <p class=\"risks\">\n        <span class=\"icon icon-signature-unknown\"></span>\n        {{_(\"Proxy fallback is disabled, so if the proxy cannot connect to a given server, the connection will fail.\")}}\n    {% if proxy_settings.protocol in (\"tor\", \"tor-risky\") %}\n        {{_(\"Some service providers block connections from Tor, so this may prevent you from accessing your mail or other data.\")}}\n    {% endif %}\n      </p>\n      <p class=\"actions\">\n        <span class=\"icon icon-work\"></span>\n        {{_(\"You can configure destinations to bypass the proxy in the Advanced section of Networking in your Privacy Policy.\")}}\n      </p>\n  {% else %}\n      <p class=\"risks\">\n    {% if proxy_settings.protocol in (\"tor\", \"tor-risky\") %}\n        <span class=\"icon icon-privacy\"></span>\n        {{_(\"Fall-back is enabled, so a direct connection will be made if Tor cannot connect.\")}}\n        {{_(\"This may leak your IP address and allow monitoring of which servers you communicate with.\")}}\n    {% else %}\n        {{_(\"Fall-back is enabled, so a direct connection will be made if the proxy cannot connect.\")}}\n    {% endif %}\n      </p>\n  {% endif %}\n  {% if proxy_settings.protocol == \"tor-risky\" %}\n      <p class=\"risks\">\n        <span class=\"icon icon-privacy\"></span>\n        {{_(\"You are using Tor for unencrypted traffic.\")}}\n        {{_(\"This may allow exit-node operators to listen in or modify your communications.\")}}\n      </p>\n  {% endif %}\n{% elif proxy_settings.protocol not in (\"tor\", \"tor-risky\") %}\n      <p class=\"risks\">\n        <span class=\"icon icon-privacy\"></span>\n        {{_(\"You are not using Tor.\")}}\n        {{_(\"This may leak your IP address and allow monitoring of which servers you communicate with.\")}}\n      </p>\n{% endif %}\n      <p><a href=\"{{ U('/settings/privacy.html') }}\">\n        {{_(\"Edit your Privacy Policy.\")}}\n      </a></p>\n    </div>\n\n    <div class=\"settings\">\n    {%- set lines = ['uncle'] %}\n    {%- for ts, fd, text in result|reverse %}\n      <p class=\"event-summary\" title=\"{{ ts|friendly_datetime }}\"\n         {%- if lines[-1]|string() == text|string() %} style=\"color: #777;\"\n         {%- endif %}>\n        <span class=\"event-time\">{{ ts|friendly_time }}</span>\n        <span class=\"event-message\">{{ text }}</span>\n      </p>\n      {%- do lines.append(text) %}\n    {%- endfor %}{%- if result|length < 1 %}\n      <p style=\"text-align: center;\"><i>No activity</i></p>\n    {%- endif %}\n    </div>\n  </div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/message/compose/index.html",
    "content": "{% set redirected = [] %}\n{% for m in result.messages %}\n    {{ mailpile('http/redirect/url_edit', m.mid) }}\n    {% do redirected.append(m.mid) %}\n{% endfor %}\n{% if not redirected %}\n    {% include(\"message/draft/index.html\") %}\n{% endif %}\n"
  },
  {
    "path": "shared-data/default-theme/html/message/download/index.html",
    "content": ""
  },
  {
    "path": "shared-data/default-theme/html/message/draft/index.html",
    "content": "{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% block title %}{{_(\"Compose\")}}{% endblock %}\n{% block content %}\n{% if result and not result.errors %}\n{% set profiles = mailpile('profiles').result %}\n  {# Note: this code follows the pattern described on the following\n           stackexchange link, collecting information about how the loop\n           and inner IF-statements went into the found_editable list, so\n           that can influence the final processing and not redirect if\n           we found anything at all editable!\n\n    * https://stackoverflow.com/questions/4870346/can-a-jinja-variables-scope-extend-beyond-in-an-inner-block\n  #}\n  {% set found_editable = [] %}\n  {% set found_requested = [] %}\n  {% for mid in (result.thread_ids or result.message_ids) %}\n    {% set message = result.data.messages.get(mid) %}\n    {% set metadata = result.data.metadata[mid] %}\n    {% if message %}\n      {% do found_requested.append(mid) %}\n      {% if metadata.flags.draft %}\n        {% do found_editable.append(mid) %}\n        {% set composer_view = \"draft\" %}\n        {% set attachments = message.attachments %}\n        {% set editing_strings = message.editing_strings or {} %}\n        {% set editing_addresses = result.data.addresses %}\n        {% include(\"partials/compose.html\") %}\n      {% endif %}\n    {% endif %}\n  {% endfor %}\n  \n  {% if found_requested and not found_editable %}\n    {{ mailpile('http/redirect/url_thread', found_requested[0]) }}\n  {% endif %}\n\n{% else %}\n\n  {% include(\"partials/error_message_missing.html\") %}\n\n{% endif %}\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/message/forward/forward.html",
    "content": "{% for m in result.messages %}\n    {{ mailpile('http/redirect/url_edit', m.mid) }}\n{% endfor %}\n"
  },
  {
    "path": "shared-data/default-theme/html/message/index.html",
    "content": "{% if result and not result.errors %}\n  {% include(\"search/index.html\") %}\n{% else %}\n\n{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block content %}\n  {% include(\"partials/error_message_missing.html\") %}\n{% endblock %}\n\n{% endif  %}{# if result #}\n"
  },
  {
    "path": "shared-data/default-theme/html/message/reply/composer.html",
    "content": "{% extends \"message/draft/index.html\" %}\n"
  },
  {
    "path": "shared-data/default-theme/html/message/reply/index.html",
    "content": "{% for m in result.messages %}\n    {{ mailpile('http/redirect/url_edit', m.mid) }}\n{% endfor %}"
  },
  {
    "path": "shared-data/default-theme/html/message/send/index.html",
    "content": "{{ mailpile('http/redirect', '/in/Sent/') }}\n"
  },
  {
    "path": "shared-data/default-theme/html/message/single.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block content %}\n{% if result and not result.errors %}\n  {% include(\"search/index.html\") %}\n{% else %}\n  {% include(\"partials/error_message_missing.html\") %}\n{% endif  %}{# if result #}\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/message/update/index.html",
    "content": "{% for m in result.messages %}\n    {{ mailpile('http/redirect/url_edit', m.mid) }}\n{% endfor %}\n"
  },
  {
    "path": "shared-data/default-theme/html/page/contribute/bugs.html",
    "content": "{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% block title %}{{_(\"Reporting bugs\")}}{% endblock %}\n{% block content %}\n<div class=\"content-normal\" style=\"max-width: 46em;\">\n\n  <h1><span class=\"icon-code\"></span> {{_(\"Reporting Bugs\")}}</h1>\n  <div style=\"font-size: 0.9em; width: 40%; text-align: right; float: right; margin-left: 1em; line-height: 1.1em;\">\n    <b>{{_(\"Note:\")}}</b>\n    {{_(\"These instructions are in English only, because we are currently unable to accept bug reports in other languages. Sorry!\")}}\n  </div>\n  <p>\n    {{_(\"Thank you for testing Mailpile!\")}}<br>\n    Have you found a bug?<br>\n    Please follow the following instructions to report it.\n  </p>\n  <ul style=\"list-style: disc; padding: 0.5em 2em;\">\n    <li>\n      Ensure the bug was not already reported by searching on GitHub\n      under <a href=\"https://github.com/mailpile/Mailpile/issues\"\n               target=_blank>Issues</a>.\n    </li>\n    <li>\n      If you're unable to find an open issue addressing the problem,\n      <a href=\"https://github.com/mailpile/Mailpile/issues/new\"\n         target=_blank>open a new one</a>.\n      Be sure to include a title and clear description, and as much\n      relevant information as possible.\n    </li>\n    <li>\n      Remember, we cannot read your mind or see your screen, so you'll\n      probably need to answer all of these:\n      <ol style=\"list-style: disc; padding: 0.5em 2em 0 2em;\">\n        <li>What were you doing?\n        <li>What did you expect would happen?\n        <li>What actually happened?\n        <li>Operating system? GnuPG version? Mailpile version?\n      </ol>\n    </li>\n    <li>\n      Reproducability is key. If you cannot reliably trigger the bug or\n      cannot describe how to do so, then unfortunately it's less likely that\n      the Mailpile team will be able to do anything about it. It may still\n      be useful to file a report in case others are having the same issue, but\n      bugs that can be reproduced will in general get fixed much faster!\n    </li>\n  </ul>\n  <p>\n    Please consult our\n    <a href=\"https://github.com/mailpile/Mailpile/blob/master/CONTRIBUTING.md\"\n       target=_blank>Contributor Guidelines</a> for further details.<br>\n    The <a href=\"https://github.com/mailpile/Mailpile/blob/master/DEV_FAQ.md\"\n           target=_blank>Developer FAQ</a> has a section on debugging techniques.\n  </p>\n  <p>\n    Thank you!\n  </p>\n\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/page/contribute/index.html",
    "content": "{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% block title %}{{_(\"Help Mailpile Grow\")}}{% endblock %}\n{% block content %}\n<div class=\"content-normal\" style=\"max-width: 46em;\">\n\n  <h1><span class=\"icon-donate\"></span> {{_(\"Help Mailpile Grow\")}}</h1>\n  <p>\n    {{_(\"Mailpile is created by a diverse community of people and organizations who care about freedom and privacy.\") }}\n    {{_(\"Please join our community and contribute in any way you can!\")}}\n    <a target=_blank href=\"https://www.mailpile.is/thank-you/\">{{_(\"Thank you!\")}}</a>\n  </p>\n\n  <h4>{{_(\"Become a backer\")}}\n    <a class=\"button-warning right\" href=\"https://www.mailpile.is/donate/\">\n      <span class=\"icon-donate\"></span> {{_(\"Donate\")}}\n    </a>\n  </h4>\n  <p>\n    {{_(\"Employing people to work on Mailpile full time is the fastest way to improve the software.\")}}\n    {{_(\"Although the software is (and will always be) free, salaries still cost money and your support matters.\")}}\n    {{_(\"If you contribute $23 USD or more, you get access to our community voting platform, where you can have a say in the future direction of the project:\")}}\n    <a target=_blank href=\"https://www.mailpile.is/roadmap/\">www.mailpile.is/roadmap/</a>\n  </p>\n\n  <h4>{{_(\"Tell your friends\")}}\n  </h4>\n  <p>\n    {{_(\"If you like Mailpile, please tell people about it!\")}}\n    {{_(\"Tweet, share, link, write articles and blog posts, act out dramatic tutorials on Youtube, make movies...\")}}\n    {{_(\"Let us know what you create, so we can share the best content with the rest of the community.\")}}\n    {{_(\"We are:\")}}\n    <a target=_blank href=\"https://twitter.com/MailpileTeam\">@MailpileTeam</a>,\n    <a href=\"mailto:team@mailpile.is\">team@mailpile.is</a>\n  </p>\n\n\n  <h4>{{_(\"Geek out!\")}}</h4>\n  <ul class=\"list\" style=\"margin: 0.5em 2em; list-style: disc\">\n    <li><a class=\"auto-modal\" href=\"{{ U(\"/page/contribute/bugs.html\") }}\">{{_(\"If you like breaking things, you can find and report bugs.\")}}</a></li>\n    <li><a target=_blank href=\"https://www.transifex.com/otf/mailpile/\">{{_(\"If you are fluent in an interesting language, you can help translate.\")}}</a></li>\n    <li><a target=_blank href=\"https://github.com/mailpile/Mailpile\">{{_(\"If you are a hacker, you can contribute code.\")}}</a></li>\n{# FIXME!\n    <li><a target=_blank href=\"https://www.mailpile.is/partners/\">{{_(\"If you provide e-mail services, you can become a partner.\")}}</a></li>\n #}\n  </li>\n\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/page/entropy/index.html",
    "content": "{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% block title %}{{_(\"Increasing Entropy\")}}{% endblock %}\n{% block content %}\n<div class=\"content-normal\" style=\"max-width: 46em;\">\n\n  <h1><span class=\"icon-robot\"></span>\n    {{_(\"Increasing Entropy\")}}\n  </h1>\n  <p>\n    {{_(\"Mailpile needs true random numbers to generate good encryption keys.\")}}\n  </p>\n  <p>\n    {{_(\"Although your computer may be powerful, it may still have hard time finding enough randomness when generating keys.\")}}\n    {{_(\"The technical term used to describe the source of randomness is Entropy!\")}}\n  </p>\n  <p>\n    {{_(\"On the machine that is running Mailpile you can do a few things to increase the Entropy:\")}}\n    <br>\n    <ul class=\"list\" style=\"margin-left: 2em; list-style: disc\">\n      <li>{{_(\"Move the mouse.\")}}</li>\n      <li>{{_(\"Perform other activity which requires using the keyboard.\")}}</li>\n      <li>{{_(\"Start a process that utilizes the disk(s).\")}}</li>\n      <li>{{_(\"Browse the internet.\")}}</li>\n    </ul>\n  </p>\n  <p>\n    {{_(\"Because the computer cannot predict human activity these actions help the computer generate random numbers.\")}}\n  </p>\n\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/page/gmail-2-step-verification/index.html",
    "content": "{% extends \"layouts/full.html\" %}\n{% block title %}{{_(\"Gmail's 2-Step App Passwords\")}} | {{_(\"Help\")}}{% endblock %}\n{% block content %}\n\n<div class=\"content-normal\">\n\n  <h1><span class=\"icon-help\"></span> {{_(\"Help\")}}</h1>\n  \n  <div class=\"\">\n    <h3>{{_(\"Gmail's 2-Step App Passwords\")}}</h3>\n  \n    <p>If you have 2-step verification enabled on your Google (Gmail) account you will need to follow these steps to get your account working with Mailpile.</p>\n    \n    <ol>\n      <li>Go to the Google accounts website, <a href=\"http://accounts.google.com/\" target=\"_blank\">http://accounts.google.com/</a></li>\n      <li>If you haven't already, log in.</li>\n      <li>Click the 'Security' button on the top center of the screen.</li>\n      <li>In the \"2-step verification\" section, click \"Manage your application specific passwords\".</li>\n      <li>You may have to reenter your password to continue.</li>\n      <li>In the \"Application-specific passwords\" section, generate a new password for Mailpile.</li>\n    </ol>\n  \n    <ul>\n      <li>For more information about 2-step verification: <a href=\"http://www.google.com/landing/2step/\" target=\"_blank\">http://www.google.com/landing/2step/</a></li>\n      <li>To turn 2-step verification off: <a href=\"https://support.google.com/accounts/answer/1064203?hl=en\" target=\"_blank\">https://support.google.com/accounts/answer/1064203</a></li>\n      <li>For information about access tokens: <a href=\"https://accounts.google.com/IssuedAuthSubTokens#accesscodes\" target=\"_blank\">https://accounts.google.com/IssuedAuthSubTokens#accesscodes</a></li>\n    </ul>\n  </div>\n\n    <h4>Related Articles:</h4>\n    <p><span class=\"icon-help\"></span> <a href=\"/page/gmail-access-non-google-accounts/\">Enable Access for Non-Google Apps</a></p>\n\n  {% include(\"help/bottom.html\") %}\n\n</div>\n\n{% endblock %}"
  },
  {
    "path": "shared-data/default-theme/html/page/gmail-access-non-google-accounts/index.html",
    "content": "{% extends \"layouts/full.html\" %}\n{% block title %}{{_(\"Enable Access for Non-Google Apps\")}} | {{_(\"Help\")}}{% endblock %}\n{% block content %}\n\n<div class=\"content-normal\">\n\n  <h1><span class=\"icon-help\"></span> {{_(\"Help\")}}</h1>\n\n  <div class=\"clearfix\">\n    <h3>{{_(\"Enable Access for Non-Google Apps\")}}</h3>\n\n    <p>By default Google makes accessing your Gmail account difficult for non-Google e-mail programs (or e-mail programs that use less than ideal security settings). You will need to follow these steps to get your account working with Mailpile.</p>\n\n    <ol>\n      <li>Go to the Google accounts website, <a href=\"http://accounts.google.com/\" target=\"_blank\">http://accounts.google.com/</a></li>\n      <li>If you haven't already, log in.</li>\n      <li>Click the \"Security\" button on the top center of the screen.</li>\n      <li>Click into the \"Enable Less Secure Apps\" item</li>\n      <li>Select \"Enable\" from the list</li>\n    </ol>\n\n    <h4>Related Articles:</h4>\n    <p><span class=\"icon-help\"></span> <a href=\"/page/gmail-2-step-verification/\">Gmail's 2-Step App Passwords</a></p>\n\n  </div>\n  {% include(\"help/bottom.html\") %}\n</div>\n\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/page/index.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n\n{% block content %}\n\n<div id=\"page\" class=\"content-normal\">\n  \n  <h3>This is a static page</h3>\n  \n  <p>But it still gets rendered via. Jinja, so it can do things.</p>\n\n  <p>Our data:</p>\n  <pre>\n    result = {{ result }}\n  </pre>\n\n</div>\n\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/page/multipile/index.html",
    "content": "{% extends \"layouts/auth.html\" %}\n{% block title %}{{_(\"Your Mailpile may not be running\")}}{% endblock %}\n{% block content %}\n<div id=\"login\">\n  <div id=\"login-left\" class=\"animated\"></div>\n  <div id=\"login-right\" class=\"animated\"></div>\n</div>\n\n<div id=\"login-logo\" class=\"animated\">\n{% include(\"../img/logo-color.svg\") %}\n{% include(\"../img/logo-name.svg\") %}\n</div>\n\n<!--\n<div id=\"login-messages\" class=\"animated text-center\">\nDecrypting Index, Messages, and Contacts\n</div>\n-->\n\n<div id=\"login-vault-lock\" class=\"vault-lock-outer animated\">\n  <div class=\"vault-lock-inner animated\">\n    <div class=\"vault-lock icon-groups animated\"></div>\n  </div>\n</div>\n\n<div id=\"login-details\" class=\"animated\">\n  <div class=\"form-text\">{{_(\"Launch Mailpile as ...\")}}</div>\n  <form method=\"POST\" action='/cgi-bin/mailpile/admin.cgi'\n        id=\"form-login\" class=\"clearfix animated\">\n   <div class='form-login'>\n    <input id=\"login-username\" type=\"text\" name=\"username\"\n           autocomplete=\"off\" tabindex=1 alt=\"{{_(\"Unix Username\")}}\"\n           placeholder=\"{{_(\"Unix Username\")}}\">\n   </div>\n   <div class='form-login'>\n    <input id=\"login-passphrase\" type=\"password\" name=\"password\"\n           autocomplete=\"off\" tabindex=2 alt=\"{{_(\"Unix Password\")}}\"\n           placeholder=\"{{_(\"Unix Password\")}}\">\n    <button type=\"submit\" class=\"submit\">\n      <span class=\"icon-checkmark\"></span>\n    </button>\n   </div>\n  </form>\n\n  <div class=\"logged-out-message not-running\" style=\"margin-top: 50x;\">\n    <p>{{_(\"Your Mailpile may not be running\")}}</p>\n    <div style='font-weight: normal;'>\n      {{_(\"To launch the app please enter your account details above \"\n          \"or use a terminal (SSH) to start Mailpile manually.\")}}\n    </div>\n  </div>\n<!--\n  <div class=\"logged-out-message animated bounceIn\">{{ result.login_banner|safe }}</div>\n-->\n</div>\n\n<script>\n$(document).ready(function() {\n\n  var height = $(window).height() - 16;\n  $('#content-wide').css({'height': height, 'margin-top': '0px'});\n  $('#login').height(height);\n  $('#login-left').height(height);\n  $('#login-right').height(height);\n\n  $('#login-username').focus();\n});\n\n// Login Form is submitted\n$(document).on('submit', '#form-login', function(e) {\n\n  // Details\n  $('#login-details').addClass('bounceOutDown');\n\n  // Lock\n  setTimeout(function() {\n    $('#login-vault-lock').find('div.vault-lock').addClass('fadeOut');\n  }, 250);\n\n  setTimeout(function() {\n    $('#login-vault-lock').addClass('bounceOutUp');\n  }, 500);\n\n  // Panels\n  setTimeout(function() {\n    $('#login-left').addClass('bounceOutLeft');\n    $('#login-right').addClass('bounceOutRight');\n  }, 850);\n\n  // Loading\n  setTimeout(function() {\n    $('#login-logo .logo-name').addClass('fadeOut');\n    $('#login-logo').css({'left': '40%'});\n  }, 1000);\n\n  setInterval(function() {\n    $(\"#logo-bluemail\").fadeOut(2000);\n    $(\"#logo-redmail\").hide(2000);\n    $(\"#logo-greenmail\").hide(3000);\n    $(\"#logo-bluemail\").fadeIn(2000);\n    $(\"#logo-greenmail\").fadeIn(4000);\n    $(\"#logo-redmail\").fadeIn(6000);\n  }, 1000);\n\n});\n</script>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/page/release-notes/credits-code.html",
    "content": "<li class=\"commits-19\">Abhilash Raj</li>\n<li class=\"commits-47\">Alexandre Viau</li>\n<li class=\"commits-10\">Björgvin Ragnarsson</li>\n<li class=\"commits-19\">Christoph Witzany</li>\n<li class=\"commits-36\">DoubleMalt</li>\n<li class=\"commits-7\">Erik Öhrn</li>\n<li class=\"commits-17\">Folker Bernitt</li>\n<li class=\"commits-20\">Grace Roller</li>\n<li class=\"commits-27\">Gregory Igelmund</li>\n<li class=\"commits-7\">Hazel Smith</li>\n<li class=\"commits-9\">Jack</li>\n<li class=\"commits-12\">Jack Dodds</li>\n<li class=\"commits-16\">Jocelyn Delalande</li>\n<li class=\"commits-14\">Jocelyn Delande</li>\n<li class=\"commits-13\">John Faucett</li>\n<li class=\"commits-8\">Jon Bringhurst</li>\n<li class=\"commits-17\">RiseT</li>\n<li class=\"commits-7\">Ross Schulman</li>\n<li class=\"commits-36\">Smári McCarthy</li>\n<li class=\"commits-22\">Uli Köhler</li>\n<li class=\"commits-20\">Vadim Rutkovsky</li>\n<li class=\"commits-7\">infinite-Joy</li>\n<li class=\"commits-10\">largestprime</li>\n<li class=\"commits-8\">ngatnicdotmil</li>"
  },
  {
    "path": "shared-data/default-theme/html/page/release-notes/credits-i18n.html",
    "content": "<li class=\"language\"><b>Chinese</b></li><ul>\n  <li>Aaron LI</li>\n  <li>Archibald Lu</li>\n  <li>bensenq</li>\n  <li>brobuto</li>\n  <li>Ken Niu</li>\n  <li>Kevin Liao</li>\n  <li>malsony</li>\n  <li>Mingye Wang</li>\n  <li>Nan Deng</li>\n  <li>YF</li>\n  <li>Zhantong Zhang</li>\n  <li>立波 赵</li>\n</ul>\n<li class=\"language\"><b>Croatian</b></li><ul>\n  <li>Igor</li>\n</ul>\n<li class=\"language\"><b>Danish</b></li><ul>\n  <li>Birger Petroleumsen</li>\n  <li>David Nielsen</li>\n  <li>Michael Kringelhede</li>\n  <li>Morten Juhl-Johansen Zölde-Fejér</li>\n  <li>rkk</li>\n</ul>\n<li class=\"language\"><b>English</b></li><ul>\n  <li>Andi Chandler</li>\n</ul>\n<li class=\"language\"><b>French</b></li><ul>\n  <li>Adrien Cordonnier</li>\n  <li>Ant0Ine_Fr</li>\n  <li>axel</li>\n  <li>Baptiste Mille-Mathias</li>\n  <li>daamien</li>\n  <li>dest dest</li>\n  <li>Dylann CORDEL</li>\n  <li>Emilien Klein</li>\n  <li>Enguerran POULAIN</li>\n  <li>French language coordinator</li>\n  <li>Geoffroy</li>\n  <li>Jocelyn Delalande</li>\n  <li>jschaul</li>\n  <li>jvh45</li>\n  <li>liUms</li>\n  <li>Machin Chose</li>\n  <li>Mickael Goasdoué</li>\n  <li>MonsieurCroque</li>\n  <li>Nadley-Nahir Mohamed</li>\n  <li>Samy Dindane</li>\n  <li>themen</li>\n  <li>Théotix</li>\n  <li>Tokxn</li>\n  <li>Victor</li>\n  <li>VidelDevil</li>\n  <li>Ypnose</li>\n</ul>\n<li class=\"language\"><b>German</b></li><ul>\n  <li>adler32</li>\n  <li>backenklee</li>\n  <li>Beeberdopf</li>\n  <li>Christian-W. Galganek</li>\n  <li>Ettore Atalan</li>\n  <li>golive</li>\n  <li>hasdf</li>\n  <li>JohannesWilke</li>\n  <li>Koehr</li>\n  <li>malexmave</li>\n  <li>Max Mathys</li>\n  <li>misterunknown</li>\n  <li>Morris Jobke</li>\n  <li>nova</li>\n  <li>repat</li>\n  <li>Robert Pollak</li>\n  <li>Stefan</li>\n  <li>Stefan Kornemann</li>\n  <li>takel</li>\n  <li>Thegerman</li>\n  <li>Tim Hanson</li>\n  <li>triller pfeife</li>\n</ul>\n<li class=\"language\"><b>Greek</b></li><ul>\n  <li>cptg</li>\n  <li>John Giannelos</li>\n  <li>platon</li>\n</ul>\n<li class=\"language\"><b>Icelandic</b></li><ul>\n  <li>GunnarSturla</li>\n  <li>Páll Hilmarsson</li>\n  <li>smari</li>\n  <li>The Mailpile Project</li>\n</ul>\n<li class=\"language\"><b>Italian</b></li><ul>\n  <li>Alessio</li>\n  <li>Aristotele aka LiberCivis</li>\n  <li>Arturo Giunta</li>\n  <li>Claudio Salinitro</li>\n  <li>dnprossi</li>\n  <li>Emanuele</li>\n  <li>Fabio Stefanini</li>\n  <li>Francesco Zanini</li>\n  <li>Gna Style</li>\n  <li>Muriel</li>\n  <li>Sebastiano Pistore</li>\n</ul>\n<li class=\"language\"><b>Japanese</b></li><ul>\n  <li>Naofumi</li>\n  <li>Yuki</li>\n  <li>Yusei Nishioka</li>\n</ul>\n<li class=\"language\"><b>Lithuanian</b></li><ul>\n</ul>\n<li class=\"language\"><b>Norwegian Bokmål</b></li><ul>\n  <li>Allan Nordhøy</li>\n  <li>Daniel</li>\n  <li>falense</li>\n  <li>Johannes Andersen</li>\n  <li>Per Thorsheim</li>\n  <li>Sander Danielsen</li>\n</ul>\n<li class=\"language\"><b>Portuguese</b></li><ul>\n  <li>Alessandra</li>\n  <li>Diego Santos</li>\n  <li>Gus</li>\n  <li>Gustavo Gondim</li>\n  <li>Igor Oliveira</li>\n  <li>Leonardo Almeida</li>\n  <li>Leonardo Pires Felix</li>\n  <li>mama imagem</li>\n  <li>Matheus Macabu</li>\n  <li>Odilon Junior</li>\n  <li>Pedro Paulino</li>\n  <li>ramon nascimento</li>\n  <li>Renan Galeno</li>\n  <li>Romoaldo Cordeiro</li>\n  <li>Álvaro Justen</li>\n</ul>\n<li class=\"language\"><b>Russian</b></li><ul>\n  <li>Alexander Ionov</li>\n  <li>Alexander Savchuk</li>\n  <li>Alexander Shashkevich</li>\n  <li>Cyber Tailor</li>\n  <li>Dmitry Ushakov</li>\n  <li>Eugene Sharygin</li>\n  <li>Ivan polilov</li>\n  <li>kafegorod</li>\n  <li>Mihhail Sidorin</li>\n  <li>MunGell</li>\n  <li>myfreeweb</li>\n  <li>Nikita Medvedev</li>\n  <li>Sergey Leschina</li>\n  <li>webslavic</li>\n  <li>Дмитрий</li>\n  <li>Юрий Третьяков</li>\n</ul>\n<li class=\"language\"><b>Spanish</b></li><ul>\n  <li>Artopal</li>\n  <li>Cristian Salamea</li>\n  <li>Gustavo Córdova</li>\n  <li>Jeff Beatty</li>\n  <li>Liz Borzani</li>\n  <li>opensas</li>\n  <li>Rodolfo Guagnini</li>\n</ul>\n<li class=\"language\"><b>Spanish</b></li><ul>\n  <li>Angel Docampo</li>\n  <li>JCantalapiedra</li>\n  <li>Juan Rey Saura</li>\n  <li>rilder</li>\n  <li>smari</li>\n</ul>\n<li class=\"language\"><b>Swedish</b></li><ul>\n  <li>Anton Strömkvist</li>\n  <li>Henrik Mattsson-Mårn</li>\n  <li>Mats Henricson</li>\n  <li>Oliver Hörberg</li>\n  <li>Patrik Nilsson</li>\n</ul>\n"
  },
  {
    "path": "shared-data/default-theme/html/page/release-notes/credits.html",
    "content": "{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% block title %}{{ _(\"Credits\") }}{% endblock %}\n{% block content %}\n<div class=\"content-normal\"\n     style=\"max-width: 46em; max-height: 450px; overflow-y: scroll; text-align: center;\">\n  <h1>{{ _(\"Credits\") }}</h1>\n\n  <h3>{{ _(\"Core Team\") }}</h3>\n  <ul style=\"list-style-type: none; padding: 1em 0 2em 0;\">\n    <li><b>Bjarni Rúnar Einarsson</b> - {{ _(\"Papa Smurf\") }}</li>\n    <li><b>Smári McCarthy</b> - {{ _(\"Security, community, code\") }}</li>\n    <li><b>Brennan Novak</b> - {{ _(\"UI and design\") }} (2013-2015)</li>\n  </ul>\n  <p>\n    {{_(\"The Mailpile team would like to thank all the people who contributed\n    money or volunteered their time to help create a truly free, secure\n    e-mail client. You're all awesome!\") }}\n  </p>\n  <p>&nbsp;</p>\n\n\n  <h3>{{ _(\"Community\") }}</h3>\n  <ul>\n    <li>Ásta Helgadóttir</li>\n    <li>Daniel Yeow</li>\n    <li>Gus Andrews</li>\n    <li>Eleanor Saitta</li>\n    <li>Emilia Telese</li>\n    <li>Karl Pálsson</li>\n    <li>Sveinbjörn Pálsson</li>\n  </ul>\n  <p>&nbsp;</p>\n\n\n  <h3>{{ _(\"Financial Backers\") }}</h3>\n  <p>\n    <a target=_blank href=\"https://duckduckgo.com/\"><img src=\"/static/img/logo-ddg.png\" alt=\"DuckDuckGo\"></a>\n    <a target=_blank href=\"https://pagekite.net/\"><img src=\"/static/img/logo-pagekite.png\" alt=\"PageKite\"></a>\n    <a target=_blank href=\"http://freedomboxfoundation.org/\"><img src=\"/static/img/logo-freedombox.png\" alt=\"Freedombox\"></a>\n    <a target=_blank href=\"http://commonsmachinery.se/\"><img src=\"/static/img/logo-commonsmachinery.png\" alt=\"Commons Machinery\"></a>\n    <a target=_blank href=\"https://cloudfleet.io/\"><img src=\"/static/img/logo-cloudfleet.png\" alt=\"Cloudfleet\"></a>\n    <br><a target=\"_blank\" href=\"https://www.mailpile.is/thank-you/\">\n      {{_(\"... and many more!\")}}\n    </a>\n  </p>\n  <p>&nbsp;</p>\n\n\n  <h3>{{ _(\"Code Contributions\") }}</h3>\n  <ul>\n  {% include \"page/release-notes/credits-code.html\" %}\n    <li>+ <a target=_blank href=\"https://github.com/mailpile/Mailpile/graphs/contributors\">Github</a></li>\n  </ul>\n  <p>&nbsp;</p>\n\n\n  <h3>{{ _(\"Translations\") }}</h3>\n  <ul>\n  {% include \"page/release-notes/credits-i18n.html\" %}\n    <li>+ <a target=_blank href=\"http://www.localizationlab.org/\">Localization Lab</a></li>\n  </ul>\n\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/page/release-notes/index.html",
    "content": "{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% block title %}{{_(\"Mailpile %(version)s release notes\", version=config.version)}}{% endblock %}\n{% block content %}\n<div class=\"content-normal\" style=\"max-width: 46em;\">\n\n  {%- set target_version = config.version.split('rc')[0].rsplit('.', 1)[0] %}\n  <h1>\n    <span class=\"icon-donate\"></span>\n    {{_(\"Welcome to Mailpile %(version)s\", version=config.version)}}\n  </h1>\n  {% if \"rc\" in config.version %}\n  <p class=\"text-center\"><i>\n    {{_(\"Thank you for testing our release candidate!\")}}<br>\n    {{_(\"If no critical bugs are found, this will become version %(version)s.\",\n        version=target_version )}}\n  </i></p><br>\n  {% endif %}\n\n  <h3>{{_(\"What is Mailpile %(version)s?\", version=target_version)}}</h3>\n  <ul style=\"list-style: disc; margin: 1em 0 2em 2em;\">\n    <li>{{_(\"An e-mail client with a strong focus on privacy\")}}</li>\n    <li>{{_(\"A tool for organizing and searching large volumes of e-mail\")}}</li>\n    <li>{{_(\"An easy way to get started with PGP e-mail encryption\")}}</li>\n  </ul>\n\n  <h3>{{_(\"What is Mailpile not?\")}}</h3>\n  <ul style=\"list-style: disc; margin: 1em 0 2em 2em;\">\n    <li>{{_(\"A synchronizing IMAP client:\")}}\n        {{_(\"Local changes stay local, we don't tell the server\")}}</li>\n    <li>{{_(\"A money-making enterprise\")}}</li>\n    <li>{{_(\"A calendar\")}}</li>\n    <li>{{_(\"Finished.\")}}\n        <a style=\"font-size: 0.8em;\"\n           target=_blank href=\"https://www.mailpile.is/donate/\"><i>\n          {{_(\"Help make it better.\")}}\n        </i></a></li>\n  </ul>\n\n  <p class=\"text-center\" style=\"border-top: 1px solid #aaa; padding: 1em 0;\"><i>\n    {{_(\"That's it!\")}}\n    {{_(\"Read on if you've used one of the Mailpile Beta or GitHub releases.\")}}\n  </i></p>\n\n  <h5>{{_(\"What's new?\")}}</h5>\n  <ul style=\"list-style: disc; margin: 1em 0 2em 2em;\">\n    <li>{{_(\"A better (shorter) setup process\")}}</li>\n    <li>{{_(\"Support for Gmail OAuth 2.0 authentication\")}}</li>\n    <li>{{_(\"Improved PGP support, including automatic importing of keys\")}}</li>\n    <li>{{_('Per-tag automatic bayesian classification, as <a target=_blank href=\"%(blog_url)s\">discussed on our blog</a>',\n            blog_url=\"https://www.mailpile.is/blog/2014-01-12_A_Plan_For_Spam.html\")}}</li>\n    <li>{{_(\"The ability to delete e-mail and accounts\")}}</li>\n    <li>{{_(\"A more responsive (almost) mobile-friendly web interface\")}}</li>\n  </ul>\n\n  <h5>{{_(\"What's fixed?\")}}</h5>\n  <ul style=\"list-style: disc; margin: 1em 0 2em 2em;\">\n    <li>{{_('See GitHub for <a target=_blank href=\"%(closed_url)s\">all closed issues</a> and <a target=_blank href=\"%(release_url)s\">issues closed for this release</a>',\n            closed_url='https://github.com/mailpile/Mailpile/issues?q=is%3Aissue+is%3Aclosed',\n            release_url='https://github.com/mailpile/Mailpile/milestone/4?closed=1'\n            )|safe }}</li>\n  </ul>\n\n  <h5>{{_(\"What's still broken?\")}}</h5>\n  <ul style=\"list-style: disc; margin: 1em 0 2em 2em;\">\n    <li>{{_(\"Mailpile is still rather slow and RAM-hungry\")}}</li>\n    <li>{{_(\"IMAP synchronization is not yet implemented\")}}</li>\n    <li>{{_('The <a target=_blank href=\"%(roadmap_url)s\">Security Roadmap</a> is incomplete',\n            roadmap_url='https://github.com/mailpile/Mailpile/wiki/Security-roadmap'\n            )|safe }}</li>\n    <li>{{_('Github has <a target=_blank href=\"%(issues_url)s\">many open issues</a>.',\n            issues_url='https://github.com/mailpile/Mailpile/issues'\n            )|safe }} {{_(\"You can help.\")}}</li>\n  </ul>\n\n  <p class=\"center release-notes-got-it\"\n     style=\"border-top: 1px solid #aaa; padding: 1em 0 0 0;\"><i>\n    <a data-variable=\"web.release_notes\" data-remove=\"release-notes-got-it\"\n       class=\"ok-got-it\">\n      <span class=\"icon-checkmark\"></span> {{_(\"OK, got it\")}}\n    </a>\n  </p>\n\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/page/release-notes/license.html",
    "content": "{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% block title %}The GNU Affero General Public License, version 3{% endblock %}\n{% block content %}\n<div class=\"content-normal\" style=\"max-width: 46em;\">\n\n{% if config.prefs.language not in ('en', 'en_US', 'en_GB', 'C') %}\n<p class=\"text-center\"><i>\n  <a target=_blank href=\"https://www.gnu.org/licenses/translations.html\">\n    {{_(\"The Mailpile license is in English.\")}}\n    {{_(\"Translations may be available on the FSF web site.\")}}\n  </a>\n</i></p>\n{% endif %}\n\n<p class=\"text-center\"><big>\n                    GNU AFFERO GENERAL PUBLIC LICENSE<br>\n                       Version 3, 19 November 2007\n</big></p>\n<p>\n Copyright (C) 2007 Free Software Foundation, Inc.\n <a target=_blank href=\"https://fsf.org/\">http://fsf.org/</a>.<br>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n</p><p class=\"text-center\">\n                            Preamble\n</p><p>\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n</p><p>\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n</p><p>\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n</p><p>\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n</p><p>\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n</p><p>\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n</p><p>\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n</p><p>\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n</p><p class=\"text-center\">\n                       TERMS AND CONDITIONS\n</p><p>\n  0. Definitions.\n</p><p>\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n</p><p>\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n</p><p>\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n</p><p>\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n</p><p>\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n</p><p>\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n</p><p>\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n</p><p>\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n</p><p>\n  1. Source Code.\n</p><p>\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n</p><p>\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n</p><p>\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n</p><p>\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n</p><p>\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n</p><p>\n  The Corresponding Source for a work in source code form is that\nsame work.\n</p><p>\n  2. Basic Permissions.\n</p><p>\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n</p><p>\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n</p><p>\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n</p><p>\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n</p><p>\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n</p><p>\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n</p><p>\n  4. Conveying Verbatim Copies.\n</p><p>\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n</p><p>\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n</p><p>\n  5. Conveying Modified Source Versions.\n</p><p>\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n</p><p>\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n</p><p>\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n</p><p>\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n</p><p>\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n</p><p>\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n</p><p>\n  6. Conveying Non-Source Forms.\n</p><p>\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n</p><p>\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n</p><p>\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n</p><p>\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n</p><p>\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n</p><p>\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n</p><p>\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n</p><p>\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n</p><p>\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n</p><p>\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n</p><p>\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n</p><p>\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n</p><p>\n  7. Additional Terms.\n</p><p>\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n</p><p>\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n</p><p>\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n</p><p>\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n</p><p>\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n</p><p>\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n</p><p>\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n</p><p>\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n</p><p>\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n</p><p>\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n</p><p>\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n</p><p>\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n</p><p>\n  8. Termination.\n</p><p>\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n</p><p>\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n</p><p>\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n</p><p>\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n</p><p>\n  9. Acceptance Not Required for Having Copies.\n</p><p>\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n</p><p>\n  10. Automatic Licensing of Downstream Recipients.\n</p><p>\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n</p><p>\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n</p><p>\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n</p><p>\n  11. Patents.\n</p><p>\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n</p><p>\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n</p><p>\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n</p><p>\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n</p><p>\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n</p><p>\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n</p><p>\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n</p><p>\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n</p><p>\n  12. No Surrender of Others' Freedom.\n</p><p>\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n</p><p>\n  13. Remote Network Interaction; Use with the GNU General Public License.\n</p><p>\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n</p><p>\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n</p><p>\n  14. Revised Versions of this License.\n</p><p>\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n</p><p>\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n</p><p>\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n</p><p>\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n</p><p>\n  15. Disclaimer of Warranty.\n</p><p>\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n</p><p>\n  16. Limitation of Liability.\n</p><p>\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n</p><p>\n  17. Interpretation of Sections 15 and 16.\n</p><p>\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n</p><p class=\"text-center\">\n                     END OF TERMS AND CONDITIONS\n</p>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/page/template-straw-man/index.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{#\n## This is a straw-man design pattern that might get us closer to letting\n## Javascript and Jinja2 cooperate instead of duplicating effort.\n##\n## This example uses a simple Jinja2 macro to first render a static HTML\n## version of some data, and then expose both the raw data and the template\n## used to the Javascript universe, so they can be manipulated there and\n## code reused.\n#}\n{% block content %}<div id=\"page\" class=\"content-normal\">\n  \n  <h3>This is Javascript-friendly Jinja2 straw-man!</h3>\n\n  <p>Hey, check this out <a href=\"as.js\">as javascript (JSON)</a></p>\n\n  {% import 'page/template-straw-man/tags.html' as tags %}\n  {{ tags.render_html() }}\n\n  <script>var model = {{ tags.render_json() }};</script>\n\n</div>{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/page/template-straw-man/index.js",
    "content": "{% import 'page/template-straw-man/tags.html' as tags %}\n{{ tags.render_json() }}\n"
  },
  {
    "path": "shared-data/default-theme/html/page/template-straw-man/tags.html",
    "content": "{#\n## This file defines macros for rendering tag data either as HTML\n## or Javascript (JSON). See index.html and index.js for examples\n## of how it is used.\n##\n## This may be a smarter way to build partials?\n##\n############################################################################}\n\n{# 1st. Create a macro for the template fragment #}\n{%- macro render_tag(tag) -%}\n  <li id={{tag.tid}}>{{tag.name}} ({{tag.stats_new}})</li>\n{%- endmacro -%}\n\n{# 2nd. Create a structure with just the data the macro cares about\n##\n## Note: This will usually be the most complicated part of the\n##       template. Once it is stable, this logic should probably\n##       move to a DataView for speed and API awesomeness.\n#}\n{%- set data = [] -%}\n{%- for tag in mailpile(\"tags\").result.tags -%}\n  {% do data.append({\n    'tid': tag.tid,\n    'name': tag.name,\n    'stats_new': tag.stats.new\n  }) %}\n{%- endfor -%}\n\n{# 3rd. Render as HTML #}\n{%- macro render_html() -%}\n<ul>\n  {%- for tag in data -%}\n    {{ render_tag(tag) }}\n  {%- endfor -%}\n</ul>\n{%- endmacro -%}\n\n{# 4th. Expose raw data and template to Javascript #}\n{%- macro render_json() -%}\n{\n  \"data\": {{ data|json|safe }},\n  \"template\": {{ render_tag({\n    'tid': '{{tag.tid}}',\n    'name': '{{tag.name}}',\n    'stats_new': '{{tag.stats_new}}',\n  })|json|safe }}\n}\n{%- endmacro -%}\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/compose.html",
    "content": "<div class=\"thread-reply\" id=\"message-{{mid}}\" data-mid=\"{{mid}}\">\n  <form id=\"form-compose-{{mid}}\" class=\"form-compose clearfix\" data-mid=\"{{mid}}\"\n        data-should-encrypt=\"{{ 'Y' if ('x-mp-internal-should-encrypt' in editing_strings.headers) else 'N' }}\">\n    {{ csrf_field|safe }}\n    <div id=\"compose-details-{{mid}}\" class=\"{% if command == \"view\" %}hide{% endif %}\" data-mid=\"{{mid}}\">\n      <div class=\"compose-headers\">\n        <label class=\"left\">\n          {{_(\"To\")}}\n        </label>\n        <label class=\"right\">\n          <a id=\"compose-cc-show\" class=\"compose-show-field {% if editing_strings.cc %}hide{% endif %}\" data-mid=\"{{mid}}\" href=\"#\">{{_(\"Cc\")}}</a>\n          <a id=\"compose-bcc-show\" class=\"compose-show-field {% if editing_strings.bcc %}hide{% endif %}\" data-mid=\"{{mid}}\" href=\"#\">{{_(\"Bcc\")}}</a>\n        </label>\n        <input id=\"compose-to-{{mid}}\" class=\"compose-address-field\" data-mid=\"{{mid}}\" name=\"to\" type=\"text\" tabindex=\"1\" alt=\"{{_(\"To\")}}\" value=\"{{editing_strings.to}}\">\n      </div>\n      <div id=\"compose-cc-html\" class=\"compose-headers compose-cc {% if not editing_strings.cc %}hide{% endif %}\">\n        <label class=\"left\">{{_(\"Cc\")}}</label>\n        <label class=\"right\">\n          <a href=\"#cc\" class=\"compose-hide-field\" data-mid=\"{{mid}}\"><span class=\"icon-eye\"></span> {{_(\"hide\")}}</a>\n        </label>\n        <input id=\"compose-cc-{{mid}}\" class=\"compose-address-field\" data-mid=\"{{mid}}\" name=\"cc\" type=\"text\" tabindex=\"2\" alt=\"{{_(\"Cc\")}}\" value=\"{{editing_strings.cc}}\">\n      </div>\n      <div id=\"compose-bcc-html\" class=\"compose-headers compose-bcc {% if not editing_strings.bcc %}hide{% endif %}\">\n        <label class=\"left\">{{_(\"Bcc\")}}</label>\n        <label class=\"right\">\n          <a href=\"#bcc\" class=\"compose-hide-field\" data-mid=\"{{mid}}\"><span class=\"icon-eye\"></span> hide</a>\n        </label>\n        <input id=\"compose-bcc-{{mid}}\" class=\"compose-address-field\" data-mid=\"{{mid}}\" name=\"bcc\" type=\"text\" tabindex=\"3\" alt=\"{{_(\"Bcc\")}}\" value=\"{{editing_strings.bcc}}\">\n      </div>\n      <div class=\"compose-headers compose-subject\">\n        <label>{{_(\"Subject\")}}</label>\n        <input id=\"compose-subject\" name=\"subject\" tabindex=\"4\" type=\"text\" placeholder=\"{{_(\"Subject\")}}\" alt=\"{{_(\"Subject\")}}\" value=\"{{editing_strings.subject}}\">\n      </div>\n    </div>\n    <div id=\"compose-body-{{mid}}\" class=\"compose-body clearfix\" data-mid=\"{{mid}}\">\n      <textarea id=\"compose-text-{{mid}}\" data-mid=\"{{mid}}\" class=\"compose-text\" name=\"body\" tabindex=\"5\" placeholder=\"{{_(\"Your Message...\")}}\" alt=\"{{_(\"Your Message...\")}}\">{{editing_strings.body}}</textarea>\n      <div id=\"compose-attachments-{{mid}}\" class=\"compose-attachments\" data-mid=\"{{mid}}\">\n        <ul id=\"compose-attachments-files-{{mid}}\" class=\"compose-attachments horizontal clearfix\">\n          {% if editing_strings.attachments %}\n          {% for att in attachments %}\n          <li id=\"compose-attachment-{{mid}}-{{att.aid}}\" class=\"left\">\n            {% if att.mimetype in [\"image/bmp\", \"image/gif\", \"image/jpg\", \"image/jpeg\", \"image/pjpeg\", \"image/svg+xml\", \"image/x-png\", \"image/png\", \"application/vnd.google-apps.photo\"] %}\n            <div class=\"attachment-image\">\n              <a href=\"#\" data-mid=\"{{mid}}\" data-aid=\"{{att.aid}}\" class=\"compose-attachment-remove\">\n                <span class=\"icon-circle-x\"></span>\n              </a>\n              <div class=\"preview\" style=\"background-image: url('{{ U('/message/download/preview/=', mid, '/', att.aid, '/') }}');\"></div>\n            </div>\n            {% else %}\n            <div class=\"attachment\">\n              <a href=\"#\" data-mid=\"{{mid}}\" data-aid=\"{{att.aid}}\" class=\"compose-attachment-remove\">\n                <span class=\"icon-circle-x\"></span>\n              </a>\n              <div class=\"preview\">\n                <span class=\"icon-mime\" type=\"{{att.mimetype}}\"></span>\n                {% set file_parts = att.filename.split(\".\") %}\n                {% set file_parts_length = file_parts|length %}\n                <span class=\"extension\">{{ file_parts[file_parts_length - 1] }}</span>\n              </div>\n              <div class=\"filename\">\n                {% if file_parts_length > 2 or att.filename|length > 20 %}\n                  {{ att.filename[0:16] }}...\n                {% else %}\n                  {{ file_parts[0] }}\n                {% endif %}\n              </div>\n            </div>\n            {% endif %}\n\n          </li>\n          {% endfor %}\n          {% endif %}\n        </ul>\n        <div class=\"clearfix\" id=\"compose-signature-{{mid}}\"></div>\n        <ul class=\"horizontal\">\n          <li>\n            <a id=\"compose-attachment-pick-{{mid}}\" class=\"compose-attachment-pick hide\" href=\"#\"><span class=\"icon-attachment\"></span>{{_(\"Add Attachment\")}}</a>\n            <span class=\"attachment-browswer-unsupported\">{{_(\"Unable to add attachments\")}}, <a href=\"\">{{_(\"update your browser\")}}</a></span>\n          </li>\n          {% if config.prefs.gpg_email_key %}<li class=\"add-left compose-attach-key hide\">\n            <label class=\"compose-attach-key\" data-mid=\"{{mid}}\"\n                   title=\"{{_(\"Attach your public encryption key to this message\")}}\">\n              <input type=\"hidden\" id=\"compose-hidden-attach-key-{{mid}}\"\n                     value=\"{{ editing_strings.get('attach-pgp-pubkey', 'no') }}\"\n                     name=\"attach-pgp-pubkey\">\n              <input type=\"checkbox\" id=\"compose-attach-key-{{mid}}\"\n                     {%- if truthy(editing_strings.get('attach-pgp-pubkey', 'no')) %} checked{% endif %}>\n              {{_(\"Attach Key\")}}\n            </label>\n          </li>{% endif %}\n        </ul>\n      </div>\n    </div>\n    <div class=\"compose-options\" data-mid=\"{{mid}}\">\n      <div class=\"compose-options-crypto\">\n        <div id=\"compose-crypto-encryption-{{mid}}\" class=\"compose-crypto-encryption none\" data-mid=\"{{mid}}\">\n          <span class=\"icon icon-lock-open\"></span><span class=\"text hide\">{{_(\"None\")}}</span>\n        </div>\n        <div id=\"compose-crypto-signature-{{mid}}\" class=\"compose-crypto-signature none\" data-mid=\"{{mid}}\">\n          <span class=\"icon icon-signature-none\"></span><span class=\"text hide\">{{_(\"Unsigned\")}}</span>\n        </div>\n      </div>\n      <ul class=\"horizontal\" data-mid=\"{{mid}}\">\n        <li id=\"compose-to-summary-{{mid}}\" class=\"compose-to-summary\" data-mid=\"{{mid}}\"></li>\n        <li>\n          <a id=\"compose-show-details-{{mid}}\" class=\"compose-show-details {% if command != \"view\" %}hide{% endif %}\" href=\"#\" data-mid=\"{{mid}}\" data-message=\"{{_(\"hide details\")}}\">{{_(\"To\")}}: {{recipient_summary(editing_strings, editing_addresses, 36)}}</a>\n        </li>\n        <li>\n          <label id=\"compose-message-autosaving-{{mid}}\" data-mid=\"{{mid}}\" data-autosave_msg=\"{{_(\"autosaving...\")}}\" data-autosave_error_msg=\"{{_(\"error autosaving\")}}\"></label>\n        </li>\n      </ul>\n      {% if command == \"view\" %}\n      <label class=\"compose-apply-quote right\" data-mid=\"{{mid}}\" data-quoted_reply=\"{{config.web.quoted_reply}}\">\n        <input id=\"compose-quoted-reply-{{mid}}\" type=\"checkbox\" {% if config.web.quoted_reply in ['enabled', 'unset'] %}checked{% endif %}> {{_(\"Quote\")}}\n      </label>\n      {% endif %}\n    </div>\n    <div class=\"compose-actions clearfix\">\n      <div class=\"dropdown left\">\n        <!--\n        <a class=\"dropdown-toggle\" data-toggle=\"dropdown\" id=\"reply-datetime\" href=\"#\"><span class=\"icon icon-archive\"></span> Send <span id=\"reply-datetime-display\">Now</span></a>\n        <ul id=\"menu1\" class=\"dropdown-menu\" role=\"menu\" aria-labelledby=\"reply-datetime\">\n          <li role=\"presentation\"><a role=\"menuitem\" tabindex=\"-1\" class=\"pick-send-datetime\" data-datetime=\"now\" href=\"#\">Now</a></li>\n          <li role=\"presentation\"><a role=\"menuitem\" tabindex=\"-1\" class=\"pick-send-datetime\" data-datetime=\"60\" href=\"#\">1 min</a></li>\n          <li role=\"presentation\"><a role=\"menuitem\" tabindex=\"-1\" class=\"pick-send-datetime\" data-datetime=\"3600\" href=\"#\">1 hr</a></li>\n          <li role=\"presentation\"><a role=\"menuitem\" tabindex=\"-1\" class=\"pick-send-datetime\" data-datetime=\"10800\" href=\"#\">3 hrs</a></li>\n          <li role=\"presentation\"><a role=\"menuitem\" tabindex=\"-1\" class=\"pick-send-datetime\" data-datetime=\"21600\" href=\"#\">6 hrs</a></li>\n          <li role=\"presentation\"><a role=\"menuitem\" tabindex=\"-1\" class=\"pick-send-datetime\" data-datetime=\"43200\" href=\"#\">12 hrs</a></li>\n          <li role=\"presentation\"><a role=\"menuitem\" tabindex=\"-1\" class=\"pick-send-datetime\" data-datetime=\"86400\" href=\"#\">1 day</a></li>\n          <li role=\"presentation\"><a role=\"menuitem\" tabindex=\"-1\" class=\"pick-send-datetime\" data-datetime=\"172800\" href=\"#\">2 days</a></li>\n          <li role=\"presentation\"><a role=\"menuitem\" tabindex=\"-1\" class=\"pick-send-datetime\" data-datetime=\"604800\" href=\"#\">1 week</a></li>\n          <li role=\"presentation\"><a role=\"menuitem\" tabindex=\"-1\" class=\"pick-send-datetime\" data-datetime=\"1209600\" href=\"#\">2 weeks</a></li>\n          <li role=\"presentation\"><a role=\"menuitem\" tabindex=\"-1\" class=\"pick-send-datetime\" data-datetime=\"2592000\" href=\"#\">1 month</a></li>\n        </ul>\n        -->\n        {% set from = editing_addresses and editing_strings.from_aids and editing_addresses[editing_strings.from_aids.0] or '' %}\n        <div class=\"dropup\">\n          <div class=\"compose-from-select dropdown-toggle\" data-toggle=\"dropdown\">\n            <span id=\"compose-from-selected-{{mid}}\" class=\"compose-from-selected\">\n              {% if from and from.photo %}\n              <div class=\"avatar\"><img src=\"{{from.photo}}\"></div>\n              {% else %}\n              <div class=\"avatar\"><img src=\"{{ U('/static/img/avatar-default-white.png') }}\"></div>\n              {% endif %}\n              <div class=\"name-and-address\">\n              {% if from %}\n                <div class=\"name\">{{from.fn}}</div>\n                <div class=\"address\">{{from.address}}</div>\n              {% else %}\n                <div class=\"name\">{{_(\"Choose Sending Account\")}}</div>\n                <div class=\"address\">...</div>\n              {% endif %}\n              </div>\n            </span>\n            <span class=\"compose-from-caret\">\n              <span class=\"caret\"></span>\n            </span>\n          </div>\n          <ul class=\"dropdown-menu\">\n          {% set found = [] %}\n          {% set emails = profiles.emails|sort %}\n          {% if from.address not in emails %}\n            {% set emails = [from.address] + emails %}\n          {% endif %}\n          {% for profile_email in emails %}\n            {% set profile = (profile_email in profiles.emails)\n                             and profiles.profiles[profiles.emails[profile_email]]\n                             or {'fn': from.fn} -%}\n            {%- if profile['x-mailpile-profile-route'] -%}\n            <li role=\"presentation\">\n              <a href=\"#\" class=\"compose-from\"\n                 data-mid=\"{{mid}}\" data-email=\"{{ profile_email }}\"\n                 data-sig=\"{{ profiles.default_sig if ('x-mailpile-profile-signature' not in profile) else profile['x-mailpile-profile-signature'] }}\">\n                <div class=\"avatar\">\n                  {%- if profile.photo %}\n                  <img src=\"{{profile.photo[0].photo}}\">\n                  {%- else %}\n                  <img src=\"{{ U('/static/img/avatar-default-white.png') }}\">\n                  {%- endif %}\n                </div>\n                <div class=\"name-and-address\">\n                  <div class=\"name\">{{profile.fn}}</div>\n                  <div class=\"address\">{{ profile_email }}</div>\n                </div>\n              </a>\n            </li>\n            {%- do found.append(profile) -%}{%- endif %}\n          {% endfor %}\n          {% if found|length == 0 %}\n            <li role=\"presentation\">\n              <a target='_blank' href=\"{{ U('/profiles/') }}\">\n                No usable profiles found, click to check your settings!\n              </a>\n            </li>\n          {% endif %}\n          </ul>\n        </div>\n      </div>\n      <div class=\"compose-buttons right\" data-mid=\"{{mid}}\">\n        <a class=\"compose-message-trash\" data-mid=\"{{mid}}\" href=\"#\" title=\"{{_(\"Move Draft to Trash\")}}\"><span class=\"icon-trash\"></span> Trash</a>\n        <button class=\"compose-action button-primary\" type=\"submit\" name=\"save\" value=\"save\" alt=\"{{_(\"Save\")}}\">\n          <span class=\"icon-compose\"></span> {{_(\"Save\")}}\n        </button>\n        <button id=\"compose-send-{{mid}}\"\n                class=\"compose-action button-secondary{% if not from %} hide{% endif %}\"\n                type=\"submit\" name=\"send\" value=\"{% if command == \"view\" %}reply{% else %}send{% endif %}\"\n                alt=\"{{_(\"Send\")}}\">\n          <span class=\"icon-sent\"></span> {{_(\"Send\")}}\n        </button>\n      </div>\n    </div>\n    <input id=\"compose-from-{{mid}}\" type=\"hidden\" name=\"from\" value=\"{{from.fn}} &lt;{{from.address}}&gt;\">\n    <input id=\"compose-mid-{{mid}}\" type=\"hidden\" name=\"mid\" value=\"{{mid}}\">\n    <input id=\"compose-crypto-{{mid}}\" type=\"hidden\" name=\"encryption\" value=\"{{editing_strings.encryption}}\">\n    <input id=\"compose-signature-{{mid}}\" type=\"hidden\" value=\"\">\n    <input id=\"compose-encryption-{{mid}}\" type=\"hidden\" value=\"\">\n  </form>\n  <script>\n  $(document).ready(function() {\n    Mailpile.Composer.init('{{mid}}',\n                           {{ editing_strings|json|safe }},\n                           {{ editing_addresses|json|safe }});\n  });\n  </script>\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/error_message_missing.html",
    "content": "<div id=\"error\" class=\"content-normal\">\n  {%- if not result.errors %}\n    {%- do result.__setitem__('errors', {'*': {'error': _(\"Message Not Found\")}}) %}\n  {%- endif %}\n\n  <h2><span class=\"icon-inbox\"></span> {{result.errors[result.errors.keys()[0]].error}}</h2>\n  {%- if result.errors|length > 1 %}\n    <p><b>Other errors:</b>\n    {%- for e in result.errors %}\n      {{- e.error }}{% if not loop.last %}, {% endif %}\n    {%- endfor %}</p>\n  {%- endif %}\n  <br>\n\n  {%- set details = [] %}\n  {%- for k in result.errors %}\n  <p>\n    {%- set e = result.errors[k] %}\n    {% if e.details %}{% do details.append(e.details) %}{{e.details}}<br>{% endif %}\n    <a id=\"show-techstuff-{{loop.index}}\" href=\"javascript:_errmsg_display_techstuff({{loop.index}});\">{{_(\"Display technical details.\")}}</a>\n    <div id=\"techstuff-{{loop.index}}\" class=\"hide\">\n      <pre>{{e.traceback}}\nMessage MID = {{ k }}\nError = {{ e.error }}\nDetails = {{ e.details }}\nData = {{ e.locations|json }}</pre>\n    </div>\n  </p>\n  {%- endfor %}\n  {%- if not details %}\n  <p>\n    {{_(\"We were unable to find the message you requested, perhaps it was deleted?\")}}\n  </p>\n  {%- endif %}\n\n  <p class='bugreport'>\n    {{_(\"If you think this is a bug, please <a href=\\\"%(url)s\\\" target=\\\"_blank\\\">file a report</a> including the technical details above.\",\n        url=\"https://github.com/mailpile/Mailpile/issues/new?title=Message%20not%20loading\")}}\n  </p>\n\n  <script>\n    function _errmsg_display_techstuff(tbno) {\n      $('#show-techstuff-' + tbno).hide();\n      $('#techstuff-' + tbno).slideDown();\n      var $br = $('.bugreport a');\n      var details = encodeURIComponent(\n        '({{_(\"DESCRIBE YOUR PROBLEM HERE\")}})\\n```\\n'\n        + $('#techstuff-' + tbno + ' pre').html()\n            .replace(/^/gm, '    ')\n            .replace(/\\/\\S+\\/mailpile\\//ig, '.../')  // PRIVACY IS GOOD!\n        + '\\n```');\n      $br.attr('href', $br.attr('href') + '&body=' + details);\n    }\n  </script>\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/errors_content.html",
    "content": "<div id=\"error\" class=\"content-normal\">\n  {% if error_title == \"contact_missing\" %}\n  <h2><span class=\"icon-user\"></span> {{_(\"Contact Missing\")}}</h2>\n  <p>{{_(\"We were unable to find the contact you requested, perhaps it was typed (or linked to) incorrectly?\")}}</p>  \n  {% elif error_title == \"tag_missing\" %}\n  <h2><span class=\"icon-tag\"></span> {{_(\"Tag Missing\")}}</h2>\n  <p>{{_(\"We were unable to find the tag you requested, perhaps it was typed (or linked to) incorrectly?\")}}</p>  \n  {% else %}\n  <h2><span class=\"icon-inbox\"></span> {{_(\"Oops, You've Found a Quirk\")}}</h2>\n  <p>{{_(\"There's something happening here. What it is ain't exactly clear. Everybody look what's going down.\")}}</p>\n  {% endif %}\n  <p>{{_(\"If you think this is a bug, please file a <a href=\\\"%(url)s\\\" target=\\\"_blank\\\">bug report</a>\", url=\"https://github.com/pagekite/Mailpile/issues\")}}</p>\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/head.html",
    "content": "  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n  <meta name=\"referrer\" content=\"no-referrer\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\n  <link rel=\"manifest\" href=\"{{ U('/jsapi/pwa/manifest.json?ts=', version_identifier()) }}\">\n  <link rel=\"stylesheet\" href=\"{{ U('/static/css/default.css?ts=', version_identifier()) }}\" />\n  <link rel=\"stylesheet\" href=\"{{ U('/static/css/print.css?ts=', version_identifier()) }}\" media=\"print\" />\n  {% if state.command_url not in (\"/auth/login/\", \"/auth/logout/\") %}\n  <link rel=\"stylesheet\" href=\"{{ U('/jsapi/as.css?ts=', version_identifier(), '-', config.timestamp) }}\" />\n  {% endif %}\n\n  <!-- Apple Icons -->\n  <link rel=\"apple-touch-icon\" sizes=\"57x57\" href=\"{{ U('/static/img/apple-touch-icon.png') }}\" />\n  <link rel=\"apple-touch-icon\" sizes=\"72x72\" href=\"{{ U('/static/img/apple-touch-icon-72x72.png') }}\" />\n  <link rel=\"apple-touch-icon\" sizes=\"114x114\" href=\"{{ U('/static/img/apple-touch-icon-114x114.png') }}\" />\n  <!-- Disable Google Translate in Chrome so it doesn't leak our e-mail -->\n  <meta name=\"google\" value=\"notranslate\">\n\n  <!-- Favicon -->\n  <link rel=\"shortcut icon\" id=\"basic-favicon\" href=\"{{ U('/static/img/favicon.png') }}\" />\n  <link rel=\"icon\" id=\"basic-favicon\" type=\"image/png\" href=\"{{ U('/static/img/favicon.png') }}\" />\n\n  {%- if 0 and is_dev_version() %}{# Uglify dev versions ... #}\n  <style type=\"text/css\">\n    .topbar { filter: blur(1px); -webkit-filter: blur(1px); }\n    .topbar:hover { filter: none;; -webkit-filter: none; }\n    form#form-search:after { content: \"[DEV MODE] {{ random([\n      'Oops, coder probably drunk.',\n      'Are you sure that button works?',\n      'Blurry, because development. :-)',\n      'How old are those contact lenses?',\n      'Thanks for testing Mailpile! =8-]',\n      'Time and focus reduce bugs and blur.',\n      'We know about the blur. Working on it.',\n      'Shoftware prgoramming i sserius bisnus',\n      'Why can`t this thing just work??',\n      'Where did I leave my glasses?',\n      'Tried turning it off and on again?',\n      'I love lamp! :D',\n      'git commit -a -m `please work this time`',\n      'Can I crash on your couch?']) }}\" }\n  </style>\n  {%- endif %}\n\n  <!-- JS Global Libraries -->\n  <script src=\"{{ U('/static/js/libraries.min.js?ts=', version_identifier()) }}\"></script>\n  {% if state.command_url not in (\"/auth/login/\", \"/auth/logout/\", \"/setup/welcome/\") %}\n  <script src=\"{{ U('/api/0/jsapi/as.js?ts=', version_identifier(), '-', config.timestamp) }}\"></script>\n\n  <!-- JS - App Specific -->\n  <script>\n  $(document).ready(function() {\n    // Print JSON for JS Use\n    Mailpile.instance = {};\n  });\n\n  /* Plugins */\n  $(document).ready(\n    {{ui_elements_setup('.plugin-activity-%(name)s', get_ui_elements('activities', state, '/'))}}\n  );\n  </script>\n  <script src=\"{{ U('/api/0/jsapi/app.js?ts=', version_identifier(), '-', config.timestamp) }}\"></script>\n  {% endif %}\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/helpers.html",
    "content": "<!-- Helpers -->\n\n<script id=\"template-modal-helper\" type=\"text/template\">\n  <div class=\"modal-dialog\">\n    <div class=\"modal-content\">\n      <div class=\"modal-header\">\n        <button type=\"button\" class=\"close button-info\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n        <h4 class=\"modal-title\"><span class=\"icon-help\"></span> <%= title %></h4>\n      </div>\n      <div class=\"modal-body\"><%= content %></div>\n      <% if (actions) { %>\n      <div class=\"modal-footer\">\n        <% if (_.indexOf(actions, \"hideable\") > -1) { %>\n        <button type=\"submit\" class=\"btn-helper-make-hidden\" title=\"{{_(\"Don't Show\")}}\" alt=\"{{_(\"Don't Show\")}}\">\n          <span class=\"icon-eye\"></span> {{_(\"Don't Show\")}}\n        </button>\n        <% } %>\n      </div>\n      <% } %>\n    </div>\n  </div>\n</script>\n\n\n<script id=\"template-helper-what-is-encryption-key\" type=\"text/template\">\n  <p>Encryption keys are used to encrypt messages. Mailpile uses the encryption standard <a href=\"https://en.wikipedia.org/wiki/Pretty_Good_Privacy#OpenPGP\" target=\"_blank\">OpenPGP</a>. In order to encrypt a message, you need encryption keys for all the recipients you are messaging. Here's an example of the encryption key that belongs to the company that made Mailpile.</p>\n  <h5><span class=\"icon-key\"></span> Mailpile Team's Key</h5>\n  <ul>\n    <li id=\"item-encryption-key-4161CD18CCA11A0B1F4C3EA0AAD965A4707775F9\" class=\"searchkey-result-item animated fadeIn\">\n      <div class=\"clearfix\">\n        <div class=\"avatar\"><img src=\"{{ U('/static/img/avatar-default.png') }}\"></div>\n        <div class=\"name\">\n          Mailpile Team<br>\n          <span>team@mailpile.is</span>\n        </div>\n        <div class=\"right\">\n          <span class=\"icon-fingerprint\"></span>\n          <div class=\"fingerprint\">\n            4161 CD18 CCA1 1A0B 1F4C 3EA0 AAD9 65A4 7077 75F9\n          </div>\n        </div>\n      </div>\n      <div class=\"searchkey-result-details add-top clearfix hide\">\n        <div class=\"add-bottom\">\n          <strong>{{_(\"Contact Info\")}}</strong>\n          <table>\n          <tr>\n            <td>Mailpile Team</td>\n            <td>team@mailpile.is</td>\n            <td></td>\n          </tr>\n          </table>\n        </div>\n        <div class=\"add-bottom\">\n          <strong>{{_(\"Score\")}} +20</strong><br>\n          Key is in keychain<br>\n          Key is strong\n        </div>\n        <div class=\"half-bottom\">\n          <strong>{{_(\"Details\")}}</strong><br>\n          {{_(\"Strength\")}}: 4096<br>\n          {{_(\"Algorithm\")}}: RSA<br>\n          {{_(\"Created\")}}: 29, 11, 2013\n        </div>\n      </div>\n      <div class=\"half-top clearfix\">\n        <a href=\"#\" class=\"searchkey-result-score left\"\n            data-fingerprint=\"4161CD18CCA11A0B1F4C3EA0AAD965A4707775F9\"\n            data-score_reason=\"\"\n            data-score_stars=\"3\">\n          <span class=\"icon-star color-06-blue\"></span>\n          <span class=\"icon-star color-06-blue\"></span>\n          <span class=\"icon-star color-06-blue\"></span>\n        </a>\n        <div class=\"searchkey-result-actions right\">\n          <select name=\"contact-key\" class=\"crypto-key-policy\" data-fingerprint=\"4161CD18CCA11A0B1F4C3EA0AAD965A4707775F9\">\n            <option value=\"true\" selected=\"selected\">{{_(\"Use This Key\")}}</option>\n            <option value=\"false\">{{_(\"Don't Use This Key\")}}</option>\n          </select>\n        </div>\n      </div>\n    </li>\n  </ul>\n  <p>There are multiple ways to find and import encryption keys into your Mailpile. Most commonly, you can search your messages and the internet for encryption keys.</p>\n  <p><a href=\"#\" class=\"btn-helper-action\" data-action=\"CryptoFindKeys\"><span class=\"icon-search\"></span> Search for Encryption Keys</a></p>\n  <p>You can also import encryption keys from a file on your computer. Usually, encryption keys have the following file extensions <strong>.asc .key .pub</strong>\n  </p>\n  <p><a href=\"#\" class=\"btn-helper-action\" data-action=\"CryptoUploadKey\"><span class=\"icon-upload\"></span> Upload Encryption Keys</a></p>\n  <p class=\"remove-bottom\"><em class=\"text-detail\">Note: Mailpile created you a new encryption key or imported existing keys already on your computer.</em></p>\n</script>\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/hidden.html",
    "content": "<!-- Connection Down -->\n<script id=\"template-connection-down\" type=\"text/template\">\n  <div id=\"connection-down\">\n    <div class=\"message\">\n      <div id=\"connection-down-logo\" class=\"add-top animated bounceIn\">\n      {% include(\"../img/logo-color.svg\") %}\n      </div>\n      <h1 class=\"half-top\"><%= message %></h1>\n      <h3 class=\"half-top\"><%= advice %></h3>\n      <p>&nbsp;</p>\n    </div>\n  </div>\n</script>\n\n\n<!-- Search Param Editor -->\n<script id=\"template-search-params\" type=\"text/template\">\n  <div id=\"search-params\" class=\"clearfix hide\">\n    <ul>\n      <li><span class=\"icon-calendar\"></span> {{_(\"From\")}} <a href=\"#\" class=\"\" id=\"search-start-time\">{{_(\"Start Date\")}}</a> {{_(\"to\")}} <a href=\"#\" class=\"\" id=\"search-end-time\">{{_(\"Present\")}}</a></li>\n      <li><span class=\"icon-tag\"></span> {{_(\"With\")}} <a href=\"#\" id=\"search-tags\">{{_(\"No Tags\")}}</a></li>\n      <li><span class=\"icon-groups\"></span> {{_(\"In\")}} <a href=\"#\" id=\"search-all-groups\">{{_(\"All Groups\")}}</a></li>\n      <li><span class=\"icon-user\"></span> {{_(\"And\")}} <a href=\"#\" id=\"search-tags\">{{_(\"All Contacts\")}}</a></li>\n    </ul>\n  </div>\n</script>\n\n\n<!-- Debug -->\n<script id=\"template-debug\" type=\"text/template\">\n  <div id=\"debug\">\n    <h3>DEBUG</h3>\n    <b>command:</b> {{ command }}<br>\n    <b>args:</b> {{ args }}<br>\n    <b>kwargs:</b> {{ kwargs }}\n    {{logged}}\n  </div>\n</script>\n\n\n<!-- Contact -->\n<script id=\"template-contact-add\" type=\"text/template\">\n  <form id=\"form-contact-add\" class=\"standard\">{{ csrf_field|safe }}\n    <fieldset class=\"contact-add-fields\">\n      <label>{{_(\"Name\")}}</label>\n      <input type=\"text\" name=\"name\" data-type=\"name\" class=\"contact-add-name\" value=\"\" placeholder=\"Chelsea Manning\">\n      <label>{{_(\"E-mail\")}}</label>\n      <input type=\"text\" name=\"email\" data-type=\"email\" class=\"contact-add-email\" value=\"\" placeholder=\"chelsea@manning.com\">\n      <div class=\"contact-add-signature\"></div>\n    </fieldset>\n    <div id=\"contact-add-key\"></div>\n    <button class=\"button-primary\" type=\"submit\"><span class=\"icon-plus\"></span> {{_(\"Add\")}}</button>\n  </form>\n</script>\n\n\n<!-- Crypto - Results and templates for interacting with keyservers -->\n<script id=\"template-find-keys-running\" type=\"text/template\">\n  <span class=\"icon-search\"></span> {{_(\"Searching for encryption keys for:\")}} <span class=\"color-01-gray-mid\"><%= query %></span>\n</script>\n\n<script id=\"template-find-keys-none\" type=\"text/template\">\n  <span class=\"icon-x\"></span> {{_(\"No encryption keys found matching:\")}} <span class=\"color-01-gray-mid\"><%= query %></span>\n</script>\n\n<script id=\"template-find-keys-error\" type=\"text/template\">\n  <span class=\"icon-signature-unknown\"></span> {{_(\"Make sure your internet connection is working\")}}\n</script>\n\n<script id=\"template-crypto-encryption-key\" type=\"text/template\">\n  <li id=\"item-encryption-key-<%= fingerprint %>\" class=\"searchkey-result-item animated fadeIn\">\n    <div class=\"clearfix\">\n      <div class=\"avatar\"><img src=\"<%= avatar %>\"></div>\n      <div class=\"name\">\n        <%= uid.name %><br>\n        <span><%= uid.email %></span>\n      </div>\n      <div class=\"right\">\n        <span class=\"icon-fingerprint\"></span>\n        <div class=\"fingerprint\">\n          <%= Mailpile.nice_fingerprint(fingerprint) %>\n        </div>\n      </div>\n    </div>\n    <div class=\"searchkey-result-details add-top clearfix hide\">\n      <% if (uids) { %>\n      <div class=\"add-bottom\">\n        <strong>{{_(\"Contact Info\")}}</strong>\n        <table>\n        <% _.each(uids, function(uid, key) { %>\n        <tr>\n          <td><%= uid.name %></td>\n          <td><%= uid.email %></td>\n          <td><%= uid.comment %></td>\n        </tr>\n        <% }) %>\n        </table>\n      </div>\n      <% } %>\n      <div class=\"add-bottom\">\n        <strong>{{_(\"Score\")}} <% if (score > 0) { %>+<% } %><%= score %></strong><br>\n        <% _.each(scores, function(item, key) { %>\n        <% if (item[1]) { %><%= item[1] %> <% if (item[0] > 0) { %>+<% } %><%= item[0] %><br><% } %>\n        <% }); %>\n      </div>\n      <div class=\"half-bottom\">\n        <strong>{{_(\"Details\")}}</strong><br>\n        {{_(\"Strength\")}}: <%= keysize %><br>\n        {{_(\"Algorithm\")}}: <%= keytype_name %><br>\n        {{_(\"Created\")}}: <%= creation_date %>\n      </div>\n    </div>\n    <div class=\"half-top clearfix\">\n      <a href=\"#\" class=\"searchkey-result-score left\"\n          data-fingerprint=\"<%= fingerprint %>\"\n          data-score_reason=\"<%= score_reason %>\"\n          data-score_stars=\"<%= score_stars %>\">\n        <% if (score > 0) { %>\n        <% for (i = 0; i < Math.abs(score_stars); i++) { %>\n        <span class=\"icon-star <%= score_color %>\"></span>\n        <% } %>\n        <% } else { %>\n        <span class=\"icon-signature-revoked <%= score_color %>\"></span>\n        <em>{{_(\"Don't use this key\")}}</em>\n        <% } %>\n      </a>\n      <div class=\"searchkey-result-actions right\">\n        <% if (on_keychain && in_vcards) { %>\n        <i>{{_(\"This key is available for use.\")}}</i>\n        <% } else { %>\n        <a href=\"#\" class=\"crypto-key-import\"\n           data-email=\"<%= uid.email %>\"\n           data-fingerprint=\"<%= fingerprint %>\" data-action=\"<%= action %>\">\n          <span class=\"icon-key\"></span> {{_(\"Use This Key\")}}\n        </a>\n        <% } %>\n      </div>\n    </div>\n  </li>\n</script>\n<script id=\"unuxed-key-pinnig-ui-draft\" type=\"text/template\">\n      <div class=\"searchkey-result-actions right\">\n        <% if (on_keychain && in_vcards) { %>\n        <i>{{_(\"This key is available for use.\")}}</i>\n        <select name=\"contact-key\" class=\"crypto-key-policy\"\n                data-email=\"<%= uid.email %>\"\n                data-fingerprint=\"<%= fingerprint %>\">\n          <option value=\"true\"<% if (is_preferred && !is_pinned) { %> selected=\"selected\"<% } %>>{{_(\"Use This Key\")}}</option>\n          <option value=\"pin\"<% if (is_preferred && is_pinned) { %> selected=\"selected\"<% } %>>{{_(\"Pin and Use This Key\")}}</option>\n          <option value=\"false\"<% if (!is_preferred) { %> selected=\"selected\"<% } %>>{{_(\"Don't Use This Key\")}}</option>\n        </select>\n        <% } else { %>\n        <a href=\"#\" class=\"crypto-key-import-pinned searchkey-result-details hide\"\n           data-email=\"<%= uid.email %>\"\n           data-fingerprint=\"<%= fingerprint %>\" data-action=\"<%= action %>\">\n          <span class=\"icon-star\"></span> {{_(\"Pin and Use This Key\")}}\n        </a> &nbsp;\n        <a href=\"#\" class=\"crypto-key-import\"\n           data-email=\"<%= uid.email %>\"\n           data-fingerprint=\"<%= fingerprint %>\" data-action=\"<%= action %>\">\n          <span class=\"icon-key\"></span> {{_(\"Use This Key\")}}\n        </a>\n        <% } %>\n      </div>\n</script>\n\n\n<script id=\"template-crypto-encryption-key-importing\" type=\"text/template\">\n  <li id=\"item-encryption-key-<%= fingerprint %>\" class=\"searchkey-result-item animated fadeIn\">\n  <div class=\"text-center\">\n    <% if (failed) { %>\n    <h4 class=\"half-bottom\">{{_(\"Importing Failed\")}}</h4>\n    <em class=\"text-detail\">{{_(\"Try Again\")}}?</em>\n    <% } else { %>\n    <h4 class=\"half-bottom\">{{_(\"Importing Encryption Key\")}}</h4>\n    <p class=\"half-bottom\">{% include(\"../img/loading-ellipsis.svg\") %}</p>\n    <% if (file) { %>\n    <em class=\"text-detail\">{{_(\"Uploading:\")}} <%= file.name %> (<%= plupload.formatSize(file.size) %>)</em>\n    <% } else { %>\n    <em class=\"text-detail\">{{_(\"This may take a few moments...\")}}</em>\n    <% }} %>\n  </div>\n  </li>\n</script>\n\n\n<script id=\"template-search-keyserver-results\" type=\"text/template\">\n  <div class=\"contact-add-search-keyserver\">\n    <div id=\"contact-search-keyserver-input\"></div>\n    <div id=\"contact-search-keyserver-result\"></div>\n  </div>\n</script>\n\n\n<script id=\"template-search-keyserver-show-hidden\" type=\"text/template\">\n  <li class=\"text-center remove-bottom\">\n    <a href=\"#\" class=\"crypto-show-hidden-keys\">\n      <span class=\"icon-signature-revoked color-12-red\"></span> {{_(\"Show Hidden Encryption Keys\")}}\n    </a>\n    <div class=\"half-top\">\n      <em class=\"text-detail\">{{_(\"Hidden keys are either revoked or expired and should not be used\")}}</em>\n    </div>\n  </li>\n</script>\n\n\n<script id=\"template-search-tags-link\" type=\"text/template\">\n  <span class=\"pile-message-tag color-<%= label_color %>\" id=\"pile-message-tag-<%= tid %>-<%= mid %>\" data-tid=\"<%= tid %>\" data-mid=\"<%= mid %>\">\n    <span class=\"pile-message-tag-icon <%= icon %>\"></span>\n    <span class=\"pile-message-tag-name\"><%= name %></span>\n  </span>\n</script>\n\n\n<script id=\"template-modal-loading\" type=\"text/template\">\n  <div class=\"modal-dialog\">\n    <div class=\"modal-content\" style=\"width: 42em; text-align: center;\">\n      <br><br>\n      <h3>{{ _(\"Working\") }} ...</h3>\n      <br>\n      <p>{% include(\"../img/loading-ellipsis.svg\") %}</p>\n      <br clear=both>\n    </div>\n  </div>\n</script>\n\n\n<script id=\"template-modal-private-key-item\" type=\"text/template\">\n  <li class=\"grouped\">\n    <h4><span class=\"icon-fingerprint\"></span> <%= Mailpile.nice_fingerprint(fingerprint) %></h4>\n    <% if (expiration_date) { %>\n    <em>Key expires: <%= expiration_date %></em>\n    <% } %>\n    <% _.each(uids, function(uid, key) { %>\n    <strong><%= uid.name %></strong><br>\n    <%= uid.email %><br><br>\n    <% }); %>\n    <%= keytype_name %> Key / <%= keysize %> bits\n    <label class=\"right\">\n    Send Key <input type=\"checkbox\" name=\"send_key\" value=\"<%= fingerprint %>\">\n    </label>\n  </li>\n</script>\n\n\n<script id=\"template-thread-notification-draft-trash\" type=\"text/template\">\n  <a href=\"#\" onclick=\"window.location.reload(true);\"><span class=\"icon-trash\"></span> {{_(\"Draft was deleted\")}} <span class=\"instruction\">({{_(\"click to reload\")}})</span></a>\n</script>\n\n\n<script id=\"template-notification-bubble\" type=\"text/template\">\n  <div id=\"event-<%= event_id %>\" class=\"notification-bubble <%= status %> hide\">\n    <span class=\"icon <%= icon %>\"></span>\n    <span class=\"text\">\n      <span class=\"message\"><%= message %></span><br>\n      <% if (message2) { %>\n      <span class=\"action\"><%= message2 %>\n      <%   if (action_js) { %>\n        - <a <%= action_js %> data-event_id=\"<%= event_id %>\" class=\"action <%= action_cls %>\"><%= action_text %></a>\n      <%   } else if (action_url) { %>\n        - <a href=\"<%= action_url %>\" data-event_id=\"<%= event_id %>\" class=\"action <%= action_cls %>\"><%= action_text %></a>\n      <%   } %>\n      </span>\n      <% } else if (type === 'nagify') { %>\n      <span class=\"action\">{{\"Want to\"}} <a href=\"<%= action %>\" data-event_id=\"<%= event_id %>\" class=\"action notification-nag <%= action_cls %>\">{{_(\"do it now?\")}}</a></span>\n      <% } else if (undo) { %>\n      <span class=\"action\">{{_(\"A mistake?\")}} <a href=\"#\" data-event_id=\"<%= event_id %>\" class=\"action notification-undo <%= action_cls %>\">{{_(\"undo\")}}</a></span>\n      <% } %>\n    </span>\n    <a href=\"#\" data-type=\"<%= type %>\" class=\"notification-close\"><span class=\"icon-x\"></span></a>\n  </div>\n</script>\n\n\n<script id=\"template-messsage-inline-pgp-key-import\" type=\"text/template\">\n  <a href=\"<%= pgp_href %>\" data-mid=\"<%= mid %>\" download=\"{{_('Encryption Key from')}} <%= name %>.asc\" class=\"message-crypto-import-key button-secondary half-bottom\"><span class=\"icon-key\"></span> {{_(\"Import Encryption Key\")}}</a>\n  <a href=\"#\" data-mid=\"<%= mid %>\" class=\"message-crypto-show-inline-key link-detail\"><span class=\"icon-eye\"></span> {{_(\"Show Encryption Key\")}}</a>\n  <div id=\"message-crypto-inline-key-<%= mid %>\" class=\"message-crypto-inline-key half-top hide\"><%= pgp_key %></div>\n</script>\n\n\n<script id=\"template-composer-attachment-image\" type=\"text/template\">\n  <li id=\"compose-attachment-<%= mid %>-<%= aid %>\" class=\"left hide\">\n    <div class=\"attachment-image\">\n      <a href=\"#\" data-mid=\"<%= mid %>\" data-aid=\"<%= aid %>\" class=\"compose-attachment-remove\">\n        <span class=\"icon-circle-x\"></span>\n      </a>\n      <div class=\"preview\" style=\"background-image: url(<%= attachment_data %>);\"></div>\n    </div>\n  </li>\n</script>\n\n\n<script id=\"template-composer-attachment\" type=\"text/template\">\n  <li id=\"compose-attachment-<%= mid %>-<%= aid %>\" class=\"left hide\">\n    <div class=\"attachment\">\n      <a href=\"#\" data-mid=\"<%= mid %>\" data-aid=\"<%= aid %>\" class=\"compose-attachment-remove\">\n        <span class=\"icon-circle-x\"></span>\n      </a>\n      <div class=\"preview\">\n        <span class=\"icon-mime\" type=\"<%= mimetype %>\"></span>\n        <span class=\"extension\"><%= extension %></span>\n      </div>\n      <div class=\"filename\">\n        <%= name_fixed %>\n      </div>\n    </div>\n  </li>\n</script>\n\n\n<script id=\"template-encryption-helper-complete-message\" type=\"text/template\">\n  <div class=\"text-center\">\n    <p>Great job. You have successfully found encryption keys for all the contacts you are trying to send an encrypted e-mail to. Way to be more secure ;)</p>\n    <button class=\"button-secondary\" onclick=\"javascript:Mailpile.UI.hide_modal();Mailpile.Composer.Crypto.EncryptionToggle('encrypt', mid);\">Back To Message</button>\n  </div>\n</script>\n\n\n<script id=\"template-contact-list-item\" type=\"text/template\">\n  <div class=\"rectangles-outer\" id=\"contact-card-<%= email %>\">\n    <div class=\"rectangles-inner\">\n      <a class=\"contact-card-avatar left\"\n{%- if 0 and is_dev_version() %}\n         href=\"{{ U('/contacts/view/<%= email %>/') }}\"\n{%- endif %}\n         >\n        <img src=\"{{ U('/static/img/avatar-default.png') }}\">\n      </a>\n      <a class=\"contact-card-name\"\n{%- if 0 and is_dev_version() %}\n         href=\"{{ U('/contacts/view/<%= email %>/') }}\"\n{%- endif %}\n         >\n        <%= fn %>\n      </a>\n      <input type=\"checkbox\" class=\"contact-card-checkbox right\">\n    </div>\n  </div>\n</script>\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/javascript.html",
    "content": "<script>\n$(document).ready(function() {\n  Mailpile.render();\n  setTimeout(function() {Mailpile.UI.init();}, 1);\n});\n</script>\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/modals.html",
    "content": "<!-- Modal container -->\n<div class=\"modal fade\" id=\"modal-full\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"myModalLabel\" aria-hidden=\"true\">\n  <div class=\"modal-dialog\">\n    <div class=\"modal-content\">\n      <div class=\"modal-header\">\n        <button type=\"button\" class=\"close button-info\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n        <h4 class=\"modal-title\"></h4>\n      </div>\n      <div class=\"modal-body\"></div>\n      <div class=\"modal-footer\"></div>\n    </div>\n  </div>\n</div>\n\n{#\n## NOTE: Individual modal templates live in ../jsapi/templates/ and are\n##       loaded asynchronously.\n#}\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/pile_compose.html",
    "content": "{%- set mid = mid or 'ephemeral-new' %}\n{%- set editing_strings = editing_strings or {'to_aids': [], 'cc_aids': [], 'bcc_aids': []} %}\n{%- set editing_addresses = [] %}\n{%- set profiles = profiles or mailpile('profiles').result %}\n<form id=\"form-compose-{{mid}}\" class=\"form-compose clearfix has-mid\" data-mid=\"{{mid}}\">\n{{ csrf_field|safe }}\n<tr class=\"pile-message pile-message-{{mid}} pile-composer full-message result\">\n  <td class=\"{% if '-' not in mid %}draggable{% endif %}\"></td>\n  <td class=\"message-nav\">\n  {% if '-' not in mid %}\n    <a class=\"compose-message-trash\" title=\"{{_(\"Move Draft to Trash\")}}\"\n       href=\"#\"><span class=\"icon icon-trash\"></span></a>\n    <br>\n  {% endif %}\n  </td>\n  <td class=\"from\">\n    <div class=\"compose-headers message-details\">\n      {%- for field, name in (('to', _(\"To\")),\n                              ('cc', _(\"Cc\")),\n                              ('bcc', _(\"Bcc\"))) %}\n      <div class=\"header-cooked input-{{field}}\">\n        <h3 class=\"header-name\">{{name}}:</h3>\n        <div class=\"header-value header-input{% if loop.index != 1 %} hide{% endif %}\">\n          <span class=\"icon icon-plus\"></span>\n          <input type=text value=\"\">\n        </div>\n      </div>\n      {%- endfor %}\n    </div>\n    <div class=\"composer-status\">\n      <label id=\"compose-message-autosaving-{{mid}}\"\n             data-autosave_msg=\"{{_(\"autosaving...\")}}\"\n             data-autosave_error_msg=\"{{_(\"error autosaving\")}}\"></label>\n    </div>\n  </td>\n  <td class=\"crypto-and-tags\">\n    {# FIXME: List tags, add ability to add tags while composing #}\n  </td>\n  <td class=\"subject\">\n    <div class=\"thread-reply\" id=\"message-{{mid}}\">\n\n      <div id=\"compose-details-{{mid}}\" class=\"compose-subject-container\">\n        <h3>{{_(\"Subject\")}}:</h3>\n        <div class=\"compose-headers compose-subject\">\n          <input id=\"compose-subject\" name=\"subject\" tabindex=\"4\"\n                 placeholder=\"{{_(\"Subject\")}}\" alt=\"{{_(\"Subject\")}}\"\n                 type=\"text\" value=\"{{editing_strings.subject}}\">\n        </div>\n      </div>\n\n      <div id=\"compose-body-{{mid}}\" class=\"compose-body clearfix\">\n        <textarea id=\"compose-text-{{mid}}\" class=\"compose-text\" name=\"body\" tabindex=\"5\" placeholder=\"{{_(\"Your Message...\")}}\" alt=\"{{_(\"Your Message...\")}}\">{{editing_strings.body}}</textarea>\n        <div id=\"compose-attachments-{{mid}}\" class=\"compose-attachments\">\n          <ul id=\"compose-attachments-files-{{mid}}\" class=\"compose-attachments horizontal clearfix\">\n            {% if editing_strings.attachments %}\n            {% for att in attachments %}\n            <li id=\"compose-attachment-{{mid}}-{{att.aid}}\" class=\"left\">\n              {% if att.mimetype in [\"image/bmp\", \"image/gif\", \"image/jpg\", \"image/jpeg\", \"image/pjpeg\", \"image/svg+xml\", \"image/x-png\", \"image/png\", \"application/vnd.google-apps.photo\"] %}\n              <div class=\"attachment-image\">\n                <a href=\"#\" data-aid=\"{{att.aid}}\" class=\"compose-attachment-remove\">\n                  <span class=\"icon-circle-x\"></span>\n                </a>\n                <div class=\"preview\" style=\"background-image: url('{{ U('/message/download/preview/=', mid, '/', att.aid, '/') }}');\"></div>\n              </div>\n              {% else %}\n              <div class=\"attachment\">\n                <a href=\"#\" data-aid=\"{{att.aid}}\" class=\"compose-attachment-remove\">\n                  <span class=\"icon-circle-x\"></span>\n                </a>\n                <div class=\"preview\">\n                  <span class=\"icon-mime\" type=\"{{att.mimetype}}\"></span>\n                  {% set file_parts = att.filename.split(\".\") %}\n                  {% set file_parts_length = file_parts|length %}\n                  <span class=\"extension\">{{ file_parts[file_parts_length - 1] }}</span>\n                </div>\n                <div class=\"filename\">\n                  {% if file_parts_length > 2 or att.filename|length > 20 %}\n                    {{ att.filename[0:16] }}...\n                  {% else %}\n                    {{ file_parts[0] }}\n                  {% endif %}\n                </div>\n              </div>\n              {% endif %}\n  \n            </li>\n            {% endfor %}\n            {% endif %}\n          </ul>\n          <div class=\"clearfix\" id=\"compose-signature-{{mid}}\"></div>\n          <ul class=\"horizontal\">\n            <li>\n              <a id=\"compose-attachment-pick-{{mid}}\" class=\"compose-attachment-pick hide\" href=\"#\"><span class=\"icon-attachment\"></span>{{_(\"Add Attachment\")}}</a>\n              <span class=\"attachment-browswer-unsupported\">{{_(\"Unable to add attachments\")}}, <a href=\"\">{{_(\"update your browser\")}}</a></span>\n            </li>\n            {% if config.prefs.gpg_email_key %}<li class=\"add-left compose-attach-key hide\">\n              <label class=\"compose-attach-key\"\n                     title=\"{{_(\"Attach your public encryption key to this message\")}}\">\n                <input type=\"hidden\" id=\"compose-hidden-attach-key-{{mid}}\"\n                       name=\"attach-pgp-pubkey\"\n                       value=\"{{ editing_strings.get('attach-pgp-pubkey', 'no')\n                                }}\">\n                <input type=\"checkbox\" id=\"compose-attach-key-{{mid}}\"\n                       {%- if truthy(editing_strings.get('attach-pgp-pubkey', 'no')) %} checked{% endif %}>\n                {{_(\"Attach Key\")}}\n              </label>\n            </li>{% endif %}\n          </ul>\n        </div>\n      </div>\n\n      <div class=\"compose-actions\">\n        <div class=\"dropdown\">\n          {% set from = editing_addresses and editing_strings.from_aids and editing_addresses[editing_strings.from_aids.0] or '' %}\n          <div class=\"dropup\">\n            <div class=\"compose-from-select dropdown-toggle\" data-toggle=\"dropdown\">\n              <span id=\"compose-from-selected-{{mid}}\" class=\"compose-from-selected\">\n                {% if from.photo %}\n                <span class=\"avatar\"><img src=\"{{from.photo}}\"></span>\n                {% else %}\n                <span class=\"avatar\"><img src=\"{{ U('/static/img/avatar-default-white.png') }}\"></span>\n                {% endif %}\n                <span class=\"name\">{{from.fn}}</span>\n                <span class=\"address\">{{from.address}}</span>\n              </span>\n              <span class=\"compose-from-caret\">\n                <span class=\"caret\"></span>\n              </span>\n            </div>\n            <ul class=\"dropdown-menu\">\n            {% set emails = profiles.emails|sort %}\n            {% set found = [] %}\n            {% if from.address not in emails %}\n              {% do emails.append(from.address) %}\n            {% endif %}\n            {% for profile_email in emails %}\n              {% set profile = (profile_email in profiles.emails)\n                               and profiles.profiles[profiles.emails[profile_email]]\n                               or {'fn': from.fn} -%}\n              {%- if profile['x-mailpile-profile-route'] -%}\n              <li role=\"presentation\">\n                <a href=\"#\" class=\"compose-from\"\n                   data-mid=\"{{mid}}\" data-email=\"{{ profile_email }}\"\n                   data-sig=\"{{ profiles.default_sig if ('x-mailpile-profile-signature' not in profile) else profile['x-mailpile-profile-signature'] }}\">\n                  <span class=\"avatar\">\n                  {% if profile.photo %}\n                  <img src=\"{{profile.photo[0].photo}}\">\n                  {% else %}\n                  <img src=\"{{ U('/static/img/avatar-default-white.png') }}\">\n                  {% endif %}\n                  </span>\n                  <span class=\"name\">{{profile.fn}}</span>\n                  <span class=\"address\">{{ profile_email }}</span>\n                </a>\n              </li>\n              {%- do found.append(profile) -%}{%- endif %}\n            {% endfor %}\n            {% if found|length == 0 %}\n              <li role=\"presentation\">\n                <a target='_blank' href=\"{{ U('/profiles/') }}\">\n                  No usable profiles found, click to check your settings!\n                </a>\n              </li>\n            {% endif %}\n            </ul>\n          </div>\n        </div>\n\n        <div class=\"compose-buttons\">\n          <button class=\"compose-action button-primary\" type=\"submit\" name=\"save\" value=\"save\" alt=\"{{_(\"Save\")}}\">\n            <span class=\"icon-compose\"></span> {{_(\"Save\")}}\n          </button>\n          <button class=\"compose-action button-secondary\" type=\"submit\" name=\"send\" value=\"{% if command == \"view\" %}reply{% else %}send{% endif %}\" alt=\"{{_(\"Send\")}}\">\n            <span class=\"icon-sent\"></span> {{_(\"Send\")}}\n          </button>\n        </div>\n\n        <div class=\"compose-options-crypto\">\n          <div id=\"compose-crypto-encryption-{{mid}}\" class=\"compose-crypto-encryption none\">\n            <span class=\"icon icon-lock-open\"></span>\n          </div>\n          <div id=\"compose-crypto-signature-{{mid}}\" class=\"compose-crypto-signature none\">\n            <span class=\"icon icon-signature-none\"></span>\n          </div>\n          <div id=\"compose-message-settings-{{mid}}\" class=\"compose-message-settings none\">\n            <span class=\"icon icon-settings\"></span>\n          </div>\n        </div>\n\n      </div>\n      <input id=\"compose-from-{{mid}}\" type=\"hidden\" name=\"from\" value=\"{{from.fn}} &lt;{{from.address}}&gt;\">\n      <input id=\"compose-mid-{{mid}}\" type=\"hidden\" name=\"mid\" value=\"{{mid}}\">\n      <input id=\"compose-crypto-{{mid}}\" type=\"hidden\" name=\"encryption\" value=\"{{editing_strings.encryption}}\">\n      <input id=\"compose-signature-{{mid}}\" type=\"hidden\" value=\"\">\n      <input id=\"compose-encryption-{{mid}}\" type=\"hidden\" value=\"\">\n    </div>\n  </td>\n  <td class=\"date\"></td>\n  <td  class=\"checkbox\">\n  {% if '-' not in mid %}\n    <input type=\"checkbox\" name=\"mid\" value=\"{{mid}}\">\n  {% endif %}\n  </td>\n</tr>\n</form>\n<script>\n/*\n  $(document).ready(function() {\n    Mailpile.Composer.init('{{mid}}',\n                           {{ editing_strings|json|safe }},\n                           {{ editing_addresses|json|safe }});\n  });\n*/\n</script>\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/pile_email_hints.html",
    "content": "{%- if config.web.email_html_hint and\n      (message.html_parts|length > 0) and\n      (from.get('html-policy', 'none') == 'none')\n-%}\n    <div class=\"message-app-note email-html-hint\"><p>\n      <span class=\"icon icon-news\"></span>\n      {{_(\"This message has HTML formatted content.\")}}\n      {{_(\"For security reasons, Mailpile will by default only display plain text.\")}}\n    {% if allow_html %}\n      {{_(\"You can change display modes by clicking the icons to the right of the Subject line.\")}}\n    {% endif %}\n    </p><p>\n      <a data-variable=\"web.email_html_hint\" data-remove=\"email-html-hint\"\n         class=\"ok-got-it\">\n        <span class=\"icon-checkmark\"></span> {{_(\"OK, got it\")}}\n      </a>\n    </p></div>\n{%- elif config.web.email_crypto_hint and False %}\n{%- elif config.web.email_tag_hint and False %}\n{%- elif config.web.email_reply_hint %}\n    <div class=\"message-app-note email-reply-hint\"><p>\n      <span class=\"icon icon-reply\"></span>\n      {{_(\"Usually when you reply to a message, all participants in the conversation will be sent a copy of your reply.\")}}\n    </p><p>\n      {{_(\"If you want to send a private reply, only to the author of this message, mouse over their name and click the reply icon that appears.\")}}\n    </p><p>\n      <a data-variable=\"web.email_reply_hint\" data-remove=\"email-reply-hint\"\n         class=\"ok-got-it\">\n        <span class=\"icon-checkmark\"></span> {{_(\"OK, got it\")}}\n      </a>\n    </p></div>\n\n{%- endif %}\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/pile_email_tags.html",
    "content": "<span class=\"item-tags\" style=\"opacity: 0.5\"> {# FIXME #}\n    {% for tid in metadata.tag_tids %}\n      {% set tag = result.data.tags[tid] %}\n      {% if tag.label and not tag.searched %}\n    <span class=\"pile-message-tag pile-message-tag-{{tag.tid}}\n          color-{{tag.label_color}}\"\n          data-tid=\"{{tag.tid}}\" data-mid=\"{{mid}}\">\n        <span class=\"icon pile-message-tag-icon {{tag.icon}}\"></span>\n    </span>\n      {% endif %}\n    {% endfor %}\n</span>\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/pile_message.html",
    "content": "<div id=\"message-{{mid}}\" class=\"pile-message {% for tid in metadata.tag_tids %}{% if result.data.tags[tid].slug == \"new\" %}{{result.data.tags[tid].slug}}{% endif %}{% endfor %}\" data-mid=\"{{mid}}\">\n  {%- set activities = (activities or []) + get_ui_elements('email_activities', state) -%}\n  {%- if thread|length > 1 %}\n    {%- do activities.extend(get_ui_elements('thread_activities', state)) %}\n  {%- endif %}\n  {%- set from = metadata.from %}\n  {%- set allow_html = ((message.crypto.encryption.status == 'none') or not config.prefs.encrypted_block_html) %}\n  {%- set allow_images = ((message.crypto.encryption.status == 'none') or not config.prefs.encrypted_block_web) %}\n  {%- set encryption_status = [] %}\n  {%- set signature_status = [] %}\n  {%- set special_text_parts = [] %}\n  {%- macro msg_crypto(part, forcedisplay) %}\n    {#-\n     # Parts that do not have own crypto attribute inherit from message itself\n     # Watch for changes to the \"status\" which is present in each encryption and\n     # signature section - if either has changed, then we have moved from\n     # one security context to the next, and need to let the user know.\n     #}\n    {%- set part_encryption = (part.crypto and part.crypto.encryption or message.crypto.encryption) %}\n    {%- set part_signature = (part.crypto and part.crypto.signature or message.crypto.signature) %}\n    {%- set encryption = show_text_part_encryption(part_encryption and part_encryption.status) %}\n    {%- set signature = show_text_part_signature(part_signature and part_signature.status) %}\n\n    {%- if encryption_status and (forcedisplay or part_encryption.status != encryption_status[-1]) %}\n    <div class=\"part-crypto-status clearfix crypto-changed border-{{ encryption.color }}\">\n      <span class=\"message-inline-crypto-info {{encryption.color}}\">&#x2198;</span>\n    {%- elif signature_status and part_signature.status != signature_status[-1] %}\n    <div class=\"part-crypto-status clearfix crypto-changed border-{{ signature.color }}\">\n      <span class=\"message-inline-crypto-info {{signature.color}}\">&#x2198;</span>\n    {%- else %}\n    <div class=\"part-crypto-status clearfix border-{{ encryption.color }}\">\n    {%- endif %}\n\n    {%- if part_encryption %}\n      {%- if not encryption_status or forcedisplay or part_encryption.status != encryption_status[-1] %}\n      <span class=\"message-inline-crypto-info inline-encryption-info\"\n            title=\"{{ encryption.text }}\"\n            data-crypto_color=\"{{ encryption.color }}\"\n            data-crypto_icon=\"{{ encryption.icon }}\"\n            data-crypto_message=\"{{ encryption.message }}\"\n            data-crypto_keyinfo=\"{{ part_encryption.keyinfo }}\">\n        <span class=\"icon {{ encryption.color }} {{ encryption.icon }}\"></span>\n        <span class=\"text {{ encryption.color }}\">{{ encryption.text }}</span>\n      </span>\n      {%- endif %}\n      {%- do encryption_status.append(part_encryption.status) %}\n    {%- endif %}\n\n    {%- if part_signature %}\n      {%- if not signature_status or part_signature.status != signature_status[-1] %}\n      <span class=\"message-inline-crypto-info inline-signature-info\"\n            title=\"{{ signature.text }}\"\n            data-crypto_color=\"{{ signature.color }}\"\n            data-crypto_icon=\"{{ signature.icon }}\"\n            data-crypto_message=\"{{ signature.message }}\"\n            data-crypto_keyinfo=\"{{ part_signature.keyinfo }}\">\n        <span class=\"icon {{ signature.color + ' ' + signature.icon }}\"></span>\n        <span class=\"text {{ signature.color }}\">{{ signature.text }}</span>\n      </span>\n      {%- endif %}\n      {%- do signature_status.append(part_signature.status) %}\n    {%- endif %}\n    </div>\n  {%- endmacro %}\n  {%- set mailer_daemon = ('mailer-daemon@' in from.address) or ('MAILER-DAEMON@' in from.address) or ('postmaster@' in from.address) %}\n  {%- set risky_content = config.prefs.antiphishing and (metadata.flags.spam or mailer_daemon or message.trust.problem or (message.trust.warning and message.attachments)) and True %}\n  {%- set risky_links = config.prefs.antiphishing and (risky_content or message.trust.trust_unknown) and True %}\n  {%- macro risky_content_classes() %}\n    {%- if risky_content %} risky hide{% endif -%}\n  {%- endmacro %}\n  {%- macro msg_warning(reason, nolinks, noatts, actions) %}\n    <div style=\"border: 1px solid #c70; background: #ffb; margin: 8px 0 2px -10px; padding: 7px;\"\n         class=\"message-content-warning\">\n      <span style=\"float: left; color: #b60; margin: 10px 10px 0 5px; font-size: 18px;\"\n            class=\"icon icon-signature-unknown\"></span>\n      {{ reason -}}\n      {%- if nolinks and noatts and message.attachments %} {{_(\"Blocked attachments, replies, and links.\")}}\n      {%- elif noatts and message.attachments %} {{_(\"Attachments and replies are blocked.\")}}\n      {%- elif nolinks %} {{_(\"Links are blocked.\")}}{%- endif %}\n      <div class=\"message-content-warning-actions\" style=\"margin: 6px 0 0 0;\">\n        {%- for act in actions %}{% if not act.hide %}\n          <a style=\"margin: 0 0.5em;\" href=\"javascript:{{ act.act }}\"><span class=\"icon icon-{{ act.icon }}\" style=\"opacity: 0.7\"></span> {{ act.msg }}</a>\n        {% endif %}{%- endfor %}\n      </div>\n    </div>\n  {%- endmacro %}\n  <div class=\"pile-message-content\">\n  {%- if config.prefs.antiphishing %}\n    {%- if metadata.flags.spam %}\n      {{- msg_warning(\"This message is probably spam.\", True, True, [\n        {\"icon\": \"not-spam\", \"msg\": _(\"Remove from spam\")},\n        {\"icon\": \"attachment\", \"msg\": _(\"Show risky content\")},\n      ]) -}}\n    {%- elif message.trust.problem or (message.trust.warning and message.attachments) %}\n      {{- msg_warning((message.trust.problem or message.trust.warning) + \".\", True, True, [\n        {\"icon\": \"attachment\", \"act\": \"Mailpile.stuff();\", \"msg\": _(\"Show risky content\")},\n        {\"icon\": \"spam\", \"msg\": _(\"Send to spam\")},\n      ]) -}}\n    {%- elif risky_content or risky_links %}\n      {{- msg_warning(\"This sender's reputation is unknown.\", True, risky_content, [\n        {\"icon\": \"groups\", \"msg\": _(\"Add to contacts\"), \"hide\": (mailer_daemon or metadata.flags.from_contact) and True or False },\n        {\"icon\": \"attachment\", \"msg\": _(\"Show risky content\")},\n        {\"icon\": \"spam\", \"msg\": _(\"Send to spam\")},\n      ]) -}}\n    {%- endif %}\n  {%- endif %}\n  {%- if message.vcal_parts %}\n    {% for event in message.vcal_parts %}\n    <div class=\"pile-message-vcal-event\">\n        <h2><span class=\"icon icon-calendar\"></span> {{event.summary}}</h2>\n        <div class=\"event-details\">\n            <h3 class=\"header-name crypto-color-gray\">Starts:</h3>\n            <span class=\"header-value\" title=\"{{event.dtstart}}\">\n                <span class=\"time\">{{event.dtstart}}</span>\n            </span>\n            <h3 class=\"header-name crypto-color-gray\">Ends:</h3>\n            <span class=\"header-value\" title=\"{{event.dtend}}\">\n                <span class=\"time\">{{event.dtend}}</span>\n            </span>\n            <h3 class=\"header-name crypto-color-gray\">\n              Organizer:\n            </h3>\n            <span class=\"header-value\">\n              <span class=\"name\"><span class=\"punct\">\"</span>{{event.organizer.cn}}<span class=\"punct\">\"</span></span>\n              <span class=\"email\"><span class=\"punct\">&lt;</span>{{event.organizer.email}}<span class=\"punct\">&gt;</span></span>\n            </span>\n\n            <h3 class=\"header-name crypto-color-gray\">\n              Attendees:\n            </h3>\n            <span class=\"header-value\">\n                {% for att in event.attendees %}\n                <span class=\"name\"><span class=\"punct\">\"</span>{{att.cn}}<span class=\"punct\">\"</span></span>\n                <span class=\"email\"><span class=\"punct\">&lt;</span>{{att.email}}<span class=\"punct\">&gt;</span></span>\n                {% endfor %}\n            </span>\n\n        </div>\n\n        <div>{{event.description|to_br}}</div>\n    </div>\n    {% endfor %}\n  {%- endif %}\n\n  {%- if message.text_parts %}\n    {%- set last_part = message.text_parts|length - 1 %}\n    {%- for part in message.text_parts %}\n    <div class=\"pile-message-text-part\">\n      {%- if part.aid and config.web.developer_mode %}\n      <a href=\"{{ U('/message/download/get/=', mid, '/', part.aid, '/') }}\"\n         title=\"{{_('Download')}}: {{ part.aid }}, {{ part.mimetype}}\"\n         style=\"position: absolute; right: 0; opacity: 0.25;\">\n        <span class=\"icon icon-download\"></span>\n      </a>\n      {%- endif %}\n      {%- if part.data|trim %}\n        {{- msg_crypto(part, False) }}\n        {%- autoescape false %}\n        {%- set part_text = part.data|nice_text|e|urlize|fix_urls(40, risky_links) %}\n        {%- if part.type in (\"text\", \"pgpsignedtext\") %}\n      <div class=\"message-part-text\">{{ part_text|to_br }}</div>\n        {%- elif part.type in (\"pgptext\",) %}\n          {%- if part.crypto.encryption.status in (\"lockedkey\", \"missingkey\", \"error\") %}\n            {%- set failed_crypto = part.crypto.encryption %}\n            {%- include(\"partials/thread_message_cryptofail.html\") %}\n          {%- else %}\n      <div class=\"message-part-text\">{{ part_text|to_br }}</div>\n          {%- endif %}\n        {%- elif part.type in (\"pgpbegin\", \"pgpend\") %}\n      <div class=\"message-part-{{part.type}} hide\">{{part.data}}</div>\n        {%- elif part.type == \"quote\" %}\n          {# If this is the 2nd-to-last part, and the LAST part is a quote\n             or signature, we shall also hide. #}\n          {%- if loop.index == (loop.length - 1) and\n                 message.text_parts[last_part].type in (\"signature\", \"quote\") -%}\n            {% do special_text_parts.append(\"quote\") %}\n      <div class=\"message-part-quote hide\">{{ part_text|to_br }}</div>\n          {# If this part is a quote at end of message, hide #}\n          {%- elif loop.last %}\n            {%- do special_text_parts.append(\"quote\") %}\n      <div class=\"message-part-quote hide\">{{ part_text|to_br }}</div>\n          {%- else %}\n      <div class=\"message-part-quote\">{{ part_text|to_br }}</div>\n          {%- endif -%}\n        {%- elif part.type == \"signature\" %}\n          {%- do special_text_parts.append(\"signature\") %}\n      <div class=\"message-part-signature hide\">{{ part_text|to_br }}</div>\n        {%- else %}\n      <div class=\"message-part-text\"><em>{{_(\"Unknown Text Part\")}}</em></div>\n        {%- endif %}\n        {%- endautoescape %}\n      {%- endif %}\n    </div>\n    {%- endfor %}\n    {%- if allow_html %}{%- for part in message.html_parts %}\n    <div id=\"html-part-{{ mid }}-{{ loop.index }}\"\n         class=\"pile-message-html-part hide\">\n      {{ msg_crypto(part, loop.first) }}\n      {%- if part.aid and config.web.developer_mode %}\n      <a href=\"{{ U('/message/download/get/=', mid, '/', part.aid, '/') }}\"\n         title=\"{{_('Download')}}: {{ part.aid }}, {{ part.mimetype}}\"\n         style=\"position: absolute; right: 0; opacity: 0.25;\">\n        <span class=\"icon icon-download\"></span>\n      </a>\n      {%- endif %}\n      <div class=\"message-part-html\">\n        <i class=\"noframe\">{{ _(\"HTML rendering requires Javascript, sorry!\") }}</i>\n      </div>\n    </div>\n    {%- endfor %}{%- endif %}\n  {%- else %}\n    {%- if message.crypto.encryption.status in (\"lockedkey\", \"missingkey\", \"error\") %}\n      {%- set failed_crypto = message.crypto.encryption %}\n      {%- include(\"partials/thread_message_cryptofail.html\") %}\n    {%- elif not message.attachments %}\n      <div class=\"message-part-text\"><em>{{_(\"Message content is empty\")}}</em></div>\n    {%- endif %}\n  {%- endif %}\n  {%- if message.attachments %}\n    <div class=\"pile-message-attachments\">\n      <ul class=\"pile-message-attachments horizontal clearfix{{ risky_content_classes() }}\">\n    {%- for att in message.attachments %}\n        <li class=\"left\">\n      {# FIXME: Too ugly, for now. { msg_crypto(att, False) } #}\n      {%- set type = attachment_type(att.mimetype) %}\n      {%- if (att.filename) %}\n        {%- set filename = att.filename %}\n      {%- else %}\n        {%- set filename = _(\"Untitled\") %}\n      {%- endif %}\n      {%- set is_image = att.mimetype in [\"image/bmp\", \"image/gif\", \"image/jpg\", \"image/jpeg\", \"image/pjpeg\", \"image/svg+xml\", \"image/x-png\", \"image/png\", \"application/vnd.google-apps.photo\"] %}\n      {%- set is_pdf = (att.mimetype in [\"application/pdf\"]) %}\n      {%- set file_parts = filename.split(\".\") %}\n      {%- set file_parts_length = file_parts|length %}\n      {#- if type == \"keys\" #}\n        {#- do special_text_parts.append(\"pgp-key\") #}\n        {#- FIXME: We should omit these (and other boring attachments), but\n                   instead add a mode that lets users easily download any part\n                   they like. Like mutt! #}\n      {#- else #}\n        <a href=\"{{ U('/message/download/get/=', mid, '/part-', att.count, '/') }}\"\n        {%- if is_image %}\n           class=\"attachment-image\" target=_blank\n        {%- else %}\n           class=\"attachment\"{% if is_pdf %} target=\"_blank\"{% endif %}\n        {%- endif %}\n        {%- if att.crypto.encryption.description %}\n           data-description=\"{{ att.crypto.encryption.description }}\"\n        {%- endif %}\n           data-size=\"{{att.length|friendly_bytes}}\"\n           title=\"{{filename}}\" type=\"{{att.mimetype}}\">\n        {%- if is_image %}\n          <div class=\"preview\" style=\"background-image: url('{{ U('/message/download/preview/=', mid, '/part-', att.count, '/') }}');\"></div>\n        {%- else %}\n          <div class=\"preview\">\n            <span class=\"icon-mime\" type=\"{{att.mimetype}}\"></span>\n          {%- if filename != _(\"Untitled\") %}\n            <span class=\"extension\">\n              {{- file_parts[file_parts_length - 1] -}}\n            </span>\n          {%- endif %}\n          </div>\n          <div class=\"filename\">\n          {%- if att.crypto.encryption.status == \"decrypted\" %}\n            <span class=\"icon-lock-closed crypto-encrypted\"></span>&nbsp;\n          {%- elif att.crypto.encryption.status == \"encrypted\" %}\n            <span class=\"icon-lock-closed crypto-error\"></span>&nbsp;\n          {%- endif %}\n          {%- if file_parts_length > 2 or filename|length > 20 %}\n              {{- att.filename[0:15] -}}...\n          {%- elif file_parts[0] %}\n              {{- file_parts[0] -}}\n          {%- endif %}\n          </div>\n        {%- endif %}\n        </a>\n      {#- endif #}\n        </li>\n    {%- endfor %}\n      </ul>\n    </div>\n  {%- endif %}\n  </div>\n  <div class=\"message-actions clearfix\">\n    <ul class=\"message-actions horizontal left\">\n      {%- if \"quote\" in special_text_parts or \"signature\" in special_text_parts %}\n      <li class=\"action show-quotes\">\n        <a data-show=\"#message-{{mid}} .pile-message-content .hide\"\n           data-hide=\"#message-{{mid}} .show-quotes\"\n           class=\"do-show message-actions-quote\"\n           href=\"#\">&middot;&middot;&middot;&middot;</a>\n      </li>\n      {%- endif %}\n    </ul>\n  </div>\n  <div class=\"{{ risky_content_classes() }}\">\n    <p class=\"message-actions-padding\">&nbsp;</p>\n    <ul class=\"message-actions horizontal bottom left\">\n      <li class=\"action\"><a class=\"message-action-reply-all\" href=\"#\"><span class=\"icon-reply-all\"></span> {{_(\"Reply\")}}</a></li>\n      <li class=\"action\"><a class=\"message-action-forward\" href=\"#\"><span class=\"icon-forward\"></span> {{_(\"Forward\")}}</a></li>\n      {%- for item in activities %}\n      <li class=\"action\"><a class=\"message-action-{{ item.name }}\"\n                            href=\"{{ item.url }}\" title=\"{{ item.description }}\">\n        <span class=\"icon-{{ item.icon }}\"></span>\n        {% if loop.index < 3 %}{{_(item.text)}}{% endif %}\n      </a></li>\n      {%- endfor %}\n    </ul>\n  </div>\n</div>\n<script>\n<!-- allow_html={{ allow_html }}, allow_images={{ allow_images }} -->\n$(document).ready(function() {\n  Mailpile.Message.AnalyzeMessageInline('{{mid}}');\n  $('#message-{{mid}}').data('html', {{ message.html_parts|json|safe }});\n  {%- if allow_html and\n         message.html_parts|length > 0 and\n         from.get('html-policy', 'none') != 'none' %}\n    Mailpile.Message.ShowHTML(\n      '{{mid}}', '{{from[\"html-policy\"]}}',\n      {% if allow_images %}true{% else %}false{% endif %});\n  {%- endif %}\n});\n</script>\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/pile_threading.html",
    "content": "    <div class=\"thread-container\">\n      <h3 class=\"{{ unsigned.color }}\">\n        {{_(\"Conversation\")}}:\n        <span class=\"icon message-metadata-crypto-info {{ unsigned.icon }}\"\n              title=\"{{ unsigned.text }}\"\n              data-crypto_icon=\"{{ unsigned.icon }}\"\n              data-crypto_color=\"{{ unsigned.color }}\"\n              data-crypto_message=\"{{ unsigned.message }}\"></span>\n      </h3>\n      {%- set hidden = [] %}\n      {%- for p2_ti, p1_ti, tinfo, n1_ti, n2_ti\n              in thread|thread_upside_down|with_context(2) %}\n        {%- set tmid = tinfo[0] %}\n        {%- set ascii = tinfo[1] %}\n        {%- set tmeta = result.data.metadata[tmid] %}\n        {%- set tsubject = bare_subject(tmeta.subject) %}\n        {%- set tfrom = result.data.addresses[tmeta.from.aid] %}\n        {%- set nearby = ((5 >= thread|length) or mid in (p2_ti[0], p1_ti[0], tmid, n1_ti[0], n2_ti[0])) %}\n        {%- if not nearby %}{% do hidden.append(tmid) %}{% endif %}\n        {%- set new_subject = ((p1_ti and (tsubject !=\n                                bare_subject(result.data.metadata[p1_ti[0]].subject))) or\n                               (n1_ti and (tsubject !=\n                                bare_subject(result.data.metadata[n1_ti[0]].subject)))) %}\n        <a class=\"thread-message\n                  {%- if tmid == mid %} thread-selected{% endif %}\n                  {%- if tmeta.flags.unread %} thread-unread{% endif %}\n                  {%- if not nearby %} hide{% endif %}\"\n           title=\"{{tmeta.body.snippet}}\n - &lt;{{tmeta.from.address}}&gt; ({{tmeta.timestamp|friendly_time}}, {{tmeta.timestamp|friendly_datetime}})\"\n           href=\"{{msg_url(tmid, this_mid)}}\" data-noblank=1>\n          <span class=\"thread-tree\">{{ ascii }}</span>\n          <span class=\"thread-info{% if not new_subject %} thread-simple{% endif %}\">\n            <span class=\"thread-avatar\"><img src=\"{{ show_avatar(tfrom) }}\"></span>\n            <span class=\"thread-from\">{{ tmeta.from.fn }}</span>\n            {%- if new_subject %}\n              <span class=\"thread-details\">{{ tsubject }}</span>\n            {%- endif %}\n          </span>\n        </a>\n      {%- endfor %}\n      {% if hidden %}\n        <div class=\"clearfix\">\n          <a href=\"#\" class=\"show-hide\" title=\"{{_(\"Show entire conversation\")}}\"\n             data-show=\".pile-message-{{mid}} .thread-container a.thread-message.hide\"\n             data-hide=\".pile-message-{{mid}} .thread-container a.show-hide\">\n            &middot;&middot;&middot;&middot;\n          </a>\n        </div>\n      {% endif %}\n    </div>\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/search_item.html",
    "content": "{%    set message = result.data.messages.get(mid)\n%}{%  set message_errors = (result.errors or {}).get(mid)\n%}{%  set metadata = result.data.metadata[mid]\n%}{%  set thread = result.data.threads[metadata.thread_mid]\n%}{%  set conversation_count = 0 if (state.context_url == '/message/' or ('flat' in result.search_order)) else thread|length\n%}{%  set cleartext = show_text_part_encryption('none')\n%}{%  set unsigned = show_text_part_signature('none')\n%}{%  set to_cc = metadata.to_aids + metadata.cc_aids\n%}{%  set from = result.data.addresses[metadata.from.aid]\n%}{%  if not from\n%}{%    set from = {'fn': 'Unknown sender', 'email': ''}\n%}{%  endif\n%}{%  set allow_html = (message and message.crypto and ((message.crypto.encryption.status == 'none') or not config.prefs.encrypted_block_html))\n%}{%  macro msg_url(msg_mid, context_mid)\n%}{%    set m_metadata = result.data.metadata.get(msg_mid)\n%}{%    if m_metadata and state.context_url == '/message/':\n%}{{      m_metadata.urls.thread + '#pile-message-' + msg_mid\n}}{%    elif m_metadata and m_metadata.urls.editing:\n%}{{      m_metadata.urls.editing + '#pile-message-' + msg_mid\n}}{%    elif True:\n%}{%      set v = (context_mid + '/' + msg_mid) if (context_mid) else msg_mid\n%}{{      U(add_state_query_string(state.command_url, state, {\n             'url_args_remove': [['view','']],\n             'url_args_add': [['view', v]]})\n           ) + '#pile-message-' + msg_mid\n}}{%    else:\n%}{{      m_metadata.urls.thread\n}}{%    endif\n%}{%  endmacro %}\n{%- if not display_attachments or metadata.body.parts %}\n<tr class=\"pile-message pile-message-{{mid}}{% if mid != this_mid %} pile-message-{{this_mid}}{% endif %} result has-mid\n           {%- for tid in metadata.tag_tids %} in_{{result.data.tags[tid].slug}}{% endfor %}\n           {%- if message %} full-message has-url{% endif %}\n           {%- if display_attachments %} display-attachments{% endif %}\n           {%- if metadata.flags.replied %} replied{% endif %}\"\n    data-tids=\"{{ metadata.tag_tids|join(',')  }}\"\n{%- if message %}\n    data-url=\"{{ msg_url(this_mid, 0) }}\"\n  {%- if message.headerprints %}\n    {% if message.headerprints.mua %}data-mua=\"{{ message.headerprints.mua }}\"{% endif %}\n    data-mua-fingerprint=\"{{ message.headerprints.tools }}\"\n    data-sender-fingerprint=\"{{ message.headerprints.sender }}\"\n  {%- endif %}\n{%- endif %}\n    data-state=\"normal\"\n    data-list=\"{{ metadata.body.list }}\"\n    data-to-cc=\"{% for cid in to_cc %}{{ result.data.addresses[cid].address }} {% endfor %}\"\n    data-mid=\"{{mid}}\">\n  <td class=\"draggable\"></td>\n{% if message or message_errors %}\n  <td class=\"message-nav\">\n  {%- if previous_mid %}\n    <a href=\"{{msg_url(previous_mid, 0)}}\" title=\"{{_('Previous Conversation')}}\"\n       id=\"previous-message\" data-noblank=1 data-keep-selection=1\n       class=\"icon\">&#x25B2;</a><br>\n  {%- endif %}\n    <a href=\"{{ U(add_state_query_string(state.command_url, state, {\n                    'url_args_remove': [['view','']]}))}}\"\n       data-noblank=1 data-keep-selection=1 title=\"{{_('Close')}}\"\n       id=\"close-message\"><span class='icon icon-circle-x'></span></a><br>\n  {%- if next_mid %}\n    <a href=\"{{msg_url(next_mid, 0)}}\" title=\"{{_('Next Conversation')}}\"\n       id=\"next-message\" data-noblank=1 data-keep-selection=1\n       class=\"icon\">&#x25BC;</a><br>\n  {%- endif %}\n  </td>\n  <td class=\"from people\" data-fn=\"{{metadata.from.fn}}\" data-address=\"{{metadata.from.address}}\">\n  {%- if message.editing_strings %}\n    <!-- FIXME -->\n  {%- else %}\n    <div class=\"message-details\">\n      <div class=\"header-cooked header-date\">\n        <h3 class=\"header-name {{ unsigned.color }}\"\n            title=\"{% for header, value in message.header_list|sort|reverse -%}\n{%- set hlc = header|lower -%}\n{% if hlc == 'user-agent' -%}{{_('Composed With')}}: {{value}}\n{% elif hlc == 'autocrypt' -%}{{_('Message has an Autocrypt header')}}{% if 'prefer-encrypt=mutual' in value %}, {{_('prefers encryption')}}{% endif %}.\n{% elif hlc == 'received' -%}{{_('Received')}}: {{value|friendly_received}}\n{% elif hlc == 'date' -%}{{_('Message Date')}}: {{value}}\n{% endif -%}{%- endfor %}\">\n          {{metadata.timestamp|friendly_time}}, {{metadata.timestamp|friendly_datetime}}\n        </h3>\n      </div>\n      <div class=\"header-cooked header-from\">\n        <h3 class=\"header-name {{ unsigned.color }}\">\n          {{_(\"From\")}}:\n{#\n #  FIXME: We are using the From data given to us in the metadata section,\n #         which may differ from the message contents, especially in a\n #         memory-hole substitution world. This needs reworking.\n #}\n          <span class=\"icon message-metadata-crypto-info {{ unsigned.icon }}\"\n                title=\"{{ unsigned.text }}\"\n                data-crypto_icon=\"{{ unsigned.icon }}\"\n                data-crypto_color=\"{{ unsigned.color }}\"\n                data-crypto_message=\"{{ unsigned.message }}\"></span>\n          <ul class=\"message-sender-actions\">\n{%- if 0 and is_dev_version() %}\n          {%- if not metadata.from.flags.contact and not metadata.from.flags.profile %}\n            <li class=\"action\"><a title=\"{{_(\"Add Contact\")}}\" class=\"message-action-add-contact\" href=\"#\" data-name=\"{{metadata.from.fn}}\" data-address=\"{{metadata.from.address}}\"><span class=\"icon icon-plus\"></span></a></li>\n          {%- endif %}\n{%- endif %}\n            <li class=\"action\"><a title=\"{{_(\"Private Reply\")}}\" class=\"message-action-reply\" data-mid=\"{{mid}}\" href=\"#\"><span class=\"icon icon-reply\"></span></a></li>\n          </ul>\n          <div class=\"crypto-and-tags\">\n            {%- include \"partials/pile_email_tags.html\" %}\n          </div>\n        </h3>\n        <span class=\"header-value\" title='{{metadata.from.fn}} &lt;{{metadata.from.address}}&gt;'>\n          <span class=\"address-avatar\"><img src=\"{{ show_avatar(metadata.from) }}\"></span>\n          <span class=\"address-name\"><span class=\"punct\">\"</span>{{metadata.from.fn}}<span class=\"punct\">\"</span></span>\n          <span class=\"address-email\"><span class=\"punct\">&lt;</span>{{metadata.from.address}}<span class=\"punct\">&gt;</span></span>\n        </span>\n      </div>\n    {%- for txt, which, rcpts in (('To', 'to', get_addresses(message.header_list, \"to\")),\n                                  ('CC', 'cc', get_addresses(message.header_list, \"cc\")),\n                                  ('BCC', 'bcc', get_addresses(message.header_list, \"bcc\"))) %}\n      {%- if rcpts %}\n      <div class=\"header-cooked header-{{which}}\">\n        <h3 class=\"header-name {{ unsigned.color }}\">\n          {{_(txt)}}:\n          <span class=\"icon message-metadata-crypto-info {{ unsigned.icon }}\"\n                title=\"{{ unsigned.text }}\"\n                data-crypto_icon=\"{{ unsigned.icon }}\"\n                data-crypto_color=\"{{ unsigned.color }}\"\n                data-crypto_message=\"{{ unsigned.message }}\"></span>\n        </h3>\n        {%- for ai in rcpts %}\n        <span class=\"header-value\" title='{{ai.fn}} &lt;{{ai.address}}&gt;'>\n          <span class=\"address-avatar\"><img src=\"{{ show_avatar(ai) }}\"></span>\n          <span class=\"address-name\"><span class=\"punct\">\"</span>{{ai.fn or ai.address}}<span class=\"punct\">\"</span></span>\n          <span class=\"address-email\"><span class=\"punct\">&lt;</span>{{ai.address}}<span class=\"punct\">&gt;</span></span><span class=\"punct\">,</span>\n        </span>\n        {%- endfor %}\n      </div>\n      {%- endif %}\n    {%- endfor %}\n    {%- if thread|length < 2 %}\n      {%- include \"partials/pile_email_hints.html\" %}\n    {%- endif %}\n    </div>\n  {%- endif %}\n  {%- if thread|length > 1 %}\n    {%- include \"partials/pile_threading.html\" %}\n  {%- endif %}\n{% else %}\n  <td class=\"avatar\">\n    <a><img src=\"{{ show_avatar(from) }}\"></a>\n  </td>\n {%- if to_cc and display_recipients %}\n  {%- set to1 = result.data.addresses[to_cc[0]] %}\n  <td class=\"to people\" data-fn=\"{{metadata.from.fn}}\" data-address=\"{{metadata.from.address}}\">\n    <a href=\"{{ msg_url(mid, 0) }}\" data-noblank=1 data-keep-selection=1\n       title=\"{% if display_attachments -%}\n{{ _(\"Subject\") }}: {{ metadata.subject }}\n{% endif %}{{_(\"From\") }}: {{metadata.from.fn}} &lt;{{metadata.from.address}}&gt;\n{{ _(\"To\") }}: {% if to_cc %}\n &lt;{{ to1.address }}&gt;{% if to_cc|length > 1 %} +{{ to_cc|length -1 }}{% endif %}\n{%- else %}({{_(\"unknown\")}}){% endif %}\">\n      {%- if conversation_count > 1 %}\n      {%-   if to_cc|length > 1 %}<span class=\"icon icon-reply-all\"></span>\n      {%-   else %}<span class=\"icon icon-reply\"></span>{% endif %}\n      {%- else %}<span class=\"icon icon-forward\"></span>\n      {%- endif %} &nbsp;\n      {%- if to1.fn %}{{ to1.fn|nice_name(28) }}{% else %}(&lt;{{ to1.address }}&gt;){% endif %}\n      {% if to_cc|length > 1 %}<span class=\"rcpt-count\">+{{ to_cc|length -1 }}</span>{% endif %}\n    </a>\n {%- else %}\n  <td class=\"from people\" data-fn=\"{{metadata.from.fn}}\" data-address=\"{{metadata.from.address}}\">\n    <a href=\"{{msg_url(mid, 0)}}\" data-noblank=1 data-keep-selection=1\n       title=\"{% if display_attachments -%}\n{{ _(\"Subject\") }}: {{ metadata.subject }}\n{% endif %}{{_(\"From\") }}: {{metadata.from.fn}} &lt;{{metadata.from.address}}&gt;\n{{ _(\"To\") }}: {% if to_cc %}\n {%- set to1 = result.data.addresses[to_cc[0]] %}\n &lt;{{ to1.address }}&gt;{% if to_cc|length > 1 %} +{{ to_cc|length -1 }}{% endif %}\n{%- else %}({{_(\"unknown\")}}){% endif %}\">\n      {%- if metadata.from.fn %}{{ metadata.from.fn|nice_name(28) }}{% else %}({{_(\"No Name\")}}){% endif %}\n      {%- if conversation_count > 1 %}<span class=\"conversation-count\">{{conversation_count}}</span>{% endif %}\n      {%- if metadata.flags.replied %}<span class=\"icon-reply\"></span>{% else %}\n      {%-   if metadata.flags.forwarded %}<span class=\"icon-forward\"></span>{% endif %}\n      {%- endif %}\n    </a>\n {%- endif %}\n{% endif %}\n  {%- if not message and not message_errors %}\n    <div class=\"crypto-and-tags\">\n      {%- include \"partials/pile_email_tags.html\" %}\n    {% if metadata.crypto.encryption in ('decrypted', 'mixed-decrypted', 'lockedkey', 'mixed-lockedkey') %}\n    <span class=\"icon crypto icon-lock-closed color-08-green\"></span>\n    {% elif metadata.crypto.encryption in ('error', 'mixed-error', 'missingkey', 'mixed-missingkey') %}\n    <span class=\"icon crypto icon-lock-closed color-12-red\"></span>\n    {% endif %}\n  {%- endif %}\n    </div>\n  </td>\n  <td class=\"subject{% if message %} full-message{% endif %}\">\n{%- if message_errors %}\n    {% include(\"partials/error_message_missing.html\") %}\n{%- elif not message %}\n  {%- set displayed_atts = [] %}\n  {%- if display_attachments %}\n    {%- for bp in (metadata.body.parts or []) %}\n      {%- set bp = body_part_metadata(bp) %}\n      {%- if bp.mimetype not in ('text/plain', 'text/html') or bp.filename not in ('T', 'H', '', None) %}\n        {%- set fp = bp.filename.split('.') %}\n      <a class=\"item-attachment\" style=\"line-height: 18px;\" {# FIXME! #}\n           title=\"{{_('Download')}} {{ bp.bytes }} - {{ bp.filename }}\"\n           href=\"{{ U('/message/download/get/=', mid, '/part-', loop.index, '/') }}\"\n           data-noblank=1 data-keep-selection=1 target=_blank>\n        <span style=\"position: absolute; display: inline-block;\">\n          <span class=\"icon-mime\" style=\"margin: 0 3px 0 7px;\"\n                type=\"{{ bp.mimetype }}\"></span> {# FIXME! #}\n          <b style=\"font-family: monospace; margin: 0;\">\n        {%- if fp|length > 1 %}\n          {{ fp[-1]|upper }}\n        {%- else %}\n          BIN\n        {%- endif %}</b>\n        </span>\n        <span style=\"color: #666; margin: 0 0 0 5em;\">\n        {%- if fp|length > 1 %}\n          {{ fp[:-1]|join('.') }}\n        {%- else %}\n          {{ bp.filename }}\n        {%- endif %}\n        </span>\n        {%- do displayed_atts.append(bp.filename) %}\n      </a>\n        {%- if not loop.last %}<br clear='both'>{% endif %}\n      {% endif %}\n    {%- endfor %}\n    {%- for url_url, url_text in (metadata.body.att_urls or []) %}\n      {%- if displayed_atts %}<br clear='both'>{% endif %}\n      <a class=\"item-attachment\" style=\"line-height: 18px;\" {# FIXME! #}\n           title=\"{{_('Download from the web')}}\" target=_blank\n           href=\"{{ url_url }}\" data-noblank=1 data-keep-selection=1>\n        <span style=\"position: absolute; display: inline-block;\">\n          <b style=\"font-family: monospace; margin: 0;\">WWW</b>\n        </span>\n        <span style=\"color: #666; margin: 0 0 0 5em;\">\n          {{- url_text -}}\n        </span>\n        {%- do displayed_atts.append(url_url) %}\n      </a>\n    {%- endfor %}\n    {%- if not displayed_atts %}\n    <a data-noblank=1 data-keep-selection=1\n       href=\"{{msg_url(mid, 0)}}\">\n      <i style=\"color: #bbb; margin: 0 0 0 5em;\">\n        {{_('Oops, no attachments found!')}}\n        {{_('A bug?')}}\n        {{_('Click to view the message.')}}\n      </i>\n    </a>\n    {%- endif %}\n  {%- else %}\n    <a class=\"item-subject\" title=\"{{metadata.body.snippet}}\"\n       data-noblank=1 data-keep-selection=1\n       href=\"{{msg_url(mid, 0)}}\">{{ nice_subject(metadata.subject) }}\n    </a>\n  {%- endif %}\n{%- else %}\n    <div class=\"message-container\">\n    {%- if message.editing_strings %}\n      {% set editing_strings = message.editing_strings %}\n      {% set editing_addresses = result.data.addresses %}\n      {% set attachments = message.attachments %}\n      {% include(\"partials/compose.html\") %}\n    {%- else %}\n      <div class=\"message-subject-container\">\n        <h3 class=\"{{ (cleartext.color == 'crypto-color-gray') and unsigned.color or cleartext.color }}\">\n          {{_(\"Subject\")}}:\n          <span class=\"icon message-metadata-crypto-info {{ cleartext.color }} {{ cleartext.icon }}\"\n                title=\"{{ cleartext.text }}\"\n                data-crypto_icon=\"{{ cleartext.icon }}\"\n                data-crypto_color=\"{{ cleartext.color }}\"\n                data-crypto_message=\"{{ cleartext.message }}\"></span>\n          <span class=\"icon message-metadata-crypto-info {{ unsigned.color }} {{ unsigned.icon }}\"\n                title=\"{{ unsigned.text }}\"\n                data-crypto_icon=\"{{ unsigned.icon }}\"\n                data-crypto_color=\"{{ unsigned.color }}\"\n                data-crypto_message=\"{{ unsigned.message }}\"></span>\n        </h3>\n        {%- if allow_html and message.html_parts|length > 0 %}\n        <ul class=\"display-modes\">\n          <li><a title=\"{{ _('Display HTML formatted message content') }}\"\n                 class=\"message-show-html\">\n            <span class=\"icon icon-news\"></span></a></li>\n          {%- if message.text_parts|length > 0 %}\n          <li><a title=\"{{ _('Display plain-text message content') }}\"\n                 class=\"message-show-text\">\n            <span class=\"icon icon-document\"></span></a></li>\n          {%- endif %}\n        </ul>\n        {%- endif %}\n        <ul class=\"alternate-views\">\n          {%- for elem in get_ui_elements('display_modes', state, context='/message/') %}\n          <li><a title=\"{{ elem.description }}\"\n                 data-noblank=1 data-keep-selection=1\n                 href=\"{{ elem.url|add_state_query_string({'query_args': {'mid': [mid]}}, elem)|url_path_fix }}\">\n            {%- if elem.icon and '.' in elem.icon -%}\n            <img class=\"navigation-icon\" src=\"{{ U(elem.icon) }}\">\n            {%- elif elem.icon -%}\n            <span class=\"navigation-icon icon-{{elem.icon}}\"></span>\n            {%- endif %}\n          </li>\n          {%- endfor %}\n          <li><a title=\"{{ _('Switch to full conversation view') }}\"\n                 data-noblank=1 data-keep-selection=1 href=\"{{ U('/thread/=', mid, '/') }}\">\n            <span class=\"icon icon-forum\"></span></a></li>\n          <li><a title=\"{{ _('Display message source code') }}\"\n                 target=_blank href=\"{{ U('/message/raw/=', mid, '/as.text') }}\">\n            <span class=\"icon icon-work\"></span></a></li>\n          <li><a title=\"{{ _('Display technical message data as JSON') }}\"\n                 target=_blank href=\"{{ U('/message/=', mid, '/as.json') }}\">\n            <span class=\"icon icon-code\"></span></a></li>\n        </ul>\n        <div class=\"message-subject\">\n          {%- set subjects = get_all(message.header_list, \"subject\") %}\n          {%- if subjects %}\n            {%- for value in subjects %}\n              {{ nice_subject(value) }}\n            {%- endfor %}\n          {%- else %}\n            {{ nice_subject(None) }}\n          {%- endif %}\n        </div>\n      </div>\n{%- include('partials/pile_message.html') -%}\n    </div>\n    {%- endif %}\n{%- endif %}\n  </td>\n  <td class=\"date{% if not config.web.friendly_dates %} wide-date{% endif %}\"\n      data-ts=\"{{ metadata.timestamp }}\">\n    <span title=\"{{metadata.timestamp|friendly_time}}, {{metadata.timestamp|friendly_datetime}}\">\n      {% if config.web.friendly_dates -%}\n        {{metadata.timestamp|elapsed_datetime}}\n      {%- else -%}\n        {{metadata.timestamp|friendly_time}}, {{metadata.timestamp|friendly_datetime}}\n      {%- endif %}\n    </span>\n  </td>\n  <td class=\"checkbox\">\n    <input type=\"checkbox\" name=\"mid\" value=\"{{mid}}\">\n  </td>\n</tr>\n{% endif %}\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/sidebar.html",
    "content": "<!-- FIXME: make \"density\" class be pulled from user profile setting -->\n<div id=\"sidebar\" class=\"{{ config.web.display_density }}\"><div id=\"sidebar-wrapper\">\n  <div id=\"sidebar-scroll-area\" class=\"scroll-viewport\">\n    <div id=\"sidebar-lists\">\n      {{ mailpile_render(\"sidebar.html!minimal\", \"tags\", \"mode=tree\")[1]|safe }}\n      <nav id='sidebar-user-contacts'></nav>\n    </div>\n  </div>\n  <nav id=\"sidebar-bottom\">\n    <div id=\"sidebar-untag-dropzone\"\n         class=\"sidebar-untag-dropzone sidebar-tag sidebar-tags-draggable hide\"\n         style=\"color: {{theme_settings().colors['02-gray']}}; text-align: center;\">\n      <a class=\"sidebar-untag\" data-tid=\"\">\n        <span class=\"icon icon-circle-x\"></span>\n        <span class=\"name\">{{_(\"Remove tags\")}}</span>\n      </a>\n    </div>\n    <hr class=\"sidebar-untag-dropzone hide\">\n{%- if is_configured() %}\n  {%- if 0 and is_dev_version() %}\n    <a style='float: right' href=\"{{ U('/contacts/') }}\" id=\"button-sidebar-people\" title=\"{{_(\"Contacts\")}}\">\n      <span class=\"icon icon-groups\"></span>\n    </a>\n  {%- endif %}\n    <a style='float: left' id=\"add-tag\" class=\"auto-modal auto-modal-reload\"\n       data-icon=\"icon-tag\"\n       href=\"{{ U('/tags/add/') }}\" title=\"{{_(\"Add Tag\")}}\">\n      <span class=\"icon-plus\"></span>\n      <span class=\"text\">{{_(\"Add\")}}</span>\n    </a>\n    <a style='float: left' href=\"#\" id=\"button-sidebar-organize\"\n       data-message=\"{{_(\"Done\")}}\" data-state=\"initial\"\n       title=\"{{_(\"Edit Sidebar\")}}\">\n      <span class=\"icon icon-settings\"></span>\n      <span class=\"text\">{{_(\"Organize\")}}</span>\n    </a>\n{% endif %}\n  </nav>\n</div></div>\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/tag_add.html",
    "content": "<div id=\"tag-add\" class=\"content-normal hide\">\n\n  <h3>{{_(\"Add New Tag\")}}</h3>\n\n  <form id=\"form-tag-add\" class=\"standard\" method=\"POST\">{{ csrf_field|safe }}\n\n    <label>{{_(\"Tag\")}}</label>\n    <input type=\"text\" id=\"data-tag-add-tag\" name=\"name\" value=\"\" placeholder=\"{{_(\"Friends & Family\")}}\">\n\n    <label>{{_(\"Slug\")}}</label>\n    <input type=\"text\" id=\"data-tag-add-slug\" name=\"slug\" value=\"\" placeholder=\"{{_(\"friends-family\")}}\">\n\n    <label>{{_(\"Display\")}}</label>\n    <select name=\"display\" id=\"data-tag-add-display\">\n      <option value=\"tag\">{{_(\"Tag\")}}</option>\n      <option value=\"archive\">{{_(\"Archive\")}}</option>\n    </select>\n\n    <label>{{_(\"Parent\")}}</label>\n    <select name=\"parent\" id=\"data-tag-add-parrent\">\n      <option value=\"\">-------- {{_(\"none\")}} --------</option>\n    {% for tag in result.tags %}\n      {% if tag.display == \"tag\" %}\n      <option value=\"{{tag.tid}}\">{{tag.name}}</option>\n      {% endif %}    \n    {% endfor %}\n    </select>\n\n    <label>{{_(\"Template\")}}</label>\n    <select name=\"template\" id=\"data-tag-add-template\">\n      <option value=\"default\">{{_(\"Default\")}}</option>\n    </select>\n\n    <label>{{_(\"Search Terms\")}}</label>\n    <textarea name=\"search_terms\" id=\"data-tag-add-search-terms\" placeholder=\"in:inbox\"></textarea>\n\n    <button class=\"button-primary\" type=\"submit\"><span class=\"icon-plus\"></span> {{_(\"Add\")}}</button>\n  </form>\n\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/thread_message_cryptofail.html",
    "content": "{# This snippet expects the variable \"failed_crypto\" to be set to\n   either message.crypto.encryption or part.crypto.encryption ...\n#}\n{% set crypto = show_message_encryption(failed_crypto.status) %}\n<div class=\"message-inline-crypto-error text-center\">\n  <span class=\"icon {{ crypto.color + ' ' + crypto.icon }}\"></span>\n  <h3 class=\"status {{ crypto.color }}\">{{ crypto.text }}</h3>\n  <p>{{ crypto.message }}</p>\n  <p>\n  {%- if failed_crypto.status in (\"missingkey\", \"error\") %}\n    <button class=\"message-crypto-action\" data-mid=\"{{mid}}\">\n      <span class=\"icon-key\"></span> {{_(\"Send Your Encryption Key\")}}\n    </button>\n  {%- else %}\n    {% if state.query_args.ui_key_auth %}\n      <p><b><i>{{_(\"Password incorrect?  Try again!\")}}</i></b></p>\n    {% endif %}\n    <a href=\"{{ U('/settings/set/password/keys.html?id=', failed_crypto.locked_keys[0], '&ui_redirect_back=1') }}\"\n       class=\"auto-modal\" data-header=\"off\">\n      <button class=\"message-crypto-pinentry\" data-mid=\"{{mid}}\">\n        <span class=\"icon-key\"></span> {{_(\"Decrypt Message\")}}\n      </button>\n    </a>\n  {%- endif %}\n  </p>\n{#### FIXME: hiding this in Beta Freeze\n  {% if part.crypto.encryption.missing_keys %}\n  <a href=\"#\" class=\"message-crypto-investigate\" data-mid=\"{{mid}}\" data-part=\"{{ loop.index - 1 }}\">{{_(\"Investigate Message Further\")}}</a>\n  {% endif %}\n####}\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/tools_contacts.html",
    "content": "{%- set hard_coded_activities = safe('\n    <li class=\"dropdown\">\n      <a class=\"dropdown-toggle\" data-toggle=\"dropdown\" title=\"' + _(\"Search and import encryption keys\") + '\" href=\"#\">\n        <span class=\"navigation-icon icon-key\"></span> ' + _(\"Import Encryption Keys\") + '\n      </a>\n      <ul id=\"menu1\" class=\"dropdown-menu dropdown-menu-right\" role=\"menu\" aria-labelledby=\"display-density\">\n        <li role=\"presentation\">\n          <a class=\"btn-crypto-search-key\" href=\"#\">\n            <span class=\"icon-search\"></span> ' + _(\"Search for Key\") + '\n          </a>\n        </li>\n        <li role=\"presentation\">\n          <a class=\"btn-crypto-upload-key\" href=\"#\">\n            <span class=\"icon-upload\"></span> ' + _(\"Upload Key\") + '\n          </a>\n        </li>\n        <!-- FIXME: https://github.com/mailpile/Mailpile/issues/1225\n        <li role=\"presentation\">\n          <a class=\"btn-crypto-url-key\" href=\"#\">\n            <span class=\"icon-links\"></span> ' + _(\"Import from URL\") + '\n          </a>\n        </li>\n        -->\n        <li role=\"presentation\" class=\"divider\"></li>\n        <li role=\"presentation\">\n          <a class=\"btn-helper\" data-helper=\"what-is-encryption-key\" href=\"#\">\n            <span class=\"icon-help\"></span> ' + _(\"What's This?\") + '\n          </a>\n        </li>\n      </ul>\n    </li>\n') -%}\n\n{%- set activities = [{\n    'name': 'contact_list',\n    'url': '/contacts/',\n    'text': _(\"Contacts\"),\n    'description': _(\"View your contacts\"),\n    'icon': 'user'\n},{\n    'name': 'contact_add',\n    'url': '#contact-add',\n    'text': _(\"Add Contact\"),\n    'description': _(\"Add a new contact\"),\n    'icon': 'plus'\n}] -%}\n\n{%- include('partials/tools_default.html') -%}\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/tools_default.html",
    "content": "{%- set activities = (activities or []) + get_ui_elements('activities', state) -%}\n{%- set display_refiners = (display_refiners or []) + get_ui_elements('display_refiners', state) -%}\n{%- set display_modes = (display_modes or []) + get_ui_elements('display_modes', state) -%}\n{%- set selection_actions = (selection_actions or []) + get_ui_elements('selection_actions', state) -%}\n{%- set have_initial_prompt = selection_initial_prompt or False -%}\n{%- set selection_initial_prompt = selection_initial_prompt or _('Click item or checkbox to select') -%}\n{%- if (activities or display_refiners or display_modes or\n        hard_coded_activities or\n        have_initial_prompt or\n        hard_coded_display_refiners or\n        hard_coded_display_modes) %}\n<nav class=\"sub-navigation clearfix\">\n  <ul class=\"left horizontal\">\n  {{ hard_coded_activities|safe }}{% for elem in activities %}\n    <li class=\"btn-activity-{{elem.name}} {{elem.class}}\n        {%- if state.command_url == elem.url %} navigation-on{% endif %}\">\n      <a class=\"bulk-action-{{elem.name}} {{elem.aclass}}\"\n         data-keep-selection=1\n         href=\"{{ U(elem.url) }}\" title=\"{{elem.description}}\">\n      {%- if elem.icon and '.' in elem.icon -%}\n        <img class=\"navigation-icon\" src=\"{{ U(elem.icon) }}\">\n      {%- elif elem.icon -%}\n        <span class=\"navigation-icon icon-{{elem.icon}}\"></span>\n      {%- endif %}\n        <span class=\"navigation-text{% if loop.index >= 5 %} hide{% endif %}\"\n         >{{elem.text}}</span>\n      </a>\n    </li>\n    {%- if loop.last %}<li>&nbsp;</li>{% endif %}\n  {%- endfor %}\n  {%- if activities %}<script>\n    $(document).ready({{\n      ui_elements_setup('.btn-activity-%(name)s', activities)\n    }});\n  </script>{% endif %}\n  {%- for elem in display_refiners %}\n    <li class=\"display-refiner btn-display-refiner-{{elem.name}} {{elem.class}}\n        {%- if state.command_url|add_state_query_string(state) == url %} navigation-on{% endif %}\">\n      <a href=\"{{elem.url|add_state_query_string(state, elem)|url_path_fix}}\"\n         data-keep-selection=1\n         class=\"{{elem.aclass}}\" title=\"{{elem.description}}\">\n      {%- if elem.icon and '.' in elem.icon -%}\n        <img class=\"navigation-icon\" src=\"{{ U(elem.icon) }}\">\n      {%- elif elem.icon -%}\n        <span class=\"navigation-icon icon-{{elem.icon}}\"></span>\n      {%- endif %}<span class=\"navigation-text\">{{elem.text}}</span>\n      </a>\n    </li>\n  {%- endfor %}{{ hard_coded_refiners|safe }}\n  {%- if display_refiners %}<script>\n    $(document).ready({{\n      ui_elements_setup('.btn-display-refiner-%(name)s', display_refiners)\n    }});\n  </script>{% endif %}\n  </ul>\n  <ul class=\"right horizontal\">\n  {%- if display_modes|length > 1 %}\n   {%- for elem in display_modes %}\n    <li class=\"btn-display-mode-{{elem.name}} {{elem.class}}\n        {%- if state.command_url == elem.url %} navigation-on{% endif %}\">\n      <a href=\"{{elem.url|add_state_query_string(state, elem)|url_path_fix}}\"\n         data-keep-selection=1\n         title=\"{{elem.description}}\" class=\"{{elem.aclass}}\">\n      {%- if elem.icon and '.' in elem.icon -%}\n        <img class=\"navigation-icon\" src=\"{{ U(elem.icon) }}\">\n      {%- elif elem.icon -%}\n        <span class=\"navigation-icon icon-{{elem.icon}}\"></span>\n      {%- endif %}<span class=\"navigation-text\">{{elem.text}}</span>\n      </a>\n    </li>\n   {%- endfor %}\n  {%- endif %}{{ hard_coded_display_modes|safe }}\n  {%- if display_modes %}<script>\n    $(document).ready({{\n      ui_elements_setup('.btn-display-mode-%(name)s', display_modes)\n    }});\n  </script>{% endif %}\n  </ul>\n</nav>\n{%- endif %}\n{%- if selection_actions or hard_coded_selection_actions or have_initial_prompt %}\n<div class=\"bulk-actions clearfix mobile-hide\">\n  {%- if have_initial_prompt or not tools_disable_checkbox %}\n  <div id=\"bulk-actions-message\" class=\"left\"\n       data-bulk_selected=\"{{_(\"conversations selected\")}}\"\n       data-select_all=\"{{_(\"conversations selected, click to select all matching this search\")}}\"\n       data-unselect_all=\"{{_(\"Entire search selected, click to undo\")}}\"\n       data-bulk_selected_none=\"{{ selection_initial_prompt }}\">\n    {{ selection_initial_prompt|safe }}\n  </div>\n  {%- endif %}\n  <div class=\"bulk-actions-hints\" style='display: inline-block;'></div>\n  <ul class=\"horizontal right\">\n  {{ hard_coded_selection_actions|safe }}\n  {%- for elem in selection_actions %}\n    <li class=\"btn-selection-action-{{elem.name}} hide {{elem.class}}\n        {%- if state.command_url == elem.url %} navigation-on{% endif %}\">\n      <a class=\"bulk-action-{{elem.name}}\" href=\"#\"\n         title=\"{{elem.description}}\" class=\"{{elem.aclass}}\">\n      {%- if elem.icon and '.' in elem.icon -%}\n        <img class=\"navigation-icon\" src=\"{{ U(elem.icon) }}\" alt=\"{{elem.text}}\">\n      {%- elif elem.icon -%}\n        <span class=\"navigation-icon icon-{{elem.icon}}\"></span>\n      {%- else -%}{{elem.text}}{%- endif -%}\n      </a>\n    </li>\n  {%- endfor %}\n  {%- if (not tools_disable_checkbox) and (selection_actions or hard_coded_selection_actions) %}\n    <li><input type=\"checkbox\" id=\"pile-select-all-action\"\n               class=\"bulk-action-select-all\" tabindex=\"-1\"\n               value=\"\"></li>\n  {%- endif %}\n  </ul>\n  {%- if selection_actions %}<script>\n    $(document).ready({{\n      ui_elements_setup('.btn-selection-action-%(name)s', selection_actions)\n    }});\n  </script>{% endif %}\n</div>\n{%- endif %}\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/tools_message.html",
    "content": "{%- set activities = [] + get_ui_elements('email_activities', state) -%}\n{%- set selection_initial_prompt = \"... FIXME ...\" %}\n{%- include('partials/tools_default.html') -%}\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/tools_search.html",
    "content": "{%- set oc = {state.query_args.get('order', [config.prefs.default_order])[0]: ' selected'} %}\n{%- set ou = {\n    'rev-freshness': U(add_state_query_string(state.command_url, state, {\n                           'url_args_remove': [['order', '']],\n                           'url_args_add': [['order', 'rev-freshness']]\n                       })),\n    'rev-date':      U(add_state_query_string(state.command_url, state, {\n                           'url_args_remove': [['order', '']],\n                           'url_args_add': [['order', 'rev-date']]\n                       })),\n    'date':          U(add_state_query_string(state.command_url, state, {\n                           'url_args_remove': [['order', '']],\n                           'url_args_add': [['order', 'date']]\n                       })),\n    'rev-flat-date': U(add_state_query_string(state.command_url, state, {\n                           'url_args_remove': [['order', '']],\n                           'url_args_add': [['order', 'rev-flat-date']]\n                       })),\n    'rev-index':     U(add_state_query_string(state.command_url, state, {\n                           'url_args_remove': [['order', '']],\n                           'url_args_add': [['order', 'rev-index']]\n                       })),\n}%}\n{%- set dm = {config.web.display_density: ' selected'} %}\n{%- set display_things = [] -%}\n{%- if result.index_capabilities.can_sort -%}\n  {%- do display_things.append('\n    <li class=\"dropdown\">\n      <a class=\"dropdown-toggle\" data-toggle=\"dropdown\" href=\"#\">\n        <span class=\"navigation-icon icon-list\"></span>\n        <span class=\"navigation-text\">' + _(\"Order\") + '</span>\n      </a>\n      <ul id=\"menu1\" class=\"dropdown-menu dropdown-menu-right\"\n          role=\"menu\" aria-labelledby=\"display-density\">\n        <li role=\"presentation\">\n          <a class=\"change-search-order' + oc.get('rev-freshness', '') + '\"\n             data-order=\"rev-freshness\" data-keep-selection=1\n             href=\"' + ou['rev-freshness'] + '\">' +  _(\"Freshness\") + '</a>\n        </li>\n        <li role=\"presentation\">\n          <a class=\"change-search-order' + oc.get('rev-date', '') + '\"\n             data-order=\"rev-date\" data-keep-selection=1\n             href=\"' + ou['rev-date'] + '\">' + _(\"Newest First\") + '</a>\n        </li>\n        <li role=\"presentation\">\n          <a class=\"change-search-order' + oc.get('date', '') + '\"\n             data-order=\"date\" data-keep-selection=1\n             href=\"' + ou['date'] + '\">' + _(\"Oldest First\") + '</a>\n        </li>\n        <li role=\"presentation\">\n          <a class=\"change-search-order' + oc.get('rev-flat-date', '') + '\"\n             data-order=\"rev-flat-date\" data-keep-selection=1\n             href=\"' + ou['rev-flat-date'] + '\">' + _(\"Messages\") + '</a>\n        </li>\n        <li role=\"presentation\">\n          <a class=\"change-search-order' + oc.get('rev-index', '') + '\"\n             data-order=\"rev-index\" data-keep-selection=1\n             href=\"' + ou['rev-index'] + '\">' + _(\"Unsorted\") + '</a>\n        </li>\n      </ul>\n    </li>\n') -%}\n{%- endif %}\n{%- do display_things.append('\n    <li class=\"dropdown mobile-hide\" id=\"display-density-selector\">\n      <a class=\"dropdown-toggle\" data-toggle=\"dropdown\" href=\"#\"><span class=\"navigation-icon icon-eye\"></span> ' + _(\"Display\") + '</a>\n      <ul id=\"menu1\" class=\"dropdown-menu dropdown-menu-right\" role=\"menu\" aria-labelledby=\"display-density\">\n        <li role=\"presentation\">\n          <a class=\"change-view-size' + dm.get('snug', '') + '\"\n             data-view_size=\"snug\" href=\"#\">' + _(\"Snug\") + '</a>\n        </li>\n        <li role=\"presentation\">\n          <a class=\"change-view-size' + dm.get('cozy', '') + '\"\n             data-view_size=\"cozy\" href=\"#\">' + _(\"Cozy\") + '</a>\n        </li>\n        <li role=\"presentation\">\n          <a class=\"change-view-size' + dm.get('comfy', '') + '\"\n             data-view_size=\"comfy\" href=\"#\">' + _(\"Comfy\") + '</a>\n        </li>\n      </ul>\n    </li>\n') -%}\n\n{%- set search_actions = [] -%}\n\n{%- if result.index_capabilities.has_tags -%}\n{%-   for tid in config.tags -%}\n{%-     set tag = config.tags[tid] -%}\n{%-     if tag.toolbar or tag.type in ('unread', ) -%}\n{%-       if tag.type in ('attribute', 'unread') -%}\n{%-         set msg = _(\"Toggle %(tag_name)s\", tag_name=tag.name) -%}\n{%-         set act = \"toggle\" -%}\n{%-       else -%}\n{%-         set msg = _(\"Move to %(tag_name)s\", tag_name=tag.name) -%}\n{%-         set act = \"move\" -%}\n{%-       endif -%}\n{%-       set tag_color = theme_settings().colors.get(tag.label_color, tag.label_color) -%}\n{%-       do search_actions.append('\n  <li class=\"hide\">\n    <a class=\"bulk-action-tag-op\" href=\"#\" title=\"'+ msg +'\"\n       data-op=\"'+ act +'\" data-tag=\"'+ tid +'\" data-tagname=\"'+ tag.name +'\">\n      <span class=\"icon '+ tag.icon +'\" style=\"color: ' + tag_color +';\"></span>\n    </a>\n  </li>') -%}\n{%-     endif -%}\n{%-   endfor -%}\n\n{# FIXME, Issue #879  %-   do search_actions.append('\n  <li class=\"hide\">\n    <a class=\"bulk-action-tag\" href=\"#\" title=\"' + _(\"Assign Tags\") + '\">\n      <span class=\"icon icon-tags\"></span>\n    </a>\n  </li>') -% #}\n{%-   if \"in:spam\" not in result.search_terms %}\n{%-     do search_actions.append('\n  <li class=\"hide\">\n    <a class=\"bulk-action-tag-op\" href=\"#\" title=\"' + _(\"Move to Spam\") + '\"\n       data-ui=\"spam\" data-op=\"tag\" data-untag=\"type:unread\" data-tag=\"spam\">\n      <span class=\"icon-spam\"></span>\n    </a>\n  </li>') -%}\n{%-   endif %}\n{%-   if \"in:trash\" not in result.search_terms %}\n{%-     do search_actions.append('\n  <li class=\"hide\">\n    <a class=\"bulk-action-tag-op\" href=\"#\" title=\"' + _(\"Move to Trash\") + '\"\n       data-ui=\"trash\" data-op=\"tag\" data-untag=\"type:unread\" data-tag=\"trash\">\n      <span class=\"icon icon-trash\"></span>\n    </a>\n  </li>') -%}\n{%-   endif %}\n{%-   if result.tag_capabilities.allow_del -%}\n{%-     do search_actions.append('\n  <li class=\"hide\">\n    <a class=\"bulk-action-tag-op\" href=\"#\" title=\"' + _(\"Untag Selection\") + '\"\n       data-op=\"untag\">\n      <span class=\"icon icon-circle-x\"></span>\n    </a>\n  </li>') -%}\n{%-   endif %}\n{%-   do search_actions.append('\n  <li class=\"hide\">\n    <a class=\"bulk-action-tag-op\" href=\"#\" data-op=\"archive\"\n       title=\"' + _(\"Archive Selection\") +': '+ _(\"untag completely\") +'\">\n      <span class=\"icon icon-archive\">\n    </a>\n  </li>') -%}\n{%- endif -%}\n\n{%- set hard_coded_display_modes = safe(display_things|join('')) -%}\n{%- set hard_coded_selection_actions = safe(search_actions|join('')) -%}\n\n{%- set display_refiners = [] -%}\n{%- if result.index_capabilities.has_unread -%}\n{%-   do display_refiners.append({\n        'name': 'unread',\n        'icon': 'new',\n        'url': '/search/',\n        'url_args_remove': [['qr', ''], ['context', ''], ['start', ''], ['end', ''], ['view', ''], ['order', '']],\n        'url_args_add': [['qr', 'is:unread']],\n        'text': _(\"New\"),\n        'description': _(\"Unread messages\")})\n-%}\n{%- endif -%}\n{%- if result.index_capabilities.has_atts %}\n{%-   do display_refiners.append({\n        'name': 'has_photos',\n        'icon': 'photos',\n        'url': '/search/photos.html',\n        'url_args_remove': [['qr', ''], ['context', ''], ['start', ''], ['end', ''], ['view', ''], ['order', '']],\n        'url_args_add': [['qr', 'has:image'], ['order', 'rev-flat-date']],\n        'text': _(\"Images\"),\n        'description': _(\"Photos and images in mail matching this search\")})\n-%}\n{%-   do display_refiners.append({\n        'name': 'has_attachment',\n        'icon': 'attachment',\n        'url': '/search/atts.html',\n        'url_args_remove': [['qr', ''], ['context', ''], ['start', ''], ['end', ''], ['view', ''], ['order', '']],\n        'url_args_add': [['qr', 'has:attachment'], ['order', 'rev-flat-date']],\n        'text': _(\"Attachments\"),\n        'description': _(\"Attachments in mail matching this search\")})\n-%}\n{%- endif %}\n{%- if display_refiners %}\n{%-   do display_refiners.append({\n        'name': 'display_all',\n        'icon': 'logo',\n        'url': '/search/',\n        'url_args_remove': [['qr', ''], ['context', ''], ['start', ''], ['end', ''], ['view', ''], ['order', '']],\n        'text': _(\"All\"),\n        'description': _(\"All messages\")})\n-%}\n{%- endif %}\n\n{%- set display_modes = [{\n    'name': 'display_list',\n    'icon': 'list',\n    'url': '/search/',\n    'text': _(\"List\"),\n    'description': _(\"List view\")}]\n-%}\n\n{%- set search_terms_in = (result.search_terms or [''])[0][3:] -%}\n{%- set activities = [] -%}\n{%- set parent = state.query_args.get('parent', [''])[0] -%}\n{%- if parent %}\n{%-   do activities.append({\n        'name': 'parent',\n        'icon': 'upload',\n        'url': parent,\n        'text': _(\"Parent\"),\n        'description': _(\"Open Parent Folder\")})\n-%}\n{%- endif %}\n{%- if 'in:' == (result.search_terms or [''])[0][:3] -%}\n{%-   do activities.append({\n        'name': 'edit_tag',\n        'icon': 'settings',\n        'aclass': 'auto-modal auto-modal-reload',\n        'url': '/tags/edit.html?only=' + search_terms_in,\n        'text': _(\"Edit\"),\n        'description': _(\"Edit\") + ': '  + search_terms_in})\n-%}\n{%- elif result.search_terms not in ([], [''], ['all:mail']) %}\n{%-   do activities.append({\n        'name': 'save_search',\n        'icon': 'star',\n        'aclass': '',\n        'url': '#save_search',\n        'text': _(\"Save\"),\n        'description': _(\"Save the results of this search to a new tag\")})\n-%}\n{%- else %}\n{%-   do activities.append({\n        'name': 'home',\n        'icon': 'home',\n        'aclass': '',\n        'url': '/',\n        'text': _(\"Home\"),\n        'description': _(\"E-mail Accounts\")})\n-%}\n{%- endif %}\n{%- include('partials/tools_default.html') -%}\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/tools_settings.html",
    "content": "{#\n<nav class=\"sub-navigation clearfix\">\n  <ul class=\"left horizontal\">\n    <li {% if \"profiles\" in result %} class=\"navigation-on\" {% endif %}><a data-filter=\"all\" href=\"{{ U('/settings/profiles/) }}\"><span class=\"navigation-icon icon-profiles\"></span><span class=\"navigation-text\">{{_(\"Profiles\")}}</span></a></li>\n    <li {% if \"routes\" in result %} class=\"navigation-on\" {% endif %}><a data-filter=\"all\" href=\"{{ U('/settings/routes/') }}\"><span class=\"navigation-icon icon-routes\"></span><span class=\"navigation-text\">{{_(\"Routes\")}}</span></a></li>\n    <li {% if \"prefs\" in result %} class=\"navigation-on\" {% endif %}><a data-filter=\"all\" href=\"{{ U('/settings/prefs/') }}\"><span class=\"navigation-icon icon-preferences\"></span><span class=\"navigation-text\">{{_(\"Preferences\")}}</span></a></li>\n    <li {% if \"sys\" in result %} class=\"navigation-on\" {% endif %}><a data-filter=\"all\" href=\"{{ U('/settings/sys/') }}\"><span class=\"navigation-icon icon-settings\"></span><span class=\"navigation-text\">{{_(\"Advanced\")}}</span></a></li>\n  </ul>\n</nav>\n#}\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/tools_tags.html",
    "content": "{%- set activities = [{\n    'name': 'tag_list',\n    'url': '/tags/',\n    'text': _(\"Tags\"),\n    'description': _(\"View all tags\"),\n    'icon': 'tag'\n},{\n    'name': 'tag_add',\n    'url': '/tags/add/',\n    'text': _(\"Add Tag\"),\n    'description': _(\"Create a new tag\"),\n    'icon': 'plus'\n},{\n    'name': 'filter_list',\n    'url': '/filter/list/',\n    'text': _(\"Filters\"),\n    'description': _(\"All current filters\"),\n    'icon': 'filters'\n}] -%}\n{%- set display_modes = [{\n    'name': 'display_tag_type',\n    'icon': 'eye',\n    'url': '/tags/',\n    'text': _(\"Type\"),\n    'description': _(\"Show Tag by type\")\n}] -%}\n{%- include('partials/tools_default.html') -%}\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/tooltips.html",
    "content": "<script id=\"tooltip-pile-tag-details\" type=\"text/template\">\n  <div>\n    <a href=\"<%= url %>\" data-mid=\"<%= mid %>\">\n      <h4 class=\"text-center\" style=\"color: <%= Mailpile.theme.colors[label_color] %> \">\n        <span class=\" <%= icon %> \"></span> <%= name %>\n      </h4>\n    </a>\n    <p><a href=\"{{ U('/tags/edit.html?only=') }}<%= slug %>\"\n          title=\"{{ _('Edit') }}: <%= name %>\"\n          data-icon=\"icon-settings\" class=\"auto-modal auto-modal-reload\"><span class=\"icon-settings\"></span> {{_(\"Edit Tag\")}}</a></p>\n    <p><a href=\"#\" data-tid=\"<%= tid %>\" data-mid=\"<%= mid %>\" class=\"pile-tag-delete\"><span class=\"icon-circle-x\"></span> {{_(\"Remove Tag\")}}</a></p>\n  </div>\n</script>\n\n\n<script id=\"tooltip-contact-details\" type=\"text/template\">\n  <div class=\"user clearfix half-bottom\">\n    <% if (photo) { %>\n    <a class=\"avatar\"\n{%- if 0 and is_dev_version() %}\n       href=\"{{ U('/contacts/view/<%= address %>/') }}\"\n{%- endif %}\n       target=\"_blank\"><img src=\"<%= photo %>\"></a>\n    <% } else { %>\n    <a target=\"_blank\"\n{%- if 0 and is_dev_version() %}\n       href=\"{{ U('/contacts/view/<%= address %>/') }}\"\n{%- endif %}\n       ><span class=\"icon-user\"></span></a>\n    <% } %>\n    <div class=\"name\">\n      <a target=\"_blank\"\n{%- if 0 and is_dev_version() %}\n         href=\"{{ U('/contacts/view/<%= address %>/') }}\"\n{%- endif %}\n         ><%= fn %></a>\n      <span class=\"address\"><%= address %></span>\n    </div>\n  </div>\n  <p class=\"half-bottom text-center\">\n    <a data-email=\"<%= address %>\" data-mid=\"<%= mid %>\"\n       class=\"compose-contact-find-keys\"><span class=\"icon-key\"></span>\n    <% if (flags.secure) { %>\n      {{_(\"Show Encryption Keys\")}}\n    <% } else { %>\n      {{_(\"Find Encryption Keys\")}}\n    <% } %>\n    </a>\n  </p>\n</script>\n\n\n<script id=\"tooltip-tag-subtags\" type=\"text/template\">\n  <ul>\n    <% _.each(tag.subtag_ids, function(stid, key) { var subtag = _.findWhere(Mailpile.instance.tags, {tid: stid}); %>\n    <li class=\"clearfix\">\n      <a href=\"{{ U('/in/<%= subtag.slug %>/') }}\" style=\"color: <%= subtag.label_color %>\" class=\"left\"><span class=\"<%= subtag.icon %>\"></span> <%= subtag.name %></a>\n      <a class=\"right link-detail\" href=\"{{ U('/tags/edit.html?only=<%= subtag.slug %>') }}\"><span class=\"icon-settings\"></span></a>\n    </li>\n    <% }); %>\n  </ul>\n</script>\n"
  },
  {
    "path": "shared-data/default-theme/html/partials/topbar.html",
    "content": "<div id=\"header\" class=\"topbar\">\n  <div class=\"topbar-logo\">\n  {% set status = _(\"%(name)s's Mailpile is version %(version)s with %(size)s messages\", name=(name or _(\"Somebody\")), size=mailpile_size, version=config.version) %}\n    <a href=\"{{ U('/profiles/') }}\" class='status-in-title' title=\"{{status}}\">\n      {% include(\"../img/logo-color.svg\") %}\n    </a>\n    <span id='page-title-icon' class='icon hide'></span>\n  </div>\n  <div class=\"topbar-logo-name\">\n    <a href=\"{{ U('/profiles/') }}\" class='status-in-title' title=\"{{status}}\">\n      {% include(\"../img/logo-name.svg\") %}\n    </a>\n    <span id='page-title-text' class=\"mobile-hide\"></span>\n  </div>\n  <div class=\"topbar-actions\">\n    <form id=\"form-search\" class=\"form-search clearfix mobile-pt-hide\"\n          action=\"{{ U('/search/') }}\">\n      <input id=\"search-query\" class=\"typeahead\" type=\"text\"\n             name=\"q\" placeholder=\"{{_(\"Search\")}}\" autocomplete=\"off\"\n             tabindex=1 alt=\"{{_(\"Search\")}}\"\n             data-context=\"{{state.context}}\"\n             data-q=\"{% for t in result.search_terms %}{{ t }} {% endfor %}\"\n             value=\"{% for t in result.search_terms %}{{ t }} {% endfor %}\">\n      <span class=\"clear-search icon-circle-x\"></span>\n      <button type=\"submit\" class=\"submit\"><span class=\"icon-search\"></span></button>\n    </form>\n    <nav class=\"topbar-nav\">\n      <ul>\n        <li class=\"nav-search-hide hide\">\n          <a href=\"#\" id='nav-search-hide'\n            ><span class=\"link-icon icon-x\"></span></a>\n        </li>\n  {%- if is_dev_version() %}{# FIXME ! #}\n        <li style=\"margin: 10px 0 -10px 0\"\n            class=\"tablet-hide mobile-hide\">\n          <a class=\"auto-modal\" style=\"width: unset\" data-flags=\"\" data-icon=\"icon-code\"\n             title=\"{{_('How to report bugs')}}\"\n             href=\"{{ U('/page/contribute/bugs.html') }}\">{{_(\"Report Bugs\")}}</a>\n        </li>\n  {%- endif %}\n        <li class=\"{% if command == \"edit\" %} navigation-on{% endif %}\">\n          <a href=\"{{ U('/message/compose/') }}\" class=\"button-compose\" title=\"{{_(\"Compose\")}}\"\n            ><span class=\"link-icon icon-compose\"></span></a>\n        </li>\n        <li class=\"nav-search mobile-pt-inline\">\n          <a href=\"#\" id='nav-search'\n            ><span class=\"link-icon icon-search\"></span></a>\n        </li>\n<!--\n        <li class=\"{% if command == \"contacts\" %} navigation-on{% endif %}\">\n          <a href=\"{{ U('/contacts/') }}\" title=\"{{_(\"Contacts\")}}\"><span class=\"link-icon icon-user\"></span></a>\n        </li>\n        <li class=\"{% if state.command_url in (\"/tags/\", \"/filter/list/\") %}navigation-on{% endif %}\">\n          <a href=\"{{ U('/tags/') }}\" title=\"{{_(\"Tags\")}}\"><span class=\"link-icon icon-tag\"></span></a>\n        </li>\n -->\n        {%- for a in get_ui_elements('activities', state, '/') -%}\n        <li class=\"{% if command == a.name %}navigation-on{% endif %}\">\n          <a href=\"{{ U(a.url) }}\" class=\"plugin-activity-{{ a.name }}\" title=\"{{ a.description }}\"\n            ><img alt=\"{{ a.text }}\" class=\"icon-plugin-activity\" src=\"{{ U(a.icon) }}\"></a>\n        </li>\n        {%- endfor -%}\n\n        <li class=\"{% if command == \"profiles\" %} navigation-on{% endif %} tablet-hide mobile-hide\">\n          <a class=\"home\" href=\"{{ U(\"/profiles/\") }}\" title=\"{{_(\"Home\")}}\"\n            ><span class=\"link-icon icon-home\"></span></a>\n        </li>\n        {%- if config.web.donate_visibility %}\n        <li class=\"{% if command == \"page\" and state.query_args.arg.0 == \"contribute\" %} navigation-on{% endif %} tablet-hide mobile-hide\">\n          <a class=\"donate auto-modal\" data-flags=\"\" data-icon=\"icon-code\"\n             href=\"{{ U(\"/page/contribute/\") }}\" title=\"{{_(\"Contribute\")}}\"\n            ><span class=\"link-icon icon-donate\"></span></a>\n        </li>\n        {%- endif %}\n        <li class=\"{% if command == \"settings\" %} navigation-on{% endif %} mobile-pt-hide\">\n          <a href=\"{{ U(\"/settings/\") }}\" title=\"{{_(\"Settings and Tools\")}}\"\n            ><span class=\"link-icon icon-settings\"></span></a>\n        </li>\n        <li>\n           <a href=\"{{ U(\"/auth/logout/\") }}\" title=\"{{_(\"Logout\")}}\"\n             ><span class=\"link-icon icon-logout\"></span></a>\n        </li>\n{#\n        <li class=\"\">\n          <a class=\"secure\" href=\"#\" title=\"{{_(\"Secure Mode\")}}\"><span class=\"link-icon icon-lock-open\"></span></a>\n        </li>\n#}\n      </ul>\n    </nav>\n  </div>\n</div>\n<div id=\"notifications\">\n  <div id=\"notifications-header\">\n    <span class=\"text\">\n      <a href=\"{{ U('/logs/events/') }}\">{{ _(\"Notifications\") }}</a>\n    </span>\n    <a class=\"notifications-close-all\" href=\"#\"><span class=\"icon icon-x hide\"></span></a>\n  </div>\n  <div id=\"notification-bubbles\"></div>\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/plugins/index.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block content %}\n\n{% if result %}\n\n  <ul>\n  {% for plugin in result %}\n    <li>{{ plugin }} {% if plugin.builtin %} CORE {% endif %}</li>\n  {% endfor %}\n  </ul>\n\n{% endif %}\n\n{% endblock %}"
  },
  {
    "path": "shared-data/default-theme/html/profiles/account-form.html",
    "content": "{#\n\n  This is a self-contained modal for adding (and editing) account details.\n  This is a single form which handles profiles, routes and basic source\n  configuration.\n\n  The basic section of the form captures name, e-mail and password.\n\n  If the user allows auto-detection of settings, the Thunderbird ISPDB\n  will be queried and possibly other heuristics employed to figure out\n  correct connection settings. If successful, the route and source forms\n  will be filled in automatically (and skipped on Next).\n\n  Manual settings for routes and sources are as expected; local or\n  remote options, SMTP, IMAP, POP3, SSL, ports, hostnames, auth, ...\n\n  The security section of the form allows the user to choose which PGP key\n  to associate with this account or create a new one if he has none, which\n  will take place in a long-running background process.\n\n  The user can at any time skip forward or backwards in the form by\n  clicking on the section headlines, which are kept visible for that\n  purpose.\n\n#}{% set new_src_id = result.new_src_id %}\n\n  <script>\n    var _fpa = (function() {\n      var pf = '#form-profile-editor ';\n      var new_src_id = '{{ new_src_id }}';\n      var old = {username: ''};\n      return {\n        pre_submit: function(elem) {\n        },\n        display: function(elem, disp) {\n          if (disp) { elem.show() } else { elem.hide() };\n        },\n        validate: function() {\n          var basic_problems = 0;\n          if ($(pf + 'input[name=name]').val() == \"\" ||\n              $(pf + 'input[name=email]').val().indexOf('@') < 0) {\n            basic_problems += 1;\n            $(pf + 'input[name=name], ' + pf + 'input[name=email]'\n              ).on('change', _fpa.validate);\n          }\n          // FIXME: Validate other things too, improve e-mail validation\n          //_fpa.display($('.fpa-basics-ok'), basic_problems < 1);\n          _fpa.display($('.fpa-basics-bad'), basic_problems > 0);\n          _fpa.display($('#fpa-submit'),\n                       (basic_problems < 1) &&\n                       (!$(pf + '.fpa-autoconfig').is(':checked')));\n        },\n        next: function(show) {\n          $(pf + 'div.section').slideUp();\n          $(pf + 'div.' + show).slideDown().find(':tabbable').eq(0).focus();\n          $(pf + '.fpa-login-failed').hide();\n          $(pf + '.fpa-detection-failed').hide();\n          // Any navigation disables the auto-detection\n          $(pf + '.fpa-autoconfig').prop('checked', false);\n          _fpa.validate();\n          return false;\n        },\n        more: function(show) {\n          $(pf + '.' + show + '-show').hide();\n          $(pf + '.' + show).slideDown();\n          return false;\n        },\n        select: function(sel, cls) {\n          var val = $(sel).val();\n          $(pf + '.' + cls).hide();\n          if (val) {\n            $(pf + '.' + cls + '.any').show();\n            if (val[0] != '!') $(pf + '.' + cls + '.' + val).show();\n          }\n          else {\n            $(pf + '.' + cls + '.undefined').show();\n          }\n        },\n        mark_copied: function(elem, what) {\n          var $elem = $(elem);\n          if ($elem.val() == old[what]) {\n            $elem.parent().find('.fpa-' + what + '-copied').show();\n          }\n          else {\n            $elem.parent().find('.fpa-' + what + '-copied').hide();\n          }\n        },\n        copy_basic: function(what) {\n          var value = $(pf + '.fpa-basic-' + what).val();\n          var old_old = old[what];\n          old[what] = value;\n          $(pf + '.fpa-' + what).each(function(i) {\n            var $t = $(this);\n            if (old_old == $t.val() || '' == $t.val()) $t.val(value);\n            _fpa.mark_copied(this, what);\n          });\n        },\n        exclude: function(elem, s1, s2, s3, s4, s5, s6, s7) {\n          if ($(elem).is(':checked')) {\n            if (s1) $(pf + 'input[name=\"' + s1 +'\"]').removeAttr('checked');\n            if (s2) $(pf + 'input[name=\"' + s2 +'\"]').removeAttr('checked');\n            if (s3) $(pf + 'input[name=\"' + s3 +'\"]').removeAttr('checked');\n            if (s4) $(pf + 'input[name=\"' + s4 +'\"]').removeAttr('checked');\n            if (s5) $(pf + 'input[name=\"' + s5 +'\"]').removeAttr('checked');\n            if (s6) $(pf + 'input[name=\"' + s6 +'\"]').removeAttr('checked');\n            if (s7) $(pf + 'input[name=\"' + s7 +'\"]').removeAttr('checked');\n          }\n        },\n        require: function(elem, s1, s2, s3) {\n          if ($(elem).is(':checked')) {\n            if (s1) $(pf + 'input[name=\"' + s1 +'\"]').prop('checked', true);\n            if (s2) $(pf + 'input[name=\"' + s2 +'\"]').prop('checked', true);\n            if (s3) $(pf + 'input[name=\"' + s3 +'\"]').prop('checked', true);\n          }\n        },\n        requiredby: function(elem, s1, s2, s3) {\n          if (!$(elem).is(':checked')) {\n            if (s1) $(pf + 'input[name=\"' + s1 +'\"]').removeAttr('checked');\n            if (s2) $(pf + 'input[name=\"' + s2 +'\"]').removeAttr('checked');\n            if (s3) $(pf + 'input[name=\"' + s3 +'\"]').removeAttr('checked');\n          }\n        },\n        email_changed: function() {\n          // If the e-mail in basics changes, try to select the matching\n          // PGP key, if there is one. This may override user selections,\n          // which might be lame but should rarely be an issue because of\n          // the \"order of progression\" of the form.\n          var email = $(pf + '.fpa-email').val();\n          var found = 0;\n          $(pf + '.fpa-pgp-key option').each(function(i) {\n            var uid = $(this).data('uid');\n            $(this).prop('selected', false);\n            if (uid == email) {\n              $(this).removeClass('hide');\n              if (found == 0) $(this).prop('selected', true);\n              found = 1;\n            }\n            else {\n              if (uid) $(this).addClass('hide');\n            }\n            if (!found) {\n              $(pf + '.fpa-pgp-key-default').prop('selected', true);\n              _fpa.select($('.fpa-pgp-key'), 'security-opt');\n            }\n          });\n          _fpa.copy_basic('username');\n        },\n        basics_next: function() {\n          var email = $(pf + '.fpa-email').val();\n          if (!email) {\n            // FIXME: Improve validation...\n            alert('{{_(\"You need at least an e-mail address to proceed.\")|escapejs}}');\n          }\n          else if ($(pf + '.fpa-autoconfig').is(':checked')) {\n            $(pf + 'div.section, ' + pf + 'div.fpa-network-settings').slideUp();\n            $(pf + 'div.fpa-detection-in-progress').slideDown();\n            $(pf + '.fpa-detection-progress').html('{{_(\"Detecting settings for: \")|escapejs}}' + email);\n            var ev_source = '.*.SetupGetEmailSettings';\n            var watch_id = EventLog.subscribe(ev_source, function(ev) {\n              if (ev.private_data['track-id'] == new_src_id) {\n                $(pf + '.fpa-detection-progress').html(ev.message);\n              }\n            });\n            Mailpile.API.setup_email_servers_get({\n              'track-id': new_src_id,\n              '_timeout': (Mailpile.ajax_timeout * 30), // AJAX timeout\n              'timeout': 240000,  // Server-side deadline\n              'email': email,\n              '_error_callback': function(response, _status) {\n                console.log(\"Detection error \" + _status);\n                $(pf + 'div.fpa-detection-in-progress').slideUp();\n                $(pf + 'div.fpa-network-settings').slideDown();\n                _fpa.next('profile-add-route');\n                $(pf + '.fpa-detection-failed').slideDown();\n                EventLog.unsubscribe(ev_source, watch_id);\n              }\n            }, function(data) {\n              EventLog.unsubscribe(ev_source, watch_id);\n              $(pf + 'div.fpa-detection-in-progress').slideUp();\n              $(pf + 'div.fpa-network-settings').slideDown();\n              $('.edit-provider-settings').hide();\n              var nxt = undefined;\n              var result = undefined;\n              if (data.result) result = data.result[email];\n              if (result) nxt = _fpa.copy_email_settings(result);\n              if (data.result && data.result['login_failed']) {\n                _fpa.next('profile-add-basics');\n                $(pf + '.fpa-autoconfig').prop('checked', true);\n                $(pf + '.fpa-login-failed').slideDown();\n              }\n              else if (nxt) {\n                _fpa.next(nxt);\n              }\n              else {\n                _fpa.next('profile-add-route');\n                $(pf + '.fpa-detection-failed').slideDown();\n              }\n            });\n            return false;\n          }\n          else {\n            return _fpa.next(\"profile-add-route\");\n          }\n        },\n        copy_email_settings: function(result) {\n          var next = 0;\n          var oauth = 0;\n          if (result.routes && result.routes.length > 0) {\n            var found = result.routes[0];\n            if (found.auth_type == 'OAuth2') oauth += 1;\n            $(\"input[name='route-host']\").val(found.host);\n            $(\"input[name='route-port']\").val(found.port);\n            $(\"input[name='route-username']\").val(found.username);\n            var $s = $(\"select[name='route-protocol']\");\n            $s.find('option').prop('selected', false);\n            $s.find(\"option[value='\" + found.protocol + \"']\").prop('selected', true);\n            var $a = $(\"select[name='route-auth_type']\");\n            $a.find('option').prop('selected', false);\n            $a.find(\"option[value='\" + found.auth_type.substring(0, 8) + \"']\").prop('selected', true);\n            _fpa.select($s, 'route-settings');\n            next = 'profile-add-source';\n          }\n          if (result.sources && result.sources.length > 0) {\n            var found = result.sources[0];\n            if (found.auth_type == 'OAuth2') oauth += 1;\n            $(\"input[name='source-{{ new_src_id }}-host']\").val(found.host);\n            $(\"input[name='source-{{ new_src_id }}-port']\").val(found.port);\n            $(\"input[name='source-{{ new_src_id }}-username']\").val(found.username);\n            var $s = $(\"select[name='source-{{ new_src_id }}-protocol']\");\n            $s.find(\"option\").prop('selected', false);\n            $s.find(\"option[value='\" + found.protocol + \"']\").prop('selected', true);\n            var $a = $(\"select[name='source-{{ new_src_id }}-auth_type']\");\n            $a.find('option').prop('selected', false);\n            $a.find(\"option[value='\" + found.auth_type.substring(0, 8) + \"']\").prop('selected', true);\n            _fpa.select($s, 'source-settings-{{ new_src_id }}');\n            next = 'profile-add-security';\n          }\n          if (oauth) {\n            // Do nothing...?\n          }\n          else if (result.enable) {\n            $('.edit-provider-settings').attr('href', result.enable[0].url).show();\n            $('.fpa-warning .description').html(result.enable[0].description);\n            $('.fpa-warning ul.docs').html(' ');\n            if (result.docs) {\n              for (var i = 0; i < result.docs.length; i++) {\n                 var doc = result.docs[i];\n                 if (doc.description.indexOf('TB') == -1 &&\n                     doc.description.indexOf('Thunder') == -1) {\n                   $('.fpa-warning ul.docs').append($(\n                     '<li><a target=_blank href=\"' + doc.url + '\">' + doc.description + '</a></li>'\n                   ));\n                 }\n              }\n            }\n            next = 'fpa-warning';\n          }\n          return next;\n        },\n        setup: function() {\n          $('input[name=name]').focus();\n          $('#form-profile-editor').on('keydown', ':tabbable', function(e) {\n            // Make ENTER behave like TAB, to avoid accidental form submission.\n            if (document.activeElement.tagName == \"TEXTAREA\") {\n              // Do nothing\n            }\n            else if (e.which == 13 || e.keyCode == 13) {\n              e.preventDefault();\n              var nxt = $(document.activeElement).data(\"next\");\n              if (nxt) {\n                $(nxt).trigger('click');\n              }\n              else {\n                var $canfocus = $(':tabbable:visible')\n                var index = $canfocus.index(document.activeElement) + 1;\n                if (index >= $canfocus.length) index = 0;\n                $canfocus.eq(index).focus();\n              }\n            }\n          });\n        }\n      };\n    })();\n    setTimeout(\"_fpa.setup();\", 100);\n\n    $(function() {\n      // Make authentication popups forget about the current profile:\n      EventLog.forget_about_event(\"{{result.rid}}\");\n    });\n  </script>\n\n  <form id=\"form-profile-editor\" class=\"standard\"\n        method=\"POST\" action=\"{{ U(state.command_url) }}\"\n        style=\"position: relative; max-width: 60em;\">{{ csrf_field|safe }}\n    {%- if result.rid %}\n    <input type=\"hidden\" name=\"rid\" value=\"{{ result.rid }}\">\n    {%- endif %}\n\n    <p class=\"message paragraph-important\"\n       onclick='javascript:_fpa.next(\"profile-add-basics\");'>\n      <span class=\"icon-user\"></span> {{_(\"Basic Details\")}}\n      <span class=\"icon-signature-unknown fpa-basics-bad right hide\" style=\"padding: 5px; color: #ff5;\"\n            title=\"{{_('At least a name and e-mail are required!')}}\"></span>\n      <span class=\"icon-checkmark fpa-basics-ok right hide\" style=\"padding: 5px; color: #0d0;\"></span>\n    </p>\n    <div class=\"section profile-add-basics {% if ui_open and ui_open != 'basics' %}hide{% endif %}\"\n         style=\"position: relative;\">\n\n{%- if 0 %}{# TODO: Partner and make it possible to create e-mail accounts #}\n      {%- if not result.rid %}\n      <select class=\"right\" style=\"width: auto;\"\n              onchange=\"javascript:_fpa.select(this, 'basics');\">\n        <option value=\"old\" selected>{{_(\"Existing Account\")}}</option>\n        <option value=\"new\">{{_(\"New Address\")}}</option>\n      </select>\n      {% endif %}\n{%- endif %}\n\n      <div style=\"padding-right: 0em; width: 29em;\">\n        <label>{{_(\"Name\")}}</label>\n        <input type=\"text\" name=\"name\" style=\"width: 100%\"\n               value=\"{{ result.name }}\" placeholder=\"Ada Lovelace\">\n      </div>\n      <div class=\"basics old\">\n        <div class=\"left\" style=\"margin-right: 1em; width: 29em;\">\n          <label>{{_(\"E-mail\")}}</label>\n          <input type=\"text\" name=\"email\" style=\"width: 100%\"\n                 class='fpa-email fpa-basic-username' data-next=\"#basics-next\"\n                 placeholder=\"ada@example.com\" value=\"{{ result.email }}\"\n                 onchange=\"javascript:_fpa.email_changed();\">\n        </div>\n      </div>\n      <div class=\"basics new hide\">\n        <br><i>FIXME ... add signup with partners here!</i><br>\n      </div>\n      <div class=\"more-basics left{%- if not result.rid %} hide{% endif %}\"\n           style=\"width: 39em; padding: 0;\">\n        <label>{{_(\"Signature\")}}</label>\n        <textarea placeholder=\"{{_(\"Everyone needs a unique, witty signature!\")}}\"\n                  style=\"width: 100%; font-size: 0.85em;\"\n                  name=\"signature\">{{ result.signature }}</textarea>\n      </div>\n      <br clear=\"both\"><label>&nbsp;</label>\n      <div style=\"position: absolute; text-align: right; right: 0; bottom: 1.5em;\">\n        {%- if not result.rid %}\n        <a class=\"more-basics-show clickable\" onclick=\"javascript:_fpa.more('more-basics');\">\n          {{_(\"Add custom signature\")}} ...\n        </a> &nbsp;&nbsp;\n        {% endif %}\n        {%- if not result.sources %}\n        <input type=\"checkbox\" class=\"fpa-autoconfig\" value=\"yes\"\n               {%- if not result.rid %}checked{% endif %}>\n        <span class=\"checkbox\">\n          {{ _(\"Detect settings\") }}\n        </span> &nbsp;\n        {% endif %}\n        <button id=\"basics-next\" onclick='javascript:_fpa.basics_next();'\n                class=\"button button-secondary\" type=\"button\">{{_(\"Next\")}} ...</button>\n      </div>\n      <br clear=\"both\">\n    </div>\n\n    <div class=\"fpa-detection-in-progress hide\">\n      <p class=\"message paragraph-important\">\n        <span class=\"icon-robot\"></span> {{_(\"Auto-detecting settings\")}} ...\n      </p>\n      <div class=\"text-center\">\n      {% if config.sys.proxy.protocol in (\"tor\", \"tor-risky\") %}\n        <p>{{_(\"Connecting over Tor, this may take a while.\")}}</p>\n      {% endif %}\n        <p>{% include(\"../img/loading-ellipsis.svg\") %}</p>\n        <p><i class='fpa-detection-progress'></i></p>\n      </div>\n    </div>\n\n    <div class=\"fpa-network-settings\">\n      <div class=\"fpa-detection-failed hide\">\n        <p class=\"message paragraph-alert\">\n          <span class=\"icon-robot\"></span>\n          <b>{{_(\"Failed to detect settings, manual configuration required\")}}!</b>\n          <span class=\"description\" href=\"\"></span>\n        </p>\n        <p class=\"text-center\">\n          <a href=\"{{ U('/logs/network/') }}\" data-dismiss=\"modal\">\n             <span class=\"icon icon-work\"></span>\n             {{_(\"Troubleshoot recent Network Activity.\")}}\n          </a>\n        </p>\n      </div>\n      <p class=\"message paragraph-alert fpa-login-failed hide\">\n        <span class=\"icon-robot\"></span> <b>{{_(\"Failed to log in, check the username and password\")}}!</b>\n        <span class=\"description\" href=\"\"></span>\n      </p>\n      <p class=\"message paragraph-important\"\n         onclick='javascript:_fpa.next(\"profile-add-route\");'>\n        <span class=\"icon-outbox\"></span> {{_(\"Sending Mail\")}}\n        <span class=\"icon-checkmark fpa-route-ok right hide\" style=\"padding: 5px; color: #4f4;\"></span>\n      </p>\n      <div class=\"section profile-add-route {% if ui_open != 'route' %}hide{% endif %}\"\n           style=\"position: relative;\">\n        <select class=\"right\" name=\"route-protocol\" style=\"width: auto;\"\n                data-next=\"#next-route\"\n                onchange=\"javascript:_fpa.select(this, 'route-settings');\">\n          {%- set protocol = result['route-protocol'] %}\n          {%- for val, txt in (('smtp', 'SMTP'),\n                              ('smtpssl', 'SMTP/TLS'),\n                              ('smtptls', 'SMTP/STARTTLS'),\n                              ('local', _(\"Local\")),\n                              ('none', _(\"None\"))) %}\n          <option value=\"{{val}}\"{% if val == protocol %} selected\n                                 {%- endif %}>{{txt}}</option>\n          {%- endfor %}\n        </select>\n        <div class=\"route-settings smtp smtpssl smtptls\n                    {%- if protocol not in ('smtp', 'smtpssl', 'smtptls') %} hide{% endif %}\">\n          <div class=\"left\" style=\"margin-right: 1em; width: 14em;\">\n            <label>{{_(\"Host name\")}}</label>\n            <input type=\"text\" name=\"route-host\" value=\"{{ result['route-host'] }}\" placeholder=\"mail.server.com\">\n          </div>\n          <div class=\"left\" style=\"margin-right: 0; width: 10em;\">\n            <label>{{_(\"Port number\")}}</label>\n            <input type=\"text\" name=\"route-port\" value=\"{{ result['route-port'] }}\" placeholder=\"25, 465 or 587\">\n          </div>\n          <br clear=\"both\">\n          <div class=\"left\" style=\"margin-right: 1em; width: 14em;\">\n            <small class=\"right fpa-username-copied hide\">({{ _(\"copied\") }})</small>\n            <label>{{_(\"Username\")}}</label>\n            <input type=\"text\" name=\"route-username\" class=\"fpa-username\"\n                   onchange=\"javascript:_fpa.mark_copied(this, 'username');\"\n                   value=\"{{ result['route-username'] }}\"\n                   placeholder=\"you123\" style=\"margin-bottom: 0;\">\n            <p style=\"margin-top: 0; font-size: 0.7em; text-align: right;\"><i>\n              {{ _(\"Leave blank for no authentication\") }}\n            </i></p>\n          </div>\n          <div class=\"left\" style=\"margin-right: 1em; width: 14em;\">\n            {%- set auth_type = result['route-auth_type'][:8] %}\n            <label>{{_(\"Authentication\")}}</label>\n            <select name=\"route-auth_type\" style=\"width: 14em;\">\n              {%- for val, txt in (('password', _('Password')),\n                                   ('OAuth2', 'OAuth2'),\n                                   ('', _('None'))) %}\n              <option value=\"{{val}}\"{% if val == auth_type %} selected\n                                     {%- endif %}>{{txt}}</option>\n              {%- endfor %}\n            </select>\n          </div>\n\n          {%- set password = result['route-password'] %}\n          {%- if password %}\n          <div class=\"left\" style=\"margin-right: 0; width: 29em;\">\n            <input type=\"checkbox\" name=\"route-password\" value=\"\">\n            <span class=\"checkbox\">\n              {{_(\"Forget password\")}}\n            </span><br>\n          </div>\n          {%- endif %}\n        </div>\n        <div class=\"route-settings local\n                    {%- if protocol != 'local' %} hide{% endif %}\">\n          <div style=\"margin-right: 0; width: 25em;\">\n            <p><i>\n              {{_(\"Send mail using local Unix tools.\")}}\n              {{_(\"Use this setting if you have a working mail server on this machine.\")}}\n            </i></p>\n          </div>\n          <div class=\"left\" style=\"margin-right: 0; width: 29em;\">\n            <label>{{_(\"Shell command\")}}</label>\n            <input type=\"text\" name=\"route-command\" style=\"width: 100%;\"\n                   value=\"{{ result['route-command'] }}\"\n                   placeholder=\"- {{_('Leave blank to auto-detect')}} -\">\n          </div>\n        </div>\n        <div style=\"width: 70%;\" class=\"route-settings none\n                                        {%- if protocol != 'none' %} hide{% endif %}\">\n          <br>\n          <br>\n          <p class=\"text-center\"><i>\n            {{_(\"No outgoing mail for this account.\")}}\n          </i></p>\n          <br>\n        </div>\n        <br clear=\"both\">\n        <div style=\"position: absolute; right: 0; bottom: 1.5em;\">\n          <button onclick='javascript:_fpa.next(\"profile-add-source\");'\n                  class=\"button button-secondary\" type=\"button\">{{_(\"Next\")}} ...</button>\n        </div>\n        <br clear=\"both\">\n      </div>\n\n      <p class=\"message paragraph-important\"\n         onclick='javascript:_fpa.next(\"profile-add-source\");'>\n        <span class=\"icon-mailsource\"></span> {{_(\"Receiving Mail\")}}\n        <span class=\"icon-checkmark right hide\" style=\"padding: 5px; color: #5f5;\"></span>\n      </p>\n      <div class=\"section profile-add-source {% if ui_open != 'sources' %}hide{% endif %}\"\n           style=\"position: relative;\">\n{% macro source_editor(rid, new_rid) %}\n        {% set protocol = result['source-' + rid + '-protocol'] %}\n        <select class=\"right\" style=\"width: auto;\"\n                onchange=\"javascript:_fpa.select(this, 'source-settings-{{ new_rid }}');\"\n                name=\"source-{{ new_rid }}-protocol\">\n          {% for val, txt in (('imap', 'IMAP'),\n                              ('imap_ssl', 'IMAP/TLS'),\n                              ('pop3', 'POP3'),\n                              ('pop3_ssl', 'POP3/TLS'),\n                              ('spool', _(\"Mail spool\")),\n                              ('local', _(\"Local files\")),\n                              ('none', _(\"None\"))) %}\n           {%- if rid != new_rid or val[:4] == protocol[:4] %}\n            <option value=\"{{val}}\"\n                    {%- if val == protocol or\n                           (val == \"imap\" and protocol == \"imap_tls\") %}\n                    selected{% endif %}>{{txt}}</option>\n           {%- endif %}\n          {%- endfor %}\n        </select>\n\n        <div class=\"source-settings-{{ new_rid }} imap imap_ssl pop3 pop3_ssl\n                    {%- if protocol[:4] not in ('imap', 'pop3') %} hide{% endif %}\">\n          <div class=\"left\" style=\"margin-right: 1em; width: 14em;\">\n            <label>{{_(\"Host name\")}}</label>\n            <input type=\"text\" name=\"source-{{ new_rid }}-host\"\n                   value=\"{{ result['source-' + rid + '-host'] }}\"\n                   placeholder=\"mail.server.com\">\n          </div>\n          <div class=\"left\" style=\"margin-right: 0; width: 13em;\">\n            <label>{{_(\"Port number\")}}</label>\n            <input type=\"text\" name=\"source-{{ new_rid }}-port\"\n                   value=\"{{ result['source-' + rid + '-port'] }}\"\n                   placeholder=\"110, 143, 993 or 995\">\n          </div>\n          <br clear=\"both\">\n          <div class=\"left\" style=\"margin-right: 1em; width: 14em;\">\n            <small class=\"right fpa-username-copied hide\">({{ _(\"copied\") }})</small>\n            <label>{{_(\"Username\")}}</label>\n            <input type=\"text\" name=\"source-{{ new_rid }}-username\" class=\"fpa-username\"\n                   onchange=\"javascript:_fpa.mark_copied(this, 'username');\"\n                   value=\"{{ result['source-' + rid + '-username'] }}\"\n                   placeholder=\"you123\">\n          </div>\n          <div class=\"left\" style=\"margin-right: 1em; width: 14em;\">\n            {%- set auth_type = result['source-' + rid + '-auth_type'] %}\n            <label>{{_(\"Authentication\")}}</label>\n            <select name=\"source-{{ new_rid }}-auth_type\" style=\"width: 14em;\">\n              {%- for val, txt in (('password', _('Password')),\n                                   ('OAuth2', 'OAuth2')) %}\n              <option value=\"{{val}}\"{% if val == auth_type %} selected\n                                     {%- endif %}>{{txt}}</option>\n              {%- endfor %}\n            </select>\n          </div>\n          <div class=\"left\" style=\"margin-right: 0; width: 29em;\">\n          {%- set password = result['source-' + rid + '-password'] %}\n          {%- if password %}\n            <input type=\"checkbox\" name=\"source-{{ new_rid }}-password\" value=\"\">\n            <span class=\"checkbox\">\n              {{_(\"Forget password\")}}\n            </span><br>\n          {%- endif %}\n\n            <input type=\"checkbox\" name=\"source-{{ new_rid }}-leave-on-server\" value=\"yes\"\n                   {% if result['source-' + rid + '-leave-on-server'] %}checked{% endif %}>\n            <span class=\"checkbox\">\n              {{_(\"Leave mail on server\")}}\n            </span><br>\n\n            <input type=\"checkbox\" name=\"source-{{ new_rid }}-index-all-mail\" value=\"yes\"\n                   {% if result['source-' + rid + '-index-all-mail'] %}checked{% endif %}>\n            <span class=\"checkbox\">\n              {{_(\"Copy all mail and add to search engine\")}}\n            </span>\n\n            <span class=\"source-settings-{{ new_rid }}\n                         {%- if protocol not in ('imap', 'imap_tls') %} hide{% endif %}\n                         imap\">\n              <br>\n              <input type=\"checkbox\" name=\"source-{{ new_rid }}-force-starttls\" value=\"yes\"\n                     {% if result['source-' + rid + '-force-starttls'] or\n                           result['source-' + rid + '-protocol'] == 'imap_tls'\n                           %}checked{% endif %}>\n              <span class=\"checkbox\">\n                {{_(\"Require STARTTLS encryption\")}}\n              </span>\n            </span>\n\n            <div class='edit-provider-settings hide'><br>\n              <a target=_blank class='edit-provider-settings button-secondary'>\n                <span class=\"icon-settings\"></span> {{_(\"Enable IMAP\")}}\n              </a>\n            </div>\n          </div>\n        </div>\n\n        <div class=\"source-settings-{{ new_rid }} spool\n                    {%- if protocol[:4] != 'spool' %} hide{% endif %}\">\n          <div class=\"source-settings-{{ new_rid }} spool left\" style=\"margin-right: 0; width: 29em;\">\n            <p><i>\n              {{_(\"Receive mail from local Unix mail spool.\")}}\n              {{_(\"Use this setting if you have a working mail server on this machine.\")}}\n            </i></p>\n          </div>\n          <div class=\"left\" style=\"margin-right: 0; width: 29em;\">\n            <input type=\"checkbox\" name=\"source-{{ new_rid }}-copy-local\" value=\"yes\"\n                   onchange='javascript:_fpa.requiredby(this, \"source-{{ new_rid }}-delete-source\");'\n                   {% if result['source-' + rid + '-copy-local'] %}checked{% endif %}>\n            <span class=\"checkbox\">\n              {{_(\"Copy mail to Mailpile secure storage\")}}\n            </span><br>\n            <input type=\"checkbox\" name=\"source-{{ new_rid }}-delete-source\" value=\"yes\"\n                   onchange='javascript:_fpa.require(this, \"source-{{ new_rid }}-copy-local\");'\n                   {% if result['source-' + rid + '-delete-source'] %}checked{% endif %}>\n            <span class=\"checkbox\">\n              {{_(\"Delete from Unix mail spool\")}} ({{_(\"after copying\")}})\n            </span>\n          </div>\n        </div>\n\n        <div class=\"source-settings-{{ new_rid }} none local\n             {%- if protocol not in ('none', 'local') %} hide{% endif %}\"\n             style=\"width: 70%;\">\n          <div class=\"source-settings-{{ new_rid }} none left\n               {%- if protocol[:4] != 'none' %} hide{% endif %}\"\n               style=\"margin-right: 0; width: 29em;\">\n            <br><br>\n            <p class=\"text-center\"><i>\n            {%- if result.sources %}\n              {{_(\"Choose a protocol for the new mail source...\")}}\n            {%- else %}\n              {{_(\"No incoming mail for this account.\")}}\n            {%- endif %}\n            </i></p>\n            <br>\n          </div>\n          <div class=\"source-settings-{{ new_rid }} local left\n               {%- if protocol != 'local' %} hide{% endif %}\"\n               style=\"margin-right: 0; width: 29em;\">\n            <p><i>\n              {{_(\"Use this setting if you would like Mailpile to read e-mails already downloaded by Thunderbird, Mac Mail or another local application on this machine.\")}}\n            </i></p>\n            <p>\n              {{_(\"Use the Browse tool to import local mailboxes later on.\")}}\n            </p>\n          </div>\n        </div>\n\n        {%- if rid == new_rid %}\n        <div class=\"source-settings-{{ new_rid }}\n                    imap imap_ssl pop3 pop3_ssl local spool\n                    {%- if protocol[:4] == 'none' %} hide{% endif %}\">\n          <br clear=\"both\">\n          <input type=\"checkbox\" name=\"source-{{ new_rid }}-enabled\" value=\"yes\"\n                 onchange='javascript:_fpa.require(this, \"source-{{ new_rid }}-copy-local\");'\n                 {% if result['source-' + rid + '-enabled'] %}checked{% endif %}>\n          <span class=\"checkbox\">\n            {{_(\"Enable this mail source\")}}\n          </span>\n        </div>\n        {% else %}\n        <input type=\"hidden\" name=\"source-{{ new_rid }}-enabled\" value=\"yes\">\n        {% endif %}\n{%- endmacro %}\n        {%- for sid in result.sources %}\n\n          {%- if not loop.first %}<br clear=\"both\"><hr style=\"margin: 1em 0 5px 0;\">{% endif %}\n          {{- source_editor(sid, sid) }}\n        {%- endfor %}\n        <div class=\"source-add-new{% if result.sources %} hide{% endif %}\">\n          {%- if result.sources %}<br clear=\"both\"><hr style=\"margin: 1em 0 5px 0;\">{% endif %}\n          {{- source_editor('NEW', new_src_id) }}\n        </div>\n        <br clear=\"both\">\n        <div style=\"position: absolute; right: 0; bottom: 1.5em;\">\n          {% if result.sources %}\n          <input type=\"checkbox\" class=\"fpa-add-new-source\" value=\"yes\"\n                 style=\"padding-right: 0;\"\n                 onchange=\"javascript:$('.source-add-new').toggle();\">\n          <span class=\"checkbox\">{{ _(\"Add New\") }} &nbsp; </span>\n          {% endif %}\n          <button onclick='javascript:_fpa.next(\"profile-add-security\");'\n                  class=\"button button-secondary\" type=\"button\">{{_(\"Next\")}} ...</button>\n        </div>\n        <br clear=\"both\">\n      </div>\n    </div>\n\n    {% set enable_imap_pop3_message = _(\"Mailpile may not be able to access your mail unless you log on to your account and enable IMAP and/or POP3.\") %}\n    <div class='section fpa-warning hide' style='position: relative'>\n      <p class=\"message paragraph-alert\">\n        <span class=\"icon-settings\"></span> <b>{{_(\"Important\")}}!</b>\n        <span class=\"description\" href=\"\"></span>\n      </p>\n      <p>\n        {{ enable_imap_pop3_message }}\n        {{_(\"Without this, some providers will even mistake Mailpile for an intruder!\")}}\n      </p>\n      <ul style=\"list-style: disc; margin-left: 2em;\" class=\"docs\"></ul>\n      <p>\n        <a target=_blank class='edit-provider-settings button-secondary right'\n           onclick=\"javascript:$(this).removeClass('button-secondary').addClass('button-info');\n                               $('#fpa-gotit').removeClass('button-info').addClass('button-secondary');\">\n          <span class=\"icon-settings\"></span> {{_(\"Enable IMAP\")}}\n        </a>\n        <br clear=\"both\">\n      </p>\n      <div style=\"position: absolute; left: 0; bottom: 0;\">\n        <button onclick='javascript:_fpa.next(\"profile-add-security\");'\n                class=\"button-info\" id=\"fpa-gotit\" type=\"button\">{{_(\"Got it\")}} ...</button>\n      </div>\n    </div>\n\n    <p class=\"message paragraph-important\"\n       onclick='javascript:_fpa.next(\"profile-add-security\");'>\n      <span class=\"icon-lock-closed\"></span> {{_(\"Security and Privacy\")}}\n      <span class=\"icon-checkmark right hide\" style=\"padding: 5px; color: #5f5;\"></span>\n    </p>\n    <div class=\"section profile-add-security  {% if ui_open != 'security' %}hide{% endif %}\"\n         style=\"position: relative;\">\n      <div class=\"left\" style=\"margin-right: 0; width: 100%;\">\n        <label>{{_(\"Encryption key\")}}</label>\n        <select class='fpa-pgp-key' style=\"width: 100%\"\n                onchange=\"javascript:_fpa.select(this, 'security-opt');\"\n                name=\"security-pgp-key\">\n          <option value=\"!CREATE:CURVE25519\" class=\"fpa-pgp-key-default\">{{_(\"Create a new Autocrypt Level 1.1 compatible key (Ed25519)\")}}</option>\n        {%- set pgp_keys = \n             mailpile('crypto/gpg/keylist/secret','--usable').result.values() |\n                             sort(reverse=True, attribute='creation_date') -%}\n        {%- for key in pgp_keys -%}\n          {%- set fingerprint = key['fingerprint'] -%}\n          {%- for uid in key.uids %}\n          <option value=\"{{fingerprint}}\" data-uid=\"{{ uid.email }}\"\n                  {%- if (fingerprint == result['security-pgp-key']) and (uid.email == result.email) %} selected{% endif %}\n                  {%- if (uid.email != result.email) %} class=\"hide\"{% endif %}>\n            {{key.creation_date}}/{{key.keytype_name}}{{key.keysize}}:\n            {{uid.name}} &lt;{{uid.email}}&gt;\n            ({% if uid.comment %}{{uid.comment}}{% else %}0x{{ fingerprint[-8:] }}{% endif %})\n          </option>\n          {%- endfor %}\n        {%- endfor %}\n          <option value=\"!CREATE:RSA4096\">{{_(\"Create a new 4096 bit RSA key\")}} ({{_(\"Legacy, strong\")}})</option>\n          <option value=\"!CREATE:RSA4096\">{{_(\"Create a new 3072 bit RSA key\")}} ({{_(\"Autocrypt Level 1\")}})</option>\n          <option {% if not result['security-pgp-key'] %}selected {% endif -%}\n                  value=\"\">{{_(\"Disable encryption for this account\")}}</option>\n        </select>\n        <div class=\"security-opt any text-right\n             {%- if not result['security-pgp-key'] %} hide{% endif %}\"\n             style=\"margin: -13px 0 13px 0;\">\n          <a class=\"more-crypto-show\" onclick=\"javascript:_fpa.more('more-crypto');\">\n            {{_(\"Show too many encryption settings\")}}\n          </a>\n        </div>\n      </div>\n      <div class=\"security-opt any left\n           {%- if not result['security-pgp-key'] %} hide{% endif %}\"\n           style=\"margin-right: 0; width: 29em;\">\n        <div class=\"more-crypto {% if not result['security-best-effort-crypto'] %}hide{% endif %}\">\n          <input type=\"checkbox\" name=\"security-best-effort-crypto\" value=\"yes\"\n                 onchange='javascript:_fpa.exclude(this, \"security-autocrypt-crypto\", \"security-always-sign\", \"security-always-encrypt\", \"security-obscure-metadata\", \"security-prefer-inline\");'\n                 {% if result['security-best-effort-crypto'] %}checked{% endif %}>\n          <span class=\"checkbox\">\n            {{_(\"Sign and encrypt to frequent OpenPGP users\")}}\n          </span>\n          - <a target=_blank href='https://autocrypt.org/'><i>{{_(\"learn more\")}}</i></a>\n        </div>\n        <div class=\"more-crypto {% if not result['security-autocrypt-crypto'] %}hide{% endif %}\">\n          <input type=\"checkbox\" name=\"security-autocrypt-crypto\" value=\"yes\"\n                 onchange='javascript:{_fpa.exclude(this, \"security-best-effort-crypto\", \"security-always-encrypt\", \"security-always-sign\", \"security-obscure-metadata\", \"security-prefer-inline\", \"security-openpgp-header-none\", \"security-attach-keys\");\n                                       _fpa.require(this, \"security-use-autocrypt\", \"security-openpgp-header-sign\", \"security-openpgp-header-encrypt\");}'\n                 {% if result['security-autocrypt-crypto'] %}checked{% endif %}>\n          <span class=\"checkbox\">\n            {{_(\"Autocrypt Level 1.1 encryption policy\")}}\n          </span>\n          - <a target=_blank href='https://autocrypt.org/'><i>{{_(\"learn more\")}}</i></a>\n        </div>\n        <div class=\"more-crypto {% if not result['security-always-sign'] %}hide{% endif %}\">\n          <input type=\"checkbox\" name=\"security-always-sign\" value=\"yes\"\n                 onchange='javascript:_fpa.exclude(this, \"security-autocrypt-crypto\", \"security-best-effort-crypto\");'\n                 {% if result['security-always-sign'] %}checked{% endif %}>\n          <span class=\"checkbox\">\n            {{_(\"Always digitally sign outgoing mail\")}}\n          </span>\n        </div>\n        <div class=\"more-crypto {% if not result['security-always-encrypt'] %}hide{% endif %}\">\n          <input type=\"checkbox\" name=\"security-always-encrypt\" value=\"yes\"\n                 onchange='javascript:_fpa.exclude(this, \"security-autocrypt-crypto\", \"security-best-effort-crypto\");\n                                      _fpa.require(this, \"security-always-sign\");'\n                 {% if result['security-always-encrypt'] %}checked{% endif %}>\n          <span class=\"checkbox\">\n            {{_(\"Always encrypt (warn when sending unencrypted mail)\")}}\n          </span>\n        </div>\n        <div class=\"more-crypto {% if not result['security-obscure-metadata'] %}hide{% endif %}\">\n          <input type=\"checkbox\" name=\"security-obscure-metadata\" value=\"yes\"\n                 onchange='javascript:_fpa.exclude(this, \"security-prefer-inline\");'\n                 {% if result['security-obscure-metadata'] %}checked{% endif %}>\n          <span class=\"checkbox\">\n            {{_(\"Minimize metadata (may make mail unreadable)\")}}\n          </span>\n        </div>\n        <div class=\"more-crypto {% if not result['security-prefer-inline'] %}hide{% endif %}\">\n          <input type=\"checkbox\" name=\"security-prefer-inline\" value=\"yes\"\n                 onchange='javascript:_fpa.exclude(this, \"security-autocrypt-crypto\", \"security-obscure-metadata\", \"security-prefer-pgpmime\");'\n                 {% if result['security-prefer-inline'] %}checked{% endif %}>\n          <span class=\"checkbox\">\n            {{_(\"Prefer compatibility; avoid PGP/MIME (makes mail ugly)\")}}\n          </span>\n        </div>\n{% if config.web.developer_mode %}\n        <div class=\"more-crypto {% if not result['security-prefer-pgpmime'] %}hide{% endif %}\">\n          <input type=\"checkbox\" name=\"security-prefer-pgpmime\" value=\"yes\"\n                 onchange='javascript:_fpa.exclude(this, \"security-prefer-inline\");'\n                 {% if result['security-prefer-pgpmime'] %}checked{% endif %}>\n          <span class=\"checkbox\">\n            {{_(\"Prefer PGP/MIME\")}}\n          </span>\n        </div>\n{% endif %}\n        <div style=\"margin-top: 0.7em\" class=\"more-crypto hide\"></div>\n        <div class=\"more-crypto {% if not result['security-openpgp-header-encrypt'] %}hide{% endif %}\">\n          <input type=\"checkbox\" name=\"security-openpgp-header-encrypt\" value=\"yes\"\n                 onchange='javascript:_fpa.exclude(this, \"security-openpgp-header-none\");\n                                      _fpa.require(this, \"security-openpgp-header-sign\");'\n                 {% if result['security-openpgp-header-encrypt'] %}checked{% endif %}>\n          <span class=\"checkbox\">\n            {{_(\"Signal a preference for encrypted mail\")}}\n          </span>\n        </div>\n        <div class=\"more-crypto {% if not result['security-openpgp-header-sign'] %}hide{% endif %}\">\n          <input type=\"checkbox\" name=\"security-openpgp-header-sign\" value=\"yes\"\n                 onchange='javascript:_fpa.exclude(this, \"security-openpgp-header-none\");'\n                 {% if result['security-openpgp-header-sign'] %}checked{% endif %}>\n          <span class=\"checkbox\">\n            {{_(\"Signal a preference for signed mail\")}}\n          </span>\n        </div>\n        <div class=\"more-crypto {% if not result['security-openpgp-header-none'] %}hide{% endif %}\">\n          <input type=\"checkbox\" name=\"security-openpgp-header-none\" value=\"yes\"\n                 onchange='javascript:_fpa.exclude(this, \"security-openpgp-header-sign\", \"security-openpgp-header-encrypt\");'\n                 {% if result['security-openpgp-header-none'] %}checked{% endif %}>\n          <span class=\"checkbox\">\n            {{_(\"Signal a preference for un-signed, un-encrypted mail\")}}\n          </span>\n        </div>\n        <div style=\"margin-top: 0.7em\" class=\"more-crypto hide\"></div>\n        <div class=\"more-crypto {% if not result['security-use-autocrypt'] %}hide{% endif %}\">\n          <input type=\"checkbox\" name=\"security-use-autocrypt\" value=\"yes\"\n                 onchange='javascript:_fpa.exclude(this, \"security-attach-keys\");'\n                 {% if result['security-use-autocrypt'] %}checked{% endif %}>\n          <span class=\"checkbox\">\n            {{_(\"Use Autocrypt to exchange encryption keys\")}}\n          </span>\n          - <a target=_blank href='https://autocrypt.org/'><i>{{_(\"learn more\")}}</i></a>\n        </div>\n        <div class=\"more-crypto {% if not result['security-attach-keys'] %}hide{% endif %}\">\n          <input type=\"checkbox\" name=\"security-attach-keys\" value=\"yes\"\n                 onchange='javascript:_fpa.exclude(this, \"security-autocrypt-crypto\", \"security-use-autocrypt\");'\n                 {% if result['security-attach-keys'] %}checked{% endif %}>\n          <span class=\"checkbox\">\n            {{_(\"Use attachments to exchange encryption keys\")}}\n          </span>\n        </div>\n{# FIXME - make this work!\n        <div class=\"more-crypto {% if not result['security-publish-to-keyserver'] %}hide{% endif %}\">\n          <input type=\"checkbox\" name=\"security-publish-to-keyserver\" value=\"yes\"\n                 {% if result['security-publish-to-keyserver'] %}checked{% endif %}>\n          <span class=\"checkbox\">\n            {{_(\"Upload key to public directory (key server)\")}}\n          </span>\n        </div>\n#}\n      </div>\n    </div>\n\n    <br clear=\"both\">\n    <button type=\"submit\" id=\"fpa-submit\" class=\"button-primary right{%- if not result.rid %} hide{% endif %}\">\n      <span class=\"icon {{ form_icon or \"icon-plus\"}}\"></span>\n      {{ form_action or _(\"Add\")}}\n    </button>\n  </form>\n"
  },
  {
    "path": "shared-data/default-theme/html/profiles/add/index.html",
    "content": "{%- extends \"layouts/\" + render_mode + \".html\" %}\n{%- block title %}{{_(\"Add Account\")}}{% endblock %}\n{%- block content %}\n{%- if result.form %}\n<div class=\"content-normal\" style=\"max-width: 42em\">\n  <h1><span class=\"icon-user\"></span> {{_(\"Create a new Account\")}}</h1>\n  {% include \"profiles/account-form.html\" %}\n</div>\n{%- else %}\n  {{ mailpile('http/redirect', U('/profiles/')) }}\n{%- endif %}\n{%- endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/profiles/edit/index.html",
    "content": "{%- extends \"layouts/\" + render_mode + \".html\" %}\n{%- block title %}{{_(\"Edit Account\")}}{% endblock %}\n{%- block content %}\n{%- if result.form %}\n<div class=\"content-normal\" style=\"max-width: 42em\">\n  <h1><span class=\"icon-user\"></span> {{_(\"Edit your Account\")}}</h1>\n  {% set form_icon = \"icon-checkmark\" %}\n  {% set form_action = _(\"Save\") %}\n  {% include \"profiles/account-form.html\" %}\n</div>\n{%- else %}\n  {{ mailpile('http/redirect', U('/profiles/')) }}\n{%- endif %}\n{%- endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/profiles/index.html",
    "content": "{%- extends \"layouts/\" + render_mode + \".html\" %}\n{%- block head %}\n  <style type='text/css'>\n    .unconfigured.icon {color: #aaa;}\n    .configured.icon {color: #070;}\n    .misconfigured.icon {color: #700;}\n  </style>\n{%- endblock %}\n{%- block title %}{{_(\"Welcome Home\")}}{% endblock %}\n\n{%- block contenttools %}\n  {%- if is_configured() %}\n    {%- set activities = [{\n      'name': 'browser',\n      'icon': 'list',\n      'url': U('/browse/'),\n      'text': _(\"Browse\"),\n      'description': _(\"Browse Files and Mailboxes\")\n    },{\n      'name': 'addprofile',\n      'icon': 'plus',\n      'aclass': 'auto-modal',\n      'url': U('/profiles/add/'),\n      'text': _(\"Add Account\"),\n      'description': _(\"Create a new Account\")\n    }] %}\n    {%- set selection_initial_prompt = _(\"Configure your accounts, profiles and other settings\") %}\n    {%- include(\"partials/tools_default.html\") %}\n  {% else %}\n    {%- set activities = [{\n      'name': 'settings',\n      'icon': 'settings',\n      'url': U('/settings/privacy.html?ui_from_profiles=True'),\n      'text': _(\"Privacy\"),\n      'description': _(\"Security and Privacy Settings\")\n    }] %}\n    {%- set selection_initial_prompt = _(\"You can configure accounts and profiles once you have reviewed your privacy settings.\") %}\n    {%- include(\"partials/tools_default.html\") %}\n  {% endif %}\n{%- endblock %}\n\n{%- block content %}\n<div id=\"page\" class=\"content-normal\">\n\n  <h1 class=\"page-title-data mobile-hide\">\n    <span class=\"icon icon-home\"></span>\n    {{_(\"Welcome Home\")}}\n  </h1>\n\n{%- if result.profiles|length > 0 %}\n  <section class=\"account-list\">\n      <h3>{{_(\"E-mail Accounts\")}}</h3>\n      <table>\n        <tr>\n          <th colspan=\"2\"></th>\n          <th>{{_(\"Your name\")}}</th>\n          <th>{{_(\"E-mail address\")}}</th>\n          <th style=\"text-align: right; padding-left: 1.5em;\"><span class=\"icon icon-new\"></span> {{_(\"New\")}}</th>\n          <th style=\"text-align: right\"><span class=\"icon icon-logo\"></span> {{_(\"Total\")}}</th>\n          <th style=\"text-align: right; padding-left: 1.5em;\"><span class=\"icon icon-settings\"></span> {{_(\"Settings\")}}</th>\n          <th></th>\n        </tr>\n        {%- if result.loading or not result.loaded %}\n        <tr class=\"message\">\n            <td colspan=\"9\">\n                <div class=\"text-center\">\n                    <p style=\"margin: 1em; padding: 0;\">\n                        <i>{{_(\"Still loading profiles and contacts, please wait...\")}}</i>\n                    </p>\n                    <script type=\"text/javascript\">\n                    setTimeout('window.location.href=window.location.href', 5000);\n                    </script>\n                </div>\n            </td>\n        </tr>\n        {%- elif result.rids %}\n        {%- for profile_rid in result.rids %}\n         {%- set p = result.profiles[result.rids[profile_rid]] %}\n         {%- if 'x-mailpile-profile-tag' in p and p['x-mailpile-profile-tag'] in config.tags %}\n           {%- set t = mailpile('tags', config.tags[p['x-mailpile-profile-tag']].slug).result.tags.0 %}\n         {%- else %}\n           {%- set t = {'slug': 'all-mail', 'stats': {'new': '', 'all': ''}} %}\n         {%- endif %}\n         {%- if 'x-mailpile-profile-route' in p %}\n          {%- set route = config.routes[p['x-mailpile-profile-route']] %}\n         {%- else %}\n          {%- set route = '' %}\n         {%- endif %}\n        <tr valign=\"top\" class=\"message\">\n          <td id=\"compose-message\">\n            {%- if route %}\n            <a class=\"button-compose\" title=\"Compose as {{ p.email.0.email }}\"\n                href=\"/message/compose/?from={{ p.email.0.email }}\">\n              <span class=\"icon icon-compose\"></span>\n            </a>\n            {%- endif %}\n          </td>\n          <td id=\"photo\" style=\"padding: 0; margin: 0;\">\n            {%- if p.photo %}\n            <img width=\"20\" height=\"20\" style=\"margin: 0 5px -4px 5px;\"\n                 src=\"data:{{ p.photo.0.photo }}\">\n            {%- endif %}\n          </td>\n          <td id=\"first-name\">\n            <a href=\"{{ U('/in/inbox/?q=in:', t.slug) }}\">\n              {{ p.fn }}\n            </a>\n          </td>\n          <td id=\"email-address\">{% for e in p.email %}\n            {{ e.email }}{% if not loop.last %}<br>{% endif %}\n          {%- endfor %}</td>\n          <td id=\"stats-new\">\n            <span class=\"mobile-inline icon icon-new\"></span>\n            <a href=\"{{ U('/in/', t.slug, '/?q=is:unread') }}\"><b>{{ friendly_number(t.stats.new) }}</b></a>\n          </td>\n          <td id=\"stats-all\">\n            <span class=\"mobile-inline icon icon-logo\"></span>\n            <a href=\"{{ U('/in/', t.slug, '/') }}\">{{ friendly_number(t.stats.all) }}</a>\n           </td>\n           <td id=\"settings-actions\">\n             {%- if not (p['x-mailpile-profile-source'] or []) %}\n             <a class=\"auto-modal\" data-flags=\"reload\" data-icon=\"icon-mailsource\"\n                title=\"{{_('Configure Incoming Mail')}}\"\n                href=\"{{ U('/profiles/edit/?rid=', profile_rid, '&ui_open=sources') }}\">\n               <span class=\"unconfigured icon icon-mailsource\"></span>\n             </a>\n             {%- endif %}\n             {%- for ms in (p['x-mailpile-profile-source'] or []) %}\n               {%- set source_id = ms['profile-source'] %}\n               {%- set source = config.sources[source_id] %}\n             <a class=\"source-{{source_id}} auto-modal\" data-flags=\"reload\" data-icon=\"icon-mailsource\"\n                data-title=\"{{_('Incoming Mail:')}} {{source.name}}\"\n                title=\"{{_('Incoming Mail:')}} {{source.name}}\"\n                href=\"{{ U('/profiles/edit/?rid=', profile_rid, '&ui_open=sources') }}\">\n               <span class=\"configured icon icon-mailsource\"></span>\n             </a>\n             {%- endfor %}\n             <a class=\"auto-modal\" data-flags=\"reload\" data-icon=\"icon-outbox\"\n                title=\"{{_('Outgoing Mail Settings')}}\"\n                href=\"{{ U('/profiles/edit/?rid=', profile_rid, '&ui_open=route') }}\">\n               <span class=\"{% if not route %}un{% endif %}configured icon icon-outbox\"></span>\n             </a>\n\n             <a class=\"auto-modal\" data-flags=\"reload\" data-icon=\"icon-lock-closed\"\n                title=\"{{_(\"Security Settings\")}}\"\n                href=\"{{ U('/profiles/edit/?rid=', profile_rid, '&ui_open=security') }}\">\n               <span class=\"profile-{{profile_rid}}-key\n                            {% if p.key %}configured icon icon-lock-closed\n                            {% else %}unconfigured icon icon-lock-open{% endif %}\"></span>\n             </a>\n\n             <a class=\"auto-modal\" data-flags=\"reload\" data-icon=\"icon-user\"\n                title=\"{{_(\"Edit Profile\")}}\"\n                        href=\"{{ U('/profiles/edit/?rid=', profile_rid) }}\">\n                       <span class=\"icon icon-user\" style=\"color: #070;\"></span>\n             </a>\n           </td>\n           <td id=\"delete-account\">\n             <a class=\"auto-modal\" data-flags=\"reload\" data-icon=\"icon-trash\"\n                title=\"{{_(\"Remove Account\")}}\"\n                href=\"{{ U('/profiles/remove/?rid=', profile_rid) }}\">\n               <span class=\"icon icon-trash\" style=\"color: #750;\"></span>\n             </a>\n           </td>\n        </tr>\n        {%- endfor %}\n      {% endif %}\n      </table>\n    </section>\n      {% elif not is_configured() %}\n      <section class=\"message text-center\">\n        <br>\n        <p><i>{{_(\"Please review your privacy settings before adding any accounts.\")}}</i></p>\n        <p><a title=\"{{_(\"Security and Privacy Settings\")}}\"\n              href=\"{{ U('/settings/privacy.html?ui_from_profiles=True') }}\"><button type=button class=\"button-secondary\">\n          <span class=\"icon icon-settings\"></span> {{_(\"Privacy & Security\")}}\n        </button></a></p>\n    </section>\n      {%- elif result.profiles|length < 1 %}{# > #}\n      <section class=\"message text-center\">\n        <br>\n        <p><i>{{_(\"You have not configured any accounts yet.\")}}</i></p>\n        <p><a class=\"auto-modal\" data-flags=\"reload\" data-icon=\"icon-user\"\n              title=\"{{_(\"Create a new Account\")}}\"\n              href=\"{{ U('/profiles/add/') }}\"><button type=button class=\"button-primary\">\n          <span class=\"icon icon-plus\"></span> {{_(\"Add Account\")}}\n        </button></a></p>\n    </section>\n      {%- endif %}\n\n  {% set motd = mailpile('motd', '--noupdate').result %}\n  {% if motd._motd %}\n  <section class=\"motd {%- if motd.timestamp > config.timestamp %} recent{% endif %}\">\n    <h3>\n      {{_(\"Message Of The Day, %(date)s\", date=motd.timestamp|friendly_datetime)}}\n    </h3>\n    <p class=\"motd\">\n      {{ motd._motd|to_br }}\n    {% if motd.source %}\n      <a class=\"motd-signed\" target=_blank href=\"{{ motd.source }}\">\n        - {{ motd.signed }}\n      </a>\n    {% endif %}\n    </p>\n    <p class=\"version-info\">\n      {{_(\"This is Mailpile version %(version)s\", version=config.version)}}:\n      <i {% if config.version|string() != motd.latest_version|string() -%}\n         class=\"updated\"{% endif %}>{{ motd._version_info }}</i>.\n    </p>\n  </section>\n  {% endif %}\n\n</div>\n<div class=\"clearfix\"></div>\n{% if config.web.release_notes %}\n<script type='text/javascript'>\n  $(document).ready(function() {\n    $('.bulk-action-relnotes').click();\n  });\n</script>\n{% endif %}\n<script type='text/javascript'>\n  Mailpile.API.logs_events_get({incomplete: true}, EventLog.invoke_callbacks);\n</script>\n{%- endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/profiles/remove/index.html",
    "content": "{%- extends \"layouts/\" + render_mode + \".html\" %}\n{%- block title %}{{_(\"Remove Account\")}}{% endblock %}\n{%- block content %}\n{%- if result and result.form and result.rid %}\n<div class=\"content-normal\" style=\"max-width: 42em\">\n  <h1><span class=\"icon-trash\"></span> {{_(\"Remove Account\")}}</h1>\n  <form method=\"POST\" action=\"{{ U('/profiles/remove/') }}\">{{ csrf_field|safe }}\n    <input type=\"hidden\" name=\"rid\" value=\"{{ result.rid }}\">\n    <p>\n      {{ _(\"You are about to remove this account from your Mailpile\") }}:\n    </p>\n    <ul style=\"padding: 0; border: 1px solid #aaa;\">\n      <li class=\"account-info text-left\" style=\"overflow: hidden; margin: 0;\">\n        <label class=\"radio-list-item\">\n          <div class=\"text-center\" style=\"font-size: 54px; width: 20%; padding-right: 0;\">\n            {%- if result.profile.photo %}\n            <img width=\"64\" height=\"64\"\n                 style=\"width: 64px; height: 64px;\"\n                 src=\"data:{{ result.profile.photo.0.photo }}\">\n            {%- else %}\n            <span class=\"icon icon-user\"></span>\n            {%- endif %}\n          </div>\n          <div style=\"white-space: nowrap; font-size: 0.9em; width: 40%;\">\n            <span style=\"font-size: 1.3em; line-height: 1.5em;\">\n              {{ result.profile.fn }}\n            </span>\n            {%- for email in result.profile.email %}\n              <br><span class=\"icon icon-inbox\"></span>\n              {{ email.email }}\n            {%- endfor %}\n            {%- for key in result.profile.key %}\n              <br><span class=\"icon icon-lock-closed\"></span>\n              0x{{ key.key.split(',')[1][-8:] }}\n            {%- endfor %}\n          </div>\n          <div style=\"white-space: nowrap; font-size: 0.9em;\">\n            {%- if result.profile['x-mailpile-profile-route'] %}\n              {%- set route = config.routes[result.profile['x-mailpile-profile-route']] %}\n              <br><span class=\"icon icon-outbox\"></span>\n              {%- if route.protocol == 'local' %}\n                {{ _('Unix shell') }}\n              {%- else %}\n                {{ route.host }}:{{ route.port }}\n              {%- endif %}\n            {%- endif %}\n            {%- for s in result.profile['x-mailpile-profile-source'] %}\n              {%- set source = config.sources[s['profile-source']] %}\n              <br><span class=\"icon icon-mailsource\"></span>\n              {%- if source.protocol == 'local' %}\n                {{ _('Local files') }}\n              {%- else %}\n                {{ source.host }}:{{ source.port }}\n              {%- endif %}\n            {%- endfor %}\n            {%- if 'x-mailpile-profile-tag' in result.profile and result.profile['x-mailpile-profile-tag'] in config.tags %}\n              {%- set t = mailpile('tags', config.tags[result.profile['x-mailpile-profile-tag']].slug).result.tags.0 %}\n            {%- else %}\n              {%- set t = {'slug': 'all-mail', 'stats': {'new': '', 'all': ''}} %}\n            {%- endif %}\n            {%- if t.stats.all %}\n              <br><span class=\"icon icon-logo\"></span>\n              {{ _('{TOTAL} e-mails').format(TOTAL=t.stats.all) }}\n            {%- endif %}\n          </div>\n        </label>\n      </li>\n    </ul>\n    <ul style=\"margin-left: 1.5em; float: right;\">\n      <li><input type=\"checkbox\" name=\"delete-tags\" value=\"yes\">\n          <span class=\"checkbox\">{{ _(\"Delete Account Tags\") }}</span></li>\n    {%- if result.profile.key %}\n      <li><input type=\"checkbox\" name=\"delete-keys\" value=\"yes\">\n          <span class=\"checkbox\">{{ _(\"Delete Encryption Keys\") }}</span></li>\n    {%- endif %}\n    {%- if result.trash_email_is_safe %}\n      <li><input type=\"checkbox\" name=\"trash-email\" value=\"yes\">\n          <span class=\"checkbox\">{{ _(\"Move E-mail to Trash\") }}</span></li>\n    {%- endif %}\n    </ul>\n    <p>\n      {{ _(\"By default this will remove: account details, OAuth credentials, saved passwords, linked mail sources, and route settings.\") }}\n      {{ _(\"This only affects local data, e-mail and settings on remote mail servers will not be modifed.\") }}\n    </p>\n    {%- if result.profile.key %}\n    <p>\n      {{ _(\"If you also delete the encryption keys, you may be unable to read old encrypted e-mail.\") }}\n    </p>\n    {%- endif %}\n    <p>\n      {{ _(\"Be careful!\") }}\n      <b>{{ _(\"This operation can not be undone.\") }}</b>\n    </p>\n    <br clear=\"both\">\n    <button class=\"button button-secondary\" data-dismiss=\"modal\" aria-hidden=\"true\">\n      {{ _(\"Cancel\") }}\n    </button>\n    <button type=\"submit\" class=\"button-primary right\">\n      <span class=\"icon icon-trash\"></span> {{ _(\"Remove Account\")}}\n    </button>\n  </form>\n</div>\n{%- else %}\n  {{ mailpile('http/redirect', U('/profiles/')) }}\n{%- endif %}\n{%- endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/quitquitquit/index.html",
    "content": "<h1>Graceful shutdown started ...</h1>\n"
  },
  {
    "path": "shared-data/default-theme/html/search/atts.html",
    "content": "{%- set prev_more_next_url = '/search/atts.html' %}\n{%- set display_attachments = True %}\n{%- include \"search/default.html\" %}\n"
  },
  {
    "path": "shared-data/default-theme/html/search/default.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block content %}\n{% set view_description = view_description or [] %}\n{% if result %}\n  {%- set profiles = mailpile('profiles').result %}\n  {%- set theme_colors = theme_settings().colors %}\n  {%- set with_top_composer = [] %}\n  {%- for tid in result.search_tag_ids -%}\n    {% if config.tags[tid].type == 'drafts' %}{% do with_top_composer.append(tid) %}{% endif %}\n  {%- endfor %}\n  {%- for qv in state.query_args.q -%}\n    {% if '@' in qv|list %}{% do with_top_composer.append(tid) %}{% endif %}\n  {%- endfor %}\n  {%- set with_top_composer = (False and profiles.profiles and with_top_composer) %}\n  {# FIXME! ^^ false #}\n\n  <span class=\"page-title-data\" style=\"display: none;\">\n  {%- if result.search_tag_ids %}\n    {%- set tag = config.tags[result.search_tag_ids[0]] %}\n    {%- if tag.type != 'inbox' %}\n    <span class=\"page-title-icon\">\n      <span class=\"icon {{ tag.icon }}\"\n            style=\"color: {{ theme_colors.get(tag.label_color, tag.label_color) }};\"\n        ></span>\n    </span>\n    {%- endif %}\n    <span class=\"page-title-text\">{% for tid in result.search_tag_ids %}\n      {{- config.tags[tid].name }}{% if not loop.last %}: {% endif %}\n    {%- endfor %}</span>\n    {%- if result.search_tag_ids|length == 1 %}\n      {% if not view_description %}\n        {%- if tag.auto_tag and tag.auto_tag != \"false\" %}\n          {%- do view_description.append(_(\"Auto-tagging is enabled.\")) %}\n        {%- endif %}\n        {%- if tag.auto_after > 0 %}\n          {%- if tag.auto_action == '!delete' %}\n            {%- if config.prefs.allow_deletion %}\n              {%- do view_description.append(\n                    _(\"Messages will be permanently deleted after {days} days.\"\n                      ).format(days=tag.auto_after)) %}\n            {%- endif %}\n          {%- else %}\n            {% if '+trash' in tag.auto_action %}\n              {%- do view_description.append(\n                    _(\"Messages will be moved to Trash after {days} days.\"\n                      ).format(days=tag.auto_after)) %}\n            {% else %}\n              {%- do view_description.append(\n                    _(\"Automation is enabled. Consult tag settings for details.\")) %}\n            {%- endif %}\n          {%- endif %}\n        {%- elif tag.auto_tag and tag.auto_tag != \"false\" %}\n          {%- do view_description.append(_(\"Add and remove messages to train the system.\")) %}\n        {%- endif %}\n      {%- endif %}\n    {%- endif %}\n    {%- if view_description %}\n      {%- do view_description.insert(0, \"<b>\" + tag.name + \"</b> : \") %}\n      {%- do view_description.insert(0, \"<span class='icon \" + tag.icon + \"'></span>\") %}\n    {%- endif %}\n  {%- else %}\n    <span class=\"page-title-icon\">\n      <span class=\"icon icon-search\" style=\"color: {{ theme_colors['08-green'] }};\"></span>\n    </span>\n    <span class=\"page-title-text\">{{ _(\"Search\") }}</span>\n  {%- endif %}\n  </span>\n\n  <table id=\"pile-results\"\n         data-tids=\"{{ result.search_tag_ids|join(' ') }}\"\n         data-tags=\"{% for tid in result.search_tag_ids -%}\n                      {{ config.tags[tid].name }}\n                      {%- if not loop.last %}, {% endif %}\n                    {%- endfor %}\"\n         data-index-capabilities=\"{% for c in result.index_capabilities %}\n                                    {%- if result.index_capabilities[c] %}{{ c }} {% endif -%}\n                                  {%- endfor %}\"\n         data-tag-capabilities=\"{% for c in result.tag_capabilities %}\n                                  {%- if result.tag_capabilities[c] %}{{ c }} {% endif -%}\n                                {%- endfor %}\"\n         class=\"pile-results {{ config.web.display_density }}\">\n  <tbody>\n {%- if view_description and result.stats.total > 0 and not result.data.messages %}\n    <div id=\"view_description\" {# FIXME: Move styling to less! #}\n         style=\"background: #dfa; border-bottom: 1px solid #897; padding: 5px; text-align: center;\"\n         class=\"mobile-pt-hide\">\n      <div style=\"display: inline-block; margin: 0 auto; text-align: left;\">\n        {%- for d in view_description %}{{ d|safe }} {% endfor %}\n      </div>\n    </div>\n {%- endif %}\n {%- if not result.data or not result.data.messages %}\n   {# This puts a handy composer at the top of some search results #}\n   {%- if with_top_composer %}\n     {%- include(\"partials/pile_compose.html\") %}\n   {%- endif %}\n {%- endif %}\n {%- for previous_mid, this_mid, next_mid in result.thread_ids|with_context %}\n   {%- set mid = result.view_pairs.get(this_mid, this_mid) %}\n   {%- if false and mid in result.data.messages and result.data.metadata[mid].urls.editing %}\n     {# FIXME! ^^ false #}\n     {%- include(\"partials/pile_compose.html\") %}\n   {%- else %}\n     {%- include(\"partials/search_item.html\") %}\n   {%- endif %}\n {%- endfor %}\n  </tbody>\n  </table>\n\n {% if result.stats.total > 0 %}\n  {%- include(\"search/prev_more_next.html\") %}\n {% elif not with_top_composer %}\n  <div id=\"pile-empty\" class=\"clearfix add-bottom text-center\">\n  {%- if profiles.profiles|length <= 0 %}\n    <h3 class=\"add-top\">{{_(\"Nothing Happened.\")}}</h3>\n    <p><i>{{_(\"Usually, matching e-mails would be listed here.\")}}</i></p>\n    <br>\n    <p>{{_(\"You need to create an account and add some mail first!\")}}</p>\n    <p class=\"add-bottom\">\n      <a href=\"{{ U('/profiles/') }}\" id=\"pile-empty-search-terms-help\"\n         class=\"button-secondary\">\n         <span class=\"icon-home\"></span>\n         {{_(\"Manage your Accounts\")}}\n      </a>\n    </p>\n  {%- else %}\n\n    {%- if 'in:spam' in result.search_terms %}\n    <h3 class=\"add-top\">{{_(\"No Spam Found, Hooray!\")}}</h3>\n    {%- elif 'in:inbox' in result.search_terms %}\n    <h3 class=\"add-top\">{{_(\"Inbox zero? Impressive!\")}}</h3>\n    {%- else %}\n    <h3 class=\"add-top\">{{_(\"Nothing Happened.\")}}</h3>\n    <p>{{_(\"It seems your Mailpile does not contain any messages for the search\")}}:</p>\n    <p id=\"pile-empty-search-terms\">\"{% for term in result.search_terms %}{{term}}{% if not loop.last %} {% endif %}{% endfor %}\"</p>\n    {%- endif %}\n\n    <br>\n    <p>{{_(\"Here are some other options for you\")}}:</p>\n    <p>\n      <a href=\"{{ U('/browse/') }}\" class=\"button-primary\">\n        <span class=\"icon-list\"></span>\n        {{_(\"Browse for mailboxes\")}}\n      </a>\n      &nbsp;\n      <a href=\"{{ U('/message/compose/') }}\" class=\"button-compose button-primary\">\n        <span class=\"icon-compose\"></span>\n        {{_(\"Compose a message\")}}\n      </a>\n    </p>\n    <p class=\"add-bottom\">\n      <a href=\"{{ U('/help/searching/') }}\" id=\"pile-empty-search-terms-help\"\n           class=\"button-secondary\">\n         <span class=\"icon-help\"></span>\n         {{_(\"Search Tips & Tricks\")}}\n      </a>\n    </p>\n\n  {% endif %}\n  </div>\n {% endif %}\n{% else %}\n  <div class=\"add-top add-bottom text-center\">\n    <h2 class=\"add-top center\">{{_(\"Hrm, We Could Not Find Anything\")}}</h2>\n  </div>\n{% endif %}\n<script>\n$(document).ready(function() {\n  Mailpile.Search.init();\n  Mailpile.Tags.init();\n});\n</script>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/search/drafts.html",
    "content": "{%- set view_description = [_(\"Your unfinished messages.\")] %}\n{%- set display_recipients = True %}\n{%- set prev_more_next_url = '/search/drafts.html' %}\n{%- include \"search/default.html\" %}\n"
  },
  {
    "path": "shared-data/default-theme/html/search/index.html",
    "content": "{%- if result and result.tag_capabilities.template %}\n  {%- include(\"search/\" + result.tag_capabilities.template + \".html\") %}\n{%- else %}\n  {%- include(\"search/default.html\") %}\n{%- endif %}\n"
  },
  {
    "path": "shared-data/default-theme/html/search/outbox.html",
    "content": "{%- set view_description = [_(\"Messages that are ready to send.\")] %}\n{%- set display_recipients = True %}\n{%- set prev_more_next_url = '/search/outbox.html' %}\n{%- include \"search/default.html\" %}\n"
  },
  {
    "path": "shared-data/default-theme/html/search/outgoing.html",
    "content": "{%- set display_recipients = True %}\n{%- set prev_more_next_url = '/search/outgoing.html' %}\n{%- include \"search/default.html\" %}\n"
  },
  {
    "path": "shared-data/default-theme/html/search/photos.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block content %}\n{%- set displayed = [] %}\n{% if result %}\n  {%- set theme_colors = theme_settings().colors %}\n\n  <span class=\"page-title-data\" style=\"display: none;\">\n  {%- if result.search_tag_ids %}\n    {%- set tag = config.tags[result.search_tag_ids[0]] %}\n    {%- if tag.type != 'inbox' %}\n    <span class=\"page-title-icon\">\n      <span class=\"icon {{ tag.icon }}\"\n            style=\"color: {{ theme_colors.get(tag.label_color, tag.label_color) }};\"\n        ></span>\n    </span>\n    {%- endif %}\n    <span class=\"page-title-text\">{% for tid in result.search_tag_ids %}\n      {{- config.tags[tid].name }}{% if not loop.last %}: {% endif %}\n    {%- endfor %}</span>\n  {%- endif %}\n  </span>\n\n  <div class='pile-results'>\n    <ul class='pile-message-attachments horizontal pile-images'>\n {%- for previous_mid, this_mid, next_mid in result.thread_ids|with_context %}\n   {%- set mid = result.view_pairs.get(this_mid, this_mid) %}\n   {%- set metadata = result.data.metadata[mid] %}\n   {%- for bp in (metadata.body.parts or []) %}\n     {%- set bp = body_part_metadata(bp) %}\n     {%- if bp.pixels and bp.pixels > 128000 %}\n       {%- do displayed.append(mid) %}\n       <li class=\"left\" style=\"padding: 5px;\" data-mid=\"{{ mid }}\">\n         <a class=\"attachment-image\" target=\"_blank\"\n            data-size=\"{{ bp.bytes }}\"\n            title=\"{{ metadata.subject }}\"\n            href=\"{{ U('/message/download/get/=', mid, '/part-', loop.index, '/') }}\">\n           <div class=\"preview\" style=\"background-image: url('{{ U('/message/download/preview/=', mid, '/part-', loop.index, '/') }}');\"></div>\n         </a>\n       </li>\n     {% endif %}\n   {%- endfor %}\n {%- endfor %}\n    </ul><br clear='both'>\n  </div>\n\n {%- set prev_more_next_url = \"/search/photos.html\" %}\n {%- include(\"search/prev_more_next.html\") %}\n{%- endif %}\n{%- if not displayed %}\n  <div class=\"add-top add-bottom text-center\">\n    <h2 class=\"add-top center\">{{_(\"Hrm, We Could Not Find Anything\")}}</h2>\n  </div>\n{%- endif %}\n<script>\n$(document).ready(function() {\n  Mailpile.Search.init();\n  Mailpile.Tags.init();\n});\n</script>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/search/prev_more_next.html",
    "content": "{% if result.stats.total > 0 %}\n {% set url = prev_more_next_url or '/search/' %}\n  <div id=\"pile-bottom\" class=\"clearfix\">\n    {% set searchterms = result.search_terms|join(' ') %}\n    {% if result.stats.start > 1 %}\n    {% set newstart = result.stats.start-result.stats.count %}\n    {% set newend = newstart+result.stats.count-1 %}\n    {% set prv = {\n      'url': url,\n      'url_args_remove': [['start', ''], ['end', ''], ['view', '']],\n      'url_args_add': [['start', newstart], ['end', newend]]\n    } %}\n    <a href=\"{{prv.url|add_state_query_string(state, prv)|url_path_fix}}\"\n       class=\"button-primary left\" id=\"pile-previous\"\n       data-keep-selection=1>{{_(\"Previous\")}}</a>\n    {% endif %}\n    {% if result.stats.start + result.stats.count < result.stats.total %}\n    {% set moar = {\n      'url': url,\n      'url_args_remove': [['start', ''], ['end', '']],\n      'url_args_add': [['start', result.stats.start],\n                       ['end', result.stats.start+(2*result.stats.count)-1]]\n    } %}\n    <a href=\"{{moar.url|add_state_query_string(state, moar)|url_path_fix}}\"\n       class=\"button-primary left\" id=\"pile-more\"\n       data-noblank=1 data-noscroll=1 data-keep-selection=1>{{_(\"More\")}}</a>\n    {% set newstart = result.stats.start+result.stats.count %}\n    {% set newend = newstart+result.stats.count-1 %}\n    {% set nxt = {\n      'url': url,\n      'url_args_remove': [['start', ''], ['end', ''], ['view', '']],\n      'url_args_add': [['start', newstart], ['end', newend]]\n    } %}\n    <a href=\"{{nxt.url|add_state_query_string(state, nxt)|url_path_fix}}\"\n       class=\"button-primary left\" id=\"pile-next\"\n       data-keep-selection=1>{{_(\"Next\")}}</a>\n    {% endif %}\n    <h5 class=\"text-right\">\n    {% if result.stats.total > 1 %}\n      {{result.stats.start}} - {{result.stats.end}} {{_(\"of\")}} {{result.stats.total}} {{_(\"Conversations\")}}\n    {% elif result.stats.total == 1 %}\n      {{_(\"1 Conversation\")}}\n    {% endif %}\n    </h5>\n  </div>\n  <div id=\"pile-speed\" class=\"text-center clearfix\">\n    <span class=\"icon-speed\"></span>{{_(\"Searched <strong>%(number)s</strong> messages in <strong>%(elapsed)s</strong> seconds.\",\n                                        number=mailpile_size, elapsed=elapsed)}}\n    {% if elapsed < \"0.25\" %}{{_(\"Vroom!\")}}{% endif %}\n  </div>\n{% endif %}\n"
  },
  {
    "path": "shared-data/default-theme/html/search/sent.html",
    "content": "{%- set view_description = [_(\"These messages have been sent.\")] %}\n{%- set display_recipients = True %}\n{%- set prev_more_next_url = '/search/sent.html' %}\n{%- include \"search/default.html\" %}\n"
  },
  {
    "path": "shared-data/default-theme/html/search/trash.html",
    "content": "{#- FIXME: Replace this with a custom template which modifines the display\n #-        to more appropriately suit a listing of messages in the trash.\n #}\n{%- set prev_more_next_url = '/search/trash.html' %}\n{%- include \"search/default.html\" %}\n"
  },
  {
    "path": "shared-data/default-theme/html/settings/index.html",
    "content": "{% if ui_from_profiles %}\n{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% else %}\n{% extends \"logs/layout.html\" %}\n{% endif %}\n\n{% block title %}{{_(\"Settings & Tools\")}}{% endblock %}\n\n{% block content %}\n{%- set plugin_settings = get_ui_elements('settings', state) %}\n<div class=\"content-normal settings-page\">\n\n  <h1 class=\"page-title-data mobile-hide\">\n    <span class=\"page-title-icon\"><span class=\"icon icon-settings\"></span></span>\n    <span class=\"page-title-text\">{{_(\"Settings & Tools\")}}</span>\n  </h1>\n\n  {% set motd = mailpile('motd', '--noupdate').result %}\n  \n  <p class=\"version-info mobile-hide\" style=\"margin-left: 2em;\">\n    {{_(\"This is Mailpile version %(version)s\",\n        version=config.version)}}{% if motd._version_info %}:\n    <i>{{ motd._version_info }}</i>{% endif %}.\n  </p>\n\n  <a name=\"settings\"></a><div class=\"setting-group\" style=\"width: 33em; float: left;\">\n    <h3 class=\"mobile-hide\">{{_(\"Settings\")}}</h3>\n    <div style=\"margin-left: 1em; display: inline-block;\">\n      <p>\n        <a href=\"{{ U('/profiles/') }}\">\n          <button class=\"button-secondary\" style=\"width: 175px; text-align: left;\">\n            <span class=\"icon icon-home\"></span> {{_(\"Accounts\")}}\n          </button>\n        </a> &nbsp;\n        {{_(\"Your Accounts\")}}\n      </p>\n      <p>\n        <a href=\"{{ U('/settings/preferences.html') }}\">\n          <button class=\"button-secondary\" style=\"width: 175px; text-align: left;\">\n            <span class=\"icon icon-animals\"></span> {{_(\"Preferences\")}}\n          </button>\n        </a> &nbsp;\n        {{_(\"Your Preferences\")}}\n      </p>\n      <p>\n        <a href=\"{{ U('/settings/plugins.html') }}\">\n          <button class=\"button-secondary\" style=\"width: 175px; text-align: left;\">\n            <span class=\"icon icon-lightbulb\"></span> {{_(\"Plugins\")}}\n          </button>\n        </a> &nbsp;\n        {{_(\"Plugins and Addons\")}}\n      </p>\n{%- for elem in plugin_settings %}\n      <p>\n        <a href=\"{{ U(elem.url) }}\" title=\"{{ elem.description }}\">\n          <button style=\"width: 175px; text-align: left;\">\n            <span class=\"icon icon-{{ elem.icon }}\"></span> {{elem.text}}\n          </button>\n        </a> &nbsp;\n        {{elem.text}}\n      </p>\n{%- endfor %}\n      <p>\n        <a href=\"{{ U('/settings/privacy.html') }}\">\n          <button style=\"width: 175px; text-align: left;\">\n            <span class=\"icon icon-privacy\"></span> {{_(\"Privacy\")}}\n          </button>\n        </a> &nbsp;\n        {{_(\"Security and Privacy Settings\")}}\n      </p>\n      <p>\n        <a href=\"{{ U('/setup/password/') }}\">\n          <button class=\"button-warning\" style=\"width: 175px; text-align: left;\">\n            <span class=\"icon icon-lock-closed\"></span> {{_(\"Password\")}}\n          </button>\n        </a> &nbsp;\n        {{_(\"Change Your Mailpile Password\")}}\n      </p>\n\n    </div>\n  </div>\n\n  <a name=\"tools\"></a><div class=\"setting-group\" style=\"width: 33em; float: left;\">\n    <h3>{{_(\"Tools\")}}</h3>\n    <div style=\"margin-left: 1em; display: inline-block;\">\n      <p>\n        <a href=\"{{ U('/logs/events/') }}\">\n          <button class=\"button-secondary\" style=\"width: 175px; text-align: left;\">\n            <span class=\"icon icon-notifications\"></span> {{_(\"Event Log\")}}\n          </button>\n        </a> &nbsp;\n        {{_(\"Mailpile Event Log\")}}\n      </p>\n      <p>\n        <a href=\"{{ U('/logs/network/') }}\">\n          <button class=\"button-secondary\" style=\"width: 175px; text-align: left;\">\n            <span class=\"icon icon-force-graph\"></span> {{_(\"Network\")}}\n          </button>\n        </a> &nbsp;\n        {{_(\"Recent Network Activity\")}}\n      </p>\n      <p>\n        <a href=\"{{ U('/backup/download/?csrf=') }}{{ csrf_token }}\">\n          <button class=\"button-secondary\" style=\"width: 175px; text-align: left;\">\n            <span class=\"icon icon-help\"></span> {{_(\"Backup\")}}\n          </button>\n        </a> &nbsp;\n        {{_(\"Backup Your Settings and Keys\")}}\n      </p>\n      <p>\n        <a class=\"auto-modal auto-modal-sticky\" href=\"{{ U('/crypto/tls/getcert/') }}\">\n          <button style=\"width: 175px; text-align: left;\">\n            <span class=\"icon icon-links\"></span> {{_(\"TLS Certs\")}}\n          </button>\n        </a> &nbsp;\n        {{_(\"TLS Certificate Tool\")}}\n      </p>\n      <p>\n        <a class=\"auto-modal auto-modal-sticky\" href=\"{{ U('/settings/set/password/') }}\">\n          <button style=\"width: 175px; text-align: left;\">\n            <span class=\"icon icon-user\"></span> {{_(\"Passwords\")}}\n          </button>\n        </a> &nbsp;\n        {{_(\"Account Password Management\")}}\n      </p>\n      <p>\n        <a class=\"auto-modal auto-modal-sticky\" href=\"{{ U('/settings/set/password/keys.html') }}\">\n          <button style=\"width: 175px; text-align: left;\">\n            <span class=\"icon icon-key\"></span> {{_(\"Keys\")}}\n          </button>\n        </a> &nbsp;\n        {{_(\"Encryption Key Management\")}}\n      </p>\n      <p>\n        <a href=\"#\" onclick=\"Mailpile.Terminal.toggle('small');\">\n          <button class=\"button-warning\" style=\"width: 175px; text-align: left;\">\n            <span class=\"icon icon-code\"></span> {{_(\"CLI\")}}\n          </button>\n        </a> &nbsp;\n        {{_(\"Command Line Interface\")}}\n      </p>\n      <p>\n        <a href=\"#\" class=\"shutdown-mailpile\">\n          <button class=\"button-warning\" style=\"width: 175px; text-align: left;\">\n            <span class=\"icon icon-logout\"></span> {{_(\"Shutdown\")}}\n          </button>\n        </a> &nbsp;\n        {{_(\"Shutdown Mailpile\")}}\n      </p>\n    </div>\n  </div>\n  <br clear=\"both\">\n\n</div>\n<script language=\"javascript\">\n\n  $('a.shutdown-mailpile').click(function(ev) {\n    if (confirm(\n        \"{{_('Are you sure you want to shutdown Mailpile?')|escapejs}}\")) {\n      Mailpile.API.quitquitquit_post({}, function() {\n        Mailpile.API._notify_dead(\n          'user', \"{{_('Shutting down ...')|escapejs}}\", true);\n      });\n    };\n  });\n\n</script>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/settings/mailbox/index.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block title %}{{ message }}{% endblock %}\n{% block content %}\n<div id=\"mailbox-settings\" class=\"content-normal\"\n     style=\"position: relative; max-width: 42em;\">\n<form class=\"standard\" method=\"POST\"\n      action=\"{{ U('/settings/mailbox/') }}\">{{ csrf_field|safe }}\n\n  <script type=\"text/javascript\">\n  </script>\n\n  <p class=\"message paragraph-important modal-basic-settings-title\">\n    {% if result.recurse %}\n      <span class=\"icon-search\"></span>\n      <input type=\"hidden\" name=\"recurse\" value=\"true\">\n      {{_(\"Search Folder\")}}:\n      {%- for path in result.paths %}\n        <input type=\"hidden\" name=\"path\" value=\"{{ path }}\"> {{ path }}\n      {%- endfor %}\n    {% else %}\n      <span class=\"icon-settings\"></span>\n      {{_(\"Mailbox Settings\")}}:\n      {%- for path in result.adding %}\n        <input type=\"hidden\" name=\"path\" value=\"{{ path }}\"> {{ path }}\n      {%- endfor %}\n      {%- for path, info in result.configure %}\n        <input type=\"hidden\" name=\"path\" value=\"{{ path }}\"> {{ path }}\n      {%- endfor %}\n    {% endif %}\n  </p>\n  {% if result.recurse %}\n  <p>FIXME: <i>\n    This folder contains {{ result.adding|length }} new mailboxes.\n    This folder contains {{ result.configure|length }} mailboxes already in your pile.\n    Add them to your mailpile using the settings below?\n  </i></p><hr>\n  {% endif %}\n  <div class=\"modal-basic-settings\">\n\n    <div class=\"left\" style=\"width: 18em; margin-right: 1em;\">\n      <label>Mailbox role</label>\n      <input type=\"hidden\" name=\"guess_tags\" value=\"False\">\n      <select name='apply_tags' style=\"width: 100%;\">\n        <option value='' {% if not result.apply_tags %}selected{% endif %}>{{_(\"Archives\")}}</option>\n        {% for tid in config.tags %}\n          {%- set tag = config.tags[tid] %}\n          {%- if tag.display == 'priority' %}\n        <option value='{{ tid }}' {% if tid in result.apply_tags %}selected{% endif %}>{{ tag.name }}</option>\n          {%- endif %}\n        {%- endfor %}\n      </select>\n\n    {%- if not result.has_source %}\n      <label>Account</label>\n      <select name='profile' style=\"width: 100%;\">\n        {% set profile_data = mailpile('profiles').result %}\n        {% for rid in profile_data.rids %}\n          {% set profile = profile_data.profiles[profile_data.rids[rid]] %}\n        <option value='{{ rid }}' {% if rid == result.profile %}selected{% endif %}>{{ profile.fn }} &lt;{{ profile.email.0.email }}&gt;</option>\n        {% endfor %}\n      </select>\n    {%- endif %}\n    </div>\n\n    <div class=\"right\">\n      <label>Search settings</label>\n      <ul>\n        <li><input type=\"checkbox\" name=\"auto_index\" value=\"true\" {% if result.auto_index %}checked{% endif %}>\n            <span class=\"checkbox\">{{_(\"Add messages to search engine\")}}</span></li>\n      </ul>\n\n      <label>Mailbox settings</label>\n      <ul>\n        <li><input type=\"checkbox\" name=\"local_copy\" value=\"true\" {% if result.local_copy %}checked{% endif %}>\n            <span class=\"checkbox\">{{_(\"Copy mail to Mailpile secure storage\")}}</span></li>\n        {# FIXME: delete from source #}\n      </ul>\n    </div>\n\n  </div>\n\n  <div>\n    <hr style=\"margin-bottom: 10px;\">\n    <button class=\"button-primary right\" type=\"submit\">\n      <span class=\"icon-checkmark\"></span>\n      {% if result.recurse %}\n        {{_(\"Configure Mailboxes\")}}\n      {% else %}\n        {{_(\"Save\")}}\n      {% endif %}\n    </button>\n  </div>\n\n</form>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/settings/plugins.html",
    "content": "{% if ui_from_profiles %}\n{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% else %}\n{% extends \"logs/layout.html\" %}\n{% endif %}\n\n{% block title %}{{_(\"Plugins\")}}{% endblock %}\n\n{% block content %}\n<div class=\"content-normal settings-page\">\n  <h1 class=\"page-title-data mobile-hide\">\n    <span class=\"page-title-icon\"><span class=\"icon icon-lightbulb\"></span></span>\n    <span class=\"page-title-text\">{{_(\"Plugins\")}}</span>\n  </h1>\n\n  <div class=\"notices\">\n  {% if ui_saved %}\n    <p>\n      <span class=\"icon icon-checkmark\"></span>\n      {{_(\"Your settings have been saved.\")}}\n    </p>\n  {% endif %}\n  </div>\n\n  {%- set plugins = mailpile(\"plugins\").result %}\n  {%- for pname in plugins|sort %}\n  {%- set plugin = plugins[pname] %}\n  {%- if (plugin.manifest and\n          plugin.manifest.display and\n          (plugin.manifest.public or is_dev_version())) %}\n  <a name=\"plugin-{{ pname }}\"></a><div class=\"setting-group\">\n    {%- set ui = plugin.manifest.user_interface %}\n    {%- set icon = (\n         (ui and ui.settings and ui.settings[0].icon) or\n         (ui and ui.activities and ui.activities[0].icon) or\n         (ui and ui.selection_actions and ui.selection_actions[0].icon) or\n         (ui and ui.display_refiners and ui.display_refiners[0].icon) or\n         'code') %}\n    {%- set icon = 'code' if '/' in icon else icon %}\n    <h3>\n      {{ plugin.manifest.name or pname }}\n    </h3>\n    <div class=\"explanation\">\n      {% if plugin.loaded %}\n      <p class=\"what\">\n        <span class=\"icon icon-{{ icon }}\"></span>\n        {{_(\"This plugin is currently enabled!\")}}\n      </p>\n      {% else %}\n      <p>\n        <span class=\"icon icon-{{ icon }}\"></span>\n        {{_(\"This plugin is disabled.\")}}\n      </p>\n      {% endif %}\n      <p>\n        {{_(\"Author\")}}: <i>{{ plugin.manifest.author }}</i>\n      </p>\n      {% if not plugin.manifest.public %}\n      <p class=\"risks\">\n        <span class=\"icon icon-signature-unknown\"></span>\n        {{_(\"Be careful!\")}}\n        {{_(\"This plugin is meant for developers and may be incomplete or dangerous.\")}}\n      </p>\n      {% endif %}\n    </div>\n    <div class=\"settings\">\n      <p>\n        {{ _(plugin.manifest.description) }}\n      </p>\n      <p>\n      {%- if plugin.loaded %}\n       {%- if pname not in config.sys.plugins %}\n        <span class=\"right\">\n          <i>{{_(\"Plugin will be disabled on restart.\")}}</i> &nbsp;\n          <button class=\"button-warning plugin-restart\">\n            <span class=\"icon icon-logout\"></span>\n            {{_(\"Restart Now\")}}\n          </button>\n        </span>\n       {%- else %}\n        <button class=\"button-primary right plugin-disable\"\n                data-plugin=\"{{ pname }}\">\n          <span class=\"icon icon-x\"></span>\n          {{_(\"Disable:\")}} {{ pname }}\n        </button>\n        {%- if plugin.manifest.user_interface and plugin.manifest.user_interface.settings %}\n        {%- set elem = plugin.manifest.user_interface.settings[0] %}\n        <a href=\"{{ U(elem.url) }}\">\n          <button class=\"button-primary right\" style=\"margin-right: 10px;\">\n            <span class=\"icon icon-settings\"></span> {{_(\"Settings\")}}\n          </button>\n        </a>\n        {%- endif %}\n       {%- endif %}\n      {%- else %}\n        <button class=\"button-secondary right plugin-enable\"\n                data-plugin=\"{{ pname }}\">\n          <span class=\"icon icon-{{ icon }}\"></span>\n          {{_(\"Enable\")}}: {{ pname }}\n        </button>\n      {%- endif %}\n      </p>\n    </div>\n    <br clear=\"both\">\n  </div>\n  {% endif %}{%- endfor %}\n</div>\n<script language=\"javascript\">\n\n  $('button.plugin-disable').click(function(ev) {\n    var plugin = $(this).data('plugin');\n    Mailpile.API.plugins_disable_post({\n      plugin: plugin\n    }, function() {\n      document.location.href = document.location.href;\n    });\n  });\n\n  $('button.plugin-enable').click(function(ev) {\n    var plugin = $(this).data('plugin');\n    Mailpile.API.plugins_load_post({\n      plugin: plugin\n    }, function() {\n      // Plugin loaded, just refresh page.\n      document.location.href = document.location.href;\n    });\n  });\n\n  $('button.plugin-restart').click(function(ev) {\n    if (confirm(\n        \"{{_('Are you sure you want to restart Mailpile?')|escapejs}}\")) {\n      Mailpile.API.quitquitquit_post({\n        restart: 'now'\n      }, function() {\n        Mailpile.API._notify_dead(\n          'user', \"{{_('Restarting ...')|escapejs}}\", true);\n      });\n    }\n  });\n\n</script>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/settings/preferences.html",
    "content": "{% if ui_from_profiles %}\n{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% else %}\n{% extends \"logs/layout.html\" %}\n{% endif %}\n\n{% block title %}{{_(\"Preferences\")}}{% endblock %}\n\n{% block content %}\n<div class=\"content-normal settings-page\">\n <form method=\"POST\"\n       action=\"{{ U('/settings/set/') }}?ui_return={{ U('/settings/preferences.html?ui_saved=True') }}\"\n       >{{ csrf_field|safe }}\n\n  <h1 class=\"page-title-data mobile-hide\">\n    <span class=\"page-title-icon\"><span class=\"icon icon-animals\"></span></span>\n    <span class=\"page-title-text\">{{_(\"Preferences\")}}</span>\n  </h1>\n\n  <div class=\"notices\">\n  {% if ui_saved %}\n    <p>\n      <span class=\"icon icon-checkmark\"></span>\n      {{_(\"Your settings have been saved.\")}}\n    </p>\n  {% endif %}\n  </div>\n\n  <a name=\"searching\"></a><div class=\"setting-group\">\n    <h3>{{_(\"Searches and Tags\")}}</h3>\n    <div class=\"explanation\">\n      <p class=\"what\">\n        <span class=\"icon icon-tags\"></span>\n        {{_(\"Here you can customize how search results and tag views are displayed.\")}}\n        {{_(\"The ordering and grouping options determine whether messages are grouped together into conversations or not, and which results are sorted to the top of the list.\")}}\n      </p>\n      <p class=\"what\">\n        <span class=\"icon icon-eye\"></span>\n        {{_(\"The display density determines how tightly the results are packed together on your screen.\")}}\n      </p>\n    </div>\n    <div class=\"settings\">\n      {%- set rpp = {config.prefs.num_results: ' selected'} %}\n      {%- set rpp_opts = [20, 40, 80, 120] %}\n      {%- if config.prefs.num_results not in rpp_opts %}\n        {%- do rpp_opts.insert(0, config.prefs.num_results) %}\n      {%- endif %}\n      <p style=\"line-height: 1.8em;\">\n        <label>{{_(\"Search results per page\")}}:</label>\n        <select name=\"prefs.num_results\" style=\"float: right;\">\n          {% for val in rpp_opts %}\n          <option value=\"{{ val }}\"{{ rpp.get(val, '') }}>{{ val }}</option>\n          {% endfor %}\n        </select>\n      </p>\n      {%- set dgo = {config.prefs.default_order: ' selected'} %}\n      {%- set orders = [\n              ('rev-date',      _(\"Conversations\") + \", \" + _(\"Newest First\")),\n              ('date',          _(\"Conversations\") + \", \" + _(\"Oldest First\")),\n              ('rev-flat-date', _(\"Messages\") + \", \" + _(\"Newest First\")),\n              ('flat-date',     _(\"Messages\") + \", \" + _(\"Oldest First\")),\n              ('rev-index',     _(\"Conversations\") + \", \" + _(\"Unsorted\"))] %}\n      {%- if config.prefs.default_order not in ('rev-date', 'date', 'rev-flat-date', 'flat-date', 'rev-index') %}\n        {%- do orders.insert(0, (config.prefs.default_order, _(\"Custom\") + ': ' + config.prefs.default_order)) %}\n      {%- endif %}\n      <p style=\"line-height: 1.8em;\">\n        <label>{{_(\"Ordering and grouping\")}}:</label>\n        <select name=\"prefs.default_order\" style=\"float: right;\">\n          {% for val, txt in orders %}\n          <option value=\"{{ val }}\"{{ dgo.get(val, '') }}>{{ txt }}</option>\n          {% endfor %}\n        </select>\n      </p>\n      {%- set dd = {config.web.display_density: ' selected'} %}\n      <p style=\"line-height: 1.8em;\">\n        <label>{{_(\"Display density\")}}:</label>\n        <select name=\"web.display_density\" style=\"float: right;\">\n          <option value=\"comfy\"{{ dd.get('comfy', '') }}>{{_(\"Comfy\")}}</option>\n          <option value=\"cozy\"{{  dd.get('cozy',  '') }}>{{_(\"Cozy\")}}</option>\n          <option value=\"snug\"{{  dd.get('snug',  '') }}>{{_(\"Snug\")}}</option>\n        </select>\n      </p>\n    </div>\n    <br clear=\"both\">\n  </div>\n\n  {%- set languages = mailpile(\"languages\").result %}\n  <a name=\"language\"></a><div class=\"setting-group\">\n    <h3>{{_(\"User Interface\")}}</h3>\n    <div class=\"explanation\">\n      <p class=\"what\">\n        <span class=\"icon icon-map\"></span>\n        {{_(\"Mailpile is translated to many languages.\")}}\n        {{_(\"The translation only applies to the application, not the e-mails themselves.\")}}\n      </p>\n      <p class=\"risks\">\n        <span class=\"icon icon-dislike\"></span>\n        {{_(\"Note that some translations may be incomplete.\")}}\n      </p>\n      <p class=\"more\">\n        <a href=\"https://www.transifex.com/otf/mailpile/\"\n           target=_blank>{{_(\"Help translate Mailpile\")}}</a>.\n      </p>\n    </div>\n    <div class=\"settings\">\n  {%- if languages %}\n      <p style=\"line-height: 1.8em;\">\n        <label>{{_(\"User interface language\")}}</label>:\n        <select name=\"prefs.language\" style=\"float: right;\">\n        {%- set selected = { (config.prefs.language or 'C'): ' selected' } %}\n        {%- for lc, txt in languages %}\n          <option value=\"{{ lc }}\"{{ selected.get(lc, '') }}>{{ txt }}</option>\n        {%- endfor %}\n        </select>\n      </p>\n  {% endif %}\n    {%- for pref, val, label in (\n            ('web.keybindings', config.web.keybindings, _(\"Enable keyboard short-cuts\")+':'),\n            ('web.donate_visibility', config.web.donate_visibility, _(\"Display donate link in topbar?\")),\n            ('web.friendly_dates', config.web.friendly_dates, _(\"Use compact, approximate dates/times in UI\")),\n            ) %}\n      {%- set sel = {val: ' checked'} %}\n      <p style=\"line-height: 1.6em;\">\n        <label>{{ label }}</label>\n        <span class=\"float: right\">\n          <input type=radio name=\"{{ pref }}\" value=\"false\" {{  sel.get(false,  '') }}>\n          <span class=\"checkbox\">{{_(\"Off\")}}</span>\n          <input type=radio name=\"{{ pref }}\" value=\"true\" {{  sel.get(true,  '') }}>\n          <span class=\"checkbox\">{{_(\"On\")}}</span>\n        </span>\n      </p>\n    {% endfor %}\n    </div>\n    <br clear=\"both\">\n  </div>\n\n  <a name=\"searching\"></a><div class=\"setting-group\">\n    <h3>{{_(\"Danger Zone\")}}</h3>\n    <div class=\"explanation\">\n      <p class=\"risks\">\n        <span class=\"icon icon-signature-unknown\"></span>\n        {{_(\"Be careful!\")}}\n        {{_(\"These are technical settings which may interfere with normal use of Mailpile.\")}}\n        {{_(\"If Mailpile came with a warranty, changing these would invalidate it.\")}}\n      </p>\n    </div>\n    <div class=\"settings\">\n    {%- set danger_zone = [\n            ('web.developer_mode', config.web.developer_mode, _(\"Enable developer-only features\")+':'),\n            ('prefs.open_in_browser', config.prefs.open_in_browser, _(\"Open in browser on startup\")+':'),\n            ('prefs.always_bcc_self', config.prefs.always_bcc_self, _(\"Always BCC self when sending mail\")+':'),\n            ('prefs.auto_mark_as_read', config.prefs.auto_mark_as_read, _(\"Automatically mark e-mail as read\")+':'),\n            ] %}\n    {%- if config.web.developer_mode %}\n      {%- do danger_zone.extend([\n            ('prefs.gpg_use_agent', config.prefs.gpg_use_agent, _(\"Use the local GnuPG agent\")+':'),\n            ('prefs.gpg_html_wrap', config.prefs.gpg_html_wrap, _(\"Wrap keys and signatures in helpful HTML\")+':'),\n            ]) %}\n    {%- endif %}\n    {%- for pref, val, label in danger_zone %}\n      {%- set sel = {val: ' checked'} %}\n      <p style=\"line-height: 1.6em;\">\n        <label>{{ label }}</label>\n        <span class=\"float: right\">\n          <input type=radio name=\"{{ pref }}\" value=\"false\" {{  sel.get(false,  '') }}>\n          <span class=\"checkbox\">{{_(\"Off\")}}</span>\n          <input type=radio name=\"{{ pref }}\" value=\"true\" {{  sel.get(true,  '') }}>\n          <span class=\"checkbox\">{{_(\"On\")}}</span>\n        </span>\n      </p>\n    {% endfor %}\n    </div>\n    <br clear=\"both\">\n  </div>\n\n  <span style=\"position: fixed; bottom: 15px; border: 5px solid #eee;\">\n    <button class=\"button-primary\" type=\"submit\">{{_(\"Save Settings\")}}</button>\n  </span>\n </form>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/settings/privacy.html",
    "content": "{% if ui_from_profiles %}\n{% extends \"layouts/\" + render_mode + \"-tall.html\" %}\n{% else %}\n{% extends \"logs/layout.html\" %}\n{% endif %}\n\n{% block title %}{{_(\"Security and Privacy Settings\")}}{% endblock %}\n\n{% block content %}\n{%- set unconfigured = \"unknown\" in (config.prefs.motd_url,\n                                     config.prefs.web_content) -%}\n\n<div class=\"content-normal settings-page\">\n <form method=\"POST\"\n{% if ui_from_profiles %}\n       action=\"{{ U('/settings/set/') }}?ui_return={{ U('/profiles/') }}\"\n{% else %}\n       action=\"{{ U('/settings/set/') }}?ui_return={{ U('/settings/privacy.html?ui_saved=True') }}\"\n{% endif %}\n       >{{ csrf_field|safe }}\n\n  <h1 class=\"page-title-data mobile-hide\">\n    <span class=\"page-title-icon\"><span class=\"icon icon-settings\"></span></span>\n    <span class=\"page-title-text\">{{_(\"Security and Privacy Settings\")}}</span>\n  </h1>\n\n  <div class=\"notices\" style=\"margin: 10px 0;\">\n  {% if unconfigured %}\n    <p style=\"text-align: center;\">\n      <span class=\"icon icon-lightbulb\"></span>\n      {{_(\"Please review and save your settings.\")}}\n      {{_(\"Some features may not be enabled until you have saved.\")}}\n      <br>\n    </p>\n    <p style=\"text-align: center\">\n      <button class=\"button-primary\" type=\"submit\">{{_(\"Save Settings\")}}</button>\n    </p>\n    {% elif ui_saved %}\n    <p>\n      <span class=\"icon icon-checkmark\"></span>\n      {{_(\"Your settings have been saved.\")}}\n    </p>\n    {% else %}\n    <p class=\"mobile-hide\">\n      {{_(\"Choose your own privacy policy!\")}}\n    </p>\n    {% endif %}\n  </div>\n\n  <a name=\"network\"></a><div class=\"setting-group\">\n    <h3>{{_(\"Networking Privacy\")}}</h3>\n    <div class=\"explanation\">\n      <p class=\"what\">\n        <span class=\"icon icon-tor\"></span>\n        {{_(\"Tor is a community-run system for making anonymous network connections.\")}}\n        {{_(\"It masks your IP address and location, thus preventing many forms of surveillance and tracking.\")}}\n      </p>\n      <p class=\"risks\">\n        <span class=\"icon icon-privacy\"></span>\n        {{_(\"Using Tor may be considered suspicious or even illegal in some countries.\")}}\n        {{_(\"Since the Tor network is run by volunteers, bad actors can (and do) volunteer to run exit-nodes so they can listen in on traffic; as a result Tor is not suitable for accessing the unencrypted web.\")}}\n      </p>\n      <p class=\"more\">\n        <a href=\"https://www.torproject.org/\"\n           target=_blank>{{_(\"Visit the Tor project's web-site\")}}</a>.\n      </p>\n    </div>\n    <div class=\"settings\">\n    {% set proxy = {\"tor\": \"Tor\", \"tor-risky\": \"Tor\"}.get(config.sys.proxy.protocol, _(\"proxy\")) %}\n    {% if config.sys.proxy.protocol not in (\"none\", \"unknown\", \"system\") %}\n      <p>\n        {{_(\"When sending and receiving mail, or downloading from the web:\")}}\n      </p>\n      <div class=\"basic-settings\">\n    {% if config.sys.proxy.protocol not in (\"tor\", \"tor-risky\", \"system\", \"unknown\") %}\n        <input name=\"sys.proxy.protocol\" type=\"radio\" value=\"{{config.sys.proxy.protocol}}\" checked>\n        <span class=\"checkbox\">\n          {{_(\"Custom proxy setting (%(proto)s on %(host)s:%(port)s)\",\n              proto=config.sys.proxy.protocol,\n              host=config.sys.proxy.host,\n              port=config.sys.proxy.port)}}\n        </span><br>\n    {% else %}\n        <input name=\"sys.proxy.protocol\" type=\"radio\" value=\"tor\"\n               {%- if config.sys.proxy.protocol in (\"tor\", \"unknown\") %} checked{% endif %}>\n        <span class=\"checkbox\">\n          {{_(\"Prefer anonymous Tor networking for encrypted connections.\")}}\n          <span title=\"{{_('Recommended setting')}}\"\n                class=\"icon default icon-star\"></span>\n        </span><br>\n        <input name=\"sys.proxy.protocol\" type=\"radio\" value=\"tor-risky\"\n               {%- if config.sys.proxy.protocol == \"tor-risky\" %} checked{% endif %}>\n        <span class=\"checkbox\">\n          {{_(\"Prefer anonymous Tor networking for all connections.\")}}\n        </span><br>\n    {% endif %}\n        <input name=\"sys.proxy.protocol\" type=\"radio\" value=\"system\">\n        <span class=\"checkbox\">\n          {{_(\"Use shared operating system proxy settings.\")}}\n        </span><br>\n        <input name=\"sys.proxy.protocol\" type=\"radio\" value=\"none\">\n        <span class=\"checkbox\">\n          {{_(\"Do not use %(proxy)s.\", proxy=proxy)}}\n        </span><br>\n      </div>\n\n      <h6 id=\"tor-advanced-heading\"><a>\n        <span class=\"icon icon-settings\"></span>\n        {{_(\"Advanced Settings\")}} ...\n      </a></h6>\n      <div id=\"tor-advanced\" class=\"advanced-settings\">\n        <input name=\"sys.proxy.fallback\" type=\"radio\" value=\"true\"\n               {%- if config.sys.proxy.fallback %} checked{% endif %}>\n        <span class=\"checkbox\">\n          {{_(\"Fall back to direct networking if connecting over %(proxy)s fails.\",\n              proxy=proxy)}}\n          <span title=\"{{_('Recommended setting')}}\"\n                class=\"icon default icon-star\"></span>\n        </span><br>\n        <input name=\"sys.proxy.fallback\" type=\"radio\" value=\"false\"\n               {%- if not config.sys.proxy.fallback %} checked{% endif %}>\n        <span class=\"checkbox\">\n          {{_(\"Disable direct networking connections completely.\")}}\n        </span><br>\n        <br>\n\n        {{_(\"Never use %(proxy)s for these hosts:\", proxy=proxy)}}\n        <input name=\"sys.proxy.no_proxy\" type=\"text\"\n               value=\"{{config.sys.proxy.no_proxy}}\">\n{# DISABLED FOR NOW ...\n        <br>\n        {{_(\"Host:\")}} <input name=\"sys.proxy.host\" type=\"text\" size=12\n                              value=\"{{config.sys.proxy.host}}\">\n        <br>\n        {{_(\"Port:\")}} <input name=\"sys.proxy.port\" type=\"text\" size=4\n                              value=\"{{config.sys.proxy.port}}\">\n#}\n      </div>\n      <script>\n        $('#tor-advanced').hide();\n        $('#tor-advanced-heading').click(function() {$('#tor-advanced').slideDown()});\n      </script>\n\n    {% else %}\n      <p>\n        {{_(\"Tor is currently disabled.\")}}\n     {% if not platforms.BINARIES.Tor %}\n      </p>\n      <p>\n        {{_(\"The Tor application could not be found on your system, please install it!\")}}\n      </p>\n     {% else %}\n        {{_(\"To enable Tor, click the button below.\")}}\n      </p>\n      <p style=\"padding: 1em;\">\n        <script type=\"text/javascript\">\n          function enable_tor(ev) {\n            ev.preventDefault();\n            $('#enable-tor').hide();\n            $('#enable-tor-working').show();\n            $('#enable-tor-result').html('');\n            Mailpile.API.setup_tor_post({\n              _timeout: (Mailpile.ajax_timeout * 30),\n            }, function(result) {\n              $('#enable-tor-working').hide();\n              $('#enable-tor').show();\n              $('#enable-tor-result').html('<br><br>' + result.message);\n              if (result.status == \"success\") {\n                document.location.href = document.location.href + '?ui_tor=ok';\n              }\n            });\n          }\n          $(document).ready(function() {$('#enable-tor').click(enable_tor)});\n        </script>\n        <span id=\"enable-tor-working\" class=\"hide\">\n          {% include(\"../img/loading-ellipsis.svg\") %}\n        </span>\n        <button id=\"enable-tor\">{{_(\"Enable Tor\")}}</button>\n        &nbsp; <i id=\"enable-tor-result\"></i>\n      </p>\n     {% endif %}\n      <p>\n        {{_(\"Without Tor, Mailpile can not prevent service providers and network operators from monitoring or tracking your IP address and communications.\")}}\n      </p>\n    {% endif %}\n      <h6><a href=\"{{ U(\"/logs/network/\") }}\">\n        <span class=\"icon icon-work\"></span>\n        {{_(\"Troubleshoot recent Network Activity.\")}}\n      </a></h6>\n    </div>\n    <br clear=\"both\">\n  </div>\n\n  <a name=\"motd\"></a><div class=\"setting-group\">\n    <h3>{{_(\"Message Of The Day\")}}</h3>\n    <div class=\"explanation\">\n      <p class=\"what\">\n        <span class=\"icon icon-lightbulb\"></span>\n        {{_(\"The Mailpile Team publishes updates to notify users of available upgrades and potential security vulnerabilities in Mailpile.\")}}\n      </p>\n      <p class=\"risks\">\n        <span class=\"icon icon-privacy\"></span>\n        {{_(\"Update subscriptions help the Mailpile Team keep track of how many people use Mailpile, what operating systems are in use, and which languages users speak.\")}}\n        {{_(\"If Tor is not installed, this may leak your IP address.\")}}\n      </p>\n      <p class=\"more\">\n        <a href=\"https://github.com/mailpile/Mailpile/wiki/Mailpile-Analytics-Reporting-System\"\n           target=_blank>{{_(\"Consult the Mailpile wiki for more details\")}}</a>.\n      </p>\n    </div>\n    <p class=\"settings\">\n      <input name=\"prefs.motd_url\" type=\"radio\" value=\"default\"\n             {%- if config.prefs.motd_url in (\"default\", \"unknown\") %} checked{% endif %}>\n      <span class=\"checkbox\">\n        {{_(\"Subscribe to Message Of The Day updates from the Mailpile Team.\")}}\n        <span title=\"{{_('Recommended setting')}}\"\n              class=\"icon default icon-star\"></span>\n      </span><br>\n    {% if config.sys.proxy.protocol in (\"tor\", \"tor-risky\") %}\n      <input name=\"prefs.motd_url\" type=\"radio\" value=\"tor-only\"\n             {%- if config.prefs.motd_url == \"tor-only\" %} checked{% endif %}>\n      <span class=\"checkbox\">\n        {{_(\"Only download updates anonymously over Tor.\")}}\n      </span><br>\n      <input name=\"prefs.motd_url\" type=\"radio\" value=\"tor-generic\"\n             {%- if config.prefs.motd_url == \"tor-generic\" %} checked{% endif %}>\n      <span class=\"checkbox\">\n        {{_(\"Generic updates only, over Tor - keeps all details about your setup private.\")}}\n      </span><br>\n    {% else %}\n      <input name=\"prefs.motd_url\" type=\"radio\" value=\"generic\"\n             {%- if config.prefs.motd_url == \"generic\" %} checked{% endif %}>\n      <span class=\"checkbox\">\n        {{_(\"Download generic updates only, to keep details about your setup private.\")}}\n      </span><br>\n    {% endif %}\n    {%- if config.prefs.motd_url not in (\"default\", \"unknown\", \"tor-only\",\n                                         \"tor-generic\", \"generic\", \"none\") %}\n      <input name=\"prefs.motd_url\" type=\"radio\" value=\"{{config.prefs.motd_url}}\" checked>\n      <span class=\"checkbox\">\n        {{_(\"Keep your custom settings:\")}} <tt>{{config.prefs.motd_url}}</tt>\n      </span><br>\n    {% endif %}\n      <input name=\"prefs.motd_url\" type=\"radio\" value=\"none\"\n             {%- if config.prefs.motd_url == \"none\" %} checked{% endif %}>\n      <span class=\"checkbox\">\n        {{_(\"Disable the Message Of The Day.\")}}\n      </span><br>\n      <br>\n      {{_(\"Tor will be used to protect your IP address, if it is available.\")}}\n    </p>\n    <br clear=\"both\">\n  </div>\n\n  <a name=\"third-party-content\"></a><div class=\"setting-group\">\n    <h3>{{_(\"Third Party Content\")}}</h3>\n    <div class=\"explanation\">\n      <p class=\"what\">\n        <span class=\"icon icon-news\"></span>\n        {{_(\"Mailpile can download content from the web to augment your mail.\")}}\n        {{_(\"This includes user photos from Gravatar, key material from key servers, and potentially other sources.\")}}\n      </p>\n      <p class=\"risks\">\n        <span class=\"icon icon-privacy\"></span>\n        {{_(\"This may leak information about your address book and use of Mailpile to the providers of these services.\")}}\n        {{_(\"If Tor is not installed, this may leak your IP address.\")}}\n      </p>\n      <p class=\"info\">\n        {{_(\"Learn more about:\")}}\n        <a target=_blank href=\"https://en.gravatar.com/\">Gravatar</a>,\n        <a target=_blank href=\"https://en.wikipedia.org/wiki/Key_server_%28cryptographic%29\">Key Servers</a>\n      </p>\n    </div>\n    <p class=\"settings\">\n      <input name=\"prefs.web_content\" type=\"radio\" value=\"on\"\n    {% if config.sys.proxy.protocol not in (\"tor\", \"tor-risky\") %}\n             {%- if config.prefs.web_content in (\"on\", \"unknown\") %} checked{% endif %}>\n    {% else %}\n             {%- if config.prefs.web_content == \"on\" %} checked{% endif %}>\n    {% endif %}\n      <span class=\"checkbox\">\n        {{_(\"Enable downloading of third party content from the web.\")}}\n    {% if config.sys.proxy.protocol not in (\"tor\", \"tor-risky\") %}\n        <span title=\"{{_('Recommended setting')}}\"\n              class=\"icon default icon-star\"></span>\n    {% endif %}\n      </span><br>\n    {% if config.sys.proxy.protocol in (\"tor\", \"tor-risky\") %}\n      <input name=\"prefs.web_content\" type=\"radio\" value=\"anon\"\n             {%- if config.prefs.web_content in (\"anon\", \"unknown\") %} checked{% endif %}>\n      <span class=\"checkbox\">\n        {{_(\"Only download third party content anonymously over Tor.\")}}\n        <span title=\"{{_('Recommended setting')}}\"\n              class=\"icon default icon-star\"></span>\n      </span><br>\n    {% endif %}\n      <input name=\"prefs.web_content\" type=\"radio\" value=\"off\"\n             {%- if config.prefs.web_content == \"off\" %} checked{% endif %}>\n      <span class=\"checkbox\">\n        {{_(\"Do not download third party content.\")}}\n      </span><br>\n      <br>\n      {{_(\"Tor will be used to protect your IP address, if it is available.\")}}\n    </p>\n    <br clear=\"both\">\n  </div>\n\n  <a name=\"data-security\"></a><div class=\"setting-group\">\n    <h3>{{_(\"Securing Your Data\")}}</h3>\n\n    <div class=\"explanation\">\n      <p class=\"what\">\n        <span class=\"icon icon-settings\"></span>\n        {{_(\"Your Mailpile stores data here:\")}}<br>\n        <a target=_blank href='file:///{{ config.workdir }}/'>{{ config.workdir }}</a>\n      </p>\n      <p class=\"what\">\n        <span class=\"icon icon-lock-closed\"></span>\n        {{_(\"Mailpile can encrypt your e-mail, search engine and settings.\")}}\n        {{_(\"This protects your privacy, even if your computer gets lost or stolen.\")}}\n      </p>\n      <p class=\"risks\">\n        <span class=\"icon icon-dislike\"></span>\n        {{_(\"Encryption makes it harder to migrate your data to another e-mail client, slows things down and may increase the odds of data loss.\")}}\n        {{_(\"Losing your encryption key becomes equivalent to losing the data.\")}}\n      {%- if not unconfigured %}\n        <br>\n        <a href=\"{{ U('/backup/download/?csrf=') }}{{ csrf_token }}\">\n          {{ _(\"Download a backup of current settings and keys.\") }}</a>\n      {%- endif %}\n      </p>\n      <p class=\"risks\">\n        <span class=\"icon icon-key\"></span>\n        {{_(\"Also keep in mind that the security of your data depends entirely on the strength of your password.\")}}\n        <br>\n        <a href=\"{{ U('/setup/password/') }}\">\n          {{ _(\"You can change your Mailpile Password here.\") }}</a>\n      </p>\n    </div>\n    <div class=\"settings\">\n      {# We don't bother new users with this, they will be prompted to\n       # enable deletion later, once they've tried the app for a while.\n       #}\n      {%- if not unconfigured %}\n      <input name=\"prefs.allow_deletion\" type=\"radio\" value=\"false\"\n             {%- if not config.prefs.allow_deletion %} checked{% endif %}>\n      <span class=\"checkbox\">{{_(\"Off\")}}</span>\n      <input name=\"prefs.allow_deletion\" type=\"radio\" value=\"true\"\n            {%- if config.prefs.allow_deletion %} checked{% endif %}>\n      <span class=\"checkbox\">{{_(\"On\")}}</span>\n      &nbsp; - &nbsp; {{_(\"Allow deletion of e-mail from servers and mailboxes.\")}}\n      <br style=\"margin-bottom: 0.75em;\">\n      {%- endif %}\n\n{# FIXME: Make this work!\n      <input name=\"prefs.backup_to_web\" type=\"radio\" value=\"false\"\n             {%- if not config.prefs.backup_to_web %} checked{% endif %}>\n      <span class=\"checkbox\">{{_(\"Off\")}}</span>\n      <input name=\"prefs.backup_to_web\" type=\"radio\" value=\"true\"\n            {%- if config.prefs.backup_to_web %} checked{% endif %}>\n      <span class=\"checkbox\">{{_(\"On\")}}</span>\n      &nbsp; - &nbsp; {{_(\"Backup settings and keys to mobile web app\")}}\n      <span title=\"{{_('Recommended setting')}}\"\n            class=\"icon default icon-star\"></span>\n      <br>\n\n      {% set bte_on = (config.prefs.backup_to_email not in (False, None, '-', '')) %}\n      <input name=\"prefs.backup_to_email\" type=\"radio\" value=\"-\"\n             {%- if not bte_on %} checked{% endif %}>\n      <span class=\"checkbox\">{{_(\"Off\")}}</span>\n      <input name=\"prefs.backup_to_email\" type=\"radio\"\n             value=\"{{ config.prefs.backup_to_email }}\"\n             {%- if bte_on %} checked{% endif %}>\n      <span class=\"checkbox\">{{_(\"On\")}}</span>\n      &nbsp; - &nbsp; {{_(\"Backup settings and keys to e-mail\")-}}\n        <span id=\"backup_email\"{% if not bte_on %} class=\"hide\"{% endif %}>: \n        <b>{{ config.prefs.backup_to_email }}</b></span>\n      <br style=\"margin-bottom: 0.75em;\">\n#}\n\n      <input name=\"prefs.encrypt_vcards\" type=\"radio\" value=\"false\" class=\"crypto\"\n             {%- if not config.prefs.encrypt_vcards %} checked{% endif %}>\n      <span class=\"checkbox\">{{_(\"Off\")}}</span>\n      <input name=\"prefs.encrypt_vcards\" type=\"radio\" value=\"true\" class=\"crypto\"\n            {%- if config.prefs.encrypt_vcards %} checked{% endif %}>\n      <span class=\"checkbox\">{{_(\"On\")}}</span>\n      &nbsp; - &nbsp; {{_(\"Encrypt the contact database.\")}}\n      <span title=\"{{_('Recommended setting')}}: {{_('On')}}\"\n            class=\"icon default icon-star\"></span>\n      <br>\n\n      <input name=\"prefs.encrypt_events\" type=\"radio\" value=\"false\" class=\"crypto\"\n             {%- if not config.prefs.encrypt_events %} checked{% endif %}>\n      <span class=\"checkbox\">{{_(\"Off\")}}</span>\n      <input name=\"prefs.encrypt_events\" type=\"radio\" value=\"true\" class=\"crypto\"\n            {%- if config.prefs.encrypt_events %} checked{% endif %}>\n      <span class=\"checkbox\">{{_(\"On\")}}</span>\n      &nbsp; - &nbsp; {{_(\"Encrypt the system event log.\")}}\n      <span title=\"{{_('Recommended setting')}}: {{_('On')}}\"\n            class=\"icon default icon-star\"></span>\n      <br>\n\n      <input name=\"prefs.encrypt_misc\" type=\"radio\" value=\"false\" class=\"crypto\"\n             {%- if not config.prefs.encrypt_misc %} checked{% endif %}>\n      <span class=\"checkbox\">{{_(\"Off\")}}</span>\n      <input name=\"prefs.encrypt_misc\" type=\"radio\" value=\"true\" class=\"crypto\"\n            {%- if config.prefs.encrypt_misc %} checked{% endif %}>\n      <span class=\"checkbox\">{{_(\"On\")}}</span>\n      &nbsp; - &nbsp; {{_(\"Encrypt other (miscellaneous) data.\")}}\n      <span title=\"{{_('Recommended setting')}}: {{_('On')}}\"\n            class=\"icon default icon-star\"></span>\n      <br>\n\n      <input name=\"prefs.encrypt_mail\" type=\"radio\" value=\"false\" class=\"crypto\"\n             {%- if not config.prefs.encrypt_mail %} checked{% endif %}>\n      <span class=\"checkbox\">{{_(\"Off\")}}</span>\n      <input name=\"prefs.encrypt_mail\" type=\"radio\" value=\"true\" class=\"crypto\"\n            {%- if config.prefs.encrypt_mail %} checked{% endif %}>\n      <span class=\"checkbox\">{{_(\"On\")}}</span>\n      &nbsp; - &nbsp; {{_(\"Encrypt locally stored e-mail.\")}}\n      <br>\n\n      <input name=\"prefs.encrypt_index\" type=\"radio\" value=\"false\" class=\"crypto\"\n             {%- if not config.prefs.encrypt_index %} checked{% endif %}>\n      <span class=\"checkbox\">{{_(\"Off\")}}</span>\n      <input name=\"prefs.encrypt_index\" type=\"radio\" value=\"true\" class=\"crypto\"\n            {%- if config.prefs.encrypt_index %} checked{% endif %}>\n      <span class=\"checkbox\">{{_(\"On\")}}</span>\n      &nbsp; - &nbsp; {{_(\"Strongly encrypt the local search index (slow).\")}}\n      <br style=\"margin-bottom: 0.75em;\">\n\n      {%- set shared = (not unconfigured) and (not config.sys.gpg_home) %}\n      {%- set private_gpghome = (config.sys.gpg_home or config.workdir) %}\n      <input name=\"sys.gpg_home\" type=\"radio\" class=\"gpghome\"\n             value=\"{{ private_gpghome }}\"\n             {%- if not shared %} checked{% endif %}>\n      <span title=\"GNUPGHOME={{ private_gpghome }}\"\n            class=\"checkbox\">{{_(\"Off\")}}</span>\n      <input name=\"sys.gpg_home\" type=\"radio\" class=\"gpghome\"\n             value=\"{Blank}\"\n             {%- if shared %} checked{% endif %}>\n      <span class=\"checkbox\">{{_(\"On\")}}</span>\n      &nbsp; - &nbsp; {{_(\"Use shared GnuPG keychain for PGP encryption keys.\")}}\n      <br>\n\n      <br>\n      <b>{{_(\"Notes:\")}}</b>\n      {%- set crypto_note = _(\"Changing encryption settings will only affect data created or edited from now on.\") %}\n      <ul class=\"notes\">\n      {%- if not unconfigured %}\n        <li>{{ crypto_note }}</li>\n        <li>{{_(\"Backups are protected by the same password and encryption as the configuration.\")}}</li>\n      {%- endif %}\n        <li>{{_(\"The configuration is always kept encrypted, because it may contain passwords.\")}}</li>\n        <li>{{_(\"The search index is always at least partially encrypted because it is so sensitive.\")}}</li>\n      </ul>\n\n      {%- if not unconfigured %}\n      <script type=\"text/javascript\">\n        $(document).ready(function() {\n          $('input.gpghome').click(function(ev) {\n            return confirm(\n              '{{ _(\"WARNING\")|escapejs }}:\\n  * ' +\n              '{{ _(\"Changing GnuPG keychains may prevent decryption of old e-mail!\")|escapejs }}\\n' +\n              '\\n' +\n              '{{ _(\"You will need to manually copy your PGP keys from one keychain to the other.\")|escapejs }}');\n          });\n          $('input.crypto').click(function(ev) {\n            alert(\n              '{{ _(\"WARNING\")|escapejs }}:\\n  * ' +\n              '{{ crypto_note|escapejs }}');\n          });\n        });\n      </script>\n      {%- endif %}\n\n    </div>\n    <br clear=\"both\">\n  </div>\n\n  <span style=\"position: fixed; bottom: 15px; border: 5px solid #eee;\">\n    <button class=\"button-primary\" type=\"submit\">{{_(\"Save Settings\")}}</button>\n  </span>\n </form>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/settings/recipes.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n\n{% macro render_form_settings_profile(routes) -%}  \n  <label>{{_(\"Name\")}}</label>\n  <input type=\"text\" name=\"name\" value=\"\" placeholder=\"{{_(\"Chelsea Manning\")}}\" id=\"profile-add-name\">\n  <label>{{_(\"Email\")}}</label>\n  <input type=\"text\" name=\"email\" value=\"\" placeholder=\"{{_(\"chelsea@email.com\")}}\" autocorrect=\"off\" autocapitalize=\"off\" id=\"profile-add-email\">\n  <label>{{_(\"Delivery route\")}}</label>\n  <select name=\"route\">\n    {% for route in routes %}\n    <option value=\"{{route}}\">{{routes[route].name}}</option>\n    {% endfor %}\n  </select>\n{%- endmacro %}\n\n{% macro render_form_settings_route(rid, route) -%}\n  <input type=\"hidden\" name=\"route_id\" value=\"{{rid}}\"/>\n  <label>{{_(\"Route name\")}}</label>\n  <input type=\"text\" name=\"name\" value=\"{{route.name or \"\"}}\" placeholder=\"{{_(\"my delivery route\")}}\" id=\"route-add-name\"/>\n  <label>{{_(\"Login\")}}</label>\n  <input type=\"text\" name=\"username\" value=\"{{route.username or \"\"}}\" placeholder=\"{{_(\"username@smtp.mailserver.org\")}}\" autocorrect=\"off\" autocapitalize=\"off\" id=\"route-add-username\"/>\n  <label>{{_(\"Password\")}}</label>\n  <input type=\"password\" name=\"password\" value=\"{{route.password or \"\"}}\" placeholder=\"1234567890\" autocorrect=\"off\" autocapitalize=\"off\" id=\"route-add-password\"/>\n  <label>{{_(\"Server\")}}</label>\n  <input type=\"text\" name=\"host\" value=\"{{route.host or \"\"}}\" placeholder=\"{{_(\"smtp.mailserver.org\")}}\" id=\"route-add-server\"/>\n  <label>{{_(\"Port\")}}</label>\n  <select name=\"port\" id=\"route-add-port\">\n    <option {% if route.port == 25 %}selected {% endif %}value=\"25\">25</option>\n    <option {% if route.port == 587 %}selected {% endif %}value=\"587\">587</option>\n    <option {% if route.port == 465 %}selected {% endif %}value=\"465\">465 (TLS)</option>\n  </select>\n{%- endmacro %}\n\n{% block content %}\n\n{% if \"profiles\" in result %}\n<!-- Profiles - Accessed via /settings/profiles -->\n<div id=\"settings-profiles\" class=\"content-normal\">\n  <button id=\"btn-settings-profile-add\" class=\"right\"><span class=\"icon-plus\"></span> {{_(\"Add Profile\")}}</button>\n  <h3>{{_(\"Your Profiles\")}}</h3>\n  <ul class=\"items\">\n    {% for profile in result.profiles %}\n    <li class=\"separate\">\n      <h3>{{profile.name}}</h3>\n      <label>{{_(\"Address:\")}}</label> {{ profile.email }}<br/>\n      <label>{{_(\"Route:\")}}</label> {{ config.routes[profile.messageroute].name }}<br/>\n      <ul class=\"horizontal right\">\n        <li><a href=\"\" onclick=\"$('.route').hide();$('#route_{{id}}').show();\"><span class=\"icon-settings\"></span> {{_(\"Edit\")}}</a></li>\n        <li><a href=\"\"><span class=\"icon-circle-x\"></span> {{_(\"Delete\")}}</a></li>\n      </ul>\n      <form id=\"form-settings-profile-\" class=\"form-settings-profile-edit hide standard\">{{ csrf_field|safe }}\n        {{ render_form_settings_profile(config.routes) }}\n      </form>\n    </li>\n    {% endfor %}\n  </ul>\n</div>\n{% endif %}\n\n{% if \"routes\" in result %}\n<!-- Routes - Accessed via /settings/routes -->\n<div id=\"settings-routes\" class=\"content-normal\">\n  <button id=\"btn-settings-route-add\" class=\"right\"><span class=\"icon-plus\"></span> {{_(\"Add Route\")}}</button>\n  <h3>{{_(\"Routes\")}}</h3>\n  <ul class=\"items\">\n  {% for rid in result.routes %}\n  {% set route = result.routes[rid] %}\n    <li class=\"separate\">\n      <h4 class=\"\">{{route.name}}</h4>\n      <ul class=\"horizontal right\">\n        <li><a href=\"#\" onclick=\"$('.route').hide();$('#route_{{id}}').show();\"><span class=\"icon-settings\"></span> {{_(\"Edit\")}}</a></li>\n        <li><a href=\"#\"><span class=\"icon-circle-x\"></span> {{_(\"Delete\")}}</a></li>\n      </ul>\n      <form id=\"form-settings-route-{{rid}}\" class=\"form-settings-route-edit hide standard\">{{ csrf_field|safe }}\n        <h4>{{_(\"Edit Route\")}}</h4>\n        {{ render_form_settings_route(rid, route) }}\n        <input type=\"submit\" value=\"Cancel\" class=\"button-info\">\n        <input type=\"submit\" value=\"Save\" class=\"button-primary\">\n      </form>\n    </li>\n  {% endfor %}\n  </ul>\n</div>\n{% endif %}\n\n{% if \"prefs\" in result %}\n<!-- Preferences - Accessed via /settings/prefs -->\n<div id=\"settings-preferrences\" class=\"content-normal\">\n  <h3>{{_(\"Preferences\")}}</h3>\n  <a class=\"button\" id=\"notifications-permission-option\">{{_(\"Permit browser notifications\")}}</a>\n  <ul>\n    {% for p in result.prefs %}\n    <li>{{p}}</li>\n    {% endfor %}\n  </ul>\n</div>\n{% endif %}\n\n{% if \"sys\" in result %}\n<!-- Advanced - Accessed via /settings/sys -->\n<div id=\"settings-advanced\" class=\"content-normal\">\n  <h3>{{_(\"Advanced Settings\")}}</h3>\n  <ul>\n    {% for p in result.sys %}\n    <li>{{p}}</li>\n    {% endfor %}\n  </ul>\n</div>\n{% endif %}\n\n<!-- Settings - Modal for form to add a new profile -->\n<script id=\"modal-settings-profile-add\" type=\"text/template\">\n  <div class=\"modal-dialog\">\n  <form id=\"form-settings-profile-add\" class=\"standard\" action=\"\" method=\"POST\">{{ csrf_field|safe }}\n    <div class=\"modal-content\">\n      <div class=\"modal-header\">\n        <button type=\"button\" class=\"close button-info\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n        <h4 class=\"modal-title\"><span class=\"icon-profiles\"></span> {{_(\"Add Profile\")}}</h4>\n      </div>\n      <div class=\"modal-body\">\n        {{ render_form_settings_profile(config.routes) }}\n      </div>\n      <div class=\"modal-footer\">\n        <button class=\"button-primary\" type=\"submit\"><span class=\"icon-plus\"></span> {{_(\"Add Profile\")}}</button>\n      </div>\n    </div>\n  </form>\n</script>\n\n<!-- Settings - Modal for form to add a new route -->\n<script id=\"modal-settings-route-add\" type=\"text/template\">\n  <div class=\"modal-dialog\">\n  <form id=\"form-settings-route-add\" class=\"standard\" action=\"\" method=\"POST\">{{ csrf_field|safe }}\n    <div class=\"modal-content\">\n      <div class=\"modal-header\">\n        <button type=\"button\" class=\"close button-info\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n        <h4 class=\"modal-title\"><span class=\"icon-routes\"></span> {{_(\"Add Route\")}}</h4>\n      </div>\n      <div class=\"modal-body\">\n        {{ render_form_settings_route('add', config.routes) }}\n      </div>\n      <div class=\"modal-footer\">\n        <button class=\"button-primary\" type=\"submit\"><span class=\"icon-plus\"></span> {{_(\"Add Route\")}}</button>\n      </div>\n    </div>\n  </form>\n</script>\n\n{% endblock %}\n{% block title %}\n  {% if \"profiles\" in result %}\n    {{_(\"Profiles\")}} | {{_(\"Settings\")}}  \n  {% elif \"routes\" in result %}\n    {{_(\"Routes\")}} | {{_(\"Settings\")}}\n  {% elif \"prefs\" in result %}\n    {{_(\"Preferences\")}} | {{_(\"Settings\")}}\n  {% elif \"sys\" in result %}\n    {{_(\"Advanced\")}} | {{_(\"Settings\")}}\n  {% else %}\n    ??? | {{_(\"Settings\")}}\n  {% endif %}\n{% endblock %}\n\n"
  },
  {
    "path": "shared-data/default-theme/html/settings/set/index.html",
    "content": "{%- extends \"layouts/\" + render_mode + \".html\" %}\n{%- block title %}{{_(\"Changed Settings\")}}{% endblock %}\n{%- block content %}\n{{ mailpile('http/redirect', ui_return) }}\n{%- endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/settings/set/password/index.html",
    "content": "{%- extends \"logs/layout.html\" %}\n{%- block content %}\n<div class=\"content-normal\" style=\"max-width: 50em; position: relative\">\n\n  {% if not result.accounts %}\n  <div id=\"identity-vault-lock\" class=\"vault-lock-outer\">\n    <div class=\"vault-lock-inner\">\n      <div class=\"vault-lock animated\"><span class=\"icon icon-user\"></span></div>\n    </div>\n  </div>\n  {% else %}\n  <h1>\n    <span class=\"icon icon-user\"></span>\n    {{_(\"Account Password Management\")}}\n  </h1>\n  {% endif %}\n\n  {% if result.account or result.accounts %}\n  <form id=\"form-set-password\" class=\"standard text-center\"\n        method=\"POST\" action=\"{{ U(\"/settings/set/password/\") }}\">\n    {{ csrf_field|safe }}\n\n    {# FIXME: Make these user-configurable? #}\n    <input type=\"hidden\" name=\"update_mailsources\" value=\"Yes\">\n    <input type=\"hidden\" name=\"update_mailroutes\" value=\"Yes\">\n\n    {% if not result.account %}\n    <label>{{ _(\"Remote Accounts Using Passwords\") }}</label>\n    {% endif %}\n    <ul class=\"radio-list add-bottom\">\n      {%- macro render_account(acct_info, first) %}\n        <li class=\"account-info text-left\" style=\"overflow: hidden;\"\n            onclick=\"javascript:_ssp.policy_changing();\">\n        <label class=\"radio-list-item\">\n          <div class=\"radio text-right\">\n            <input name=\"id\" class='ssp_account_fp' type=\"radio\" tabindex=\"1\"\n                   value=\"{{ acct_info.username }}\"\n                   data-policy=\"{{ acct_info.policy }}\"\n                   {%- if result.account %} style=\"display: none;\"{% endif %}\n                   {%- if first %} checked{% endif %}>\n          </div>\n          <div>\n            <span class=\"icon icon-user\"></span>\n          </div>\n          <div style=\"white-space: nowrap; font-size: 0.8em;\"\n               {%- if acct_info.source and acct_info.route %}\n               title='{{ _(\"This account is used for receiving and sending e-mail.\") }}'\n               {%- elif acct_info.source %}\n               title='{{ _(\"This account is used for receiving e-mail.\") }}'\n               {%- elif acct_info.route %}\n               title='{{ _(\"This account is used for sending e-mail.\") }}'\n               {%- endif %}>\n            <span style=\"font-size: 1.3em;\">\n              {{ acct_info.username }}\n            </span>\n            ({%- if acct_info.policy == 'protect' -%}\n              {{- _(\"Managed by Mailpile\") -}}\n            {%- elif acct_info.policy == 'store' -%}\n              {{- _(\"Unlocked, password remembered\") -}}\n            {%- elif acct_info.policy == 'cache-only' -%}\n              {{- _(\"Unlocked\") -}}\n            {%- else %}\n              {{- _(\"Locked\") -}}\n            {%- endif %})\n            <br>\n            {%- if acct_info.route %}\n              {{ _(\"Sends e-mail via. {H}\").format(H=acct_info.route) }}<br>\n            {%- endif %}\n            {%- if acct_info.source %}\n              {{ _(\"Downloads e-mail from {H}\").format(H=acct_info.source) }}<br>\n            {%- endif %}\n          </div>\n        </label>\n        </li>\n      {%- endmacro %}\n      {%- if result.account %}\n        {{ render_account(result.account, 1) }}\n      {%- else %}\n        {%- for acct_info in result.accounts.values() %}\n          {{ render_account(acct_info, loop.first) }}\n        {%- endfor %}\n      {%- endif %}\n    </ul>\n\n    <div class=\"results-messages\" style=\"font-size: 1.2em;\">\n    {%- if result.error %}\n      <p class=\"text-center result-title\" data-timer=3000><b>{{ result.error }}</b></p>\n    {%- elif result.account and result.op_completed %}\n      {%- if result.stored_password %}\n      <p class=\"text-center result-title ssp-stored-password\" data-timer=10000>\n        {{ _(\"The password is:\") }} <b>{{ result.stored_password }}</b>\n      </p>\n      {%- else %}\n      <p class=\"text-center result-title\" data-timer=3000><b>{{ title }}</b></p>\n      {%- endif %}\n    {%- endif %}\n    </div>\n\n    <div class=\"ssp-ops\">\n\n      <div class=\"ssp-unlock ssp-lock\">\n        <label>{{_(\"Account Password\")}}</label>\n      </div>\n      <div class=\"hide ssp-show-passphrase\">\n        <label>{{_(\"Your Mailpile Password\")}}</label>\n      </div>\n\n      <div id=\"setup-passphrase-existing-confirm\">\n        <span id=\"validation-passphrase_confirm\" class=\"ssp-password\">\n          <label class=\"validation-message\"></label>\n          <input id=\"input-setup-passphrase_confirm\"\n                 class=\"medium center\" type=\"password\" name=\"password\"\n                 style=\"width: 25em; margin-bottom: 0.5em;\"\n                 autocorrect=\"off\" autocapitalize=\"off\"\n                 placeholder=\"top secret super duper password\" tabindex=\"3\">\n        </span>\n\n        <select id='policy-ttl' name=\"policy-ttl\" class=\"text-center\"\n                onchange=\"javascript:_ssp.policy_changed();\"\n                style=\"width: 21.5em; margin: -1px 0.5em 2em 0; display: inline; padding: 7px;\"\n                tabindex=4>\n          <option data-p='cache-only' value=\"cache-only\" selected>{{_(\"Unlock Account\")}}</option>\n          <option data-p='forget' value=\"forget\">{{_(\"Lock Account and Forget Password\")}}</option>\n          <option data-p='cache-only' value=\"cache-only\"></option>\n          <option data-p='cache-only' value=\"cache-only:600\">{{_(\"Unlock Account\")}} ({{_(\"10 minutes\")}})</option>\n          <option data-p='cache-only' value=\"cache-only:3600\">{{_(\"Unlock Account\")}} ({{_(\"1 hour\")}})</option>\n          <option data-p='cache-only' value=\"cache-only:43200\">{{_(\"Unlock Account\")}} ({{_(\"12 hours\")}})</option>\n          <option data-p='cache-only' value=\"cache-only:600\"></option>\n          <option data-p='store' value=\"store\">{{_(\"Unlock Account and Remember Password\")}}</option>\n          <option data-p='display' value=\"display\">{{_(\"Display Remembered Account Password\")}}</option>\n        </select>\n        <button style=\"width: 5em;\" class=\"button-primary\" type=\"submit\" tabindex=5>\n          <span class=\"icon icon-user\"></span>\n        </button>\n      </div>\n\n      <div class=\"ssp-hint ssp-unlock\">\n        <p><i>\n          {{ _(\"Unlocking an account allows Mailpile to use it to send and receive e-mail.\") }}\n          <span class=\"ssp-hint ssp-cache-only\"><br>\n            {{ _(\"By default, unlocked accounts stay accessible until you restart Mailpile.\") }}\n          </span>\n          <span class=\"ssp-hint ssp-store hide\"><br>\n            {{ _(\"If Mailpile remembers the password, you will not have to unlock the account again.\") }}\n          </span>\n        </i></p>\n      </div>\n      <div class=\"ssp-hint ssp-lock hide\">\n        <p><i>\n          {{ _(\"Locked accounts can not be used to send or receive e-mail.\") }}\n        </i></p>\n      </div>\n      <div class=\"ssp-hint ssp-show-passphrase hide\">\n        <p><i>\n          {{ _(\"Please authenticate using your Mailpile password.\") }}\n        </i></p>\n      </div>\n\n    {% if state.query_args.ui_redirect_back %}\n      <input type=\"hidden\" class=\"current-url\" name=\"redirect\" value=\"\">\n    {% elif False and not result.account %}\n      <input type=\"hidden\" name=\"redirect\"\n             value=\"{{ U('/settings/set/password/') }}\">\n    {% endif %}\n    </div>\n\n  </form>\n\n  <script>\n    var page_url = document.location.href;\n    var ui_hint = 'ui_account_auth=1';\n    var ui_force_login = {{ 'true' if ui_force_login else 'false' }};\n    if (page_url.indexOf(ui_hint) != -1) {\n      ui_hint = '';\n    }\n    else {\n      ui_hint = ((page_url.indexOf('?') == -1) ? '?' : '&') + ui_hint;\n    }\n    $('input.current-url').attr('value', page_url + ui_hint);\n\n    var _ssp = {\n      policy_changing: function(i) {\n        setTimeout(\"_ssp.policy_changed();\", 50);\n      },\n      policy_changed: function(i) {\n        $('input.ssp_account_fp').closest('li').css({'opacity': 0.6});\n        $('.ssp-cache-only, .ssp-store').hide();\n        $('select#policy-ttl option').show();\n        $('.ssp-password').show();\n\n        var op = $('select#policy-ttl').val();\n        var account = $('input:checked.ssp_account_fp').eq(0);\n\n        if (account && account.val()) {\n          // FIXME: Adjust which selections are available, report\n          //        whether we've already unlocked this account?\n\n          account.closest('li').css({'opacity': 1.0});\n\n          var policy = account.data('policy');\n          if (ui_force_login) {\n            ui_force_login = false;\n            policy = 'force-login';\n          }\n\n          if (policy) $('select#policy-ttl option[data-p='+ policy +']').hide();\n          if (policy == 'protect') {\n            $('select#policy-ttl option').hide();\n            $('select#policy-ttl option[value=display]').show();\n            $('select#policy-ttl option[value=export]').show();\n            $('select#policy-ttl option[value=revoke]').show();\n            if (op != 'display' && op != 'export' && op != 'revoke') {\n              op = 'display';\n            }\n          }\n          else if (policy == 'store') {\n            $('select#policy-ttl option[data-p=cache-only]').hide();\n            if (op == 'cache-only') op = 'forget';\n          }\n          else if (policy == 'cache-only') {\n            $('select#policy-ttl option[data-p=cache-only]').hide();\n            $('select#policy-ttl option[value=store]').hide();\n            $('select#policy-ttl option[value=display]').hide();\n            op = 'forget';\n          }\n          else {\n            $('select#policy-ttl option[value=forget]').hide();\n            $('select#policy-ttl option[value=display]').hide();\n            if (op == 'forget' || op == 'display') op = 'cache-only';\n          }\n        }\n\n        $('select#policy-ttl').val(op);\n        if (op == 'cache-only') { $('.ssp-cache-only').show();}\n        if (op == 'store') { $('.ssp-store').show();}\n        if (op == 'fail' || op == 'forget') {\n          $('.ssp-password').hide();\n          $('.ssp-unlock, .ssp-show-passphrase, .ssp-hint').hide();\n          $('.ssp-lock').show();\n        }\n        else if (op == 'display') {\n          $('.ssp-lock, .ssp-unlock, .ssp-hint').hide();\n          $('.ssp-show-passphrase').show();\n        }\n        else if (op == 'revoke' || op == 'export') {\n          $('.ssp-lock, .ssp-show-passphrase').hide();\n          $('.ssp-unlock').show();\n          $('.ssp-hint').hide();\n          $('.ssp-' + op).show();\n        }\n        else {\n          $('.ssp-lock, .ssp-show-passphrase, .ssp-hint').hide();\n          $('.ssp-unlock').show();\n        }\n\n        $('input[type=password]').focus();\n      },\n      hide_title: function() {\n        $('.result-title').slideUp();\n        $('.ssp-ops').slideDown();\n        $('input[type=password]').focus();\n{%- if ui_oneshot %}\n        $('.modal-header button.close').click();\n{%- endif %}\n      }\n    };\n\n    setTimeout(\"_ssp.policy_changed();\", 50);\n    if ($('.result-title').length > 0) {\n      var hideid = setTimeout(\"_ssp.hide_title();\", $('.result-title').data('timer'));\n      $('form').submit(function(ev) { clearTimeout(hideid); });\n      $('.ssp-ops').hide();\n    }\n  </script>\n  {% else %}\n\n   <div class='text-center'>\n     <h3>{{_(\"No Saved Passwords Here!\")}}</h3>\n     <p>\n       {{_(\"Hopefully that is a good thing...\")}}\n     </p>\n   </div>\n\n   <pre class=\"hide\">{{ result|json }}</pre>\n\n  {% endif %}\n\n</div>\n{%- endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/settings/set/password/keys.html",
    "content": "{%- extends \"logs/layout.html\" %}\n{%- block content %}\n{%- if result.policy %}\n\n{%- else %}\n<div class=\"content-normal\" style=\"max-width: 50em; position: relative\">\n\n  {% if result.key or not result.keylist %}\n  <div id=\"identity-vault-lock\" class=\"vault-lock-outer\">\n    <div class=\"vault-lock-inner\">\n    {% if result.key %}\n      <div class=\"vault-lock icon-lock-closed animated\"></div>\n    {% else %}\n      <div class=\"vault-lock animated\"></div>\n    {% endif %}\n    </div>\n  </div>\n  {% else %}\n  <h1>\n    <span class=\"icon icon-key\"></span>\n    {{_(\"Encryption Key Management\")}}\n  </h1>\n  {% endif %}\n\n  {% if result.key or result.keylist %}\n  <form id=\"form-set-password\" class=\"standard text-center\"\n        method=\"POST\" action=\"{{ U(\"/settings/set/password/keys.html\") }}\">\n    {{ csrf_field|safe }}\n\n    {% if not result.key %}\n    <label>{{ _(\"Keys on Your GnuPG Key-Chain\") }}</label>\n    {% endif %}\n    <ul class=\"radio-list add-bottom\">\n      {%- macro render_key(key_info, first) %}\n        {% if key_info.capabilities_map.encrypt and key_info.capabilities_map.sign and not key_info.disabled %}\n        <li class=\"key-info text-left\" style=\"overflow: hidden; position: relative;\"\n            onclick=\"javascript:_ssp.policy_changing();\">\n        <label class=\"radio-list-item\">\n          <div class=\"radio text-right\">\n            <input name=\"id\" class='ssp_key_fp' type=\"radio\" tabindex=\"1\"\n                   value=\"{{ key_info.fingerprint }}\"\n                   data-policy=\"{{ key_info.policy }}\"\n                   {%- if result.key %} style=\"display: none;\"{% endif %}\n                   {%- if first %} checked{% endif %}>\n          </div>\n          <div style=\"min-width: 7em; width: 7em; text-align: center;\"\n               title=\"{{ _(\"Key Fingerprint\") }}\">\n            <p style=\"font-family: monospace; font-style: normal; margin: 0; padding: 0; line-height: 16px\">\n              {% for grp in group_fingerprint(key_info.fingerprint) %}\n                <span style=\"background: #a{{grp[0]}}a{{grp[1:]}}; font-size: 12px; color: #000000\">\n                  {{- grp -}}\n                </span>\n              {% endfor %}\n            </p>\n          </div>\n          <div style=\"white-space: nowrap; font-size: 0.8em;\"\n               {%- if not key_info.accounts or key_info.accounts|length < 1 %}\n               title='{{ _(\"This key may be used to decrypt incoming e-mail.\") }}'\n               {%- else %}\n               title='{{ _(\"This key is used for decrypting and signing e-mail.\") }}'\n               {%- endif %}>\n            <span style=\"font-size: 1.3em;\">\n              {{ _(\"{B} bit {T} key\").format(B=key_info.keysize,\n                                             T=key_info.keytype_name) }}\n            </span>\n            <span style=\"opacity: 0.75; font-size: 0.9em;\">\n            ({%- if key_info.policy == 'protect' -%}\n              {{- _(\"Managed by Mailpile\") -}}\n            {%- elif key_info.policy == 'store' -%}\n              {{- _(\"Unlocked, password remembered\") -}}\n            {%- elif key_info.policy == 'cache-only' -%}\n              {{- _(\"Unlocked\") -}}\n            {%- else %}\n              {{- _(\"Locked\") -}}\n            {%- endif %})\n            </span>\n            <br>\n            <br>\n            {%- if key_info.expiration_date %}\n              {{- _(\"Valid from {D1} to {D2}.\"\n                    ).format(D1=key_info.creation_date,\n                             D2=key_info.expiration_date) -}}\n            {%- else -%}\n              {{- _(\"Valid from {D}.\").format(D=key_info.creation_date) -}}\n            {%- endif %}\n            <br>\n            {%- set uid_summary = (key_info.uids|length > 1) or (key_info.accounts|length > 0) %}\n            {%- if uid_summary %}\n            <i class=\"ssp-key-uid-summary\">\n               {{- _(\"Identities on key: {N}\"\n                     ).format(N=key_info.uids|length) }}\n               (<a onclick=\"javascript:_ssp.show_uids(this);\" href=\"#\"\n                   style=\"font-size: 0.9em; font-style: italic;\">{{ _(\"show all\") }}</a>)\n            <br></i>\n            {%- endif %}\n            {%- for uid in key_info.uids %}\n            <i class=\"ssp-key-uids{%- if uid_summary %} hide {% endif %}\" style=\"font-size: 1em;\">\n              {{ _(\"ID on key:\") }}\n              {% if uid.name %}{{uid.name}} {% endif%}\n              {%- if uid.email %}&lt;{{uid.email}}&gt;\n              {%- elif uid.comment %}{{uid.comment.decode('utf-8') }}\n              {%- endif %}\n            <br></i>\n            {%- endfor %}\n            {%- if not key_info.accounts or key_info.accounts|length < 1 %}\n            <i>\n              {{ _(\"Not currently linked with any accounts.\") }}\n            <br></i>\n            {%- endif %}\n            {%- for act in key_info.accounts %}\n            <i>\n              {{ _(\"Account\") }}:\n              <a style=\"font-size: 1em; font-style: italic\"\n                 title='{{ _(\"Edit account security settings\") }} ...'\n                 onclick=\"javascript:Mailpile.profile_edit('{{ act.rid }}', 'security');\"\n                 href=\"#\">{{ act.name }} &lt;{{ act.email }}&gt;</a>\n            <br></i>\n            {%- endfor %}\n            <a href=\"{{ U('/crypto/gpg/key/', key_info.fingerprint, '/') }}\"\n                 class=\"auto-modal auto-modal-sticky\"\n                 style=\"opacity: 0.4; position: absolute; top: 5px; right: 5px;\"\n                 title=\"{{ _(\"Download Public Key\") }}\">\n               <span class=\"icon icon-download\" style=\"font-size: 0.9em;\"></span>\n            </a>\n          </div>\n        </label>\n        </li>\n        {%- endif %}\n      {%- endmacro %}\n      {%- if result.key %}\n        {{ render_key(result.key, 1) }}\n      {%- else %}\n        {%- for key_info in result.keylist.values() %}\n          {{ render_key(key_info, loop.first) }}\n        {%- endfor %}\n      {%- endif %}\n    </ul>\n\n    <div class=\"results-messages\" style=\"font-size: 1.2em;\">\n    {%- if result.error %}\n      <p class=\"text-center result-title\" data-timer=3000><b>{{ result.error }}</b></p>\n    {%- elif result.key and result.op_completed %}\n      {%- if result.stored_password %}\n      <p class=\"text-center result-title ssp-stored-password\" data-timer=10000>\n        {{ _(\"The password is:\") }} <b>{{ result.stored_password }}</b>\n      </p>\n      {%- else %}\n      <p class=\"text-center result-title\" data-timer=3000><b>{{ title }}</b></p>\n      {%- endif %}\n    {%- endif %}\n    </div>\n\n    <div class=\"ssp-ops\">\n\n      <div class=\"ssp-unlock-key ssp-lock-key\">\n        <label>{{_(\"Key Password\")}}</label>\n      </div>\n      <div class=\"hide ssp-show-passphrase\">\n        <label>{{_(\"Your Mailpile Password\")}}</label>\n      </div>\n\n      <div id=\"setup-passphrase-existing-confirm\">\n        <span id=\"validation-passphrase_confirm\">\n          <label class=\"validation-message\"></label>\n          <input id=\"input-setup-passphrase_confirm\"\n                 class=\"medium center\" type=\"password\" name=\"password\"\n                 style=\"width: 25em; margin-bottom: 0.5em;\"\n                 autocorrect=\"off\" autocapitalize=\"off\"\n                 placeholder=\"top secret super duper password\" tabindex=\"3\">\n        </span>\n\n        <select id='policy-ttl' name=\"policy-ttl\" class=\"text-center\"\n                onchange=\"javascript:_ssp.policy_changed();\"\n                style=\"width: 21.5em; margin: -1px 0.5em 2em 0; display: inline; padding: 7px;\"\n                tabindex=4>\n          <option data-p='cache-only' value=\"cache-only\" selected>{{_(\"Unlock Encryption Key\")}}</option>\n          <option data-p='forget' value=\"forget\">{{_(\"Lock Encryption Key\")}}</option>\n          <option data-p='cache-only' value=\"cache-only\"></option>\n          <option data-p='cache-only' value=\"cache-only:600\">{{_(\"Unlock Encryption Key\")}} ({{_(\"10 minutes\")}})</option>\n          <option data-p='cache-only' value=\"cache-only:3600\">{{_(\"Unlock Encryption Key\")}} ({{_(\"1 hour\")}})</option>\n          <option data-p='cache-only' value=\"cache-only:43200\">{{_(\"Unlock Encryption Key\")}} ({{_(\"12 hours\")}})</option>\n          <option data-p='cache-only' value=\"cache-only:600\"></option>\n          <option data-p='store' value=\"store\">{{_(\"Unlock Key and Remember Password\")}}</option>\n          <option data-p='display' value=\"display\">{{_(\"Display Remembered Key Password\")}}</option>\n{#\n # FIXME: We should offer these options at some point!\n #\n #        <option value=\"export\">{{_(\"Download Raw Key Material (Export)\")}}</option>\n #        <option value=\"publish\">{{_(\"Publish Key in Public Key Directories)}}</option>\n #        <option value=\"revoke\">{{_(\"Revoke and Disable this Key\")}}</option>\n #}\n        </select>\n        <button style=\"width: 5em;\" class=\"button-primary\" type=\"submit\" tabindex=5>\n          <span class=\"icon icon-key\"></span>\n        </button>\n      </div>\n\n      <div class=\"ssp-hint ssp-unlock-key\">\n        <p><i>\n          {{ _(\"Unlocking a key allows Mailpile to use it for decryption and creating digital signatures.\") }}\n          <span class=\"ssp-hint ssp-cache-only\"><br>\n            {{ _(\"By default, unlocked keys stay accessible until you restart Mailpile.\") }}\n          </span>\n          <span class=\"ssp-hint ssp-store hide\"><br>\n            {{ _(\"If Mailpile remembers the password, you will not have to unlock the key again.\") }}\n          </span>\n        </i></p>\n      </div>\n      <div class=\"ssp-hint ssp-lock-key hide\">\n        <p><i>\n          {{ _(\"Locked keys can not be used for decryption or digital signatures.\") }}\n        </i></p>\n      </div>\n      <div class=\"ssp-hint ssp-show-passphrase hide\">\n        <p><i>\n          {{ _(\"Please authenticate using your Mailpile password.\") }}\n        </i></p>\n      </div>\n\n    {% if state.query_args.ui_redirect_back %}\n      <input type=\"hidden\" class=\"current-url\" name=\"redirect\" value=\"\">\n    {% elif False and not result.key %}\n      <input type=\"hidden\" name=\"redirect\"\n             value=\"{{ U('/settings/set/password/keys.html') }}\">\n    {% endif %}\n    </div>\n\n  </form>\n\n  <script>\n    var page_url = document.location.href;\n    var ui_hint = 'ui_key_auth=1';\n    if (page_url.indexOf(ui_hint) != -1) {\n      ui_hint = '';\n    }\n    else {\n      ui_hint = ((page_url.indexOf('?') == -1) ? '?' : '&') + ui_hint;\n    }\n    $('input.current-url').attr('value', page_url + ui_hint);\n\n    var _ssp = {\n      show_uids: function(elem) {\n        var $p = $(elem).closest('li.key-info');\n        $p.find('.ssp-key-uid-summary').hide();\n        $p.find('.ssp-key-uids').show();\n      },\n      policy_changing: function(i) {\n        setTimeout(\"_ssp.policy_changed();\", 50);\n      },\n      policy_changed: function(i) {\n        $('input.ssp_key_fp').closest('li').css({'opacity': 0.6});\n        $('.ssp-cache-only, .ssp-store').hide();\n        $('select#policy-ttl option').show();\n\n        var op = $('select#policy-ttl').val();\n        var key = $('input:checked.ssp_key_fp').eq(0);\n\n        if (key && key.val()) {\n          // FIXME: Adjust which selections are available, report\n          //        whether we've already unlocked this key?\n\n          key.closest('li').css({'opacity': 1.0});\n\n          var policy = key.data('policy');\n          if (policy) $('select#policy-ttl option[data-p='+ policy +']').hide();\n          if (policy == 'protect') {\n            $('select#policy-ttl option').hide();\n            $('select#policy-ttl option[value=display]').show();\n            $('select#policy-ttl option[value=export]').show();\n            $('select#policy-ttl option[value=revoke]').show();\n            if (op != 'display' && op != 'export' && op != 'revoke') {\n              op = 'display';\n            }\n          }\n          else if (policy == 'store') {\n            $('select#policy-ttl option[data-p=cache-only]').hide();\n            if (op == 'cache-only') op = 'forget';\n          }\n          else if (policy == 'cache-only') {\n            $('select#policy-ttl option[data-p=cache-only]').hide();\n            $('select#policy-ttl option[value=store]').hide();\n            $('select#policy-ttl option[value=display]').hide();\n            op = 'forget';\n          }\n          else {\n            $('select#policy-ttl option[value=forget]').hide();\n            $('select#policy-ttl option[value=display]').hide();\n            if (op == 'forget' || op == 'display') op = 'cache-only';\n          }\n        }\n\n        $('select#policy-ttl').val(op);\n        if (op == 'cache-only') { $('.ssp-cache-only').show();}\n        if (op == 'store') { $('.ssp-store').show();}\n        if (op == 'fail' || op == 'forget') {\n          $('.ssp-unlock-key, .ssp-show-passphrase, .ssp-hint').hide();\n          $('.ssp-lock-key').show();\n        }\n        else if (op == 'display') {\n          $('.ssp-lock-key, .ssp-unlock-key, .ssp-hint').hide();\n          $('.ssp-show-passphrase').show();\n        }\n        else if (op == 'revoke' || op == 'export') {\n          $('.ssp-lock-key, .ssp-show-passphrase').hide();\n          $('.ssp-unlock-key').show();\n          $('.ssp-hint').hide();\n          $('.ssp-' + op).show();\n        }\n        else {\n          $('.ssp-lock-key, .ssp-show-passphrase, .ssp-hint').hide();\n          $('.ssp-unlock-key').show();\n        }\n\n        $('input[type=password]').focus();\n      },\n      hide_title: function() {\n        $('.result-title').slideUp();\n        $('.ssp-ops').slideDown();\n        $('input[type=password]').focus();\n      }\n    };\n\n    setTimeout(\"_ssp.policy_changed();\", 50);\n    if ($('.result-title').length > 0) {\n      var hideid = setTimeout(\"_ssp.hide_title();\", $('.result-title').data('timer'));\n      $('form').submit(function(ev) { clearTimeout(hideid); });\n      $('.ssp-ops').hide();\n    }\n  </script>\n  {% else %}\n\n   <div class='text-center'>\n     <h3>{{_(\"No Keys Here!\")}}</h3>\n     <p>\n       {{_(\"Encryption keys can be created during the account creation process.\")}}\n     </p>\n   </div>\n\n   <pre class=\"hide\">{{ result|json }}</pre>\n\n  {% endif %}\n\n</div>\n{%- endif %}\n{%- endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/setup/oauth2/index.html",
    "content": "{%- extends \"layouts/auth.html\" %}\n{%- block title %}{{_(\"Grant Access\")}}{% endblock %}\n{%- block content %}\n{%- if result.oauth_url %}\n\n<div class=\"content-normal settings-page\">\n <form method=\"POST\" action=\"{{ U('/setup/oauth2/') }}\">{{ csrf_field|safe }}\n  <input type=\"hidden\" name=\"state\" value=\"{{ result.state }}\">\n  <h1>\n    <span class=\"icon icon-checkmark\"></span>\n    {{_(\"Grant Access\")}}\n  </h1>\n  <p>\n    {{ _(\"In order for Mailpile to process your e-mail, you need to grant permission for the application to access your e-mail account.\") }}\n  </p>\n  <p>\n    {{ _(\"The authorization process will take place in a new window (or tab).\") }}\n  {%- if not result.have_redirect %}\n    {{ _(\"You will need to copy and paste an access code into the form below.\") }}\n    <br>\n    <b>Code:</b>\n    <input type=\"text\" id=\"oauth-code\" name=\"code\" style=\"width: 35em;\"\n           value=\"\" placeholder=\"{{ _('Paste your access code here!') }}\">\n    <br>\n  {%- endif %}\n    <br>\n    {{ _(\"If given a choice, be sure to grant access to:\") }}\n    <b>{{ result.username }}</b>\n  </p>\n  <br>\n  <a id=\"oauth-a-gamo\" href=\"{{ result.oauth_url }}\" target=_blank\n     onclick=\"javascript:Mailpile.popup_oauth2_window(event);\">\n    <button class=\"right button-secondary\">\n      {{_(\"Authenticate\")}}\n    </button>\n  </a>\n  <button id=\"submit-gamo\" class=\"right button-secondary hide\" type=\"submit\">\n    {{_(\"Save\")}}\n  </button>\n  </a>\n  <button id=\"dismiss-gamo\" data-dismiss=\"modal\" class=\"button-info\">\n    {{_(\"Cancel\")}}\n  </button>\n  <br clear=both>\n  <script>\n    Mailpile.popup_oauth2_window = function(ev) {\n      ev.preventDefault();\n  {%- if result.have_redirect %}\n      $('#dismiss-gamo').click();\n  {%- else %}\n      $('#oauth-a-gamo').hide();\n      $('#submit-gamo').slideDown().prop(\"disabled\", true).css({ opacity: 0.25 });\n      $('#oauth-code').focus().bind(\"keyup paste input change\", function() {\n        $('#submit-gamo').prop(\"disabled\", false).css({ opacity: 1.0 });\n        Mailpile.popup_oauth2_window_ref.close();\n      });\n  {%- endif %}\n      var win = window.open(\"\", \"OAuth2\", \"width=400,height=550\");\n      win.location.href = $('a#oauth-a-gamo').attr('href');\n      Mailpile.popup_oauth2_window_ref = win;\n      return false;\n    };\n  </script>\n </form>\n</div>\n\n{%- else %}\n\n<div id=\"setup-welcome\" class=\"text-center add-top\">\n{% if render_mode != 'minimal' %}\n  <br style=\"margin-top: 25%;\">\n{% endif %}\n  <img src=\"{{ config.sys.http_path }}/static/img/logo-color.svg\"\n       class=\"animated bounceIn welcome-logo\">\n  <br><br>\n {% if result.success %}\n  <h2>{{ _(\"Success\") }}!</h2><br>\n  <p>\n    {{ _(\"Mailpile should now be able to access your account.\") }}\n  </p>\n {% else %}\n  <h2>{{ _(\"Failed\") }}.</h2><br>\n  <p>\n    {{ _(\"Oh no, something went wrong.\") }}\n    {{ _(\"Try again later?\") }}\n  </p>\n {% endif %}\n  <br><br>\n  <button id=\"close-window\" data-dismiss=\"modal\" class=\"button-info\"\n          {%- if render_mode != 'minimal' %}\n          onclick=\"javascript:window.close();\"{%- endif %}>\n    {{ _(\"Close Window\") }}\n  </button>\n</div>\n\n{%- endif %}\n{%- endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/setup/password/index.html",
    "content": "{% extends \"layouts/auth.html\" %}\n{% block title %}{{_(\"Mailpile Password\")}} | {{_('Setup')}}{% endblock %}\n{% block content %}\n\n<div id=\"setup-passphrase\" class=\"setup-box setup-box-medium add-top half-bottom animated fadeIn\">\n  <div id=\"identity-vault-lock\" class=\"vault-lock-outer\">\n    <div class=\"vault-lock-inner\">\n      <div class=\"vault-lock icon-lock-closed animated\"></div>\n    </div>\n  </div>\n\n  {# Password chosen, hooray! #}\n  <div id=\"setup-passphrase-chosen\"\n       class=\"{% if not result.configured %}hide{% endif %} text-center half-top\">\n    <h3 class=\"text-center\">{{_(\"Mailpile Secured!\")}}</h3>\n    <p class=\"text-center\">\n      {{_(\"Access to your Mailpile will now require this password.\")}}\n    </p>\n    <p class=\"text-center\">\n      {{_(\"All settings and downloaded mail will be encrypted, so even if someone steals your laptop they will not have access to your e-mail.\")}}\n    </p>\n    <p><a href=\"{{ U(\"/profiles/\") }}\"\n          class=\"button-primary\">{{_(\"Start using Mailpile\")}}</a></p>\n  </div>\n\n  {# Input form for choosing a password #}\n  <div id=\"setup-passphrase-chooser\"\n       class=\"{% if result.configured %}hide{% endif %} text-center half-top\">\n    <form method=\"POST\" action=\"{{ U(\"/setup/password/\") }}\"\n          id=\"form-setup-passphrase\" class=\"standard text-center half-top\">\n\n    {% if result.need_password %}\n      {{ csrf_field|safe }}\n      <h3 class=\"text-center\">{{_(\"Change your Mailpile Password\")}}</h3>\n      <span id=\"validation-passphrase\">\n        <label class=\"validation-message\" {% if result.incorrect %}style=\"color: #f00;\"{% endif %}>{{_(\"Authenticate\")}}</label>\n        <span class=\"validation-message\"></span>\n        <input id=\"input-setup-validate-passphrase\" class=\"medium center\"\n               autofocus autocorrect=\"off\" autocapitalize=\"off\" class=\"center\"\n      {% if result.incorrect %}\n               placeholder=\"{{_(\"Password incorrect! Try again...\")}}\"\n      {% else %}\n               placeholder=\"{{_(\"Your current Mailpile Password\")}}\"\n      {% endif %}\n               type=\"password\" name=\"existing\">\n      </span>\n      <a id=\"generate\" class=\"clickable\" title=\"{{ _(\"Click to generate new secure password\") }}\">\n        <label class=\"validation-message\">\n          {{_(\"Your new Mailpile Password\")}}\n          &nbsp;&nbsp; <span class=\"icon icon-robot\"></span>\n        </label>\n      </a>\n    {% else %}\n      <h3 class=\"text-center\" style=\"margin-bottom: 10px;\">\n        {{_(\"Choose your Mailpile Password\")}}\n      </h3>\n      <p class=\"text-center about\">\n        {{_(\"Your Mailpile Password should be something really hard to guess but memorable.\")}}<br>\n        {{_(\"This password will be used to unlock your Mailpile settings, keys and accounts.\")}}\n      </p>\n    {% endif %}\n\n      <p class=\"hide blank\"><i>\n        {{_(\"We have made a secure recommendation, but you still need to type it yourself!\")}}\n      </i></p>\n      <p class=\"hide mismatch\"><i>\n        {{_(\"The passwords don't match!\")}}\n      </i></p>\n      <p class=\"hide short\"><i>\n        {{_(\"That password is too short. This is important!\")}}\n      </i></p>\n      <p class=\"hide shortish\"><i>\n        {{_(\"That password is a bit short!\")}}<br>\n        {{_(\"We recommend using a phrase of at least four memorable words.\")}}\n      </i></p>\n\n      {% set suggestion = mailpile('setup/mkpass').result.passphrase %}\n      <span id=\"validation-passphrase\">\n        <span class=\"validation-message\"></span>\n        <input id=\"input-setup-passphrase\" class=\"medium center\"\n               type=\"password\" name=\"password1\"\n               autofocus autocorrect=\"off\" autocapitalize=\"off\" class=\"center\"\n               placeholder=\"{{ suggestion }}\">\n      </span>\n      <span id=\"validation-passphrase_confirm\">\n        <label class=\"validation-message\">{{_(\"Confirm your Mailpile Password\")}}</label>\n        <input id=\"input-setup-passphrase_confirm\" class=\"medium center\"\n               type=\"password\" name=\"password2\"\n               autocorrect=\"off\" autocapitalize=\"off\" class=\"center\"\n               placeholder=\"{{ suggestion }}\">\n      </span>\n\n      <button id=\"btn-setup-passphrase\" class=\"button-primary\"\n              type=\"submit\">{{_(\"Set Mailpile Password\")}}</button>\n\n    {% if result.need_password %}\n      <a style=\"position: absolute; opacity: 0.6; margin-top: 5px\" href=\"/\">\n        &nbsp; &nbsp; &nbsp;\n        <span class=\"icon icon-home\"></span> {{_(\"Cancel\")}}\n      </a>\n    {% else %}\n      <a id=\"generate\"\n         class=\"clickable\"\n         style=\"position: absolute; opacity: 0.6; margin-top: 5px\"\n         title=\"{{ _(\"Click to generate new secure password\") }}\">\n        &nbsp; &nbsp; &nbsp;\n        <span class=\"icon icon-robot\"></span> {{_(\"Suggest\")}}\n      </a>\n    {% endif %}\n\n    </form>\n  </div>\n</div>\n\n<script type=\"text/javascript\">\n  $(document).ready(function() {\n    $('a#generate').click(function() {\n      Mailpile.API.setup_mkpass_post({}, function(response){\n        $('input#input-setup-passphrase, input#input-setup-passphrase_confirm'\n          ).attr('placeholder', response.result.passphrase);\n      });\n    });\n\n    var $pw1 = $('input#input-setup-passphrase');\n    var $pw2 = $('input#input-setup-passphrase_confirm');\n\n    $pw2.focus(function(ev) {\n      if ($pw1.val().length <= 16) {\n        $('.about, .blank, .mismatch, .short').slideUp();\n        $('.shortish').slideDown();\n      }\n      else {\n        $('.shortish, .blank, .mismatch, .short').slideUp();\n        $('.about').slideDown();\n      }\n    });\n\n    $('form').submit(function(ev) {\n      if (!$pw1.val())\n      {\n        $('.about, .mismatch, .short').slideUp();\n        $('.blank').slideDown();\n        ev.preventDefault();\n      }\n      else if ($pw1.val() != $pw2.val())\n      {\n        $('.about, .blank, .short').slideUp();\n        $('.mismatch').slideDown();\n        ev.preventDefault();\n      }\n      else if ($pw1.val().length <= 6)\n      {\n        $('.about, .blank, .mismatch').slideUp();\n        $('.short').slideDown();\n        ev.preventDefault();\n      }\n    });\n  });\n</script>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/setup/welcome/index.html",
    "content": "{% extends \"layouts/auth.html\" %}\n{% block title %}{{_(\"Welcome to Mailpile!\")}}{% endblock %}\n{% block content %}\n<div id=\"setup-welcome\" class=\"text-center add-top\">\n  <h1 class=\"animated fadeIn\">{{_(\"Welcome to Mailpile!\")}}</h1>\n  <div class=\"add-top half-bottom\">\n    <img src=\"{{ U('/static/img/logo-color.svg') }}\"\n         class=\"animated bounceIn welcome-logo\">\n  </div>\n  <div class=\"welcome-icons add-top half-bottom clearfix\">\n  <ul class=\"animated fadeIn horizontal clearfix needjs hide\">\n    <li><span class=\"icon-mailsource\"></span></li>\n    <li><span class=\"icon-inbox\"></span></li>\n    <li><span class=\"icon-lock-closed\"></span></li>\n    <li><span class=\"icon-tag\"></span></li>\n    <li><span class=\"icon-compose\"></span></li>\n    <li><span class=\"icon-key\"></span></li>\n    <li><span class=\"icon-donate\"></span></li>\n  </ul>\n  </div>\n  <h4 class=\"text-detail animated fadeIn\">\n    {{_(\"You're about to experience secure e-mail like never before!\")}}\n  </h4>\n  <p><noscript>\n    {{_(\"Uh oh! You have JavaScript disabled. This will cause problems.\")}}\n  </noscript></p>\n  <form method=\"POST\" action=\"{{ U('/setup/welcome/') }}\"\n        class=\"animated bounceIn\">\n    <p>\n    <select id='langs' name=\"language\">\n      <option value=\"\">{{_(\"Select Language\")}}...</option>\n      {% set selected = {(result.language or 'en_US'): ' selected'} %}\n      {% for lc, txt in result.languages %}\n      <option value=\"{{ lc }}\"{{ selected[lc] }}>{{ txt }}</option>\n      {% endfor %}\n    </select>\n    </p>\n    {#\n       This tells the backend to validate and redirect to the next step of\n       the flow. AJAXY use will probably not send this variable.\n    #}\n    <input type=\"hidden\" name=\"advance\" value=\"Yes\">\n    <input type=\"submit\" value=\"{{_(\"Begin\")}}\" class=\"button-primary\">\n  </form>\n  <a id='rfab' style=\"position: absolute; bottom: 10px; right: 20px;\"\n     href=\"{{ U('/backup/restore/') }}\"\n    ><span class=\"icon icon-help\"></span> {{_(\"Restore from a Backup\")}}</a>\n</div>\n<script>\n  $(document).ready(function() {$('.needjs').show()});\n  $('#rfab').mouseover(function() {\n    $(this).attr('href', '{{ U(\"/backup/restore/\") }}' +\n                         '?lang=' + $('#langs').val());\n  });\n</script>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/tags/add/index.html",
    "content": "{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block title %}{{_(\"Add Tag\")}} {% endblock %}\n{% block content %}\n  {% set form_title = _(\"Add Tag\") %}\n  {% set tag = {\n    \"tid\": \"\",\n    \"name\": \"\",\n    \"slug\": \"\",\n    \"type\": \"tag\",\n    \"label\": \"true\",\n    \"label_color\": \"gray\",\n    \"icon\": \"icon-tag\",\n    \"display\": \"tag\"\n  } %}\n  {% set prefix = \"\" %}\n  {% include(\"tags/form.html\") %}\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/tags/edit.html",
    "content": "{% if not result.tags.0 %}\n  {% set error_title = \"tag_missing\" %}\n  {% include(\"partials/errors_content.html\") %}\n{% endif %}\n{% extends \"layouts/\" + render_mode + \".html\" %}\n{% block title %}{{_(\"Edit: \") + result.tags.0.name }}{% endblock %}\n{% block content %}\n  {% set form_title = _(\"Edit Tag\") %}\n  {% set tag = result.tags.0 %}\n  {% set prefix = \"tags.\" + tag.tid + \".\" %}\n  {% include(\"tags/form.html\") %}\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/tags/form.html",
    "content": "<div id=\"tags-settings\" class=\"content-normal\" style=\"position: relative; max-width: 42em;\">\n{% set theme_colors = theme_settings().colors %}\n<form class=\"standard\" method=\"POST\"\n{%- if state.command_url == \"/tags/add/\" %}\n      action=\"{{ U('/tags/add/') }}\"\n{%- else %}\n      action=\"{{ U('/settings/set/') }}\"\n{%- endif %}>\n  {{- csrf_field|safe }}\n\n  <div class=\"clearfix half-bottom\">\n    <h1 class=\"contact-name half-bottom\">\n      <span id=\"tag-editor-icon\" class=\"{{tag.icon}}\"\n            style=\"color: {{ theme_colors.get(tag.label_color, tag.label_color) }}\"></span>\n      {{form_title}}\n    </h1>\n  </div>\n\n  {# Note: See jsapi/search/events.js for Javascript relating to this form;\n   #       it is largely shared with the \"Save Search\" modal.\n   #}\n  <script type=\"text/javascript\">\n  </script>\n\n  <p class=\"message paragraph-important modal-basic-settings-title\">\n    <span class=\"icon-settings\"></span> {{_(\"Tag Settings\")}}</h4>\n  </p>\n  <div class=\"modal-basic-settings\">\n\n    <div class=\"left\">\n      <div class=\"right\">\n        <a class=\"{{ tag.icon }} modal-open-choose-tag-icon\"\n           title='{{_(\"Change Icon\")}}'\n           style=\"color: {{ theme_colors.get(tag.label_color, tag.label_color) }}\"></a>\n        <input type=\"hidden\" class='choose-tag-icon'\n               name=\"{{prefix}}icon\" value=\"{{ tag.icon }}\">\n        <a class=\"modal-open-choose-tag-color\"\n           style=\"color: {{ theme_colors.get(tag.label_color, tag.label_color) }}\"\n           title='{{_(\"Change Color\")}}'>{{_(\"Color\")}}</a>\n        <input type=\"hidden\" class='choose-tag-color'\n               name=\"{{prefix}}label_color\" value=\"{{tag.label_color}}\">\n      </div>\n      <label>{{_(\"Tag\")}}</label>\n      <input type=\"text\" name=\"{{prefix}}name\"\n             value=\"{{tag.name}}\" placeholder=\"{{_(\"Tag Name\")}}\">\n    </div>\n    <div class=\"right\">\n      <ul>\n        <li><input type=\"radio\" name=\"{{prefix}}display\" value=\"priority\" {% if tag.display == 'priority' %}checked{% endif %}>\n            <span class=\"checkbox\">{{_(\"Show in top of sidebar\")}}</span></li>\n        <li><input type=\"radio\" name=\"{{prefix}}display\" value=\"tag\" {% if tag.display == 'tag' %}checked{% endif %}>\n            <span class=\"checkbox\">{{_(\"Show in sidebar\")}}</span></li>\n        <li><input type=\"radio\" name=\"{{prefix}}display\" {% if tag.display in ('archive', 'invisible') %}value=\"{{tag.display}}\" checked{% else %}value=\"archive\"{% endif %}>\n            <span class=\"checkbox\">{{_(\"Hide from sidebar\")}}</span></li>\n      </ul>\n      <ul>\n        <li><input type=\"radio\" name=\"{{prefix}}label\" value=\"true\" {% if tag.label %}checked{% endif %}>\n            <span class=\"checkbox\">{{_(\"Show in search results\")}}</span></li>\n        <li><input type=\"radio\" name=\"{{prefix}}label\" value=\"false\" {% if not tag.label %}checked{% endif %}>\n            <span class=\"checkbox\">{{_(\"Hide from search results\")}}</span></li>\n      </ul>\n    </div>\n    <br clear=\"both\">\n\n  </div>\n\n  <div class=\"modal-tag-technical\">\n    <p class=\"message paragraph-important tag-edit-title\">\n      <span class=\"icon-work\"></span> {{_(\"Technical Settings\")}}</h4>\n    </p>\n    <div class=\"tag-edit-technical hide\">\n      <div class=\"right\">\n{%- if tag.type in ('tag', 'attribute') %}\n        <ul>\n          <li><input type=\"radio\" name=\"{{prefix}}type\" value=\"tag\" {% if tag.type == \"tag\" %}checked{% endif %}>\n              <span class=\"checkbox\">{{_(\"Behave as category\")}}</span></li>\n          <li><input type=\"radio\" name=\"{{prefix}}type\" value=\"attribute\" {% if tag.type == \"attribute\" %}checked{% endif %}>\n              <span class=\"checkbox\">{{_(\"Behave as attribute\")}}</span></li>\n        </ul>\n        <ul>\n          <li><input type=\"radio\" name=\"{{prefix}}toolbar\" value=\"true\" {% if tag.toolbar %}checked{% endif %}>\n              <span class=\"checkbox\">{{_(\"Display in toolbar\")}}</span></li>\n          <li><input type=\"radio\" name=\"{{prefix}}toolbar\" value=\"false\" {% if not tag.toolbar %}checked{% endif %}>\n              <span class=\"checkbox\">{{_(\"Hide from toolbar\")}}</span></li>\n        </ul>\n{%- endif %}\n      </div>\n{%- if tag.type in ('tag', 'attribute') %}\n{%-   if state.command_url != \"/tags/add/\" %}\n      <div>\n        <label>{{_(\"Keyword\")}}</label>\n        <input type=\"text\" name=\"{{prefix}}slug\" value=\"{{tag.slug}}\">\n      </div>\n{%-   endif %}\n{%- endif %}\n{%- if tag.subtags %}\n      <br clear=\"both\"><label>{{_(\"Subtags\")}}</label>\n      <ul class=\"items grouped\">\n{%-   for sub in tag.subtags %}\n        <li class=\"grouped\">\n          <a href=\"{{ sub.url }}\">\n            <span class=\"{{sub.icon}}\" style=\"color: {{ theme_colors.get(sub.label_color, sub.label_color) }}\"></span>\n          {{sub.name}}\n          </a>\n          <a href=\"{{ U('/tags/edit.html?only=', sub.slug) }}\"\n             title='Edit: {{ sub.name }}'\n             class=\"auto-modal auto-modal-reload right\">\n            <span class=\"icon-settings\"></span> {{_(\"Settings\")}}\n          </a>\n        </li>\n{%-   endfor %}\n      </ul>\n{%- else %}\n      <label>{{_(\"Parent\")}}</label>\n      <select name=\"{{prefix}}parent\">\n          <option value=\"\">-- {{_(\"None\")}} --</option>\n{%-   for ptag in mailpile(\"tags\", \"mode=tree\").result.tags %}\n{%-     if (ptag.display == \"priority\" or ptag.display == \"tag\")\n            and ptag.slug != tag.slug\n            and not ptag.parent %}\n          <option value=\"{{ptag.tid}}\"{% if ptag.tid == tag.parent %} selected=\"selected\"{% endif %}>{{ ptag.name }}</option>\n{%-     endif %}\n{%-   endfor %}\n      </select>\n{%- endif %}\n      <br clear=\"both\">\n    </div>\n  </div>\n\n\n  <div class=\"modal-choose-tag-icon hide\">\n    <p class=\"message paragraph-important\">\n      <span class=\"icon-lightbulb\"></span> {{_(\"Choose an Icon\")}}</h4>\n    </p>\n    <ul class=\"horizontal tag-icons\"></ul>\n    </ul>\n  </div>\n\n  <div class=\"modal-choose-tag-color hide\">\n    <p class=\"message paragraph-important\">\n      <span class=\"icon-themes\"></span> {{_(\"Choose a Color\")}}</h4>\n    </p>\n    <div class=\"text-center\">\n      <ul class=\"horizontal tag-colors\"></ul>\n    </div>\n  </div>\n\n{%- for src_id, source in config.sources.iteritems() %}\n  {%- for mbx_id, settings in source.mailbox.iteritems() %}\n    {%- if tag.tid and settings.primary_tag == tag.tid %}\n  <p class=\"message paragraph-important modal-mailbox-settings-title\">\n    <span class=\"icon-mailsource\"></span> {{_(\"Mailbox settings\")}}:\n    {{ config.sys.mailbox[mbx_id] }}\n  </p>\n  <div class=\"modal-mailbox-settings\">\n\n    <div class=\"right\">\n      <ul>\n        <li><input type=\"radio\"\n                   name=\"sources.{{src_id}}.mailbox.{{mbx_id}}.policy\"\n                   {% if settings.policy not in ('ignore', 'inherit') -%}\n                   value=\"{{settings.policy}}\" checked\n                   {% else %}value=\"read\"{% endif %}>\n            <span class=\"checkbox\">{{_(\"Add messages to search engine\")}}</span></li>\n        <li><input type=\"radio\" value=\"ignore\"\n                   name=\"sources.{{src_id}}.mailbox.{{mbx_id}}.policy\"\n                   {% if settings.policy == \"ignore\" %}checked{% endif %}>\n            <span class=\"checkbox\">{{_(\"Stop adding messages to search engine\")}}</span></li>\n        {%- if settings.policy == 'inherit' -%}\n        <li><input type=\"radio\" value=\"inherit\" checked\n                   name=\"sources.{{src_id}}.mailbox.{{mbx_id}}.policy\">\n            <span class=\"checkbox\">{{_(\"Use default mail source policy\")}} ({{ _(source.discovery.policy) }})</span></li>\n        {%- endif %}\n        {% if settings.local == \"\" %}\n        <li><input type=\"checkbox\" value=\"!CREATE\"  checked\n                   name=\"sources.{{src_id}}.mailbox.{{mbx_id}}.local\">\n            <span class=\"checkbox\">{{_(\"Copy mail to Mailpile secure storage\")}}</span></li>\n{#\n # FIXME: Make it possible to disable the copying. This will require\n #        shenanigans at the back-end, so we don't lose the old copies.\n #}\n        {% endif %}\n      </ul>\n    </div>\n\n  </div>\n    {%- endif %}\n  {%- endfor %}\n{%- endfor %}\n\n\n{################ FIXME - THIS IS THE FUN STUFF!  #####################\n\n{%- if tag.type == 'mailbox' %}\n  <p class=\"message paragraph-important tag-edit-title\">\n    <span class=\"icon-mailsource\"></span> {{_(\"Mailbox Settings\")}}</h4>\n  </p>\n  <div class=\"tag-edit-mailbox hide\">\n  </div>\n{%- endif %}\n{%- if tag.type == 'tag' %}\n  <p class=\"message paragraph-important tag-edit-title\">\n    <span class=\"icon-star\"></span> {{_(\"Filters and Saved Searches\")}}</h4>\n  </p>\n  <div class=\"tag-edit-filters hide\">\n  </div>\n{%- endif %}\n######################################################################}\n\n{%- if tag.type in ('tag', 'attribute', 'inbox', 'spam', 'trash',\n                    'drafts', 'blank') %}\n  <div class=\"modal-tag-edit-automation\">\n    <p class=\"message paragraph-important tag-edit-title\">\n      <span class=\"icon-robot\"></span> {{_(\"Automation\")}}</h4>\n    </p>\n    <div class=\"tag-edit-automation hide\">\n{%-  if tag.type in ('tag', 'attribute', 'spam') %}\n      <div class=\"left\">\n        <label>{{_(\"Tagging\")}}:</label>\n{%-   if tag.auto_tag in ('fancy', 'builtin') %}\n        <p style=\"padding: 1em; font-style: italic;\">\n         {{ _(\"Auto-tagging is always enabled for this tag.\") }}\n        </p>\n{%-   else %}\n        <ul>\n          <li title=\"\n  {{- _(\"Enable to tag messages automatically, like spam.\") }}\n  {{  _(\"Tag or un-tag mail by hand to train the system.\") }}\">\n              <input type=\"radio\" name=\"{{prefix}}auto_tag\"\n                     value=\"true\" {% if tag.auto_tag and tag.auto_tag != 'false' %}checked{% endif %}>\n              <span class=\"checkbox\">{{_(\"Automatic tagging\")}}</span>\n{# FIXME:     (<a href=\"\">{{_(\"help\")}}</a>) #}\n          </li>\n          <li title=\"{{ _(\"Disable automatic tagging.\") }}\">\n              <input type=\"radio\" name=\"{{prefix}}auto_tag\"\n                     value=\"false\" {% if not tag.auto_tag or tag.auto_tag == 'false' %}checked{% endif %}>\n              <span class=\"checkbox\">{{_(\"Tag by hand\")}}</span>\n          </li>\n        </ul>\n{%-   endif %}\n      </div>\n{%-  endif %}\n  \n      <div class=\"right\">\n        <label>{{_(\"Untagging\")}}:</label>\n        <select name=\"{{prefix}}auto_after\" style=\"margin-bottom: 5px;\">\n          <option value=\"0\" {% if not tag.auto_after %}selected{% endif %}>{{_(\"Never\")}}</option>\n          <option value=\"1\" {% if tag.auto_after == 1 %}selected{% endif %}>{{_(\"After 1 day\")}}</option>\n          <option value=\"2\" {% if tag.auto_after == 2 %}selected{% endif %}>{{_(\"After 2 days\")}}</option>\n          <option value=\"3\" {% if tag.auto_after == 3 %}selected{% endif %}>{{_(\"After 3 days\")}}</option>\n          <option value=\"7\" {% if tag.auto_after == 7 %}selected{% endif %}>{{_(\"After 1 week\")}}</option>\n          <option value=\"14\" {% if tag.auto_after == 14 %}selected{% endif %}>{{_(\"After 2 weeks\")}}</option>\n          <option value=\"30\" {% if tag.auto_after == 30 %}selected{% endif %}>{{_(\"After 1 month\")}}</option>\n          <option value=\"91\" {% if tag.auto_after == 91 %}selected{% endif %}>{{_(\"After 3 months\")}}</option>\n{%-   if tag.auto_after not in (0, 1, 2, 3, 7, 14, 30, 91) %}\n          <option value=\"{{ tag.auto_after }}\" selected>{{_(\"After %(days)s days\", days=tag.auto_after) }}</option>\n{%-   endif %}\n        </select>\n        <select name=\"{{prefix}}auto_action\" style=\"margin-top: 0;\">\n          {% set to_inbox = \"-\" + tag.slug + \" +inbox\" %}\n          {% set to_trash = \"-\" + tag.slug + \" +trash\" %}\n          <option value=\"!untag\" {% if tag.auto_action == \"!untag\" %}selected{% endif %}>{{ _(\"Remove from %(name)s\", name=tag.name) }}</option>\n{%-   if 'inbox' not in (tag.slug, tag.type) %}\n          <option value=\"{{ to_inbox }}\" {% if tag.auto_action == to_inbox %}selected{% endif %}>{{ _(\"Move to Inbox\") }}</option>\n{%-   endif %}\n          <option value=\"{{ to_trash }}\" {% if tag.auto_action == to_trash %}selected{% endif %}>{{ _(\"Move to Trash\") }}</option>\n{%-   if config.prefs.allow_deletion or tag.auto_action == '!delete' %}\n          <option value=\"!delete\" {% if tag.auto_action == \"!delete\" %}selected{% endif %}>{{ _(\"Delete e-mails\") }}</option>\n{%-   endif %}\n{%-   if tag.auto_action not in (\"\", \"!untag\", \"!delete\", to_after, to_trash) %}\n          <option value=\"{{ tag.auto_action }}\" selected>{{ tag.auto_action }}</option>\n{%-   endif %}\n        </select>\n      </div>\n    </div>\n  </div>\n{%- endif %}\n\n  <div>\n    <hr style=\"margin-bottom: 10px;\">\n{%- if state.command_url == \"/tags/add/\" %}\n    <button class=\"button-primary right\" type=\"submit\"><span class=\"icon-plus\"></span> {{_(\"Add\")}}</button>\n{%- else %}\n    <button class=\"button-primary right\" type=\"submit\"><span class=\"icon-checked\"></span> {{_(\"Save\")}}</button>\n{%- endif %}\n{%- if state.command_url == \"/tags/\" and tag.type in ('tag', 'attribute') and not tag.subtags %}\n    <button class=\"button-warning\" type=\"button\" id=\"button-tag-delete\"\n            data-slug=\"{{tag.slug}}\" data-dismiss=\"modal\" aria-hidden=\"true\">\n      <span class=\"icon-minus\"></span> {{_(\"Delete Tag\")}}\n    </button>\n{%- endif %}\n  </div>\n\n</form>\n</div>\n"
  },
  {
    "path": "shared-data/default-theme/html/tags/index.html",
    "content": "{% extends \"layouts/\" + render_mode + \"-wide.html\" %}\n{% block title %}{{_(\"Tags\")}}{% endblock %}\n{% block content %}\n{% macro render_tag(tag) -%}\n<div class=\"rectangles-outer\" id=\"tag-card-{{tag.tid}}\" data-tid=\"{{tag.tid}}\">\n  <div class=\"rectangles-inner\">\n      {% if tag.subtag_ids %}\n      {% set total_count = tag.stats.sum_all %}\n      {% else %}\n      {% set total_count = tag.stats.all %}\n      {% endif %}\n    <a class=\"tag-card-name left\" href=\"{{ config.sys.http_path }}/in/{{tag.slug}}/\" title=\"{{tag.name}}\" style=\"color: {{theme_settings().colors.get(tag.label_color, tag.label_color)}};\">\n      <span class=\"icon {% if tag.icon %}{{tag.icon}}{% else %}icon-tag{% endif%}\"></span>\n      {% if tag.name|length > 15 %}\n      {{tag.name[:12]}}...\n      {% else %}\n      {{tag.name}}\n      {% endif %}\n    </a>\n    <input type=\"checkbox\" class=\"right\">\n    <div class=\"tag-card-details clearfix\">\n      {% if tag.subtag_ids %}\n      <a href=\"#\" class=\"tag-card-subtags link-detail\" data-tid=\"{{tag.tid}}\">{{tag.subtag_ids|length}} {{_(\"Subtags\")}}</a>\n      {% endif %}\n      <a href=\"{{ U('/tags/edit.html?only=', tag.slug) }}\" class=\"link-detail right\"><span class=\"icon-settings\"></span> Settings</a>\n    </div>\n  </div>\n</div>\n{%- endmacro %}\n<div id=\"content-tools\">\n  {% include(\"partials/tools_tags.html\") %}\n</div>\n<div id=\"content-view\">\n  <div class=\"content-normal\">\n    <h4>{{_(\"Priority Tags\")}}</h4>\n  </div>\n  <div id=\"tags-priority-list\" class=\"container rectangles-container center\">\n    <div class=\"clearfix\"></div>\n    {% for tag in result.tags %}\n    {% if tag.display == \"priority\" and not tag.parent %}\n      {{ render_tag(tag) }}\n    {% endif %}\n    {% endfor %}\n  </div>\n  <div class=\"content-normal\">\n    <h4>{{_(\"Tags\")}}</h4>\n  </div>\n  <div id=\"tags-list\" class=\"container rectangles-container center\">\n    <div class=\"clearfix\"></div>\n    {% for tag in result.tags %}\n    {% if tag.display == \"tag\" and not tag.parent %}\n      {{ render_tag(tag) }}\n    {% endif %}\n    {% endfor %}\n  </div>\n  {% set tag_archive = [] %}\n  {% for tag in result.tags %}\n  {% if tag.display == \"archive\" %}\n      {% do tag_archive.append(tag.tid) %}\n  {% endif %}\n  {% endfor %} \n  {% if tag_archive|length > 0 %}\n  <div class=\"content-normal\">\n    <h4>{{_(\"Archived\")}}</h4>\n  </div>\n  <div id=\"tags-archived-list\" class=\"container rectangles-container center hide\">\n    <div class=\"clearfix\"></div>\n    {% for tag in result.tags %}\n    {% if tag.display == \"archive\" %}\n      {{ render_tag(tag) }}\n    {% endif %}\n    {% endfor %}\n    </div>\n    <div class=\"content-normal\">\n      <button id=\"button-tag-toggle-archive\" data-message=\"{{_(\"Hide Archived\")}}\">{{_(\"Show Archived\")}}</button>\n    </div>\n  </div>\n  {% endif %}\n</div>\n<script>\n$(document).ready(function() {\n  Mailpile.Tags.init();\n});\n</script>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/html/tags/sidebar.html",
    "content": "{% extends \"layouts/\" + render_mode + \"-wide.html\" %}\n{% block title %}{{_(\"Tags\")}}{% endblock %}\n{% block content %}\n{% set theme_colors = theme_settings().colors %}\n\n{% macro render_sidebar_tag(tag) -%}\n{#  Show proper classes #}\n{%  if tag.type in (\"drafts\", \"sent\") %}\n<li id=\"sidebar-tag-{{tag.tid}}\" data-slug=\"{{tag.slug}}\" data-tid=\"{{tag.tid}}\" data-display_order=\"{{tag.display_order}}\"\n    class=\"sidebar-tag sidebar-tags-default\">\n{%  elif tag.type == \"outbox\" %}\n<li id=\"sidebar-tag-{{tag.tid}}\" data-slug=\"{{tag.slug}}\" data-tid=\"{{tag.tid}}\" data-display_order=\"{{tag.display_order}}\"\n    class=\"sidebar-tag sidebar-tags-default {% if tag.stats.all == 0 %} hide{% endif %}\">\n{%  else %}\n<li id=\"sidebar-tag-{{tag.tid}}\" data-slug=\"{{tag.slug}}\" data-tid=\"{{tag.tid}}\" data-display_order=\"{{tag.display_order}}\"\n    class=\"sidebar-tag sidebar-tags-{% if tag.flag_allow_add %}draggable{% else %}default{% endif %}{% if tag.display == \"archive\" %} hide should-hide{% endif %}\">\n{%  endif %}\n\n{%- if tag.slug == \"outbox\" or tag.slug == \"drafts\" %}\n{%   set tag_new_count = tag.stats.all %}\n{%  elif tag.stats.get(\"sum_new\") %}\n{%   set tag_new_count = tag.stats.sum_new %}\n{%  else %}\n{%   set tag_new_count = tag.stats.new %}\n{%  endif %}\n  <a href=\"{{tag.url}}\"\n     data-tid=\"{{tag.tid}}\" data-color=\"{{ tag.label_color }}\"\n     data-new=\"{{ tag_new_count }}\" data-icon=\"{{ tag.icon }}\" class=\"sidebar-tag sidebar-tag-{{tag.slug}} color-{{tag.label_color}} {% if tag_new_count %}has-unread{% endif %}\n            {{ navigation_on(result.search_tag_ids, tag.tid) }}\"\n     title=\"{{tag.name}} {{friendly_number(tag_new_count)}}\">\n    <span class=\"icon {{tag.icon}}\" style=\"color: {{theme_colors.get(tag.label_color, tag.label_color)}};\"></span>\n    <span class=\"name\">{{tag.name or _('(unnamed)')}}</span>\n    <span class=\"notification\">{% if tag_new_count %} {{friendly_number(tag_new_count)}}{% endif %}</span>\n  </a>\n\n{%- set subtags_collapsed = (tag.tid in config.web.subtags_collapsed) %}\n{%- if tag.get(\"subtags\") %}\n    <a class=\"sidebar-tag-expand\" data-tid=\"{{tag.tid}}\"\n       data-collapsed=\"{{ subtags_collapsed }}\">\n  {% if subtags_collapsed %}\n      <span class=\"icon-arrow-right\"></span>\n  {% else %}\n      <span class=\"icon-arrow-down\"></span>\n  {% endif %}\n   </a>\n{% endif %}\n\n</li>\n{%  if tag.subtags %}\n{%   for subtag in tag.subtags if subtag.display in ('tag', 'archive') %}\n<li id=\"sidebar-tag-{{subtag.tid}}\"\n    data-tid=\"{{subtag.tid}}\" data-display_order=\"{{subtag.display_order}}\"\n    class=\"sidebar-subtag sidebar-tags-{% if subtag.flag_allow_add %}draggable{% else %}default{% endif %} subtag-of-{{tag.tid}}\n           {%- if subtags_collapsed or 'archive' in (subtag.display, tag.display) %} hide should-hide{% endif %}\">\n  <a href=\"{{subtag.url}}\" class=\"sidebar-tag {% if subtag.stats.new %}has-unread{% endif %} {{ navigation_on(result.search_tag_ids, subtag.tid) }}\" title=\"{{subtag.name}} {{subtag.stats.all}}\" data-tid=\"{{subtag.tid}}\">\n    <span class=\"icon {{subtag.icon}}\" style=\"color: {{theme_colors.get(subtag.label_color, subtag.label_color)}};\"></span>\n    <span class=\"name\">{{subtag.name or _('(unnamed)')}}</span>\n    {% if subtag.stats.new %}\n    <span class=\"notification\" id=\"sidebar-notifications-{{tag.tid}}\">{{ friendly_number(subtag.stats.new) }}</span>\n    {% endif %}\n  </a>\n</li>\n{%   endfor %}\n{%  endif %}\n{% endmacro %}\n\n<nav>\n  <ul id=\"sidebar-priority\" class=\"sidebar-sortable\">\n    {%- for tag in result.tags -%}\n      {%- if tag.display == 'priority' -%}\n        {{ render_sidebar_tag(tag) }}\n      {%- endif -%}\n    {%- endfor -%}\n  </ul>\n  <hr>\n  <ul id=\"sidebar-tag\" class=\"sidebar-sortable\">\n    {%- for tag in result.tags -%}\n      {%- if tag.display in ('tag', 'archive') -%}\n        {{ render_sidebar_tag(tag) }}\n      {%- endif -%}\n    {%- endfor -%}\n  </ul>\n</nav>\n\n<script id=\"template-sidebar-item\" type=\"text/template\">\n  <li id=\"sidebar-tag-<%= tid %>\" data-tid=\"<%= tid %>\" data-display_order=\"<%= display_order %>\" class=\"sidebar-tags-draggable\">\n    <a href=\"{{ U('/in/<%= slug %>/') }}\" class=\"sidebar-tag color-<%= label_color %>\" title=\"<%= name %>\" data-tid=\"<%= tid %>\">\n      <span class=\"icon <%= icon %>\" style=\"color: <%= label_color %>;\"></span>\n      <span class=\"name\"><%= name %></span>\n      <span class=\"notification\"></span>\n    </a>\n  </li>\n</script>\n\n<script>\n  $(document).ready(function() { Mailpile.Tags.init(); });\n</script>\n{% endblock %}\n"
  },
  {
    "path": "shared-data/default-theme/index.html",
    "content": "<!DOCTYPE html>\n<!--[if lt IE 7 ]><html class=\"ie ie6\" lang=\"en\"> <![endif]-->\n<!--[if IE 7 ]><html class=\"ie ie7\" lang=\"en\"> <![endif]-->\n<!--[if IE 8 ]><html class=\"ie ie8\" lang=\"en\"> <![endif]-->\n<!--[if (gte IE 9)|!(IE)]><!--><html lang=\"en\"> <!--<![endif]-->\n<head>\n\n\t<!-- Basic Page Needs\n    ================================================== -->\n\t<meta charset=\"utf-8\">\n\t<title>Mailpile: Default Style Guide</title>\n\t<meta name=\"description\" content=\"\">\n\t<meta name=\"author\" content=\"Brennan Novak\">\n\n\t<!-- Mobile Specific Metas\n    ================================================== -->\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1\">\n\n\t<!-- CSS\n    ================================================== -->\n\t<link rel=\"stylesheet\" href=\"css/default.css\">\n\t<link rel=\"stylesheet\" href=\"css/guide.css\">\n\n\t<!--[if lt IE 9]>\n\t\t<script src=\"http://html5shim.googlecode.com/svn/trunk/html5.js\"></script>\n\t<![endif]-->\n\n\t<!-- Favicons\n\t================================================== -->\n\t<link rel=\"shortcut icon\" href=\"img/favicon.ico\">\n\t<link rel=\"apple-touch-icon\" sizes=\"57x57\" href=\"img/apple-touch-icon.png\">\n\t<link rel=\"apple-touch-icon\" sizes=\"72x72\" href=\"img/apple-touch-icon-72x72.png\">\n\t<link rel=\"apple-touch-icon\" sizes=\"114x114\" href=\"img/apple-touch-icon-114x114.png\">\n\n</head>\n<body>\n\n\t<div id=\"messages\">\n\t\t<div class=\"error clearfix\">\n\t\t\t<span class=\"message-text\">Whoa Cowboy, you've mozyed on over to an error that you ain't supposed to have!</span>\n\t\t\t<abbr title=\"Close\" class=\"message-close\">X</abbr>\n\t\t</div>\n\t\t<div class=\"warning clearfix\">\n\t\t\t<span class=\"message-text\">This here be a warnin to yuh, just a warnin mind you!</span>\n\t\t\t<abbr title=\"Close\" class=\"message-close\">X</abbr>\n\t\t</div>\n\t\t<div class=\"debug clearfix\">\n\t\t\t<span class=\"message-text\">What kind of bug is a debug?</span>\n\t\t\t<abbr title=\"Close\" class=\"message-close\">X</abbr>\n\t\t</div>\n\t\t<div class=\"info clearfix\">\n\t\t\t<span class=\"message-text\">Hi, hi, hi, I am info</span>\n\t\t\t<abbr title=\"Close\" class=\"message-close\">X</abbr>\n\t\t</div>\n\t\t<div class=\"success clearfix\">\n\t\t\t<span class=\"message-text\">Success, you sly dog you, keep killin those ladies!</span>\n\t\t\t<abbr title=\"Close\" class=\"message-close\">X</abbr>\n\t\t</div>\n\t</div>\n\n\n\t<div class=\"container add-top\">\n\n\t\t<div class=\"sixteen columns content\">\n\t\t\t<img class=\"logo\" src=\"/_/static/icons/logo-color.svg\">\n\t\t\t<h1>Mailpile Style Guide</h1>\n\t\t\t<h5>Jumpstart your application with responsive HTML5, CSS3, SVG, and Webfonts</h5>\n\t\t</div>\n\n\t\t<!-- Three Collumns\n\t\t================================================== -->\n\n\t\t<div class=\"clearfix\">\n\t\t\t<div class=\"half column\">\n\t\t\t\t<h3>This Style Guide</h3>\n\t\t\t\t<p>This is a collection of CSS styles and corresponding HTML elements that are currently being styled. Use this code to develop the front-end of your website. A bit of this code was hacked from the <a href=\"http://www.getskeleton.com\">Skeleton Framework</a>. The CSS styles are all broken apart into smaller more easy to manage <a href=\"http://lesscss.org/\" target=\"_blank\">LESS</a> files which need to compiled to see the changes affect the main template.css file. If you're unfamiliar with LESS I suggest <a href=\"http://incident57.com/codekit/\" target=\"_blank\">CodeKit</a> for Mac OS, or <a href=\"http://gruntjs.com/\" target=\"_blank\">Grunt</a> (which uses Node.js) for other platforms</p>\n\n\t\t\t</div>\n\t\t\t<div class=\"half column\">\n\t\t\t\t<h3>About Rebar</h3>\n\t\t\t\t<p>Rebar is a collection of modular CSS files that helps rapidly develop sites that look beautiful at any size, be it a 17\" laptop screen or an iPhone. It's based on a responsive grid, but also provides very basic CSS for typography, buttons, forms and media queries. Go ahead, resize this super basic page to see the grid in action.</p>\n\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div class=\"clearfix\">\n\t\t\t<div class=\"one-third column\">\n\t\t\t\t<h3>3 Core Principles</h3>\n\t\t\t\t<p>Skeleton is built on three core principles:</p>\n\t\t\t\t<ul class=\"square\">\n\t\t\t\t\t<li><strong>A Responsive Grid Down To Mobile</strong>: Elegant scaling from a browser to tablets to mobile.</li>\n\t\t\t\t\t<li><strong>Fast to Start</strong>: It's a tool for rapid development with best practices</li>\n\t\t\t\t\t<li><strong>Style Agnostic</strong>: It provides the most basic, beautiful styles, but is meant to be overwritten.</li>\n\t\t\t\t</ul>\n\t\t\t</div>\n\t\t\t<div class=\"one-third column\">\n\t\t\t\t<h3>Docs &amp; Support</h3>\n\t\t\t\t<p>The easiest way to really get started with Skeleton is to check out the full docs and info at <a href=\"http://www.getskeleton.com\">www.getskeleton.com.</a>. Skeleton is also open-source and has a <a href=\"https://github.com/dhgamache/skeleton\">project on git</a>, so check that out if you want to report bugs or create a pull request. If you have any questions, thoughts, concerns or feedback, please don't hesitate to e-mail me at <a href=\"mailto:hi@getskeleton.com\">hi@getskeleton.com</a>.</p>\n\t\t\t</div>\n\t\t\t<div class=\"one-third column\">\n\t\t\t\t<h3>Are You Hip?</h3>\n\t\t\t\t<p>Lomo locavore swag retro stumptown four loko keytar polaroid. Portland selfies cray, plaid pop-up salvia sustainable literally. Marfa church-key 3 wolf moon narwhal aesthetic. Hoodie Marfa fixie wayfarers, Pinterest trust fund fanny pack 3 wolf moon dreamcatcher. Echo Park chillwave jean shorts ugh. IPhone Terry Richardson letterpress, literally keytar scenester kale chips tumblr dreamcatcher deep v.</p>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<hr class=\"medium\">\n\n\n\t\t<!-- Wide -->\n\t\t<div class=\"sixteen columns content\">\n\n\t\t\t<h1>A Beautiful Boilerplate for Responsive, Mobile-Friendly Development</h1>\n\t\t\t<hr class=\"large\">\n\n\t\t\t<div class=\"doc-section\" id=\"whatAndWhy\">\n\n\t\t\t\t<h3>What Is It?</h3>\n\t\t\t\t<p>Skeleton is a small collection of CSS files that can help you rapidly develop sites that look beautiful at any size, be it a 17\" laptop screen or an iPhone. Skeleton is built on three core principles:</p>\n\n\t\t\t\t<div class=\"row clearfix\">\n\t\t\t\t\t<div class=\"four columns alpha\">\n\t\t\t\t\t\t<img src=\"images/guide-responsive.jpg\" alt=\"responsive\" width=\"220\" height=\"113\">\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"eight columns omega\">\n\t\t\t\t\t\t<h5>Responsive Grid Down To Mobile</h5>\n\t\t\t\t\t\t<p>Skeleton has a familiar, lightweight 960 grid as its base, but elegantly scales down to downsized browser windows, tablets, mobile phones (in landscape and portrait). <strong>Go ahead, resize this page!</strong></p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"row clearfix\">\n\t\t\t\t\t<div class=\"four columns alpha\">\n\t\t\t\t\t\t<img src=\"images/guide-fast.jpg\" alt=\"responsive\" width=\"220\" height=\"113\">\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"eight columns omega\">\n\t\t\t\t\t\t<h5>Fast to Start</h5>\n\t\t\t\t\t\t<p>Skeleton is a tool for rapid development. Get started fast with CSS best practices, a well-structured grid that makes mobile consideration easy, an organized file structure and super basic UI elements like lightly styled forms, buttons, tabs and more.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"row clearfix\">\n\t\t\t\t\t<div class=\"four columns alpha\">\n\t\t\t\t\t\t<img src=\"images/guide-foundation.jpg\" alt=\"responsive\" width=\"220\" height=\"113\">\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"eight columns omega\">\n\t\t\t\t\t\t<h5>Style Agnostic</h5>\n\t\t\t\t\t\t<p>Skeleton is not a UI framework. It's a development kit that provides the most basic styles as a foundation, but is ready to adopt whatever your design or style is.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t</div>\n\t\t\t<hr>\n\n\n\t\t\t<!-- The Grid\n\t\t\t================================================== -->\n\t\t\t<div class=\"doc-section clearfix\" id=\"grid\">\n\n\t\t\t\t<h3>Two Item Grid</h3>\n\t\t\t\t<p>Skeleton's base grid is a variation of the 960 grid system. The syntax is simple and it's effective cross browser, but the awesome part is that it also has the flexibility to go mobile like a champ. <strong>Go ahead, resize the browser and watch as the layout reacts!</strong></p>\n\n\t\t\t\t<div class=\"example-grid\">\n\n\t\t\t\t\t<div class=\"one column alpha\">One</div>\n\t\t\t\t\t<div class=\"fifteen columns omega\">Fifteen</div>\n\n\t\t\t\t\t<div class=\"two columns alpha\">Two</div>\n\t\t\t\t\t<div class=\"fourteen columns omega\">Fourteen</div>\n\n\t\t\t\t\t<div class=\"three columns alpha\">Three</div>\n\t\t\t\t\t<div class=\"thirteen columns omega\">Thirteen</div>\n\n\t\t\t\t\t<div class=\"four columns alpha\">Four</div>\n\t\t\t\t\t<div class=\"twelve columns omega\">Twelve</div>\n\n\t\t\t\t\t<div class=\"five columns alpha\">Five</div>\n\t\t\t\t\t<div class=\"eleven columns omega\">Eleven</div>\n\n\t\t\t\t\t<div class=\"six columns alpha\">Six</div>\n\t\t\t\t\t<div class=\"ten columns omega\">Ten</div>\n\n\t\t\t\t\t<div class=\"seven columns alpha\">Seven</div>\n\t\t\t\t\t<div class=\"nine columns omega\">Nine</div>\n\n\t\t\t\t\t<div class=\"eight columns alpha\">Eight</div>\n\t\t\t\t\t<div class=\"eight columns omega\">Eight</div>\n\n\t\t\t\t\t<div class=\"nine columns alpha\">Nine</div>\n\t\t\t\t\t<div class=\"seven columns omega\">Seven</div>\n\n\t\t\t\t\t<div class=\"ten columns alpha\">Ten</div>\n\t\t\t\t\t<div class=\"six columns omega\">Six</div>\n\n\t\t\t\t\t<div class=\"eleven columns alpha\">Eleven</div>\n\t\t\t\t\t<div class=\"five columns omega\">Five</div>\n\n\t\t\t\t\t<div class=\"twelve columns alpha\">Twelve</div>\n\t\t\t\t\t<div class=\"four columns omega\">Four</div>\n\n\t\t\t\t\t<div class=\"thirteen columns alpha\">Thirteen</div>\n\t\t\t\t\t<div class=\"three columns omega\">Three</div>\n\n\t\t\t\t\t<div class=\"fourteen columns alpha\">Fourteen</div>\n\t\t\t\t\t<div class=\"two columns omega\">Two</div>\n\n\t\t\t\t\t<div class=\"fifteen columns alpha\">Fifteen</div>\n\t\t\t\t\t<div class=\"one column omega\">One</div>\n\t\t\t\t</div>\n\t\t\t\t<hr>\n\n\n\t\t\t\t<h3>Three Item Grid</h3>\n\t\t\t\t<p>These are examples of the grid with three columns.</p>\n\n\t\t\t\t<div class=\"example-grid\">\n\t\t\t\t\t<div class=\"seven columns alpha\">Seven</div>\n\t\t\t\t\t<div class=\"two columns\">Two</div>\n\t\t\t\t\t<div class=\"seven columns omega\">Seven</div>\n\n\t\t\t\t\t<div class=\"six columns alpha\">Six</div>\n\t\t\t\t\t<div class=\"four columns\">Four</div>\n\t\t\t\t\t<div class=\"six columns omega\">Six</div>\n\n\t\t\t\t\t<div class=\"five columns alpha\">Five</div>\n\t\t\t\t\t<div class=\"six columns\">Six</div>\n\t\t\t\t\t<div class=\"five columns omega\">Five</div>\n\n\t\t\t\t\t<div class=\"four columns alpha\">Four</div>\n\t\t\t\t\t<div class=\"eight columns\">Eight</div>\n\t\t\t\t\t<div class=\"four columns omega\">Four</div>\n\n\t\t\t\t\t<div class=\"three columns alpha\">Three</div>\n\t\t\t\t\t<div class=\"ten columns\">Ten</div>\n\t\t\t\t\t<div class=\"three columns omega\">Three</div>\n\n\t\t\t\t\t<div class=\"two columns alpha\">Two</div>\n\t\t\t\t\t<div class=\"twelve columns\">Twelve</div>\n\t\t\t\t\t<div class=\"two columns omega\">Two</div>\n\n\t\t\t\t\t<div class=\"one column alpha\">One</div>\n\t\t\t\t\t<div class=\"fourteen columns\">Fourteen</div>\n\t\t\t\t\t<div class=\"one column omega\">One</div>\n\n\t\t\t\t\t<div class=\"eight columns alpha\">Eight</div>\n\t\t\t\t\t<div class=\"six columns\">Six</div>\n\t\t\t\t\t<div class=\"two columns omega\">Two</div>\n\n\t\t\t\t\t<div class=\"six columns alpha\">Six</div>\n\t\t\t\t\t<div class=\"six columns\">Six</div>\n\t\t\t\t\t<div class=\"four columns omega\">Four</div>\n\n\t\t\t\t\t<div class=\"four columns alpha\">Four</div>\n\t\t\t\t\t<div class=\"eight columns\">Eight</div>\n\t\t\t\t\t<div class=\"four columns omega\">Four</div>\n\t\t\t\t</div>\n\t\t\t\t<hr>\n\n\t\t\t\t<h3>Four Item Grid</h3>\n\t\t\t\t<p>These are examples of the grid with four columns.</p>\n\n\t\t\t\t<div class=\"example-grid\">\n\t\t\t\t\t<div class=\"four columns alpha\">Four</div>\n\t\t\t\t\t<div class=\"four columns\">Four</div>\n\t\t\t\t\t<div class=\"four columns\">Four</div>\n\t\t\t\t\t<div class=\"four columns omega\">Four</div>\n\n\t\t\t\t\t<div class=\"three columns alpha\">Three</div>\n\t\t\t\t\t<div class=\"five columns\">Five</div>\n\t\t\t\t\t<div class=\"five columns\">Five</div>\n\t\t\t\t\t<div class=\"three columns omega\">Three</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"hidden-code\">\n\t\t\t\t\t<a href=\"\">Code Example</a>\n\t\t\t\t\t<script src=\"https://gist.github.com/959632.js?file=Skeleton%20Grid\"></script>\n\t\t\t\t</div>\n\n\t\t\t</div>\n\t\t\t<hr>\n\n\n\n\t\t\t<!-- Boxes\n\t\t\t================================================== -->\n\t\t\t<div class=\"doc-section clearfix\" id=\"boxes\">\n\n\t\t\t\t<h3>Responsive Boxes</h3>\n\n\t\t\t\t<div class=\"boxes\">\n\t\t\t\t   <div class=\"boxes-inner\">\n\t\t\t\t\t    <h3>Common Communication</h3>\n\t\t\t\t   </div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"boxes\">\n\t\t\t\t   <div class=\"boxes-inner\">\n\t\t\t\t\t    <h3>Common Food Phrases</h3>\n\t\t\t\t   </div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"boxes\">\n\t\t\t\t   <div class=\"boxes-inner\">\n\t\t\t\t\t    <h3>How To Discuss The Weather</h3>\n\t\t\t\t   </div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"boxes\">\n\t\t\t\t   <div class=\"boxes-inner\">\n\t\t\t\t\t    <h3>How To Make Introductions</h3>\n\t\t\t\t   </div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"boxes\">\n\t\t\t\t   <div class=\"boxes-inner\">\n\t\t\t\t\t    <h3>Simple Greetings</h3>\n\t\t\t\t   </div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"boxes\">\n\t\t\t\t   <div class=\"boxes-inner\">\n\t\t\t\t\t    <h3>Dates &amp; Months</h3>\n\t\t\t\t   </div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"boxes\">\n\t\t\t\t   <div class=\"boxes-inner\">\n\t\t\t\t\t    <h3>Sports Phrases</h3>\n\t\t\t\t   </div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"boxes\">\n\t\t\t\t   <div class=\"boxes-inner\">\n\t\t\t\t\t    <h3>Number Phrases</h3>\n\t\t\t\t   </div>\n\t\t\t\t</div>\n\n\t\t\t</div>\n\n\n\n\t\t\t<!-- Typography\n\t\t\t================================================== -->\n\t\t\t<div class=\"doc-section clearfix\" id=\"typography\">\n\t\t\t\t<h3>Typography</h3>\n\t\t\t\t<p>The typography of Skeleton is designed to create a strong hierarchy with basic styles. The primary font is the classic Helvetica Neue, but the font stack can be easily changed with just a couple adjustments. Regular paragraphs are set at a 14px base with 21px line height.</p>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"seven columns alpha headings\">\n\t\t\t\t\t\t<h1>Heading &lt;h1&gt;</h1>\n\t\t\t\t\t\t<h2>Heading &lt;h2&gt;</h2>\n\t\t\t\t\t\t<h3>Heading &lt;h3&gt;</h3>\n\t\t\t\t\t\t<h4>Heading &lt;h4&gt;</h4>\n\t\t\t\t\t\t<h5>Heading &lt;h5&gt;</h5>\n\t\t\t\t\t\t<h6>Heading &lt;h6&gt;</h6>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"five columns omega\">\n\t\t\t\t\t\t<blockquote>\n\t\t\t\t\t\t\t<p>This is a blockquote style example. It stands out, but is awesome</p>\n\t\t\t\t\t\t\t<cite>Dave Gamache, Skeleton Creator</cite>\n\t\t\t\t\t\t</blockquote>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"hidden-code\">\n\t\t\t\t\t<a href=\"\">Code Example</a>\n\t\t\t\t\t<script src=\"https://gist.github.com/973460.js?file=typography\"></script>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<hr>\n\n\n\t\t\t<!-- Buttons\n\t\t\t================================================== -->\n\t\t\t<div class=\"doc-section\" id=\"buttons\">\n\t\t\t\t<h3>Buttons</h3>\n\t\t\t\t<p>Buttons are intended for action and thus should have appropriate weight. The standard button is given that weight with a little bit of depth and a strong hover.</p>\n\n\t\t\t\t<div class=\"add-bottom\">\n\t\t\t\t\t<h5>Normal</h5>\n\t\t\t\t\t<a href=\"#buttons\" class=\"button-primary\" onclick=\"javascript:alert('This button uses .button-primary class')\">Button Primary</a>\n\t\t\t\t\t<a href=\"#buttons\" class=\"button-secondary\" onclick=\"javascript:alert('This button uses .button-secondary class')\">Button Secondary</a>\n\t\t\t\t\t<a href=\"#buttons\" class=\"button-alert\" onclick=\"javascript:alert('This button uses .button-alert class')\">Button Alert</a>\n\t\t\t\t\t<a href=\"#buttons\" class=\"button-warning\" onclick=\"javascript:alert('This button uses .button-warning class')\">Button Warning</a>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"add-bottom\">\n\t\t\t\t\t<h5>With Icons</h5>\n\t\t\t\t\t<a href=\"#buttons\" class=\"button-primary\" onclick=\"javascript:alert('This button uses .button-primary class')\"><span class=\"icon-community\"></span> Button Primary</a>\n\t\t\t\t\t<a href=\"#buttons\" class=\"button-secondary\" onclick=\"javascript:alert('This button uses .button-secondary class')\"><span class=\"icon-brain\"></span> Button Secondary</a>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"hidden-code\">\n\t\t\t\t\t<a href=\"\">Code Example</a>\n\t\t\t\t\t<script src=\"https://gist.github.com/973448.js?file=buttons\"></script>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<hr>\n\n\n\t\t\t<!-- Forms\n\t\t\t================================================== -->\n\t\t\t<div class=\"doc-section clearfix\" id=\"forms\">\n\t\t\t\t<h3>Forms</h3>\n\t\t\t\t<p>Forms can be one of the biggest pains for web developers, but just use these dead simple styles and you should be good to go. </p>\n\t\t\t\t<div class=\"four columns alpha\">\n\t\t\t\t\t<form>\n\t\t\t\t\t\t<label for=\"regularInput\">Regular Input</label>\n\t\t\t\t\t\t<input type=\"text\" id=\"regularInput\">\n\t\t\t\t\t\t<label for=\"regularTextarea\">Regular Textarea</label>\n\t\t\t\t\t\t<textarea id=\"regularTextarea\"></textarea>\n\t\t\t\t\t\t<label for=\"selectList\">Select List</label>\n\t\t\t\t\t\t<select id=\"selectList\">\n\t\t\t\t\t\t\t<option value=\"Option 1\">Option 1</option>\n\t\t\t\t\t\t\t<option value=\"Option 2\">Option 2</option>\n\t\t\t\t\t\t\t<option value=\"Option 3\">Option 3</option>\n\t\t\t\t\t\t\t<option value=\"Option 4\">Option 4</option>\n\t\t\t\t\t\t</select>\n\t\t\t\t\t\t<fieldset>\n\t\t\t\t\t\t\t<legend>Checkboxes</legend>\n\t\t\t\t\t\t\t<label for=\"regularCheckbox\">\n\t\t\t\t\t\t\t\t<input type=\"checkbox\" id=\"regularCheckbox\" value=\"checkbox 1\">\n\t\t\t\t\t\t\t\t<span>Regular Checkbox</span>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t<label for=\"secondRegularCheckbox\">\n\t\t\t\t\t\t\t\t<input type=\"checkbox\" id=\"secondRegularCheckbox\" value=\"checkbox 2\">\n\t\t\t\t\t\t\t\t<span>Regular Checkbox</span>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t</fieldset>\n\t\t\t\t\t\t<fieldset>\n\t\t\t\t\t\t\t<legend>Radio Buttons</legend>\n\t\t\t\t\t\t\t<label for=\"regularRadio\">\n\t\t\t\t\t\t\t\t<input type=\"radio\" name=\"radios\" id=\"regularRadio\" value=\"radio 1\">\n\t\t\t\t\t\t\t\t<span>Regular Radio</span>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t<label for=\"secondRegularRadio\">\n\t\t\t\t\t\t\t\t<input type=\"radio\" name=\"radios\" id=\"secondRegularRadio\" value=\"radio 2\">\n\t\t\t\t\t\t\t\t<span>Regular Radio</span>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t</fieldset>\n\t\t\t\t\t\t<button class=\"button-primary\" type=\"submit\">Submit Form</button>\n\t\t\t\t\t</form>\n\t\t\t\t</div>\n\t\t\t\t<br class=\"clear\">\n\t\t\t\t<div class=\"hidden-code\">\n\t\t\t\t\t<a href=\"\">Code Example</a>\n\t\t\t\t\t<script src=\"https://gist.github.com/973455.js?file=forms\"></script>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<hr>\n\n\n\t\t\t<!-- Media Queries\n\t\t\t================================================== -->\n\t\t\t<div class=\"doc-section\" id=\"mediaQueries\">\n\t\t\t\t<h3>Media Queries</h3>\n\t\t\t\t<p>Skeleton uses a <strong>lot</strong> of media queries to serve the scalable grid, but also for the convenience of styling your site on different size screens. Skeleton's media queries are almost exclusively targeted at max and min widths rather than device sizes or orientations. The advantage of this is browsers and future mobile devices that don't map to exact set dimensions will still benefit from the styles. That being said, all of the queries were written to be optimal on Apple iOS devices. The built in media queries include:</p>\n\t\t\t\t<ul class=\"square\">\n\t\t\t\t\t<li><strong>Smaller than 960</strong>: Smaller than the standard base grid</li>\n\t\t\t\t\t<li><strong>Tablet Portrait</strong>: Between 768px and 959px</li>\n\t\t\t\t\t<li><strong>All Mobile Sizes</strong>: Less than 767px</li>\n\t\t\t\t\t<li><strong>Just Mobile Landscape</strong>: Between 480px and 767px</li>\n\t\t\t\t\t<li><strong>Just Mobile Portrait</strong>: Less than 479px</li>\n\t\t\t\t</ul>\n\t\t\t\t<div class=\"hidden-code\">\n\t\t\t\t\t<a href=\"\">Code Example</a>\n\t\t\t\t\t<script src=\"https://gist.github.com/973467.js?file=media%20queries\"></script>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\n\t\t\t<div class=\"doc-section\" id=\"icons\">\n\t\t\t\t<h3>Icons</h3>\n\n\t\t\t\t<span class=\"icon-user\"></span>\n\t\t\t\t<span class=\"icon-trash\"></span>\n\t\t\t\t<span class=\"icon-tag\"></span>\n\t\t\t\t<span class=\"icon-settings\"></span>\n\t\t\t\t<span class=\"icon-sent\"></span>\n\t\t\t\t<span class=\"icon-rss\"></span>\n\t\t\t\t<span class=\"icon-photos\"></span>\n\t\t\t\t<span class=\"icon-notifications\"></span>\n\t\t\t\t<span class=\"icon-logo\"></span>\n\t\t\t\t<span class=\"icon-links\"></span>\n\t\t\t\t<span class=\"icon-later\"></span>\n\t\t\t\t<span class=\"icon-groups\"></span>\n\t\t\t\t<span class=\"icon-files\"></span>\n\t\t\t\t<span class=\"icon-encrypted\"></span>\n\t\t\t\t<span class=\"icon-donate\"></span>\n\t\t\t\t<span class=\"icon-compose\"></span>\n\t\t\t\t<span class=\"icon-attachment\"></span>\n\n\t\t\t</div>\n\n\n\t\t\t<!-- Color Palate\n\t\t\t================================================== -->\n\t\t\t<div class=\"doc-section\" id=\"colorPalate\">\n\t\t\t\t<h3>Color Palate</h3>\n\t\t\t\t<p>Click square to copy HEX color value to clipboard</p>\n\n\t\t\t\t<div class=\"color-palate-item\" style=\"background-color:#005283\">\n\t\t\t\t\t@blueDarkest\n\t\t\t\t</div>\n\t\t\t\t<div class=\"color-palate-item\" style=\"background-color:#3d6088\">\n\t\t\t\t\t@blueDarker\n\t\t\t\t</div>\n\t\t\t\t<div class=\"color-palate-item\" style=\"background-color:#006daf\">\n\t\t\t\t\t@blueDark\n\t\t\t\t</div>\n\t\t\t\t<div class=\"color-palate-item\" style=\"background-color:#41c2ff\">\n\t\t\t\t\t@blue\n\t\t\t\t</div>\n\t\t\t\t<div class=\"color-palate-item\" style=\"background-color:#99d9ff\">\n\t\t\t\t\t@blueLight\n\t\t\t\t</div>\n\t\t\t\t<div class=\"color-palate-item\" style=\"background-color:#92a3bf\">\n\t\t\t\t\t@grayLight\n\t\t\t\t</div>\n\t\t\t\t<div class=\"color-palate-item\" style=\"background-color:#8596b3\">\n\t\t\t\t\t@gray\n\t\t\t\t</div>\n\t\t\t\t<div class=\"color-palate-item\" style=\"background-color:#7f90ac\">\n\t\t\t\t\t@grayDark\n\t\t\t\t</div>\n\t\t\t\t<div class=\"color-palate-item\" style=\"background-color:#C94D35\">\n\t\t\t\t\t@red\n\t\t\t\t</div>\n\t\t\t\t<div class=\"color-palate-item\" style=\"background-color:#713c2a\">\n\t\t\t\t\t@redBorder\n\t\t\t\t</div>\n\t\t\t\t<div class=\"color-palate-item\" style=\"background-color:#9e6727\">\n\t\t\t\t\t@orange\n\t\t\t\t</div>\n\t\t\t\t<div class=\"color-palate-item\" style=\"background-color:#826f69\">\n\t\t\t\t\t@brown\n\t\t\t\t</div>\n\t\t\t\t<div class=\"color-palate-item\" style=\"background-color:#7ba2a7\">\n\t\t\t\t\t@greenDark\n\t\t\t\t</div>\n\t\t\t\t<div class=\"color-palate-item\" style=\"background-color:#abc78a\">\n\t\t\t\t\t@green\n\t\t\t\t</div>\n\t\t\t\t<div id=\"color-palate-copy\"></div>\n\t\t\t</div>\n\n\t</div><!-- container -->\n\n\n\t<!-- JS ================================================== -->\n\t<script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js\"></script>\n    <script src=\"http://jonrohan.github.io/ZeroClipboard/javascripts/ZeroClipboard.js\" ></script>\n\t<script>\n\n\n\t\t$('.gist').hide();\n\n\t\t$('.hidden-code').click(function(e) {\n\t\t\te.preventDefault();\n\t\t\t$(this).children('.gist').slideToggle();\n\t\t})\n\n\t\tvar originalText;\n\t\t$('.example-grid').children().hover(\n\t\t\tfunction() {\n\t\t\t\toriginalText = $(this).text();\n\t\t\t\t$(this).html($(this).width()+'px');\n\t\t\t},\n\t\t\tfunction() {\n\t\t\t\t$(this).html(originalText);\n\t\t\t}\n\t\t);\n\n\n\n\t\t$('.color-palate-item').on('click', function() {\n\n\t\t\tvar rgb_color = $(this).css('background-color'),\n\t\t\trgb_color = rgb_color.replace('rgb(', ''),\n\t\t\trgb_color = rgb_color.replace(')', '');\n\n\t\t\tvar rgb = rgb_color.split(', ');\n\t\t\tvar hex = \"#\" + ((1 << 24) + (parseInt(rgb[0]) << 16) + (parseInt(rgb[1]) << 8) + parseInt(rgb[2])).toString(16).slice(1);\n\n\t\t\talert('HEX Color is: ' + hex);\n\t\t});\n\n\t</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "shared-data/default-theme/js/helpers.js",
    "content": "/* Mailpile - Helpers\n   - a collection of random functions and things\n*/\n\nif (!String.prototype.startsWith) {\n    String.prototype.startsWith = function(searchString, position){\n      position = position || 0;\n      return this.substr(position, searchString.length) === searchString;\n  };\n}\n\n"
  },
  {
    "path": "shared-data/default-theme/js/libraries.js",
    "content": "console.log(\"Mailpile 3rd-party JS library bundle, built at: @MP_JSBUILD_INFO@\");\n/*\n * All code is Copyright the respective upstream projects. The license for\n * this combined bundle is the same as for Mailpile itself, AGPLv3.\n */\n"
  },
  {
    "path": "shared-data/default-theme/js/mousetrap.global.bind.js",
    "content": "/**\n * adds a bindGlobal method to Mousetrap that allows you to\n * bind specific keyboard shortcuts that will still work\n * inside a text input field\n *\n * usage:\n * Mousetrap.bindGlobal('ctrl+s', _saveChanges);\n */\n/* global Mousetrap:true */\nMousetrap = (function(Mousetrap) {\n    var _globalCallbacks = {},\n        _originalStopCallback = Mousetrap.stopCallback;\n\n    Mousetrap.stopCallback = function(e, element, combo, sequence) {\n        if (_globalCallbacks[combo] || _globalCallbacks[sequence]) {\n            return false;\n        }\n\n        return _originalStopCallback(e, element, combo);\n    };\n\n    Mousetrap.bindGlobal = function(keys, callback, action) {\n        Mousetrap.bind(keys, callback, action);\n\n        if (keys instanceof Array) {\n            for (var i = 0; i < keys.length; i++) {\n                _globalCallbacks[keys[i]] = true;\n            }\n            return;\n        }\n\n        _globalCallbacks[keys] = true;\n    };\n\n    return Mousetrap;\n}) (Mousetrap);\n"
  },
  {
    "path": "shared-data/default-theme/less/app/attachments.less",
    "content": "/* Attachments (used in both Compose & Thread) */\n\n  .attachment-image {\n    display: block;\n    width: 150px;\n    height: 125px;\n    border: 1px solid @gray;\n    margin: 0px;\n    padding: 0px;\n    overflow: hidden;\n    vertical-align: text-top;\n    -webkit-touch-callout: none;\n    -webkit-user-select: none;\n    -khtml-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n  }\n\n  .attachment-image div.preview {\n    display: block;\n    width: 100%;\n    height: 125px;\n    background-size: cover;\n    background-position: center center;\n    background-repeat: no-repeat;\n  }\n\n  .attachment {\n    display: block;\n    width: 150px;\n    height: 125px;\n    margin: 0px;\n    padding: 0px;\n    border: 1px solid @gray;\n  \ttext-align: center;\n    vertical-align: text-top;\n    -webkit-touch-callout: none;\n    -webkit-user-select: none;\n    -khtml-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n  }\n\n  .attachment div.preview {\n    height: 97px;\n    display: inline-table;\n  }\n  \n  .attachment div.preview span.icon-mime {\n    width: 60px;\n    display: table-cell;\n    vertical-align: middle;\n    color: @gray;\n    font-size: 40px;\n    line-height: 40px;\n  }\n  \n  .attachment div.preview span.extension {\n    display: table-cell;\n    vertical-align: middle;\n    text-align: center;\n    color: @gray;\n    text-transform: uppercase;\n    font-size: 18px;\n    font-weight: bold;\n  }\n\n  .attachment div.filename {\n    width: 100%;\n    height: 12px;\n    display: block;\n    padding: (@base-padding / 2) 0;\n    box-sizing: content-box;\n    border-top: 1px solid @gray;\n    background: @grayLight;\n    font-size: 12px;\n    font-weight: bold;\n    line-height: 12px;\n    color: @grayDark;\n  }\n\n  .attachment:hover {\n    background: @grayLight;\n    border: 1px solid @gray;\n  }\n\n  .attachment:hover div.filename {\n    color: @grayDark;\n  }\n  \n  .attachment:hover span.icon-mime,\n  .attachment:hover span.extension {\n    color: @grayDark;\n  }\n  .attachment-progress-bar {\n    padding-left: 10px;\n  }\n"
  },
  {
    "path": "shared-data/default-theme/less/app/compose.less",
    "content": "/* Compose */\n\n  @compose-max-width: 50em;\n\n  .form-compose {\n    padding: 15px 20px 10px 20px;\n    background: @grayLight;\n  }\n\n  .form-compose label {\n    display: block;\n    margin-bottom: 3px;\n    font-size: 14px;\n    font-weight: normal;\n  }\n\n  .form-compose label a {\n    font-size: 14px;\n    font-weight: normal;\n  }\n\n  .form-compose label a:hover {\n    color: @red;\n  }\n\n  .form-compose label span,\n  .form-compose label a.compose-hide-field {\n    color: @gray;\n  }\n\n  .compose-headers,\n  .compose-subject,\n  .compose-options,\n  .compose-body {\n    width: 100%;\n    max-width: @compose-max-width;\n  }\n\n\n/* Compose - Headers */\n\n  .compose-headers {\n    padding-bottom: 15px;\n  }\n\n\ta.compose-show-field {\n\t  font-family: @text-font-family;\n    font-size: 18px;\n    font-style: normal;\n    font-variant: normal;\n    font-weight: bold;\n    color: @grayDark;\n    margin-left: 10px;\n\t}\n\n\n/* Compose - Subject */\n\n  .compose-subject input[type=text] {\n    // NOTE: If margins are changed here, please update the .pile-message h3\n    //       in the section at the bottom.\n    width: 100%;\n    margin-bottom: 10px;\n    padding: 10px;\n    border: 1px solid @grayMid;\n    border-radius: 4px;\n    box-sizing: border-box;\n    font-family: Helvetica, Arial, sans-serif;\n    font-size: 14px;\n    line-height: 18px;\n  }\n\n  .compose-subject input[type=text]:focus {\n    outline: none;\n    border: 1px solid @gray;\n    box-shadow: 0 0 3px @gray;\n    -moz-box-shadow: 0 0 3px @gray;\n    -webkit-box-shadow: 0 0 3px @gray;\n  }\n\n\n/* Compose - Options */\n\n  .compose-options {\n    margin-top: -3px;\n    margin-bottom: 0px;\n\t\tpadding: 0 0 0px 0;\n  }\n\n  .compose-options-size {\n    font-size: 14px;\n    font-weight: normal;\n    line-height: 14px;\n    color: @gray;\n  }\n\n  .compose-options-crypto {\n    width: 74px;\n    position: relative;\n    top: 2px;\n    display: inline-block;\n    text-align: center;\n    background: @white;\n    border-left: 1px solid @grayMid;\n    border-bottom: 1px solid @grayMid;\n    border-right: 1px solid @grayMid;\n    border-bottom-left-radius: 4px;\n    border-bottom-right-radius: 4px;\n    float: right;\n    padding-top: 5px;\n    .compose-options-size();\n  }\n\n  .compose-options-crypto .compose-message-settings,\n  .compose-options-crypto .compose-crypto-encryption,\n  .compose-options-crypto .compose-crypto-signature {\n    display: inline-block;\n    margin: 10px 6px;\n  }\n\n  .compose-message-settings:hover,\n  .compose-crypto-signature:hover,\n  .compose-crypto-encryption:hover {\n    cursor: pointer;\n  }\n\n  .compose-options ul {\n    display: inline-block;\n    margin-bottom: 0px;\n    padding: 0px;\n  }\n\n  .compose-options ul li {\n    margin: 0px 10px 0px 0px;\n    .compose-options-size();\n  }\n\n  .compose-options ul li a,\n  .compose-options label.right,\n  .compose-options a.right {\n    font-weight: normal;\n    .compose-options-size();\n  }\n\n  .compose-options ul li a:hover,\n  .compose-options label.right:hover,\n  .compose-options a.right:hover {\n    color: @grayDark;\n  }\n\n  .compose-options label.right {\n    position: relative;\n    top: 9px;\n    right: 10px;\n    padding-bottom: 0px;\n    margin-bottom: 0px;\n    cursor: pointer;\n    font-style: italic;\n  }\n\n  .compose-options a.right {\n    position: relative;\n    top: 9px;\n    right: 5px;\n    margin-left: 10px;\n  }\n\n  .compose-to-summary {\n    max-width: 500px;\n    word-wrap: normal;\n    word-break: normal;\n    white-space: nowrap;\n    overflow: hidden;\n  }\n\n\n/* Compose - Body */\n\n  .compose-body {\n    border-radius: 4px;\n    background: @white;\n    border: 1px solid @grayMid;\n    padding-bottom: 0px;\n  }\n\n  .compose-body textarea {\n    width: 97%;\n    display: block;\n    min-height: 75px;\n    margin: 12px auto 0px auto;\n    padding: 0px 0px 12px 0px;\n    border: 0px;\n    border-radius: 4px;\n    -webkit-box-sizing: border-box;\n    -moz-box-sizing: border-box;\n    box-sizing: border-box;\n    font-family: Helvetica, Arial, sans-serif;\n    font-size: 14px;\n    line-height: 18px;\n    resize: none;\n  }\n\n  .compose-body textarea:focus {\n    outline: none;\n  }\n\n\n/* Compose - Attachments */\n\n  div.compose-attachments {\n    margin-top: 0px;\n    padding: 0px 10px 10px 10px;\n    font-size: 14px;\n  }\n\n  div.compose-attachments ul.horizontal {\n    margin-bottom: 0px;\n  }\n\n  ul.compose-attachments li {\n    margin-right: 15px;\n    margin-bottom: 15px;\n  }\n\n  a.compose-attachment-remove {\n    float: right;\n    display: inline;\n    position: relative;\n    top: 5px;\n    right: 5px;\n    padding: 4px;\n    color: @gray;\n    font-size: 14px;\n    line-height: 14px;\n    border-radius: 5px;\n  }\n\n  a.compose-attachment-remove:hover {\n    background: @red;\n    color: @white;\n  }\n\n\n/* Attachment Browser */\n\n  .attachment-browswer-unsupported {\n    color: @gray;\n    font-style: italic;\n  }\n\n  .attachment-browswer-unsupported a {\n    color: @gray;\n  }\n\n  label.compose-attach-key {\n    color: @grayDark;\n    cursor: pointer;\n    font-weight: bold;\n  }\n\n  label.compose-attach-key:hover,\n  label.compose-attach-key:hover span.icon-key {\n    color: @red;\n  }\n\n  label.compose-attach-key span.icon-key {\n    color: @grayDark;\n    font-weight: normal;\n  }\n\n\n/* Compose - From Menu */\n\n  .compose-from-select {\n    display: block;\n    width: 18em;\n    background: @white;\n    border: 1px solid @grayMid;\n    border-radius: @base-border-radius;\n    padding: 4px 6px;\n    line-height: 14px;\n  }\n\n  .compose-from-select:hover {\n    cursor: pointer;\n    background: @grayDark;\n    color: @white\n  }\n\n  .compose-from-select:hover .name {\n    color: @white;\n  }\n\n  .compose-from-selected {\n    display: inline-block;\n    vertical-align: middle;\n  }\n\n  .compose-from-caret {\n    display: inline-block;\n    padding-left: 5px;\n    float: right;\n  }\n\n  .compose-from-selected .avatar, .compose-from .avatar {\n    width: 24px;\n    display: inline-block;\n    margin-right: 5px;\n    float: left;\n  }\n\n  .compose-from-selected .avatar img {\n    width: 24px;\n    border-radius: @base-border-radius;\n  }\n\n  .compose-from-selected .name-and-address, .compose-from .name-and-address {\n    float: left;\n  }\n\n  .compose-from-selected .name {\n    display: inline-block;\n    vertical-align: middle;\n    font-size: 14px;\n    font-family: Helvetica, Arial, sans-serif;\n    font-weight: bold;\n    line-height: 14px;\n  }\n\n  .compose-from-selected .address {\n    font-size: 12px;\n    font-family: Helvetica, Arial, sans-serif;\n    font-weight: normal;\n    color: @grayMid;\n    line-height: 12px;\n  }\n\n  .compose-from {\n    height: 32px;\n    font-size: 14px;\n    font-weight: bold;\n    line-height: 14px;\n    margin: 0px;\n\n    display: block;\n    width: 18em;\n    background: @white;\n    border: 1px solid @grayMid;\n    border-radius: @base-border-radius;\n    padding: 0px;\n    line-height: 14px;\n  }\n\n  .compose-from .avatar {\n    width: 32px;\n  }\n\n  .compose-from .avatar img {\n    width: 32px;\n    border-radius: @base-border-radius;\n  }\n\n  .compose-from .name {\n    display: inline-block;\n    vertical-align: text-top;\n    font-size: 14px;\n    font-weight: bold;\n    line-height: 14px;\n  }\n\n  .compose-from .address {\n    color: @gray;\n    font-family: Helvetica, Arial, sans-serif;\n    font-size: 12px;\n    font-weight: normal;\n  }\n\n\n/* Compose - Actions */\n\n .compose-actions {\n    width: 100%;\n    max-width: @compose-max-width - 9em;\n    margin-top: -5px;\n    padding-bottom: @base-padding;\n  }\n\n  .compose-buttons {\n    text-align: right;\n  }\n\n  .compose-buttons button {\n    margin-left: 10px;\n  }\n\n  .compose-actions ul.dropdown-menu {\n    padding: 0;\n    li {\n        margin-bottom: 0px;\n        a {\n            border-radius: 0px;\n            border: 0px;\n            border-bottom: 1px solid @gray;\n        }\n    }\n  }\n\n\n/* Composer - Changes for in-pile composer */\n\n  .form-compose .pile-message .subject {\n    position: relative;\n    padding: 0 4px 0 0;\n  }\n\n/*** WORKS IN PROGRESS - COMMENTED OUT\n\n  .pile-message .compose-subject-container {\n    display: table;\n    width: 100%;\n  }\n\n  .pile-message .compose-subject-container h3 {\n    display: table-cell;\n    margin-bottom: 5px;\n    padding: 10px 10px 0 0;\n    box-sizing: border-box;\n    font-size: 16px;\n    line-height: 18px;\n    color: @gray;\n  }\n\n  .pile-message .compose-subject-container div.compose-subject {\n    display: table-cell;\n  }\n\n  .pile-message .compose-subject input[type=text] {\n    margin-bottom: 5px;\n  }\n\n  .pile-message .compose-headers {\n    padding-bottom: 0;\n  }\n\n  .pile-message div.thread-reply {\n    border: 0;\n  }\n\n  .pile-message .compose-actions {\n    display: table;\n    border-spacing: 0;\n    border-collapse: collapse;\n    width: 100%;\n    margin-bottom: 5px;\n    margin-top: 5px;\n  }\n\n  .pile-message .compose-actions .dropdown,\n  .pile-message .compose-actions .compose-buttons,\n  .pile-message .compose-actions .compose-options-crypto {\n    display: table-cell;\n    white-space: nowrap;\n    vertical-align: top;\n    padding: 0;\n  }\n\n  .pile-message .compose-actions .dropdown {\n    position: relative;\n    width: 99%;\n  }\n\n  .pile-message .compose-actions .dropdown .dropdown-toggle {\n    border-radius: 4px;\n    padding: 2px 5px;\n    margin: 1px 5px 0 0;\n    width: 100%;\n  }\n\n  .pile-message .compose-body {\n    padding-right: 2px;\n    position: relative;\n    border-bottom-right-radius: 0px;\n    width: auto;\n  }\n\n  .pile-message .compose-options-crypto {\n    width: 111px;\n    position: relative;\n    display: inline-block;\n    text-align: center;\n    background: @white;\n    padding-top: 9px;\n    margin-top: -7px;\n    margin-left: 10px;\n    border-left: 1px solid @grayMid;\n    border-bottom: 1px solid @grayMid;\n    border-right: 1px solid @grayMid;\n    border-bottom-left-radius: 4px;\n    border-bottom-right-radius: 4px;\n    border-top: 0;\n    border-top-left-radius: 0;\n    border-top-right-radius: 0;\n    .compose-options-size();\n    float: right;\n  }\n\n***** END OF WIP SECTION ************* */\n"
  },
  {
    "path": "shared-data/default-theme/less/app/contacts.less",
    "content": "/* Contact - List */\n\n  .contact-card-avatar { \n    .global-user-avatar();\n  }\n\n  .contact-card-avatar img {\n    .global-user-avatar-img();\n  }\n\n  .contact-card-name {\n    max-width: 100px;\n    display: inline-block;\n    border: 0px solid @white;\n    margin-top: 0px;\n    padding-top: 0px;\n    font-size: 14px;\n    line-height: 18px;\n    vertical-align: top;\n    word-break: break-word;\n  }\n\n  .contact-card-name:hover {\n    color: @blue;\n  }\n\n  .contact-card-checkbox {\n    margin-top: 0px;\n    vertical-align: top;\n  }\n\n/* Contact - View */\n\n  #contact-view {\n    margin-bottom: 100px;\n  }\n\n  #contact-view .contact-avatar {\n    display: block;\n    margin-right: 20px;\n    border-radius: 3px;\n  }\n\n  #contact-view .contact-name {\n    font-family: @mailpile-text-font-family-bold;\n    margin-bottom: 5px;\n  }\n\n  #contact-view .contact-subname {\n    display: block;\n    float: left;\n    color: @gray;\n  }\n\n  #contact-view h5.contact-key {\n    width: 315px;\n  }\n\n  #contact-view .icon-fingerprint {\n    font-size: 36px;\n    margin-right: 10px;\n  }\n\n\n/* Contact - Details */\n\n  .contact-detail li {\n    background: @white;\n    margin-bottom: @base-margin;\n    padding: @base-padding;\n    border: 1px solid @gray;\n    border-radius: @base-border-radius;\n  }\n\n  .contact-detail li h5 {\n    margin-bottom: @base-margin / 1.5;\n  }\n\n  .contact-detail a span.contact-detail-light {\n    color: @gray;\n    font-weight: normal;\n  }\n\n  .contact-detail a:hover span.contact-detail-light,\n  .contact-detail a span.contact-detail-light:hover {\n    color: @red;\n  }\n\n  .contact-key-details {\n    margin-top: @base-margin;\n    font-size: 14px;\n  }\n\n  .contact-tag-filter {\n    background: @white;\n    border: 1px solid @gray;\n    border-radius: @base-border-radius;\n    padding: @base-padding 0;    \n  }\n\n  .contact-tag-filter li {\n    display: inline;\n    margin: @base-margin;\n  }\n\n\n/* Contacts - Conversation */\n\n  .contact-conversation-avatar {\n    width: 45px;\n    display: inline-block;\n    border-radius: 3px;\n  }\n  \n  .contact-conversation-name {\n    display: inline-block;\n    font-size: 18px;\n    font-weight: bold;\n    line-height: 18px;\n  }\n  \n  .contact-conversation-address {\n    display: inline-block;\n    font-size: 14px;\n    font-weight: normal;\n    font-family: Helvetica, Arial, sans-serif;\n    line-height: 14px; \n    color: @grayMid;\n  }\n\n\n/* Contacts - Add */\n\n  .contact-add-fields {\n    float: left;\n    margin-right: 45px;\n  }\n\n  .contact-add-search-keyserver {\n    float: left;\n  }\n\n  .contact-add-search-item {\n    margin-bottom: 15px;\n    padding: 10px 15px;\n    border-radius: 5px;\n  }\n\n  .contact-add-search-item:hover {\n    cursor: pointer;\n    background: @grayMid;\n  }\n\n  .contact-add-search-item .name {\n    display: block;\n    font-size: 18px;\n    font-weight: bold;\n  }\n\n  .contact-add-search-item .email,\n  .contact-add-search-item .key,\n  .contact-add-search-item .keysize,\n  .contact-add-search-item .keytype,\n  .contact-add-search-item .created {\n    display: block;\n    font-size: 14px;\n    font-family: Helvetica, Arial, sans-serif;\n  }  \n  "
  },
  {
    "path": "shared-data/default-theme/less/app/crypto.less",
    "content": "/* Crypto */\n\n  .compose-crypto-signature.none {\n    color: @gray;\n  }\n  .compose-crypto-signature.signed {\n    color: @green;\n  }\n  .compose-crypto-signature.error {\n    color: @red;\n  }\n  .compose-crypto-encryption.none {\n    color: @gray;\n  }\n\n  .crypto-none,\n  .compose-crypto-encryption.none {\n    color: @gray;\n  }  \n\n  .crypto-warning,\n  .compose-crypto-encryption.cannot {\n    color: @orange;\n  }\n\n  .crypto-encrypted,\n  .compose-crypto-encryption.encrypted {\n    color: @green;\n  }\n  \n  .crypto-color-error,\n  .compose-crypto-encryption.error {\n   color: @red;\n  }"
  },
  {
    "path": "shared-data/default-theme/less/app/files.less",
    "content": "/* Files Browser */\n\n\t.item-file {\n\t\twidth: 150px;\n\t\tfloat: left; \n\t\tmargin: 25px 25px;\n\t\tpadding: 20px;\n\t\ttext-align: center;\n\t}\n\t\n\t.item-file:hover {\n\t\tbackground: @grayLight;\n\t}\n\n\t.item-file-icon {\n\t\tdisplay: block;\n\t\tfont-size: 125px;\n\t\tmargin-bottom: 10px;\n\t}\n\t\n\t.item-file-name {\n\t\tfont-size: 14px;\n\t\tfont-family: @text-font-family-bold;\n\t\tline-height: 14px;\n\t}"
  },
  {
    "path": "shared-data/default-theme/less/app/global.less",
    "content": "/* Body */\n  body {\n    overflow: hidden;\n  }\n\n\n/* Content */\n\n  #content {\n    position: fixed;\n    top: 62px;\n    left: 225px;\n    right: 0px;\n    bottom: 0px;\n    overflow: hidden;\n  }\n\n  #content-wide {\n    width: 100%;\n    margin-top: 62px;\n  }\n\n  #content-tools {\n    position: relative;\n    z-index: 10;\n  }\n\n\n/* Copyright Footer Navigation */\n\n  .footer-nav {\n    margin-top: 20px;\n    margin-bottom: 5px;\n  }\n\n  .footer-nav, .footer-nav a {\n    font-family: @mailpile-text-font-family;\n    font-size: 12px;\n    color: @gray;\n  }\n\n\n/* Sub Navigation */\n\n  .sub-navigation  {\n    width: 100%;\n    height: 46px;\n    display: block;\n    background: @grayLight;\n    border-bottom: 1px solid @gray;\n    box-sizing: border-box;\n  }\n\n  .sub-navigation > ul {\n    margin: 10px;\n  }\n\n  .sub-navigation > ul > li {\n    margin: 0px 5px;\n    padding: 0px 5px;\n  }\n\n  .sub-navigation > ul > li > a {\n    display: block;\n    padding: 5px;\n    font-family: @text-font-family-bold;\n    font-weight: normal;\n    color: @grayDark;\n    line-height: 14px;\n  }\n\n  .sub-navigation > ul > li > a:hover,\n  .sub-navigation > ul > li > a:hover span.navigation-icon {\n    color: @red;\n  }\n\n  .sub-navigation > ul > li > a img {\n    height: 14px;\n  }\n\n  .sub-navigation > ul > li > ul.dropdown-menu li {\n    display: block;\n    float: none;\n  }\n\n  .sub-navigation > ul > li > ul.dropdown-menu li .selected:before {\n    position: absolute;\n    left: 5px;\n    content: \"\\2713\"\n  }\n\n  .navigation-icon {\n    margin: 0;\n    color: @grayDark;\n    font-weight: normal;\n  }\n\n  .navigation-text {\n    margin: 0 0 0 8px;\n  }\n\n\n/* Bulk Actions */\n\n  .bulk-actions {\n    height: 38px;\n    position: relative;\n    top: 0px;\n    left: 0px;\n    background: @white;\n    border-bottom: 1px solid @gray;\n    box-sizing: border-box;\n    color: @grayDark;\n    font-size: 14px;\n    line-height: 16px;\n    padding: 11px 5px;\n  }\n\n  .bulk-actions div {\n    margin: 0px 15px;\n  }\n  .bulk-actions ul {\n    margin: -5px 15px;\n  }\n\n  .bulk-actions li {\n    padding: 0 15px;\n  }\n\n  .bulk-actions li.left {\n    padding-left: 0px;\n  }\n\n  .bulk-actions ul.right {\n    text-align: right;\n  }\n\n  .bulk-actions ul.right li {\n    padding-right: 0px;\n  }\n\n  .bulk-actions a {\n    color: @grayDark;\n    line-height: 16px;\n  }\n\n  .bulk-actions a img {\n    padding: 0; margin-top: 0;\n    height: 16px;\n  }\n\n  .bulk-actions a:hover {\n    color: @red;\n  }\n\n  .bulk-actions a span.icon {\n    padding: 0; margin-top: 0;\n    font-size: 16px;\n    line-height: 16px;\n  }\n\n  .bulk-actions-hints a span.icon {\n    font-size: 14px;\n    line-height: 16px;\n  }\n\n  .bulk-actions li.hide {\n    visibility: hidden;\n  }\n\n\n/* Content View */\n\n  #content-tall-view,\n  #content-view {\n    position: absolute;\n    top: 84px;\n    bottom: 0;\n    right: 0;\n    left: 0;\n    overflow-y: scroll;\n    z-index: 5;\n    background: @grayLight;\n  }\n  #content-tall-view {\n    top: 0px;\n  }\n\n  div.content-normal {\n    margin: 25px 20px;\n  }\n\n  div.content-small {\n    max-width: 400px;\n  }\n\n  div.content-medium {\n    max-width: 600px;\n  }\n\n  div.content-large {\n    max-width: 800px;\n  }\n\n\n/* Debug */\n\n  #debug {\n    width: 100%;\n    font-size: 14px;\n    font-family: Helvetica, Arial, sans-serif;\n    text-align: center;\n    color: #666666;\n    line-height: 14px;\n  }\n\n  #debug p {\n    margin: 0px 5px;\n    padding: 0px;\n  }\n\n\n/* Images */\n\n  .img-border {\n    border: 1px solid @grayMid;\n    -webkit-transition-duration: 0.3s;\n    -moz-transition-duration: 0.3s;\n    transition-duration: 0.3s;\n  }\n\n  .img-border:hover {\n    border: 1px solid @grayDark;\n    -webkit-transition-duration: 0.3s;\n    -moz-transition-duration: 0.3s;\n    transition-duration: 0.3s;\n  }\n\n\n/* Text */\n/* REBAR - move to rebar */\n\n  .text-detail {\n    color: @gray;\n  }\n\n  .text-detail a {\n    color: @gray;\n  }\n\n  .text-detail a:hover {\n    color: @blue;\n  }\n\n  a.link-detail,\n  a.link-detail:visited {\n    color: @gray;\n    font-weight: normal !important;\n  }\n\n  a.link-detail:hover {\n    color: @grayDark;\n  }\n\n  a.disabled {\n     pointer-events: none;\n     cursor: default;\n  }\n\n  p.paragraph-success,\n  p.paragraph-important,\n  p.paragraph-alert,\n  p.paragraph-warning {\n    padding: (@base-padding / 2) @base-padding;\n    border-radius: @base-border-radius;\n    font-weight: bold;\n  }\n\n  p.paragraph-success {\n    background: @green;\n    color: @white;\n  }\n\n  p.paragraph-important {\n    background: @blue;\n    color: @white;\n  }\n\n  p.paragraph-alert {\n    background: @orange;\n    color: @white;\n  }\n\n  p.paragraph-warning {\n    background: @red;\n    color: @white;\n  }\n\n\n/* Form Inputs */\n/* REBAR: move this rebar/forms.less library at some point */\n\n  ul.radio-list {\n    background: @white;\n    border: 1px solid @gray;\n    border-radius: @base-border-radius;\n  }\n\n  ul.radio-list li {\n    margin-bottom: 0px;\n    border-top: 1px solid @gray;\n  }\n\n  ul.radio-list li:first-child {\n    border-top: 0px;\n  }\n\n  label.radio-list-item {\n    width: 100%;\n    height: 100%;\n    display: table;\n    margin-bottom: 0px;\n    box-sizing: content-box;\n  }\n\n  label.radio-list-item:hover {\n    background: @colorSelect;\n    cursor: pointer;\n  }\n\n  label.radio-list-item div {\n    display: table-cell;\n    padding: @base-padding;\n    vertical-align: middle;\n  }\n\n  span.radio-list-item-detail {\n    color: @grayLight;\n  }\n\n\n/* List Items */\n/* REBAR: Move these more global styles to Rebar */\n\n  ul.items {\n    margin: @base-margin 0;\n  }\n\n  ul.items.grouped {\n    background: @white;\n    border-radius: @base-border-radius;\n    border: 1px solid @gray;\n  }\n\n  ul.items li.separate {\n    background: @white;\n    margin-bottom: @base-margin;\n    padding: @base-padding;\n    border: 1px solid @gray;\n    border-radius: @base-border-radius;\n  }\n\n  ul.items.grouped li.grouped:first-child {\n    border-top: 0px;\n  }\n\n  ul.items.grouped li.grouped {\n    border-top: 1px solid @gray;\n    padding: @base-padding;\n  }\n\n  ul.items li.separate h5,\n  ul.items li.grouped  h5 {\n    margin-bottom: @base-margin / 1.5;\n  }\n\n\n/* Response Rectangles - Tags & Contacts */\n\n  .rectangles-container   { width: 97%; margin-top: 1.5%; margin-bottom: 1.5%; }\n  .rectangles-outer       { background: @white; border: 1px solid @grayMid; box-sizing: border-box; border-radius: @base-border-radius; }\n  .rectangles-outer:hover { background: @grayLight; }\n\n\n/* User Card - Avatar, Name, Address */\n\n  .global-user-avatar {\n    display: inline-block;\n    width: 45px;\n    margin-right: 10px;\n  }\n\n  .global-user-avatar-img {\n    width: 45px;\n    border-radius: 3px;\n  }\n\n  .global-user-name(@width:120px) {\n    width: @width;\n    display: inline-block;\n    vertical-align: top;\n  }\n\n  .global-user-name-a(@size:14px) {\n    display: inline-block;\n    font-size: @size;\n    font-weight: bold;\n    line-height: @size + 2px;\n    word-break: break-word;\n    color: @grayDark;\n    vertical-align: top;\n    margin-bottom: 5px;\n  }\n\n  .global-user-address(@size:12px) {\n    display: inline-block;\n    color: @gray;\n    font-size: @size;\n    font-weight: normal;\n  }\n\n  .user .avatar {\n    .global-user-avatar();\n  }\n\n  .user .avatar img {\n    .global-user-avatar-img();\n  }\n\n  .user .name {\n    .global-user-name();\n  }\n\n  .user .name a {\n    .global-user-name-a(14px);\n  }\n\n  .user .address {\n    .global-user-address(12px);\n  }\n\n\n/* Crypto */\n\n  .vault-lock-outer {\n    display: inline-block;\n    width: 225px;\n    height: 225px;\n    border-radius: 112.5px;\n    border: 1px solid @grayDark;\n    box-sizing: border-box;\n    background: @gray;\n  }\n\n  .vault-lock-inner {\n    display: inline-block;\n    width: 171px;\n    height: 171px;\n    border-radius: 85.5px;\n    border: 1px solid @grayDark;\n    box-sizing: border-box;\n    background: @white;\n    position: relative;\n    top: 27px;\n    left: 27px;\n  }\n\n  .vault-lock {\n    display: inline-block;\n    font-size: 72px;\n    line-height: 72px;\n    position: relative;\n    top: 42px;\n    left: 47px;\n  }\n\n/* Keyboard shortcuts */\n\n  kbd {\n     display: inline-block;\n     padding: 5px 8px;\n     margin: 3px;\n     min-width: 10px;\n     border: 1px @gray solid;\n     border-bottom: 2px @gray solid;\n     border-radius: @base-border-radius;\n     background-color: @grayLight;\n     font-size: 14px;\n     text-align: center;\n     font-style: normal;\n  }\n\n  kbd::first-letter {\n      text-transform: capitalize;\n  }\n"
  },
  {
    "path": "shared-data/default-theme/less/app/helpers.less",
    "content": "/* Some general helper classes */\n\n.noselect(){\n  -webkit-touch-callout: none;\n  -webkit-user-select: none;\n  -khtml-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n\n/*\n  Sets cursor to pointer on a tags\n  functioning as buttons\n */\n\na.clickable {\n  cursor: pointer;\n  .noselect;\n}\n\n/*\n  As they were HTML5 labels\n */\n\nspan.checkbox {\n  cursor: default;\n  .noselect;\n}"
  },
  {
    "path": "shared-data/default-theme/less/app/icons.less",
    "content": "@font-face {\r\n\tfont-family: 'Mailpile-Interface';\r\n\tsrc:url('../../webfonts/Mailpile-Interface.eot');\r\n\tsrc:url('../../webfonts/Mailpile-Interface.eot') format('embedded-opentype'),\r\n\t\turl('../../webfonts/Mailpile-Interface.woff') format('woff'),\r\n\t\turl('../../webfonts/Mailpile-Interface.ttf') format('truetype'),\r\n\t\turl('../../webfonts/Mailpile-Interface.svg#Mailpile-Interface') format('svg');\r\n\tfont-weight: normal;\r\n\tfont-style: normal;\r\n}\r\n\r\n[class^=\"icon-\"], [class*=\" icon-\"] {\r\n\tfont-family: 'Mailpile-Interface';\r\n\tspeak: none;\r\n\tfont-style: normal;\r\n\tfont-weight: normal;\r\n\tfont-variant: normal;\r\n\ttext-transform: none;\r\n\tline-height: 1;\r\n\r\n\t/* Better Font Rendering =========== */\r\n\t-webkit-font-smoothing: antialiased;\r\n\t-moz-osx-font-smoothing: grayscale;\r\n}\r\n\r\n\r\n.icon-addresses:before            { content: \"\\e600\" }\r\n.icon-ads:before                  { content: \"\\e601\" }\r\n.icon-alerts:before               { content: \"\\e602\" }\r\n.icon-animals:before              { content: \"\\e603\" }\r\n.icon-archive:before              { content: \"\\e604\" }\r\n.icon-arrow-down:before           { content: \"\\e605\" }\r\n.icon-arrow-left:before           { content: \"\\e606\" }\r\n.icon-arrow-right:before          { content: \"\\e607\" }\r\n.icon-arrow-up:before             { content: \"\\e608\" }\r\n.icon-attachment:before           { content: \"\\e609\" }\r\n.icon-calendar:before             { content: \"\\e60a\" }\r\n.icon-checkmark:before            { content: \"\\e60b\" }\r\n.icon-circle-dotted:before        { content: \"\\e60c\" }\r\n.icon-circle-info:before          { content: \"\\e60d\" }\r\n.icon-circle-x:before             { content: \"\\e60e\" }\r\n.icon-clock:before                { content: \"\\e60f\" }\r\n.icon-code:before                 { content: \"\\e610\" }\r\n.icon-collapse:before             { content: \"\\e611\" }\r\n.icon-columns:before              { content: \"\\e612\" }\r\n.icon-comment:before              { content: \"\\e613\" }\r\n.icon-compose:before              { content: \"\\e614\" }\r\n.icon-dislike:before              { content: \"\\e615\" }\r\n.icon-document:before             { content: \"\\e616\" }\r\n.icon-donate:before               { content: \"\\e617\" }\r\n.icon-download:before             { content: \"\\e618\" }\r\n.icon-expand:before               { content: \"\\e619\" }\r\n.icon-eye:before                  { content: \"\\e61a\" }\r\n.icon-filters:before              { content: \"\\e61b\" }\r\n.icon-fingerprint:before          { content: \"\\e61c\" }\r\n.icon-flashlight:before           { content: \"\\e61d\" }\r\n.icon-food:before                 { content: \"\\e61e\" }\r\n.icon-force-graph:before          { content: \"\\e61f\" }\r\n.icon-forum:before                { content: \"\\e620\" }\r\n.icon-forward:before              { content: \"\\e621\" }\r\n.icon-geopoint:before             { content: \"\\e622\" }\r\n.icon-graph:before                { content: \"\\e623\" }\r\n.icon-groups:before               { content: \"\\e624\" }\r\n.icon-help:before                 { content: \"\\e625\" }\r\n.icon-home:before                 { content: \"\\e626\" }\r\n.icon-hosting:before              { content: \"\\e627\" }\r\n.icon-image:before                { content: \"\\e628\" }\r\n.icon-inbox:before                { content: \"\\e629\" }\r\n.icon-key:before                  { content: \"\\e62a\" }\r\n.icon-later:before                { content: \"\\e62b\" }\r\n.icon-lightbulb:before            { content: \"\\e62c\" }\r\n.icon-like:before                 { content: \"\\e62d\" }\r\n.icon-links:before                { content: \"\\e62e\" }\r\n.icon-list:before                 { content: \"\\e62f\" }\r\n.icon-lock-closed:before          { content: \"\\e630\" }\r\n.icon-lock-error:before           { content: \"\\e631\" }\r\n.icon-lock-open:before            { content: \"\\e632\" }\r\n.icon-logo:before                 { content: \"\\e633\" }\r\n.icon-logout:before               { content: \"\\e634\" }\r\n.icon-mailsource:before           { content: \"\\e635\" }\r\n.icon-map:before                  { content: \"\\e636\" }\r\n.icon-merge:before                { content: \"\\e637\" }\r\n.icon-minus:before                { content: \"\\e638\" }\r\n.icon-money:before                { content: \"\\e639\" }\r\n.icon-move:before                 { content: \"\\e63a\" }\r\n.icon-music:before                { content: \"\\e63b\" }\r\n.icon-new:before                  { content: \"\\e63c\" }\r\n.icon-news:before                 { content: \"\\e63d\" }\r\n.icon-not-spam:before             { content: \"\\e63e\" }\r\n.icon-notifications:before        { content: \"\\e63f\" }\r\n.icon-outbox:before               { content: \"\\e640\" }\r\n.icon-photos:before               { content: \"\\e641\" }\r\n.icon-plus:before                 { content: \"\\e642\" }\r\n.icon-preferences:before          { content: \"\\e643\" }\r\n.icon-privacy:before              { content: \"\\e644\" }\r\n.icon-profiles:before             { content: \"\\e645\" }\r\n.icon-purchases:before            { content: \"\\e646\" }\r\n.icon-receipts:before             { content: \"\\e647\" }\r\n.icon-reply-all:before            { content: \"\\e648\" }\r\n.icon-reply:before                { content: \"\\e649\" }\r\n.icon-robot:before                { content: \"\\e64a\" }\r\n.icon-routes:before               { content: \"\\e64b\" }\r\n.icon-rss:before                  { content: \"\\e64c\" }\r\n.icon-search:before               { content: \"\\e64d\" }\r\n.icon-sent:before                 { content: \"\\e64e\" }\r\n.icon-settings:before             { content: \"\\e64f\" }\r\n.icon-signature-expired:before    { content: \"\\e650\" }\r\n.icon-signature-invalid:before    { content: \"\\e651\" }\r\n.icon-signature-none:before       { content: \"\\e652\" }\r\n.icon-signature-revoked:before    { content: \"\\e653\" }\r\n.icon-signature-unknown:before    { content: \"\\e654\" }\r\n.icon-signature-unverified:before { content: \"\\e655\" }\r\n.icon-signature-verified:before   { content: \"\\e656\" }\r\n.icon-social:before               { content: \"\\e657\" }\r\n.icon-spam:before                 { content: \"\\e658\" }\r\n.icon-speed:before                { content: \"\\e659\" }\r\n.icon-spreadsheet:before          { content: \"\\e65a\" }\r\n.icon-star:before                 { content: \"\\e65b\" }\r\n.icon-tag:before                  { content: \"\\e65c\" }\r\n.icon-tags:before                 { content: \"\\e65d\" }\r\n.icon-text:before                 { content: \"\\e65e\" }\r\n.icon-themes:before               { content: \"\\e65f\" }\r\n.icon-tor:before                  { content: \"\\e660\" }\r\n.icon-transit:before              { content: \"\\e661\" }\r\n.icon-trash:before                { content: \"\\e662\" }\r\n.icon-travel:before               { content: \"\\e663\" }\r\n.icon-trophy:before               { content: \"\\e664\" }\r\n.icon-unknown:before              { content: \"\\e665\" }\r\n.icon-upload:before               { content: \"\\e666\" }\r\n.icon-user:before                 { content: \"\\e667\" }\r\n.icon-video:before                { content: \"\\e668\" }\r\n.icon-work:before                 { content: \"\\e669\" }\r\n.icon-x:before                    { content: \"\\e66a\" }\r\n.icon-zip:before                  { content: \"\\e66b\" }\r\n\r\n\r\n/* Sidebar - makes icons switch to dotted circle on drag */\r\n.sidebar-tags-draggable-hover .icon-mailsource:before,\r\n.sidebar-tags-draggable-hover .icon-inbox:before,\r\n.sidebar-tags-draggable-hover .icon-sent:before,\r\n.sidebar-tags-draggable-hover .icon-spam:before,\r\n.sidebar-tags-draggable-hover .icon-trash:before,\r\n\r\n.sidebar-tags-draggable-hover .icon-alerts:before,\r\n.sidebar-tags-draggable-hover .icon-animals:before,\r\n.sidebar-tags-draggable-hover .icon-calendar:before,\r\n.sidebar-tags-draggable-hover .icon-checkmark:before,\r\n.sidebar-tags-draggable-hover .icon-clock:before,\r\n.sidebar-tags-draggable-hover .icon-code:before,\r\n.sidebar-tags-draggable-hover .icon-comment:before,\r\n.sidebar-tags-draggable-hover .icon-columns:before,\r\n.sidebar-tags-draggable-hover .icon-document:before,\r\n.sidebar-tags-draggable-hover .icon-donate:before,\r\n.sidebar-tags-draggable-hover .icon-download:before,\r\n.sidebar-tags-draggable-hover .icon-flashlight:before,\r\n.sidebar-tags-draggable-hover .icon-food:before,\r\n.sidebar-tags-draggable-hover .icon-forum:before,\r\n.sidebar-tags-draggable-hover .icon-force-graph:before,\r\n.sidebar-tags-draggable-hover .icon-geopoint:before,\r\n.sidebar-tags-draggable-hover .icon-groups:before,\r\n.sidebar-tags-draggable-hover .icon-graph:before,\r\n.sidebar-tags-draggable-hover .icon-help:before,\r\n.sidebar-tags-draggable-hover .icon-home:before,\r\n.sidebar-tags-draggable-hover .icon-image:before,\r\n.sidebar-tags-draggable-hover .icon-key:before,\r\n.sidebar-tags-draggable-hover .icon-links:before,\r\n.sidebar-tags-draggable-hover .icon-list:before,\r\n.sidebar-tags-draggable-hover .icon-lock-closed:before,\r\n.sidebar-tags-draggable-hover .icon-map:before,\r\n.sidebar-tags-draggable-hover .icon-money:before,\r\n.sidebar-tags-draggable-hover .icon-music:before,\r\n.sidebar-tags-draggable-hover .icon-new:before,\r\n.sidebar-tags-draggable-hover .icon-news:before,\r\n.sidebar-tags-draggable-hover .icon-photos:before,\r\n.sidebar-tags-draggable-hover .icon-privacy:before,\r\n.sidebar-tags-draggable-hover .icon-purchases:before,\r\n.sidebar-tags-draggable-hover .icon-receipts:before,\r\n.sidebar-tags-draggable-hover .icon-spreadsheet:before,\r\n.sidebar-tags-draggable-hover .icon-rss:before,\r\n.sidebar-tags-draggable-hover .icon-robot:before,\r\n.sidebar-tags-draggable-hover .icon-star:before,\r\n.sidebar-tags-draggable-hover .icon-tag:before,\r\n.sidebar-tags-draggable-hover .icon-tags:before,\r\n.sidebar-tags-draggable-hover .icon-text:before,\r\n.sidebar-tags-draggable-hover .icon-themes:before,\r\n.sidebar-tags-draggable-hover .icon-transit:before,\r\n.sidebar-tags-draggable-hover .icon-travel:before,\r\n.sidebar-tags-draggable-hover .icon-trophy:before,\r\n.sidebar-tags-draggable-hover .icon-upload:before,\r\n.sidebar-tags-draggable-hover .icon-video:before,\r\n.sidebar-tags-draggable-hover .icon-user:before,\r\n.sidebar-tags-draggable-hover .icon-work:before,\r\n.sidebar-tags-draggable-hover .icon-zip:before {\r\n  content: \"\\e60c\";\r\n  color: @black;\r\n}\r\n\r\n\r\n/* Mimetype - icons */\r\n\r\n.icon-mime:before,\r\n.icon-mime[type=\"application/octet-stream\"]:before,\r\n.icon-mime[type=\"application/mac-binhex40\"]:before,\r\n.icon-mime[type=\"application/x-shockwave-flash\"]:before,\r\n.icon-mime[type=\"application/x-director\"]:before,\r\n.icon-mime[type=\"application/x-x509-ca-cert\"]:before,\r\n.icon-mime[type=\"application/x-director\"]:before,\r\n.icon-mime[type=\"application/x-msdownload\"]:before,\r\n.icon-mime[type=\"application/x-director\"]:before {\r\n  // application\r\n\tcontent: \"\\e609\";\r\n}\r\n\r\n.icon-mime[type=\"application/mbox\"]:before {\r\n  // mailbox\r\n  content: \"\\e635\"\r\n}\r\n\r\n.icon-mime[type=\"application/x-compress\"]:before,\r\n.icon-mime[type=\"application/x-compressed\"]:before,\r\n.icon-mime[type=\"application/x-tar\"]:before,\r\n.icon-mime[type=\"application/zip\"]:before,\r\n.icon-mime[type=\"application/x-stuffit\"]:before,\r\n.icon-mime[type=\"application/x-gzip\"]:before,\r\n.icon-mime[type=\"application/x-gzip-compressed\"]:before,\r\n.icon-mime[type=\"application/x-tar\"]:before,\r\n.icon-mime[type=\"application/x-winzip\"]:before,\r\n.icon-mime[type=\"application/x-zip\"]:before,\r\n.icon-mime[type=\"application/x-zip-compressed\"]:before,\r\n.icon-mime[type=\"application/x-rar-compressed\"]:before {\r\n  // archive\r\n\tcontent: \"\\e66b\";\r\n}\r\n\r\n.icon-mime[type=\"audio/amr\"]:before,\r\n.icon-mime[type=\"audio/mp3\"]:before,\r\n.icon-mime[type=\"audio/midi\"]:before,\r\n.icon-mime[type=\"audio/mid\"]:before,\r\n.icon-mime[type=\"audio/mpeg\"]:before,\r\n.icon-mime[type=\"audio/basic\"]:before,\r\n.icon-mime[type=\"audio/x-aiff\"]:before,\r\n.icon-mime[type=\"audio/x-pn-realaudio\"]:before,\r\n.icon-mime[type=\"audio/x-pn-realaudio\"]:before,\r\n.icon-mime[type=\"audio/mid\"]:before,\r\n.icon-mime[type=\"audio/basic\"]:before,\r\n.icon-mime[type=\"audio/x-wav\"]:before,\r\n.icon-mime[type=\"audio/x-mpegurl\"]:before,\r\n.icon-mime[type=\"audio/wave\"]:before,\r\n.icon-mime[type=\"audio/wav\"]:before,\r\n.icon-mime[type=\"audio/mp4a-latm\"]:before {\r\n  // audio\r\n\tcontent: \"\\e63b\";\r\n}\r\n\r\n.icon-mime[type=\"text/calendar\"]:before,\r\n.icon-mime[type=\"application/ics\"]:before,\r\n.icon-mime[type=\"text/x-vcalendar\"]:before {\r\n  // calendar\r\n\tcontent: \"\\e60a\";\r\n}\r\n\r\n.icon-mime[type=\"text/directory\"]:before,\r\n.icon-mime[type=\"text/x-vcard\"]:before,\r\n.icon-mime[type=\"text/x-ms-contact\"]:before {\r\n  // contacts\r\n\tcontent: \"\\e600\";\r\n}\r\n\r\n.icon-mime[type=\"image/gif\"]:before,\r\n.icon-mime[type=\"image/png\"]:before,\r\n.icon-mime[type=\"image/jpeg\"]:before,\r\n.icon-mime[type=\"image/cis-cod\"]:before,\r\n.icon-mime[type=\"image/ief\"]:before,\r\n.icon-mime[type=\"image/pipeg\"]:before,\r\n.icon-mime[type=\"image/tiff\"]:before,\r\n.icon-mime[type=\"image/x-cmx\"]:before,\r\n.icon-mime[type=\"image/x-cmu-raster\"]:before,\r\n.icon-mime[type=\"image/x-rgb\"]:before,\r\n.icon-mime[type=\"image/x-icon\"]:before,\r\n.icon-mime[type=\"image/x-xbitmap\"]:before,\r\n.icon-mime[type=\"image/x-xpixmap\"]:before,\r\n.icon-mime[type=\"image/x-xwindowdump\"]:before,\r\n.icon-mime[type=\"image/x-portable-anymap\"]:before,\r\n.icon-mime[type=\"image/x-portable-graymap\"]:before,\r\n.icon-mime[type=\"image/x-portable-pixmap\"]:before,\r\n.icon-mime[type=\"image/x-portable-bitmap\"]:before,\r\n.icon-mime[type=\"image/svg+xml\"]:before,\r\n.icon-mime[type=\"application/x-photoshop\"]:before,\r\n.icon-mime[type=\"application/postscript\"]:before {\r\n  // image\r\n\tcontent: \"\\e641\";\r\n}\r\n\r\n.icon-mime[type=\"application/pgp-signature\"]:before {\r\n  // signature\r\n\tcontent: \"\\e656\";\r\n}\r\n\r\n.icon-mime[type=\"application/pgp-keys\"]:before {\r\n  // keys\r\n\tcontent: \"\\e62a\";\r\n}\r\n\r\n.icon-mime[type=\"application/x-mobipocket-ebook\"]:before,\r\n.icon-mime[type=\"application/epub+zip\"]:before,\r\n.icon-mime[type=\"application/rtf\"]:before,\r\n.icon-mime[type=\"application/vnd.ms-works\"]:before,\r\n.icon-mime[type=\"application/msword\"]:before,\r\n.icon-mime[type=\"application/pdf\"]:before,\r\n.icon-mime[type=\"application/x-download\"]:before,\r\n.icon-mime[type=\"message/rfc822\"]:before,\r\n.icon-mime[type=\"text/x-log\"]:before,\r\n.icon-mime[type=\"text/scriptlet\"]:before,\r\n.icon-mime[type=\"text/plain\"]:before,\r\n.icon-mime[type=\"text/iuls\"]:before,\r\n.icon-mime[type=\"text/plain\"]:before,\r\n.icon-mime[type=\"text/richtext\"]:before,\r\n.icon-mime[type=\"text/x-setext\"]:before,\r\n.icon-mime[type=\"text/x-component\"]:before,\r\n.icon-mime[type=\"text/webviewhtml\"]:before,\r\n.icon-mime[type=\"text/h323\"]:before,\r\n.icon-mime[type=\"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"]:before,\r\n.icon-mime[type=\"application/vnd.oasis.opendocument.text\"]:before,\r\n.icon-mime[type=\"application/vnd.oasis.opendocument.text-template\"]:before,\r\n.icon-mime[type=\"application/vnd.sun.xml.writer\"]:before,\r\n.icon-mime[type=\"application/vnd.sun.xml.writer.template\"]:before,\r\n.icon-mime[type=\"application/vnd.sun.xml.writer.global\"]:before,\r\n.icon-mime[type=\"application/vnd.stardivision.writer\"]:before,\r\n.icon-mime[type=\"application/vnd.stardivision.writer-global\"]:before,\r\n.icon-mime[type=\"application/x-starwriter\"]:before {\r\n  // document\r\n\tcontent: \"\\e616\";\r\n}\r\n\r\n.icon-mime[type=\"application/json\"]:before,\r\n.icon-mime[type=\"application/x-javascript\"]:before,\r\n.icon-mime[type=\"text/html\"]:before,\r\n.icon-mime[type=\"text/css\"]:before,\r\n.icon-mime[type=\"text/xml\"]:before,\r\n.icon-mime[type=\"text/json\"]:before {\r\n  // code\r\n\tcontent: \"\\e610\";\r\n}\r\n\r\n.icon-mime[type=\"application/excel\"]:before,\r\n.icon-mime[type=\"application/msexcel\"]:before,\r\n.icon-mime[type=\"application/vnd.ms-excel\"]:before,\r\n.icon-mime[type=\"application/vnd.msexcel\"]:before,\r\n.icon-mime[type=\"application/csv\"]:before,\r\n.icon-mime[type=\"application/x-csv\"]:before,\r\n.icon-mime[type=\"text/tab-separated-values\"]:before,\r\n.icon-mime[type=\"text/x-comma-separated-values\"]:before,\r\n.icon-mime[type=\"text/comma-separated-values\"]:before,\r\n.icon-mime[type=\"text/csv\"]:before,\r\n.icon-mime[type=\"text/x-csv\"]:before,\r\n.icon-mime[type=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\"]:before,\r\n.icon-mime[type=\"application/vnd.oasis.opendocument.spreadsheet\"]:before,\r\n.icon-mime[type=\"application/vnd.oasis.opendocument.spreadsheet-template\"]:before,\r\n.icon-mime[type=\"application/vnd.sun.xml.calc\"]:before,\r\n.icon-mime[type=\"application/vnd.sun.xml.calc.template\"]:before,\r\n.icon-mime[type=\"application/vnd.stardivision.calc\"]:before,\r\n.icon-mime[type=\"application/x-starcalc\"]:before {\r\n  // spreadsheet\r\n\tcontent: \"\\e65a\";\r\n}\r\n\r\n.icon-mime[type=\"application/powerpoint\"]:before,\r\n.icon-mime[type=\"application/vnd.ms-powerpoint\"]:before\r\n.icon-mime[type=\"application/vnd.oasis.opendocument.presentation\"]:before,\r\n.icon-mime[type=\"application/vnd.oasis.opendocument.presentation-template\"]:before,\r\n.icon-mime[type=\"application/vnd.openxmlformats-officedocument.presentationml.presentation\"]:before,\r\n.icon-mime[type=\"application/vnd.sun.xml.impress\"]:before,\r\n.icon-mime[type=\"application/vnd.sun.xml.impress.template\"]:before,\r\n.icon-mime[type=\"application/vnd.stardivision.impress\"]:before,\r\n.icon-mime[type=\"application/vnd.stardivision.impress-packed\"]:before,\r\n.icon-mime[type=\"application/x-starimpress\"]:before {\r\n  // slideshow\r\n\tcontent: \"\\e65f\";\r\n}\r\n\r\n.icon-mime[type=\"video/quicktime\"]:before,\r\n.icon-mime[type=\"video/x-sgi-movie\"]:before,\r\n.icon-mime[type=\"video/mpeg\"]:before,\r\n.icon-mime[type=\"video/x-la-asf\"]:before,\r\n.icon-mime[type=\"video/x-ms-asf\"]:before,\r\n.icon-mime[type=\"video/x-msvideo\"]:before,\r\n.icon-mime[type=\"video/mp4\"]:before,\r\n.icon-mime[type=\"video/mp2\"]:before,\r\n.icon-mime[type=\"video/avi\"]:before {\r\n  // video\r\n\tcontent: \"\\e668\";\r\n}\r\n"
  },
  {
    "path": "shared-data/default-theme/less/app/library-override.less",
    "content": "/* Compose - Custom Styles */\n.select2-hidden-accessible {\n  visibility: hidden;\n}\n.select2-result-label .compose-select-avatar {\n  display: inline-block;\n  margin-right: 8px;\n}\n.select2-result-label .compose-select-avatar img {\n  width: 32px;\n  height: 32px;\n}\n.select2-result-label .compose-select-avatar .icon-user {\n  font-size: 32px;\n  line-height: 32px;\n}\n.select2-result-label .compose-select-name {\n  display: inline-block;\n  margin-top: 0px;\n  padding-top: 0px;\n  font-size: 14px;\n  font-weight: bold;\n  line-height: 14px;\n  color: #4d4d4d;\n}\n.select2-result-label .icon-lock-closed {\n  color: #4b9441;\n  margin-left: 8px;\n}\n.select2-result-label .compose-select-address {\n  font-family: Helvetica, Arial, sans-serif;\n  font-size: 12px;\n  font-weight: normal;\n  line-height: 12px;\n  color: #8c8c8c;\n}\n.select2-search-choice .compose-choice-name {\n  display: table-cell;\n  vertical-align: middle;\n  font-size: 14px;\n  font-weight: bold;\n  line-height: 14px;\n  color: #4d4d4d;\n}\n.select2-search-choice .avatar {\n  display: table-cell;\n  vertical-align: middle;\n  padding-right: 10px;\n}\n.select2-search-choice .avatar img {\n  width: 24px;\n  height: 24px;\n}\n.select2-search-choice .icon-user {\n  font-size: 24px;\n  line-height: 24px;\n  margin-right: 10px;\n}\n.select2-search-choice .icon-blank {\n  width: 0px;\n  height: 14px;\n  display: inline-block;\n}\n.select2-search-choice .icon-lock-closed {\n  color: #4b9441;\n  margin-left: 8px;\n}\n\n/* Tipped style */\n.qtip-tipped {\n  border: 0px solid #444;\n  -moz-border-radius: 3px;\n  -webkit-border-radius: 3px;\n  border-radius: 3px;\n  background-color: #444;\n  color: #ffffff;\n  font-size: 12px;\n  font-weight: bold;\n  font-family: Arial;\n  line-height: 14px;\n  padding: 4px 6px;\n}\n.qtip-tipped .qtip-titlebar {\n  border-bottom-width: 0;\n  color: white;\n  background: #3A79B8;\n  background-image: -webkit-gradient(linear, left top, left bottom, from(#3a79b8), to(#2e629d));\n  background-image: -webkit-linear-gradient(top, #3a79b8, #2e629d);\n  background-image: -moz-linear-gradient(top, #3a79b8, #2e629d);\n  background-image: -ms-linear-gradient(top, #3a79b8, #2e629d);\n  background-image: -o-linear-gradient(top, #3a79b8, #2e629d);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#3a79b8, endColorstr=#2e629d);\n  -ms-filter: \"progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D)\";\n}\n.qtip-tipped .qtip-content {\n  text-align: center;\n}\n.qtip-tipped .qtip-icon {\n  border: 2px solid #285589;\n  background: #285589;\n}\n.qtip-tipped .qtip-icon .ui-icon {\n  background-color: #FBFBFB;\n  color: #555;\n}\n/* IE9 fix - removes all filters */\n.qtip:not(.ie9haxors) div.qtip-content,\n.qtip:not(.ie9haxors) div.qtip-titlebar {\n  filter: none;\n  -ms-filter: none;\n}\n.qtip .qtip-tip {\n  margin: 0 auto;\n  overflow: hidden;\n  z-index: 10;\n}\n/* Opera bug #357 - Incorrect tip position https://github.com/Craga89/qTip2/issues/367 */\nx:-o-prefocus,\n.qtip .qtip-tip {\n  visibility: hidden;\n}\n.qtip .qtip-tip,\n.qtip .qtip-tip .qtip-vml,\n.qtip .qtip-tip canvas {\n  position: absolute;\n  color: #123456;\n  background: transparent;\n  border: 0 dashed transparent;\n}\n.qtip .qtip-tip canvas {\n  top: 0;\n  left: 0;\n}\n.qtip .qtip-tip .qtip-vml {\n  behavior: url(#default#VML);\n  display: inline-block;\n  visibility: visible;\n}\n"
  },
  {
    "path": "shared-data/default-theme/less/app/login.less",
    "content": "/* Login */\n\n  #login {\n    width: 100%;\n    border: 8px solid @grayMid;\n    box-sizing: border-box;\n  }\n\n  #login-left {\n    width: 65%;\n    height: 100px;\n    background: @grayLight;\n    border-right: 4px solid @grayMid;\n    box-sizing: border-box;\n    display: inline-block;\n    float: left;\n  }\n\n  #login-right {\n    width: 35%;\n    height: 100%;\n    background: @grayLight;\n    border-left: 2px solid @grayMid;\n    box-sizing: border-box;\n    display: inline-block;\n    float: right;\n  }\n\n  #login-logo {\n    width: 223px;\n    position: absolute;\n    top: 13%;\n    left: 20%;\n  }\n\n  #login-logo #logo-icon {\n    width: 150px;\n    height: 100px;\n    display: block;\n    margin: 0px auto 35px auto;\n  }\n\n  #login-logo #logo-name {\n    width: 223px;\n    height: 72px;\n    display: block;\n    margin: 0 auto;\n  }\n\n  #login-messages {\n    position: absolute;\n    top: 35%;\n    margin: @base-margin auto;\n    width: 400px;\n    font-weight: bold;\n  }\n\n  #login-vault-lock {\n    margin: 0 auto;\n    position: absolute;\n    top: 10%;\n    left: 55%;\n  }\n\n  #login-details {\n    width: 485px;\n    margin: 0px auto;\n    position: absolute;\n    top: 50%;\n    left: 33%;\n  }\n\n  .form-text {\n    display: inline-block;\n    position: relative;\n    vertical-align: top;\n    top: 15px;\n    left: 0px;\n    font-family: @mailpile-text-font-family-bold;\n    font-size: 18px;\n    line-height: 18px;\n    color: @grayDark;\n  }\n\n  #form-login {\n    width: 246px;\n    display: inline-block;\n  }\n\n  .form-login {\n    width: 246px;\n    height: 36px;\n    display: inline-block;\n    margin-top: 5px;\n    margin-left: 15px;\n    border-radius: 5px;\n    border: 1px solid @gray;\n  }\n\n  .form-login input {\n    width: 178px;\n    height: 18px;\n    padding: 9px 12px;\n    margin-bottom: 5px;\n    float: left;    \n    font: normal 18px @text-font-family-bold;\n    border: 0;\n    background: @white;\n    border-radius: 5px 0 0 5px;\n    color: @grayMid;   \n  }\n\n  .form-login input#login-username {\n    border-radius: 5px 5px 5px 5px;\n    width: 222px;\n  }\n   \n  .form-login input:focus {\n    outline: 0;\n    background: #fff;\n    box-shadow: 0 0 1px @grayDark inset;\n    color: @grayDark;\n  }\n   \n  .form-login input::-webkit-input-placeholder,\n  .form-login input:-moz-placeholder,\n  .form-login input:-ms-input-placeholder {\n    color: #999;\n    font-weight: normal;\n    font-style: italic;\n  }    \n   \n  .form-login button {\n    overflow: visible;\n    position: relative;\n    float: right;\n    border: 0;\n    padding: 0;\n    cursor: pointer;\n    height: 36px;\n    width: 44px;\n    font: bold 18px/40px @text-font-family-bold;\n    color: @white;\n    background: @blue;\n    border-radius: 0 3px 3px 0;      \n    text-shadow: 0 -1px 0 rgba(0, 0 ,0, .3);\n  }   \n     \n  .form-login button:hover{     \n    background: darken(@blue, 5%);\n  }   \n     \n  .form-login button:active,\n  .form-login button:focus{   \n    background: darken(@blue, 15%);\n    outline: 0;   \n  }\n\n  .form-login button::-moz-focus-inner { /* remove extra button spacing for Mozilla Firefox */\n    border: 0;\n    padding: 0;\n  }\n\n   .form-login button .icon-key {\n     font-size: 24px;\n     line-height: 24px;\n   }\n\n   .login-wrong-passphrase {\n     background: @orange;\n     display: inline-block;\n     margin: @base-margin auto @base-margin 23%;\n     padding: @base-padding;\n     border-radius: (@base-border-radius * 2);\n     text-align: center;\n     font-weight: bold;\n     font-size: 14px;\n     line-height: 14px;\n     color: @white;\n   }\n\n   .logged-out-message {\n     background: @blue;\n     display: inline-block;\n     margin: @base-margin auto @base-margin 23%;\n     padding: @base-padding;\n     border-radius: (@base-border-radius * 2);\n     text-align: center;\n     font-weight: bold;\n     font-size: 14px;\n     line-height: 14px;\n     color: @white;\n   }\n\n   .still-running {\n     margin: 2em auto 0 auto;\n     padding: 1em 4em 0 0;\n     max-width: 26em;\n     color: #777;\n   }\n\n   .still-running .icon {\n     color: #aaa;\n     float: right;\n     margin: 0 2px 5px 2px;\n     padding-top: 2px;\n   }\n   .still-running .icon.bigger {\n     color: #bbb;\n     font-size: 2em;\n     padding-top: 0px;\n   }\n   .still-running .icon.smaller {\n     font-size: 0.7em;\n     padding-top: 4px;\n   }\n"
  },
  {
    "path": "shared-data/default-theme/less/app/message.less",
    "content": "/* Message */\n\n  @readable-max-width: 50em;\n\n  .crypto-color-gray   { color: @gray !important; }\n  .crypto-color-red    { color: @red !important; }\n  .crypto-color-orange { color: @orange !important; }\n  .crypto-color-blue   { color: @blue !important; }\n  .crypto-color-green  { color: @green !important; }\n\n  .border-crypto-color-gray   { border-color: @gray !important; }\n  .border-crypto-color-red    { border-color: @red !important; }\n  .border-crypto-color-orange { border-color: @orange !important; }\n  .border-crypto-color-blue   { border-color: @blue !important; }\n  .border-crypto-color-green  { border-color: @green !important; }\n\n  .message-metadata {\n    width: 100%;\n    max-width: (1em + @readable-max-width);\n    display: table;\n    margin: 0px;\n  }\n\n  .message-from-avatar {\n    .global-user-avatar();\n    display: table-cell;\n    padding-top: 15px;\n    padding-left: 15px;\n    vertical-align: top;\n    text-align: left;\n  }\n\n  .message-from-avatar a img {\n    .global-user-avatar-img();\n  }\n\n  .message-from {\n    min-width: 175px;\n    max-width: 200px;\n    display: table-cell;\n    padding-top: 13px;\n    padding-left: 15px;\n    vertical-align: text-top;\n    text-align: left;\n  }\n\n  .message-from a.name {\n    display: inline-block;\n    margin-top: 0px;\n    margin-bottom: 5px;\n    padding-top: 0px;\n    color: @grayDark;\n    font-size: 16px;\n    font-weight: bold;\n    line-height: 16px;\n  }\n\n  .message-from a:hover {\n    color: @blue;\n  }\n\n  .message-metadata-address {\n    font-size: 12px;\n    line-height: 12px;\n    color: @gray;\n    display: block;\n  }\n\n  .message-details {\n    width: 200px;\n    display: table-cell;\n    vertical-align: top;\n    text-align: right;\n    padding-top: 15px;\n    padding-left: 15px;\n  }\n\n\n  .message-details a.datetime,\n  .message-details a.datetime:visited {\n    display: block;\n    margin-bottom: 5px;\n    text-align: right;\n    font-size: 14px;\n    font-weight: bold;\n    color: @gray;\n    line-height: 14px;\n  }\n\n  .message-details a.datetime:active,\n  .message-details a.datetime:hover {\n    color: @blue;\n  }\n\n  .message-details span.icon {\n    display: inline-block;\n    margin-right: 5px;\n    font-size: 14px;\n    cursor: pointer;\n  }\n\n  .message-details .icon-circle-info {\n    color: @grayMid;\n  }\n\n  .message-details span.datetime.message {\n    color: @grayDark;\n  }\n\n  .message-details a.outbox {\n    background: @grayMid;\n    padding: 2px 5px;\n    border-radius: @base-border-radius;\n    font-size: 11px;\n    font-weight: bold;\n    color: @white;\n  }\n\n  .feedback-expand {\n    display: none;\n    color: @gray;\n    font-family: Helvetica, Arial, sans-serif;\n    font-size: 11px;\n    font-weight: normal;\n    line-height: 12px;\n  }\n\n  .message-metadata-details {\n    display: none;\n    padding-bottom: 5px;\n  }\n\n  .message-metadata-details ul {\n    margin: 10px 0px 10px 20px;\n  }\n\n  .message-metadata-details ul li {\n    display: inline-block;\n    margin-right: 8px;\n    vertical-align: middle;\n    font-size: 14px;\n    font-weight: normal;\n    line-height: 14px;\n  }\n\n  .message-metadata-details a:hover {\n    color: @blue;\n  }\n\n  .message-metadata-details.border-bottom {\n    border-bottom: 1px solid @grayMid;\n  }\n\n  .message-metadata-contact {\n    color: @grayDark;\n    display: table;\n  }\n\n  .message-metadata-contact a {\n    font-size: 14px;\n    line-height: 14px;\n    display: table-cell;\n    vertical-align: middle;\n    padding-right: 5px;\n  }\n\n  .message-metadata-contact a span {\n    font-size: 11px;\n    font-weight: normal;\n    line-height: 11px;\n    color: @gray;\n  }\n\n  .message-metadata-contact a img {\n     width: 24px;\n     height: 24px;\n     border-radius: @base-border-radius;\n     margin-right: 5px;\n  }\n\n  .message-inline-crypto {\n    width: 95%;\n    margin-top: 10px;\n    margin-left: 20px;\n    margin-bottom: 0px;\n  }\n\n  .message-inline-crypto-info       { margin-right: 10px; }\n  .message-inline-crypto-info .icon { font-size: 14px; }\n  .message-inline-crypto-info .text { font-size: 12px; font-family: Helvetica, Arial, sans-serif; font-weight: bold; text-transform: uppercase; }\n\n  .message-inline-crypto-error {\n    width: 50%;\n    text-align: center;\n    color: @gray;\n    margin: 0px auto @base-margin auto;\n  }\n\n  .message-inline-crypto-error p {\n    line-height: 18px;\n  }\n\n  .message-inline-crypto-error .icon {\n    font-size: 48px;\n    line-height: 48px;\n    display: block;\n    margin: @base-margin auto;\n  }\n\n  .message-inline-crypto-error .status {\n    margin-bottom: @base-margin;\n    font-size: 21px;\n    line-height: 24px;\n    font-family: @mailpile-text-font-family-bold;\n    font-weight: bold;\n  }\n\n  .font-style-item-text() {\n    font-family: Helvetica, Arial, sans-serif;\n    font-size: 14px;\n    line-height: 18px;\n    white-space: pre-wrap;\n    word-wrap: break-word;\n  }\n\n  iframe.message-part-html {\n    width: 65%;\n    min-width: 500px;\n    margin-top: 0px;\n    margin-left: @base-margin;\n    margin-right: 15px;\n    margin-bottom: 15px;\n  }\n\n  .message-part-html-text {\n    margin: 0px;\n    padding: 0px;\n    .font-style-item-text();\n  }\n\n  .message-part-text {\n    max-width: @readable-max-width;\n    margin-top: 5px;\n    margin-left: @base-margin;\n    margin-right: 15px;\n    margin-bottom: 15px;\n    .font-style-item-text();\n    color: @black;\n  }\n\n  .message-part-text a {\n    color: @blue;\n    font-weight: bold;\n    font-size: inherit;\n    line-height: inherit;\n  }\n\n  .message-part-text a:hover {\n    color: @blueDark;\n  }\n\n  .message-part-quote,\n  .message-part-quote-text {\n    max-width: @readable-max-width;\n    margin-left: 20px;\n    margin-bottom: 15px;\n    .font-style-item-text();\n    color: lighten(@black, 30%);\n   }\n\n  .message-part-quote a,\n  .message-part-quote a:visited {\n    color: lighten(@black, 40%);\n  }\n\n  .message-part-quote a:hover {\n    color: @grayDark;\n  }\n\n\n  .message-part-signature {\n    margin-left: 20px;\n    margin-bottom: 15px;\n    .font-style-item-text();\n    border: 1px solid @white;\n  }\n\n\n  // Undo some of the margins when displayed inline in search results\n  #pile-results .message-part-quote,\n  #pile-results .message-part-quote-text,\n  #pile-results .message-part-signature,\n  #pile-results .message-part-text {\n    margin-left: 0;\n    margin-right: 0;\n  }\n\n\n/* Message HTML controls/widgets */\n\n  .message-app-note {\n    width: 80%;\n    font-size: 13px;\n    text-align: center;\n    border-top: 1px solid @gray;\n    border-bottom: 1px solid @gray;\n    margin: 30px auto;\n    padding: 10px;\n    color: @grayDark;\n    background: @grayLight;\n    opacity: 0.65;\n    white-space: normal;\n    transition: opacity 1s;\n  }\n  .message-app-note:hover {\n    opacity: 1.0;\n    transition: opacity 1s;\n  }\n\n  .message-app-note .icon,\n  .html-image-question .icon {\n    display: block !important;\n    font-size: 30px !important;\n    margin-bottom: 10px;\n  }\n\n  .message-app-note ul { text-align: left; }\n  .message-app-note ul li {\n    list-style-type: disc;\n    margin-left: 25px;\n    padding: 0;\n  }\n  .message-app-note ul li,\n  .message-app-note ul li a {\n    white-space: normal !important;\n    vertical-align: text-top;\n  }\n\n  .message-app-note a {\n    cursor: pointer;\n  }\n\n  #pile-results .display-modes a img,\n  #pile-results .alternate-views a img {\n    opacity: 0.5;\n    height: 13px;\n  }\n  #pile-results .display-modes a:hover img,\n  #pile-results .alternate-views a:hover img {\n    opacity: 1.0;\n  }\n\n  #pile-results .alternate-views a:hover,\n  #pile-results .display-modes a:hover,\n  .message-app-note ul li a:hover {\n    color: @black !important;\n    cursor: pointer;\n  }\n\n  #pile-results .alternate-views,\n  #pile-results .display-modes {\n    float: right;\n    margin: 0; padding: 0;\n    margin-right: 4px;\n    color: @gray;\n  }\n\n  #pile-results .alternate-views li,\n  #pile-results .display-modes li {\n    display: inline-block;\n    margin: 0px 2px;\n  }\n\n  #pile-results .message-subject-container .alternate-views li {\n    opacity: 0;\n    transition: opacity 1s;\n  }\n  #pile-results .message-subject-container:hover .alternate-views li {\n    opacity: 1;\n    transition: opacity 1s;\n  }\n\n\n/* Thread Message Actions */\n\n  div.message-actions {\n    width: 100%;\n    max-width: (1em + @readable-max-width);\n  }\n\n  ul.message-actions {\n    display: inline-block;\n    margin-left: 20px;\n    margin-bottom: 10px;\n  }\n\n  ul.message-actions.right {\n    margin-right: (2 - @base-margin);\n  }\n\n  ul.message-actions li.action {\n    margin-right: @base-margin;\n  }\n\n  ul.message-actions li.action a {\n    min-width: auto;\n  }\n\n  ul.message-actions li.action ul.dropdown-menu li {\n    display: block;\n    float: none;\n  }\n\n  ul.message-actions li.action ul.dropdown-menu li.hide {\n    display: none;\n  }\n\n  a.message-actions-quote {\n    display: inline-block;\n    padding: 0px 4px;\n    border: 1px solid @grayMid;\n    border-radius: 3px;\n    color: @grayDark;\n    cursor: pointer;\n    font-size: 18px;\n    font-weight: normal;\n    line-height: 14px;\n  }\n\n  a.message-actions-quote:hover {\n    background: @grayLight;\n  }\n\n  .pile-message-vcal-event {\n      border: 1px solid #ccc;\n      width: 100%;\n      margin-top: 2em;\n      padding: 1em;\n      background: #eee;\n  }\n\n  .event-details {\n    display: grid;\n    vertical-align: top;\n    text-align: left;\n    padding-top: 15px;\n    grid-template-columns: 15% 85%;\n  }\n"
  },
  {
    "path": "shared-data/default-theme/less/app/mobile.less",
    "content": "/* Mobile only components to hide under normal circumstances */\n.mobile-block { display: none !important; }\n.mobile-inline { display: none !important; }\n.mobile-pt-block { display: none !important; }\n.mobile-pt-inline { display: none !important; }\n\n/* Mobile - All Sizes (devices and browser) */\n@media only screen and (max-width: 767px), (orientation: portrait) {\n  .mobile-block { display: block !important; }\n  .mobile-inline { display: inline-block !important; }\n  .mobile-hide { display: none !important; }\n\n  /* Start by making comfy/cozy/snug be identical: */\n  #pile-results.comfy, #pile-results.cozy, #pile-results.snug {\n      td { padding-top: 13px; padding-bottom: 11px; }\n      td.avatar { padding-top: 6px; padding-bottom: 5px; }\n      td.avatar a img { width: 24px; height: 24px; }\n      td.people span.conversation-count {\n          top: 1px;\n          padding: 4px 8px;\n      }\n  }\n\n  #content-view { top: 46px; }\n  .bulk-actions {\n    top: 0px;\n    position: absolute;\n    height: 46px;\n    width: 100%;\n    padding-top: 15px;\n  }\n\n  /* Make setup and login look less horrible */\n  div.setup-box-medium {\n      width: 100%;\n  }\n\n  div.setup-box {\n      border: 0px;\n      border-radius: 0px;\n      height: 100%;\n      width: 100%;\n      padding: 0px;\n      padding-top: 20px;\n      padding-bottom: 30px;\n  }\n\n  div.add-top {\n      margin-top: 0px !important;\n  }\n\n  div#identity-vault-lock {\n      display: block;\n      left: 0px;\n      margin-left:\n      auto;\n      margin-right: auto;\n  }\n\n  body {\n      background: @grayLight;\n  }\n\n  #login {\n      display: none;\n      #login-right, #login-left { display: none; }\n  }\n\n  #login-logo {\n      zoom: 0.8;\n      left: 0%;\n      width: 100%;\n      height: 100%;\n      display: block;\n      svg {\n          margin: auto;\n      }\n  }\n\n  #login-details {\n      left: 0px;\n      width: 100%;\n\n      .form-text {\n          display: block;\n          top: 0px;\n          left: 0px;\n          text-align: center;\n      }\n\n      .form-login {\n          display: block;\n          margin: auto;\n          margin-top: 5px;\n      }\n  }\n\n  #login-vault-lock {\n      display: none;\n  }\n\n  /* Now for serious business: */\n\n  .topbar {\n    min-width: 100%;\n  }\n  .topbar-logo {\n    min-width: 1%;\n    max-width: 1%;\n    width: 67px;\n    padding: 0;\n    margin: 0;\n  }\n  .topbar-logo #logo-icon,\n  .topbar-logo-name #logo-name { width: 75%; }\n  .topbar-logo-name { min-width: 30%; }\n  .topbar-actions { min-width: 80%; }\n\n  .topbar-logo span#page-title-icon { padding: 0 0 0 10px; }\n  .topbar-logo-name span#page-title-text {\n    position: absolute;\n    display: block;\n    top: 0;\n    font-size: 24px;\n    margin: 18px 10px;\n  }\n\n  .topbar-actions {\n    white-space: nowrap;\n    text-align: right;\n  }\n  .topbar-nav {\n    right: 8px;\n  }\n  .topbar-nav > ul > li { margin: 0 0 0 7px; padding: 0; }\n  @media (min-width: 350px) { .topbar-nav > ul > li { margin-left: 10px; } }\n  @media (min-width: 400px) { .topbar-nav > ul > li { margin-left: 11px; } }\n  @media (min-width: 450px) { .topbar-nav > ul > li { margin-left: 12px; } }\n  @media (min-width: 500px) { .topbar-nav > ul > li { margin-left: 13px; } }\n  @media (min-width: 550px) { .topbar-nav > ul > li { margin-left: 14px; } }\n  .topbar-nav > ul > li > a {\n    width: 22px;\n    height: 22px;\n    margin: 0;\n    padding: 0;\n  }\n  .topbar-nav > ul > li > a span.link-icon {\n    font-size: 22px;\n    line-height: 22px;\n    margin: 0;\n    padding: 8px 0 0 0;\n  }\n  .topbar-nav .nav-search { display: list-item; }\n\n  .form-search { width: auto; margin: 0 0 0 10px; }\n  .form-search input { width: auto; max-width: 100px; }\n  @media (min-width: 600px) { .form-search input { max-width: 150px; } }\n  @media (min-width: 700px) { .form-search input { max-width: 160px; } }\n\n  #sidebar, #sidebar-wrapper { width: 72px; }\n  #sidebar-scroll-area { bottom: 0; }\n  #sidebar a.sidebar-tag > span.name,\n  #sidebar a.sidebar-tag > span.notification { display: none; }\n  #sidebar-bottom { display: none; }\n  #sidebar-lists ul,\n  #sidebar a.sidebar-tag,\n  #sidebar-lists ul li:not(.hide) {\n    display: inline-block;\n    text-align: center; vertical-align: top;\n    padding: 0; margin: 0;\n  }\n  #sidebar-lists ul { display: block; }\n  #sidebar-lists li {\n    width: 72px;\n    height: 56px;\n    padding: 3px; margin: 0;\n    overflow: hidden;\n  }\n  #sidebar li.sidebar-subtag a.sidebar-tag span.name,\n  #sidebar li.sidebar-subtag a.sidebar-tag span.notification,\n  #sidebar a.sidebar-tag > span.name,\n  #sidebar.snug a.sidebar-tag > span.name,\n  #sidebar.cozy a.sidebar-tag > span.name {\n    display: block; font-size: 12px; line-height: 14px; width: 72px;\n    padding: 0; margin: 0;\n  }\n  #sidebar a.sidebar-tag > span.notification,\n  #sidebar.snug a.sidebar-tag > span.notification,\n  #sidebar.cozy a.sidebar-tag > span.notification {\n    display: block; width: 72px; text-align: center;\n    position: absolute; top: 20px;\n    color: @black;\n    text-shadow: 1px  1px 0px @white,\n                -1px -1px 0px @white,\n                -1px  1px 0px @white,\n                 1px -1px 0px @white;\n    opacity: 0.75;\n    font-size: 11px; font-weight: bold;\n  }\n  #sidebar li.sidebar-subtag a.sidebar-tag span.icon,\n  #sidebar.cozy a.sidebar-tag span.icon,\n  #sidebar.snug a.sidebar-tag span.icon,\n  #sidebar a.sidebar-tag span.icon {\n    display: block;\n    width: auto; height: 28px; font-size: 28px; line-height: 28px;\n    padding: 6px; margin: 0;\n  }\n  #sidebar a.sidebar-tag-expand { top: 25%; left: 0; }\n\n  #page {\n    margin: 0px !important;\n  }\n\n  #content { left: 72px; }\n\n  .content-normal, .pile-bottom {\n    h1 { font-size: 24px; padding: 4px; padding-left: 10px; }\n    h2 { font-size: 21px; padding: 4px; padding-left: 10px; }\n    h3 { font-size: 16px; padding: 4px; padding-left: 10px; }\n    h4 { font-size: 14px; padding: 4px; padding-left: 10px; }\n    h5 { font-size: 10px; padding: 4px; padding-left: 4px; }\n  }\n\n  div.content-normal { margin: 1px 5px; }\n\n  div.settings-page,\n  .settings-page .setting-group,\n  .settings-page .setting-group .settings { max-width: 100%; }\n  .settings-page .setting-group .explanation { display: none; }\n\n  #pile-results td.people { width: 110px; text-overflow: ellipsis; }\n  @media (min-width: 550px) { #pile-results td.people { width: 125px; } }\n  @media (min-width: 630px) { #pile-results td.people { width: 150px; } }\n\n  .content-normal section {\n      width: 100%;\n      max-width: 100%;\n      table {\n          width: 100%;\n          max-width: 100%;\n\n          th {\n              display: none;\n          }\n\n          tr {\n              display: block;\n              border-bottom: 1px solid @grayMid;\n          }\n\n          td {\n              display: inline-block;\n          }\n\n          td#email-address, td#first-name {\n              padding-top: 5px;\n              padding-bottom: 0px;\n              display: block;\n          }\n\n          td#first-name a {\n              font-size: 20px;\n          }\n\n          td#compose-message {\n              float: right;\n              padding-bottom: 0px;\n              a {\n                  font-size: 24px;\n              }\n          }\n\n          td#settings-actions, td#stats-new, td#stats-all {\n              a { padding-right: 8px; font-size: 16px; }\n          }\n      }\n  }\n  .content-normal section.motd {\n\n      padding: 0px;\n      .version-info {\n          padding-left: 5px;\n      }\n  }\n\n  #pile-bottom {\n    h5 { font-size: 12px; margin-top: 0px !important; }\n  }\n\n  #pile-empty {\n    .button-primary { display: block; }\n  }\n\n  #pile-results td.checkbox,\n  #pile-results div.crypto-and-tags { display: none; }\n  #pile-results td.rom,\n  #pile-results td.date { padding-right: 3px; }\n\n/*  #pile-results td.subject a.item-subject { width: 370px; }*/\n\n  #content-tools .sub-navigation > ul { margin: 9px; }\n  #content-tools nav li { padding: 0 1px; margin: 0; opacity: 0.95; }\n  #content-tools nav li,\n  #content-tools nav .navigation-icon { font-size: 18px; }\n  #content-tools nav .navigation-text { display: none; }\n\n  #bulk-actions-message { position: absolute; }\n\n}\n\n/* This is too small for the tabular result list, switch to card-style */\n@media only screen and (max-width: 480px) { // , (orientation: portrait) {\n  .mobile-pt-block { display: block !important; }\n  .mobile-pt-inline { display: inline-block !important; }\n  .mobile-pt-hide { display: none !important; }\n\n  #content {\n    left: 0;\n    bottom: 72px;\n  }\n  #sidebar {\n    position: fixed; display: block;\n    top: auto; left: 0; right: 0; bottom: 0;\n    padding: 0;\n    height: 73px; width: 100%; vertical-align: middle;\n    border-top: 1px solid @gray; border-right: 0;\n    z-index: 10;\n  }\n  #sidebar-wrapper {\n    width: 100%; height: 72px; margin: 0; padding: 0;\n  }\n  #sidebar-scroll-area {\n    overflow-x: scroll; overflow-y: hidden; padding: 0; margin: 0;\n  }\n  #sidebar-lists { width: 5000px; }\n  #sidebar-lists ul { display: inline-block; }\n  #sidebar-lists li {\n    width: 72px;\n    height: 64px;\n    padding: 3px 3px 0 3px;\n    margin-bottom: 0;\n  }\n\n  #notifications {\n    bottom: 0; left: 0px; right: 0px; top: auto;\n    width: 100%;\n    max-height: 73px; opacity: 0.95;\n  }\n  div.notification-bubble { padding: 2px 5px; }\n  div.notification-bubble br { display: inline; }\n  div.notification-bubble span.text { padding-left: 5px; }\n  @media (min-width: 470px), (orientation: landscape) {\n    div.notification-bubble br { display: none; }\n  }\n\n  .compose-from-select {\n      position: relative;\n      width: 15em;\n      max-width: 15em;\n      margin-bottom: 0.5em;\n      border-top: 1px solid @grayMid;;\n      padding-top: 4px;\n      margin-top: 0px;\n      margin-left: 0px;\n  }\n\n  /* Stuff we just hide... */\n  #sidebar hr,\n  #notifications-header,\n  #pile-speed{\n    display: none;\n  }\n\n  .bulk-actions ul { margin-right: 0px; white-space: nowrap; }\n  .bulk-actions li { padding: 0px 8px; }\n  @media (min-width: 340px) { .bulk-actions li { padding: 0px 9px; } }\n  @media (min-width: 360px) { .bulk-actions li { padding: 0px 10px; } }\n  @media (min-width: 380px) { .bulk-actions li { padding: 0px 11px; } }\n  @media (min-width: 400px) { .bulk-actions li { padding: 0px 12px; } }\n\n  .topbar-logo-name { min-width: 55%; }\n  .topbar-actions { min-width: 45%; }\n  .topbar-nav > ul > li > a span.link-icon { padding: 0 2px; }\n  .topbar-nav > ul > li.nav-search-hide {\n     position: absolute;\n     top: 9px;\n     right: 0px;\n  }\n\n  .form-search input { max-width: 115px; }\n  @media (min-width: 360px) { .form-search input { max-width: 130px; } }\n  @media (min-width: 380px) { .form-search input { max-width: 140px; } }\n  @media (min-width: 400px) { .form-search input { max-width: 150px; } }\n\n  #pile-results, #pile-results.comfy {\n    display: block;\n    tbody { display: block; }\n    tbody tr.pile-message {\n      display: block;\n      position: relative;\n      border-bottom: 1px solid @grayMid;\n      overflow: hidden;\n    }\n    td { display: block; border: none; }\n    td.avatar {\n      position: absolute;\n      top: 0px;\n      left: 0px;\n      width: 50px;\n      height: 50px;\n      a img {\n        margin: 5px 5px 5px 0px;\n        width: 40px; height: 40px; }\n    }\n    div.crypto-and-tags { display: none; }\n    td.checkbox { position: absolute; bottom: -3px; right: -5px; }\n    td.date { position: absolute; top: 0px; right: 5px; }\n    td.people {\n        position: absolute;\n        top: 0px;\n        left: 54px;\n        display: inline-block;\n        padding-right: 3px;\n        width: auto;\n        max-width: 55%;\n    }\n    td.subject { left: 54px; overflow: hidden; text-overflow: ellipsis; }\n    td.full-message { width: 100%; max-width: 100%; }\n    td.full-message .pile-message-content { margin: 2px; }\n    td.draggable { display: block; visibility: hidden;}\n    td.message-nav { position: absolute; display: block; width: 15px; top: 0px; left: 1px; }\n\n    tr.full-message {\n        /* When a full message is expanded in the results */\n        background-color: inherit;\n\n        .thread-container, .thread-message {\n            width: auto;\n        }\n\n        .form-compose {\n            padding: 0;\n            padding-left: 20px;\n        }\n\n        h3 {\n            font-size: 13px;\n        }\n        .message-subject {\n            font-size: 20px;\n            margin: 5px;\n            margin-left: 20px;\n            width: auto;\n            text-overflow: ellipsis;\n        }\n\n        td.people {\n            left: 25px;\n            display: block;\n            position: relative;\n            padding: 0px;\n            top: -20px;\n            width: 100%;\n        }\n        td.subject {\n            left: 0px;\n            display: block;\n            position: relative;\n            padding: 0px;\n            top: -20px;\n        }\n    }\n  }\n  .pile-results-drag .drag-info { display: none; }\n\n  div.footer-nav {\n    display: none;\n  }\n\n  #pile-more {\n      display: none;\n  }\n\n  #pile-bottom .button-primary {\n     width: 49.3%;\n     float: none;\n     text-align: center;\n     margin-left: auto;\n     margin-right: auto;\n  }\n\n  /* Single  footer button case */\n  #pile-previous.button-primary:last-of-type,\n  #pile-next.button-primary:nth-of-type(2) {\n    width: 100%;\n  }\n\n  table.account-list {\n      margin: 0 0 !important;\n      display: block;\n  }\n\n  div.motd {\n      background: @white;\n  }\n\n  .account-list {\n      display: block;\n      tr, td, th, tbody, thead {\n          display: block;\n          border: none;\n      }\n  }\n\n}\n"
  },
  {
    "path": "shared-data/default-theme/less/app/modals.less",
    "content": "/* Modals */\n\n  .modal-title .title {\n    text-transform: capitalize;\n  }\n\n  .modal-body-light-gray {\n    background: @grayLight;\n  }\n\n  #modal-full .content-normal {\n    padding: 0;\n    margin: 0;\n  }\n\n  #modal-full h1 {\n    display: none;\n  }\n\n\n/* Modal - Table default style */\n\n  table.default-table {\n    width: 100%;\n    background: @white;\n    border-top: 1px solid @grayMid;\n    border-right: 1px solid @grayMid;\n    border-bottom: 0px;\n    border-left: 1px solid @grayMid;\n    border-radius: @base-border-radius;\n  }\n\n  table.default-table tr {\n    width: 100%;\n  }\n\n  table.default-table tr:hover {\n    cursor: pointer;\n    background: @colorExpand;\n  }\n\n  table.default-table td {\n    border-top: 0px;\n    border-bottom: 1px solid @grayMid;\n    padding: (@base-padding / 2);\n    font-style: italic;\n    color: @grayDark;\n    padding-left: 10px;\n  }\n\n  tr.modal-tag-picker-item td {\n    border-top: 0px;\n    border-bottom: 1px solid @grayMid;\n    padding: (@base-padding / 2);\n  }\n\n  tr.modal-tag-picker-item td.tag span.text {\n    font-weight: bold;\n  }\n\n  tr.modal-tag-picker-item td.selection {\n    color: @gray;\n    font-style: italic;\n    padding-right: 0px;\n  }\n\n  tr.modal-tag-picker-item td.checkbox {\n    width: 30px;\n  }\n\n/* Modal - Results from crypto searchkey command */\n\n  .searchkey-result-item {\n    list-style-type: none;\n    padding: @base-padding;\n    border: 1px solid @grayMid;\n    border-radius: @base-border-radius;\n    margin-bottom: @base-margin;\n  }\n\n  .searchkey-result-item:hover {\n    background: @grayLight;\n  }\n\n  .searchkey-result-item .avatar {\n    .global-user-avatar();\n  }\n\n  .searchkey-result-item .avatar img {\n    .global-user-avatar-img();\n  }\n\n  .searchkey-result-item .name {\n    width: 200px;\n    display: inline-block;\n    font-weight: bold;\n    word-break: break-word;\n    color: @grayDark;\n    vertical-align: top;\n  }\n\n  .searchkey-result-item .name span {\n    display: inline-block;\n    color: @gray;\n    font-size: 12px;\n    font-weight: normal;\n  }\n\n  .searchkey-result-item .icon-fingerprint {\n    display: inline-block;\n    font-size: 30px;\n    line-height: 30px;\n    vertical-align: top;\n  }\n\n  .searchkey-result-item .fingerprint {\n    display: inline-block;\n    width: 200px;\n    vertical-align: top;\n  }\n\n  .searchkey-result-details {\n    font-size: 12px;\n    line-height: 18px;\n  }\n\n  .searchkey-result-details table {\n    width: 100%;\n    border: 0px;\n    background: transparent;\n  }\n\n  .searchkey-result-details table tr:hover {\n    background: transparent;\n  }\n\n  .searchkey-result-details table td {\n    width: 150px;\n    border: 0px;\n    padding: 0px 15px 0px 0px;\n    font-size: 12px;\n  }\n\n  .searchkey-result-score {\n    padding: 5px 3px 0 3px;\n    font-weight: normal;\n  }\n\n  .searchkey-result-score:hover {\n    opacity: 0.60;\n  }\n\n  .searchkey-result-score:hover em,\n  .searchkey-result-score:active em,\n  .searchkey-result-score:visited em {\n    color: @grayDark;\n  }\n"
  },
  {
    "path": "shared-data/default-theme/less/app/navigation.less",
    "content": "/* Navigation */\n\n  .navigation-on {\n\t\tbackground: lighten(@gray, 15%);\n\t\tborder-radius: @base-border-radius;    \n  }\n\n\t.navigation-on > a {\n\t\tcursor: default;\n\t\tcolor: @grayDark;\n\t}\n\n\t.navigation-on > a:hover,\n  .navigation-on > a:hover > span {\n    color: @grayDark;\n  }\n\n\n/* Selects */\n\n  .checkbox-item-picker {\n    background: @grayLight;\n    margin: 0px 20px 20px 0px;\n    padding: 5px;\n    display: inline-block;\n    border-radius: 4px;\n    border: 1px solid @grayMid;\n    \n  }\n  \n  .checkbox-item-picker:hover {\n    background: @grayMid;\n    cursor: pointer;\n  }\n  \n  .checkbox-item-picker-selected {\n    background: @colorSelect;    \n  } \n  .checkbox-item-picker-selected:hover,{\n    background: @colorSelectHover;\n  }\n  "
  },
  {
    "path": "shared-data/default-theme/less/app/notifications.less",
    "content": "/* Notification Bubbles */\n\n  #notifications {\n    position: fixed;\n    bottom: 0px;\n    right: 15px;\n    width: 250px;\n    display: inline-block;\n    z-index: 1000;\n    box-sizing: content-box;\n    max-height: 30%;\n    overflow: auto;\n  }\n\n  #notification-bubbles, #notifications-header {\n    background: @grayDark;\n    color: @gray;\n    font-weight: normal;\n    font-size: 14px;\n  }\n\n  #notifications-header {\n    margin: 0;\n    border-top-left-radius: (@base-border-radius);\n    border-top-right-radius: (@base-border-radius);\n    border-bottom: 0;\n    padding: (@base-padding / 4) @base-padding;\n    padding-bottom: 2px;\n    box-sizing: padding-box;\n    font-size: 15px;\n    font-weight: bold;\n  }\n\n  div.notification-bubble {\n    display: table;\n    margin-top: 0;\n    padding: (@base-padding / 2) @base-padding;\n    border-top: 1px solid @black;\n    box-sizing: padding-box;\n  }\n\n  div.notification-bubble span.icon {\n    display: table-cell;\n    vertical-align: text-top;\n    color: @white;\n    margin-right: 5px;\n    font-size: 14px;\n    line-height: 14px;\n  }\n\n  div.notification-bubble.error   .icon { color: @red; }\n  div.notification-bubble.warning .icon { color: @orange; }\n  div.notification-bubble.success .icon { color: @green; }\n\n  #notifications-header span.text,\n  div.notification-bubble span.text {\n    width: 100%;\n    display: table-cell;\n    vertical-align: text-top;\n    padding-left: 10px;\n    padding-bottom: 3px;\n    line-height: 18px;\n    color: @white;\n  }\n  #notifications-header span.text { padding-bottom: 0; margin: 0; }\n\n  div.notification-bubble span.message {\n    font-weight: bold;\n  }\n\n  div.notification-bubble span.action {\n    font-weight: normal;\n    font-style: italic;\n    color: @gray;\n  }\n\n  #notifications a         { color: @gray; }\n  #notifications a:visited { color: @gray; }\n  #notifications a:hover   { color: @grayLight; }\n\n  #notifications a.notifications-close-all,\n  #notifications a.notification-close {\n    display: table-cell; vertical-align: text-top;\n  }\n"
  },
  {
    "path": "shared-data/default-theme/less/app/pile.less",
    "content": "        /* Pile */\n\n  #pile-results {\n    table-layout: fixed;\n    width: 100%;\n    min-width: 800px;\n    border-collapse: collapse;\n    border: 0px;\n  }\n\n  #pile-results tr.result          { background: @white; }\n  #pile-results tr.result:hover    { background: @grayLight; }\n  #pile-results tr.result-hover    { background: @grayLight; }\n  #pile-results tr.result-on       { background: @colorSelect }\n  #pile-results tr.result-on:hover { background: @colorSelectHover }\n\n  #pile-results td {\n    vertical-align: top;\n    position: relative;\n    border-spacing: 0px;\n    border-top: 0px;\n    border-right: 0px;\n    border-bottom: 1px solid @grayMid;\n    border-left: 0px;\n    box-sizing: padding-box;\n    font-family: \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n    font-size: 14px;\n    line-height: 14px;\n    padding-left: 0px;\n    padding-right: 0px;\n  }\n\n  #pile-results.comfy  td { padding-top: 13px; padding-bottom: 11px; }\n  #pile-results.cozy   td { padding-top: 9px; padding-bottom: 6px; }\n  #pile-results.snug   td { padding-top: 5px; padding-bottom: 2px; }\n\n  #pile-results tr a        { font-size: 14px; line-height: 14px; font-weight: normal; color: inherit; }\n  #pile-results tr.in_new a { font-weight: bold;}\n  #pile-results td span.pile-message-tag { font-weight: bold; margin-right: 5px; cursor: pointer; }\n\n  #pile-results        td.draggable       { width: 12px; cursor: move; }\n  #pile-results        td.draggable:hover { cursor: move; }\n  #pile-results        td.message-nav { text-align: center; white-space: nowrap; }\n  #pile-results        td.avatar a     { display: block; text-align: center; }\n  #pile-results        td.avatar a img { display: inline-block; border-radius: 2px; }\n\n  #pile-results.comfy  td.avatar,\n  #pile-results.cozy   td.avatar { padding-top: 6px; padding-bottom: 5px; }\n  #pile-results.snug   td.avatar { padding-top: 4px; padding-bottom: 3px; }\n\n  #pile-results.comfy  td.message-nav,\n  #pile-results.comfy  td.avatar { width: 32px; padding-right: 8px; }\n  #pile-results.cozy   td.message-nav,\n  #pile-results.cozy   td.avatar { width: 24px; padding-right: 6px; }\n  #pile-results.snug   td.message-nav,\n  #pile-results.snug   td.avatar { width: 18px; padding-right: 4px; }\n  #pile-results.comfy  td.avatar a img { width: 24px; height: 24px; }\n  #pile-results.cozy   td.avatar a img { width: 18px; height: 18px; }\n  #pile-results.snug   td.avatar a img { width: 14px; height: 14px; }\n\n  #pile-results td.message-nav a.icon,\n  #pile-results td.message-nav span.icon,\n  #pile-results td.message-nav { font-size: 18px; line-height: 18px; color: @grayMid; }\n  #pile-results td.message-nav span.icon { font-size: 16px; }\n  #pile-results.snug td.message-nav a.icon,\n  #pile-results.snug td.message-nav span.icon,\n  #pile-results.snug td.message-nav { font-size: 16px; line-height: 16px; }\n  #pile-results.snug td.message-nav span.icon { font-size: 14px; }\n\n  #pile-results td.people {\n    width: 279px /* 255+24 */;\n    overflow-x: hidden;\n    word-wrap: normal;\n    word-break: normal;\n  }\n\n  #pile-results td.people a {\n    display: inline-block;\n    white-space: nowrap;\n    border: 0; margin: 0;\n    background: none;\n  }\n\n  #pile-results td.people span.rcpt-count {\n    color: @grayDark;\n    font-size: 11px;\n    font-weight: bold;\n  }\n\n  #pile-results td.people span.conversation-count {\n    text-align: center;\n    vertical-align: top;\n    position: relative;\n    top: 1px;\n    left: 3px;\n    padding: 4px 8px;\n    box-sizing: border-box;\n    color: @grayDark;\n    background: @grayMid;\n    border-radius: @base-border-radius;\n    font-size: 11px;\n    font-weight: bold;\n    line-height: 11px;\n  }\n\n  #pile-results.cozy td.people span.conversation-count {\n    top: 2px;\n    padding: 3px 6px;\n  }\n\n  #pile-results.snug td.people span.conversation-count {\n    top: 2px;\n    padding: 2px 4px;\n  }\n\n  #pile-results td.from .icon-reply,\n  #pile-results td.from .icon-forward,\n  #pile-results td.from .icon-compose {\n    position: relative;\n    top: 0px;\n    left: 4px;\n    color: @grayMid;\n  }\n\n  #pile-results td.subject {\n    overflow: hidden;\n    word-wrap: normal;\n    word-break: normal;\n  }\n\n  #pile-results td.subject a.item-subject {\n    display: block;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n\n  /* This is because relative % positioning inside a table doesn't work\n   * because the table does not yet know its own size.\n   */\n  @media only screen and (max-width: 1500px) {\n    #pile-results td.subject .message-container { max-width: 700px; }}\n  @media only screen and (max-width: 1300px) {\n    #pile-results td.subject .message-container { max-width: 550px; }}\n  @media only screen and (max-width: 1150px) {\n    #pile-results td.subject .message-container { max-width: 370px; }}\n  @media only screen and (max-width: 950px) {\n    #pile-results td.subject .message-container { max-width: 310px; }}\n  @media only screen and (max-width: 800px) {\n    #pile-results td.subject .message-container { max-width: 250px; }}\n  @media only screen and (max-width: 700px) {\n    #pile-results td.subject .message-container { max-width: 214px; }}\n\n  // Avoid clobbering the things that are happening in mobile.less\n  @media only screen and (max-width: 480px) {\n    #pile-results td.subject {max-width: 70%;}\n    #pile-results td.subject .message-container { max-width: 70%; }}\n\n  #pile-results td.date {\n    width: 80px;\n    text-align: right;\n    white-space: nowrap;\n    color: @gray;\n  }\n  #pile-results td.date.wide-date {\n    width: 140px;\n  }\n\n  #pile-results td.checkbox {\n    width: 45px;\n    text-align: center;\n    padding-top: 2px;\n    padding-bottom: 0px;\n    color: @gray;\n  }\n  #pile-results.cozy td.checkbox { padding-top: 6px; }\n  #pile-results.comfy td.checkbox { padding-top: 10px; }\n\n\n/* Pile Inlined Messages */\n\n  // This will override the hovering behaviour from above\n  #pile-results tr.full-message td { background: @white; }\n\n  // And we replace it with a highlightable border\n  #pile-results tr.full-message {\n    border-top: 6px solid;\n    border-bottom: 6px solid;\n    border-color: @grayLight;\n  }\n  #pile-results tr.full-message td.draggable {\n    border-color: @grayLight;\n    background: @grayLight;\n  }\n  #pile-results tr.result-on.full-message,\n  #pile-results tr.result-on.full-message td.draggable {\n    border-color: @colorSelect;\n    background: @colorSelect;\n  }\n  #pile-results tr.result-on.full-message:hover,\n  #pile-results tr.result-on.full-message:hover td.draggable {\n    background: @colorSelectHover;\n    border-color: @colorSelectHover;\n  }\n\n  #pile-results .message-details,\n  #pile-results .thread-container {\n    position: relative;\n    overflow: hidden;\n    margin-bottom: 2em;\n    padding: 4px 4px;\n  }\n\n  #pile-results td.subject .message-container h3,\n  #pile-results .message-details h3,\n  #pile-results .thread-container h3 {\n    text-align: left;\n    font-size: 16px; line-height: 18px; font-weight: bold;\n    padding: 0 2px;\n    margin: 0 10px 6px 10px;\n    color: @gray;\n  }\n\n  #pile-results .message-nav { color: @gray; }\n\n  #pile-results .message-details .header-raw,\n  #pile-results .message-details .header-cooked h3,\n  #pile-results .message-details .header-cooked .header-value,\n  #pile-results .thread-container .thread-message {\n    display: block;\n    width: 264px;\n    margin: 0 10px;\n    padding: 0px 2px;\n    border: 0;\n    word-wrap: normal;\n    word-break: normal;\n    text-align: left;\n    overflow: hidden;\n  }\n\n  #pile-results .thread-container {\n    display: block;\n    width: 245px;\n    margin: 10px 4px 0 4px; padding: 0; border: 0;\n    overflow: hidden;\n  }\n\n  #pile-results .thread-container .thread-message {\n    overflow: visible;\n    margin: 0 0 0 16px;\n    width: 225px;\n  }\n\n  #pile-results .message-details .header-cooked { margin-bottom: 5px; }\n\n  #pile-results .message-details .header-cooked .header-value {\n    padding: 2px 10px;\n  }\n\n  #pile-results .thread-container .thread-message:hover {\n    background: @grayLight;\n    border-radius: @base-border-radius;\n  }\n\n  #pile-results .message-details .header-raw.hide,\n  #pile-results .message-details .header-cooked.hide,\n  #pile-results .thread-container .thread-message.hide {\n    display: none;\n  }\n\n  #pile-results .thread-container .thread-selected:hover,\n  #pile-results .thread-container .thread-selected {\n    background: @gray;\n    border-radius: @base-border-radius;\n  }\n\n  #pile-results .message-details .address-avatar img,\n  #pile-results .thread-message .thread-avatar img {\n    margin: 0 4px -3px 0; padding: 0; border: 0;\n    width: 30px; height: 30px;\n  }\n\n  #pile-results .thread-message .thread-tree {\n    margin: 0; padding: 0; border: 0;\n    position: relative; top: 4px;\n    font-size: 40px; font-family: monospace;\n    line-height: 34px;\n    color: @gray;\n    white-space: pre;\n  }\n\n  #pile-results .thread-container a.show-hide {\n    color: @gray;\n    font-weight: bold;\n    margin: 10px 0 0 20px;\n  }\n\n  #pile-results .thread-container .thread-selected .thread-tree {\n    color: @grayDark;\n  }\n\n  #pile-results .message-details .header-value,\n  #pile-results .thread-message .thread-info {\n    margin: 0; padding: 0; border: 0;\n    position: relative;\n    display: inline-block;\n    height: 28px;\n  }\n\n  #pile-results .thread-unread .thread-from { font-weight: bold; }\n\n  #pile-results .header-value .address-name,\n  #pile-results .header-value .address-email,\n  #pile-results .thread-info .thread-from,\n  #pile-results .thread-info .thread-details {\n    position: absolute;\n    margin: 0; padding: 0; border: 0;\n    display: inline-block;\n    font-size: 14px;\n  }\n\n  #pile-results .header-value .address-name { top: 4px; }\n\n  #pile-results .thread-info .thread-from { top: 1px; }\n\n  #pile-results .header-value .address-email,\n  #pile-results .thread-info .thread-details {\n    bottom: 0px;\n    margin: 0 0 -1px 2px;\n    color: @grayDark;\n    font-size: 12px; line-height: 12px;\n  }\n\n  #pile-results .header-value .address-email { color: @gray; margin-bottom: 0px; }\n\n  #pile-results .thread-info.thread-simple .thread-from { top: 9px; font-size: 14px; }\n\n  #pile-results .thread-info.thread-simple .thread-details { display: none; }\n\n  #pile-results .message-details .header-to .header-value,\n  #pile-results .message-details .header-from .header-value { height: 35px; }\n\n  #pile-results .message-details .header-to .address-name,\n  #pile-results .message-details .header-from .address-name {\n    font-size: 18px; line-height: 18px;\n  }\n\n  #pile-results td.subject .message-container .message-subject {\n    font-size: 22px; line-height: 22px;\n    margin: 0px 10px 4px 12px;\n    display: block;\n  }\n\n  #pile-results .message-details .header-to .address-email,\n  #pile-results .message-details .header-from .address-email {\n    margin-bottom: 2px;\n  }\n\n  #pile-results .message-details .header-to .address-avatar img,\n  #pile-results .message-details .header-from .address-avatar img {\n    width: 36px; height: 36px;\n  }\n  #pile-results .message-details .header-from ul.message-sender-actions {\n    display: inline-block;\n    margin: 0; padding: 2px;\n    white-space: nowrap;\n    background: @white;\n    transition: opacity 1s;\n    opacity: 0;\n  }\n  #pile-results .message-details .header-from .message-sender-actions li {\n    display: inline-block;\n  }\n  #pile-results .message-details .header-from:hover .message-sender-actions {\n    transition: opacity 1s;\n    opacity: 1.0;\n  }\n\n  #pile-results .message-details .header-to { opacity: 0.8; }\n  #pile-results .message-details .header-cc,\n  #pile-results .message-details .header-bcc { opacity: 0.6; }\n  #pile-results .message-details .header-to:hover,\n  #pile-results .message-details .header-cc:hover,\n  #pile-results .message-details .header-bcc:hover { opacity: 1.0; }\n\n  // Hide certain things, but leave them for copy-paste awesomeness\n  #pile-results .message-details .header-value span.punct {\n    position: absolute;\n    opacity: 0;\n    transition: opacity 1s;\n  }\n\n  #pile-results .full-message div.crypto-and-tags {\n    white-space: normal;\n    padding-top: 2px;\n  }\n  #pile-results .display-attachments div.crypto-and-tags {\n    opacity: 0.33;\n  }\n\n  #pile-results .full-message div.crypto-and-tags .pile-message-tag,\n  #pile-results .full-message div.crypto-and-tags .icon.crypto {\n    display: inline-block; margin: 0 0 5px 0; padding: 0; border: 0;\n    font-size: 16px; line-height: 16px;\n  }\n\n  #pile-results div.crypto-and-tags {\n    float: right;\n    text-align: right;\n  }\n\n  #pile-results .full-message div.crypto-and-tags .item-tags {\n    display: block; margin-bottom: 0 0 5px 0;\n  }\n\n  #pile-results.snug div.crypto-and-tags {\n    padding-right: 4px;\n  }\n\n  #pile-results.cozy div.crypto-and-tags {\n    padding-right: 6px;\n  }\n\n  #pile-results.comfy div.crypto-and-tags {\n    padding-right: 8px;\n  }\n\n  #pile-results td.subject .message-container h3 {\n    display: inline-block;\n    margin-right: 5px;\n  }\n\n  #pile-results td.subject .message-container {\n    display: block;\n    max-width: 50em;\n    padding: 5px 2px 5px 0px;\n    white-space: normal;\n    line-height: 16px;\n    font-size: 14px;\n  }\n\n  #pile-results .full-message .pile-message-content {\n    margin: 5px 10px;\n    padding: 0 0 0 10px;\n    border: 0;\n  }\n  #pile-results .full-message .part-crypto-status {\n    border: 0; padding: 0;\n    margin: 0 0 -4px -10px;\n  }\n  #pile-results .full-message .part-crypto-status .crypto-color-gray {\n    transition: all 1s ease-in;\n    opacity: 0;\n  }\n  #pile-results .full-message .part-crypto-status:hover .crypto-color-gray,\n  #pile-results .full-message .pile-message-content div.crypto-changed .crypto-color-gray {\n    transition: all 1s ease-in;\n    opacity: 1;\n  }\n  #pile-results .full-message .part-crypto-status .inline-encryption-info .crypto-color-gray {\n    /* We need this so individual signature status align nicely */\n    display: none;\n  }\n  #pile-results .full-message .part-crypto-status:hover .inline-encryption-info .crypto-color-gray {\n    /* FIXME: Doesn't fade in, boo hoo */\n    display: inline-block;\n  }\n  #pile-results .full-message .pile-message-content div.crypto-changed {\n    margin: 10px 0 0 -10px;\n    padding-top: 1px;\n    border-top: 2px dotted;\n    border-top-color: @grayLight;\n  }\n\n  #pile-results .message-inline-crypto-info {\n    display: inline-block;\n    margin: 0 0 7px 0;\n    text-align: left;\n  }\n\n  #pile-results .full-message h3 { position: relative; }\n\n  #pile-results .full-message .message-metadata-crypto-info {\n    font-size: 14px;\n    padding: 0 5px;\n    display: inline-block;\n  }\n\n  #pile-results .full-message .message-metadata-crypto-info {\n    font-size: 14px;\n    padding: 0 5px;\n    display: inline-block;\n  }\n\n  #pile-results .message-subject-container .message-metadata-crypto-info {\n    padding-right: 0px;\n  }\n\n  #pile-results .full-message .crypto-color-gray .message-metadata-crypto-info,\n  #pile-results .full-message .message-metadata-crypto-info.crypto-color-gray {\n    transition: opacity 1s;\n    opacity: 0;\n  }\n\n  #pile-results .full-message .crypto-color-gray:hover .message-metadata-crypto-info,\n  #pile-results .full-message .message-subject-container:hover .message-metadata-crypto-info,\n  #pile-results .full-message .thread-container:hover .message-metadata-crypto-info,\n  #pile-results .full-message .header-cooked:hover .message-metadata-crypto-info {\n    transition: opacity 1s;\n    opacity: 1;\n  }\n\n  #pile-results .message-actions-padding {\n    display: block; line-height: 18px;\n    margin: 0; padding: 5px 0 0 0; border: 0;\n  }\n\n  #pile-results td .bottom {\n    position: absolute; bottom: 0;\n  }\n\n  #pile-results iframe.message-part-html {\n    width: 100%;\n    margin: 0;\n    padding: 0;\n    //width: 111%;\n    //transform: scale(0.9);\n    //transform-origin: 0 0;\n  }\n\n\n/* Pile Bottom */\n\n  #pile-bottom {\n    margin: 15px 15px 0px 15px;\n  }\n\n  #pile-bottom h5 {\n    margin-top: 10px;\n    color: @grayDark;\n  }\n\n  #pile-bottom a {\n    margin-right: 15px;\n  }\n\n  #pile-empty {\n    padding: @base-padding;\n    background: @white;\n    border-bottom: 1px solid @grayMid;\n    font-size: 14px;\n  }\n\n  #pile-empty-search-terms {\n    font-size: 24px;\n    font-weight: bold;\n    color: @gray;\n  }\n\n\n/* Pile Speed */\n\n  #pile-speed {\n    margin-bottom: 50px;\n    font-family: @mailpile-text-font-family-bold;\n    color: @gray;\n  }\n\n  #pile-speed span {\n    font-size: 21px;\n    margin-right: 10px;\n    position: relative;\n    top: 3px;\n    left: 0px;\n  }\n\n\n/* Pile  - Drag & Drop */\n\n  #pile-results tr.result:hover    td.draggable,\n  #pile-results tr.result-hover    td.draggable,\n  #pile-results tr.result-on:hover td.draggable {\n    background: url('../../img/draggable-pattern.png'), rgba(255,255,255,1);\n    opacity: 0.3;\n    filter: alpha(opacity=30);\n  }\n\n  .pile-results-drag {\n    background: @white;\n    border: 1px solid @gray;\n    border-radius: 4px;\n    padding: 5px 10px;\n    z-index: 9999;\n    font-size: 14px;\n    font-weight: bold;\n  }\n"
  },
  {
    "path": "shared-data/default-theme/less/app/profiles.less",
    "content": "/* Mailpile - Settings */\n\n.content-normal .actions {\n    float: right;\n}\n\n.content-normal section {\n    h3 {\n        margin-top: 0.7em;\n        margin-bottom: 0.7em;\n        padding-left: 4px;\n    }\n\n    margin-left: -3px;\n    margin-right: -3px;\n\n    display: inline-block;\n    margin-top: 1.7em;\n\n    @media only screen and (min-width: 1600px), (orientation: portrait) {\n        max-width: 50%;\n        width: 100%;\n    }\n    vertical-align: top;\n}\n\nsection.account-list {\n    margin-right: 5px;\n}\n\nsection.account-list table {\n    margin: auto;\n    background: none;\n    border: 0;\n    text-align: right;\n    width: 100%;\n\n    td#email-address, td#first-name {\n        text-align: left;\n    }\n}\n\nsection.account-list table th {\n    background: none;\n    border: 0;\n    text-align: left;\n}\n\ntr.message {\n    background: @white;\n}\n\nsection.message {\n    width: 100%;\n    max-width: 100%;\n\n    background: @white;\n    border-radius: @base-border-radius;\n    border: 1px solid @grayMid;\n\n}\n\nsection.account-list .icon-logo {\n    font-size: 1.3em;\n}\n\nsection.motd {\n    @media only screen and (min-width: 1600px), (orientation: portrait) {\n        padding-left: 4%;\n    }\n}\nsection.motd.recent,\nsection.motd:hover {\n\n}\n\nsection.motd p, section.motd h4 {\n    margin: 0.5em 0 0 0;\n}\n\nsection.motd.recent,\nsection.motd.recent h3 {\n    color: @blueDark;\n}\n\nsection.motd.recent p.motd,\nsection.motd.recent p.motd a.motd-signed {\n    color: @green;\n}\n\nsection.motd .updated {\n    color: @red;\n}\n\nsection.motd p.motd {\n    margin-left: 1em;\n}\n\nsection.motd a.motd-signed {\n    display: block;\n    margin-top: 0.5em;\n    text-align: right;\n    padding-right: 2em;\n}\n\nsection.motd .version-info {\n    padding-left: 0.5em;\n}\n"
  },
  {
    "path": "shared-data/default-theme/less/app/screens.less",
    "content": "/* Connection Down */\n\n\t#connection-down { \n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    z-index: 2000;\n    background: #000000;\n    filter: alpha(opacity=100);\n    opacity: 100;\n    text-align: center;\n  }\n\n\t#connection-down .message {\n    width: 480px;\n    text-align: center;\n    position:absolute;\n    font-family: Helvetica, Arial, Sans-Serif;\n    font-size: 18px;\n    line-height: 18px;\n    color: @gray;\n    top: 100px;\n    left: 50%;\n    margin-top: 0px;\n    margin-left: -200px;\n    background: @grayLight;\n    border-radius: (@base-border-radius * 2);\n    z-index: 2001;\n  }\n\n\t#connection-down .message h1 {\n    color: @grayDark;\n    font-size: 36px;\n    line-height: 48px;\n  }\n\n\n\n\t#connection-down .message-normal {\n    color: #eeeeee !important;\n  }\n\t\n  #connection-down .message-success {\n    color: #eeeeee !important;\n  }\n\n\t#connection-down .message-error {\n    color: #b20a0a !important;\n  }\n\n"
  },
  {
    "path": "shared-data/default-theme/less/app/search.less",
    "content": "/* Search */\n\n\t#button-search-options {\n  \tbackground: red; \n\t  display: inline-block;\n\t  height: 32px;\n  \tposition: relative;\n  \tleft: -25px;\n  \ttop: -10px;\n\t}\t\n\n  #button-search-options:hover .icon-arrow-down {\n    color: @gray; \n  }\n\n\t#button-search-options .icon-arrow-down {\n  \tposition: relative;\n  \tleft: 0px;\n  \ttop: 10px;\n  \tfont-size: 12px;\n  \tcolor: @grayMid;\n\t}\n\n\n\t#search-params {\n\t  position: absolute;\n\t  top: 50px;\n\t  left: 225px;\n  \tbackground: @white;\n  \tborder: 1px solid @gray;\n  \tz-index: 1000;\n\t}\n\t\n\t#search-params li {\n  \tmargin: 15px;\n\t}\n\t\n\t#search-params a {\n\t\tpadding: 5px 10px;\n\t\tbackground: @grayLight;\n\t\tcolor: @grayDark;\n\t\tfont-family: @text-font-family-bold;\n\t\tfont-size: 14px;\n\t\tborder-radius: @base-border-radius;\n\t\tborder: 1px solid @gray;\n\t}\n\t\n\t#search-params a:hover {\n\t\tbackground-color: darken(@grayLight, 15%);\n\t}"
  },
  {
    "path": "shared-data/default-theme/less/app/settings.less",
    "content": "/* Mailpile - Settings */\n\n  div.settings-page,\n  .settings-page .setting-group,\n  .settings-page .setting-group .settings {\n    max-width: 80em;\n    position: relative;\n  }\n\n  .settings-page .setting-group .settings {\n    max-width: 50%;\n  }\n\n  .settings-page .setting-group {\n    border-top: 1px solid @grayMid;\n    margin: 15px 0;\n    padding: 10px 0;\n  }\n\n  .settings-page .notices {\n    font-size: 20px;\n    line-height: 24px;\n    padding: 0 0 6px 30px;\n  }\n\n  .settings-page h3 {\n    margin: 0 0 15px 0;\n    padding: 5px 0 0 0;\n    width: 50%;\n    float: left;\n  }\n\n  .settings-page p {\n    margin: 0 0 0.5em 0;\n    padding: 0;\n  }\n\n  .settings-page h4,\n  .settings-page h5,\n  .settings-page h6 {\n    margin: 1.25em 0 0.5em 0;\n    padding: 0;\n  }\n\n  .settings-page div.explanation {\n    margin: 0 25px 0 0;\n    width: 40%;\n    float: right;\n    opacity: 0.55;\n  }\n  .settings-page div.explanation p {\n    position: relative;\n    padding: 0 0 6px 30px;\n    margin:  5px 0 0 0;\n  }\n  .settings-page div.notices span.icon,\n  .settings-page div.explanation span.icon {\n    position: relative;\n    font-size: 24px;\n    line-height: 24px;\n  }\n  .settings-page span.icon-checkmark { color: @blue; }\n  .settings-page .what span.icon { color: @green; }\n  .settings-page .risks span.icon { color: @orangeRed; }\n  .settings-page .actions span.icon { color: @brown; }\n\n  .settings-page span.icon.default {\n    color: @blue;\n    font-size: 12px;\n    line-height: 12px;\n    position: relative;\n    top: -2px;\n  }\n\n  .settings-page div.explanation p span.icon {\n    position: absolute;\n    left: 0;\n  }\n\n  .settings-page div.settings,\n  .settings-page p.settings {\n    display: block;\n    padding: 10px 25px;\n    line-height: 20px;\n    clear: left;\n  }\n\n  .settings-page ul.notes {\n    list-style-type: circle;\n    font-size: 13px;\n  }\n\n  .settings-page .subsection,\n  .settings-page ul.notes {\n    display: block;\n    padding: 0;\n    margin: 0 0 0 2em;\n  }\n\n/* Event Log specifics */\n\n  .settings-page p.event-summary span {margin-right: 1em;}\n  .settings-page p.event-summary .event-source {\n    float: right; font-size: 11px; color: #777;\n  }\n\n"
  },
  {
    "path": "shared-data/default-theme/less/app/setup.less",
    "content": "/* Mailpile - Setup\n*  Version 0.1.0\n*\t Designed and built by @brennannovak and others\n*/\n\n/* Config - Change the settings in this file change the look and feel of your style */\n@import \"config.less\";\n\n\n/* Topbar */\n\n  .topbar-middle { \n    position: relative;\n    top: 15px;\n    text-align: center;\n    margin-left: auto;\n    margin-right: auto;\n    font-size: 30px;\n    line-height: 30px;\n  }\n  \n  .topbar-middle .title {\n    position: relative;\n    top: 0px;\n    left: -95px;\n    font-family: @mailpile-text-font-family-bold;\n  }\n  \n  .topbar-middle .icon {\n    float: right;\n    position: relative;\n    top: 0px;\n    right: 20px;\n    display: block;\n    font-size: 30px;\n    line-height: 30px;\n  }\n\n\n/* Setup */\n\n  #setup-container {\n    position: relative;\n    top: 65px;\n    left: 0px;\n  }\n\n  div.setup-box {\n    margin: 0px auto;\n    padding: 25px;\n    border-radius: 5px;\n    background: @grayLight;\n    border: 1px solid @grayMid;\n  }\n\n  div.setup-box-small {\n    width: 400px;\n  }\n\n  div.setup-box-medium {\n    width: 600px;\n  }\n\n  div.setup-box-large {\n    width: 800px;\n  }\n\n  div.setup-box h3 {\n    margin: 0 0 12px 0;\n  }\n\n  .setup-text-detail-large {\n    color: @gray;\n    font-size: 24px;\n  }\n\n  .setup-table {\n    background: @white;\n  }\n\n\n/* Setup Progress */\n\n  #setup-progress {\n    width: 520px;\n    position: relative;\n    top: 10px;\n    left: 15%;\n    text-align: center;\n  }\n\n  a.setup-progress-circle {\n    display: table-cell;\n    height: 38px;\n    width: 38px;\n    vertical-align: middle;\n    border-radius: 25px;\n    background: @white;\n    border: 1px solid @gray;\n  }\n\n  a.setup-progress-circle:hover,\n  a.setup-progress-circle:hover span.icon {\n    color: @grayDark;\n    background: darken(@grayLight, 10%);\n  }\n\n  a.setup-progress-circle span.icon {\n    display: inline-block;\n    color: @gray;\n  }\n\n  a.setup-progress-circle.on {\n    background: @blue;\n    border: 1px solid @grayLight;\n  }\n\n  a.setup-progress-circle.complete {\n    background: @green;\n    border: 1px solid @grayLight;\n  }\n\n  a.setup-progress-circle.on span.icon,\n  a.setup-progress-circle.complete span.icon {\n    color: @white;\n  }\n\n  a.setup-progress-circle.on:hover span.icon,\n  a.setup-progress-circle.complete:hover span.icon {\n    background: transparent;\n  }\n  \n  span.setup-progress-line {\n    width: 90px;\n    display: block;\n    border-bottom: 1px solid @grayMid;\n    margin: 20px 8px 0px 8px;\n  }\n\n\n/* Setup - Details */\n\n  label span.setup-help-tooltip {\n    color: @gray;\n    cursor: pointer;\n  }\n\n  a.setup-check-connection {\n    font-weight: normal; \n  }\n\n  a.setup-check-connection:hover {\n    color: @green;\n  }\n\n\n/* Setup - Welcome */\n\n  #setup-welcome {\n    width: 100%;\n    height: 100%;\n  }\n\n  .welcome-logo {\n    width: 25%;\n  }\n\n  .welcome-icons {\n    width: 470px;\n    font-size: 40px;\n    line-height: 40px;\n    display: inline-block;\n    margin-left: auto;\n    margin-right: auto; \n  }\n\n  .welcome-icons li {\n    margin: 0px (@base-margin / 1.5);\n  }\n\n\n/* Setup - Crypto */\n\n  #identity-vault-lock {\n    margin: @base-margin 0;\n    position: relative;\n    left: 32%;\n    top: 0px;\n  }\n\n  .setup-cryto-fingerprint-icon {\n    font-size: 48px;\n    line-height: 48px;\n    display: inline-block;\n  }\n  \n  .setup-crypto-fingerprint-fingerprint {\n    font-size: 21px;\n    line-height: 24px;\n    font-weight: normal;\n    font-style: italic;\n    display: inline-block;\n    width: 320px;    \n  }\n\n  label.radio-list-item div.radio {\n    width: 30px;\n  }\n\n  label.radio-list-item div.icon {\n    width: 30px;\n  }\n\n  label.radio-list-item .icon-key {\n    font-size: 30px;\n    line-height: 30px;\n  }\n\n  .setup-list-items {\n    background: @white;\n    border-radius: 5px;\n    border: 1px solid @grayMid;\n  }\n\n  .setup-list-items li:first-child {\n    border-top: 0px solid;\n  }\n\n  .setup-item {\n    padding: @base-padding;\n    border-top: 1px solid @grayMid;\n  }\n\n  .setup-item ul {\n    margin-bottom: 0px;\n  }\n  \n  .setup-item ul li {\n    margin-left: @base-margin;\n  }\n\n  .setup-item .avatar {\n    width: 50px;\n    display: inline-block;\n    float: left;\n    margin-right: @base-margin;\n  }\n\n  .setup-item .avatar img {\n    width: 50px;\n    border-radius: @base-border-radius;\n  }\n\n  .setup-item .name {\n    display: block;\n    margin-bottom: 5px;\n    vertical-align: text-top;\n    font-size: 18px;\n    font-weight: bold;\n    line-height: 18px;\n    color: @grayDark;\n  }\n\n  .setup-item .email {\n    font-size: 14px;\n    line-height: 14px;\n    color: @gray;\n  }\n\n  .setup-item.disabled,\n  .setup-item.disabled .name,\n  .setup-item.disabled .email,\n  .setup-item.disabled .setup-actions a {\n    color: @grayMid;\n  }\n\n  .setup-item-notice {\n    background: @colorSelectHover;\n    padding: @base-padding;\n    margin-bottom: 0px;\n    border-radius: (@base-border-radius * 1.5);\n  }\n\n\n/* Setup - Sources Settings */\n\n  #setup-source-settings {\n    background: @white;\n    border-radius: @base-border-radius;\n    padding: @base-padding;\n    border: 1px solid @grayMid;\n  }\n\n  #setup-source-settings div.left {\n    width: 275px;\n  }\n\n\n/* Setup - Complete */\n\n  #setup-complete-message {\n    line-height: 48px;\n  }\n\n  #setup-complete-icon {\n    font-size: 100px;\n    line-height: 100px;\n  }\n\n"
  },
  {
    "path": "shared-data/default-theme/less/app/sidebar.less",
    "content": "/* Sidebar */\n\n  #sidebar {\n    position: fixed;\n    background: @grayLight;\n    top: 62px;\n    bottom: 0px;\n    width: 225px;\n    margin: 0px;\n    padding: 0px;\n    box-sizing: border-box;\n    border-right: 1px solid @gray;\n    border-spacing: 0px;\n  }\n\n  #sidebar-wrapper {\n    position: absolute;\n    top: 0px;\n    bottom: 0px;\n    width: 224px;\n    padding: 0px;\n    margin: 0px;\n  }\n\n  #sidebar-scroll-area {\n    position: absolute;\n    top: 0px;\n    bottom: 45px;\n    width: 100%;\n    padding: 5px 0 0 0;\n    margin: 0px;\n    overflow-x: hidden;\n    overflow-y: auto;\n  }\n\n  #sidebar-lists  {\n    padding: 0px;\n    margin: 0px;\n  }\n  \n  #sidebar-bottom {\n    position: absolute;\n    width: 100%;\n    height: 25px;\n    bottom: 0px;\n    padding-top: 10px;\n    padding-bottom: 10px;\n    background: @grayLight;\n    border-top: 1px solid @gray;\n  }\n\n  #sidebar-bottom a {\n    display: inline-block;\n    margin: 0px 10px;\n    font-size: 14px;\n    line-height: 14px;\n    font-weight: normal;\n    color: @grayDark;\n  }\n\n  #sidebar-bottom a:hover {\n    color: @blue;\n  }\n\n\t#sidebar hr  { margin: 10px 0px;\t}\n\n  #sidebar      ul    { margin: 0px; padding: 0px; }\n  #sidebar      ul li { margin: 3px 0px; padding: 0px 0px; transition-duration: 0.3s; }\n  #sidebar.cozy ul li { margin-top: 2px; margin-bottom: 3px; padding-top: 2px; padding-bottom: 2px; }\n  #sidebar.snug ul li { margin-top: 1px; margin-bottom: 2px; padding-top: 1px; padding-bottom: 1px; }\n\n  #sidebar ul li.show-subtags {\n    background: @white;\n    border-top: 1px solid @grayMid;\n    border-bottom: 1px solid @grayMid;\n    padding-top: 5px;\n\t  transition-duration: 0.3s;\n  }\n\n  #sidebar a.sidebar-tag {\n    position: relative;\n    display: block;\n    margin: 0px 7px;\n    padding: 6px 5px;\n    vertical-align: middle;\n    text-align: left;\n    white-space: nowrap;\n    transition-duration: 0.2s;\n    box-sizing: content-box;\n  }\n\n  #sidebar a.sidebar-tag:hover  { color: @gray; }\n\n  #sidebar.cozy a.sidebar-tag   { padding: 3px 0px; }\n  #sidebar.snug a.sidebar-tag   { padding: 0px 0px; }\n\n  #sidebar li.is-editing,\n  #sidebar li.is-editing a.sidebar-tag { cursor: move; }\n\n  #sidebar a.sidebar-tag span.icon {\n    margin-left: 5px;\n    width: 24px;\n    height: 18px;\n    display: inline-block;\n    vertical-align: middle;\n    text-align: center;\n    font-weight: normal;\n    font-size: 18px;\n    line-height: 18px;\n  }\n\n  #sidebar.cozy a.sidebar-tag span.icon { font-size: 18px; line-height: 18px; }\n  #sidebar.snug a.sidebar-tag span.icon { font-size: 16px; line-height: 16px; }\n\n  #sidebar a.sidebar-tag > span.name,\n  #sidebar a.sidebar-tag > span.notification {\n    vertical-align: middle;\n    font-family: @mailpile-text-font-family;\n    font-weight: normal;\n    font-size: 18px;\n    line-height: 24px;\n  }\n\n  #sidebar a.sidebar-tag span.name {\n    display: inline-block;\n    max-width: 135px;\n    padding-left: 5px;\n    padding-right: 0px;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n\n  #sidebar a.sidebar-tag span.notification {\n    display: inline-block;\n    letter-spacing: -.5px;\n    color: @gray;\n  }\n\n  #sidebar.cozy span.name,\n  #sidebar.cozy span.notification { font-size: 16px; line-height: 18px; }\n  #sidebar.snug span.name,\n  #sidebar.snug span.notification { font-size: 14px; line-height: 16px; }\n\n  #sidebar a.sidebar-tag.has-unread span.name,\n  #sidebar a.sidebar-tag.has-unread span.notification {\n    font-family: @mailpile-text-font-family-bold;\n    font-weight: normal;\n  }\n\n #sidebar li.sidebar-tag {\n   position: relative;\n }\n\n #sidebar a.sidebar-tag-expand {\n   position: absolute;\n   display: inline-block;\n   color: @grayMid;\n   top: 20%;\n }\n\n #sidebar a.sidebar-tag-settings {\n   position: absolute;\n   display: inline-block;\n   color: @grayMid;\n   right: 4px;\n   top: 20%;\n }\n\n\n/* Sidebar - Subtags */\n\n  #sidebar ul.sidebar-subtags {\n    margin-bottom: 8px;\n    padding: 0px;\n  }\n\n  #sidebar ul.sidebar-subtags li.sidebar-subtag {\n    margin-top: 0px;\n    margin-bottom: 0px;\n    padding-top: 0px;\n    padding-bottom: 0px;\n  }\n\n  #sidebar li.sidebar-subtag a.sidebar-tag.has-unread span.name,\n  #sidebar li.sidebar-subtag a.sidebar-tag.has-unread span.notification {\n    font-family: @text-font-family;\n    font-weight: bold;\n  }\n\n  #sidebar li.sidebar-subtag a.sidebar-tag span.icon {\n    margin-left: 16px;\n    font-size: 16px;\n    line-height: 16px;\n  }\n\n  #sidebar li.sidebar-subtag a.sidebar-tag span.name,\n  #sidebar li.sidebar-subtag a.sidebar-tag span.notification {\n    vertical-align: middle;\n    font-weight: normal;\n    font-size: 14px;\n    font-family: @text-font-family;\n    line-height: 16px;\n  }\n\n\n/* Sidebar - Edit */\n\n  a.sidebar-tag span.sidebar-tag-archive {\n    cursor: pointer;\n    background: @gray;\n    color: @white;\n    vertical-align: middle;\n    padding: 5px;\n    position: absolute;\n    top: 4px;\n    right: 0px;\n    font-size: 8px;\n    line-height: 8px;\n    border-radius: 10px;\n  }\n\n  a.sidebar-tag span.sidebar-tag-archive:hover {\n    background: @orange;\n  }\n\n\n/* Sidebar - Drag & Drop */\n\n  .sidebar-tags-draggable {\n    border-radius: @base-border-radius;\n  }\n\n  .sidebar-tags-draggable-hover {\n\t  transition-duration: 0.3s;\t    \n  }\n\n  .sidebar-tags-draggable-hover.show-subtags {\n\n  }\n\n  .sidebar-tags-draggable-active,\n  .sidebar-tags-draggable-active.show-subtags {\n    background: @grayMid;\n\t  transition-duration: 0.3s;\n  }\n\n  .sidebar-tags-draggable-highlight {\n\t  transition-duration: 0.3s;\n  }\n\n\n/* Sidebar - Sortable */\n\n  .sidebar-tags-sortable {\n    height: 29px;\n    padding: 5px 10px;\n    margin: 4px 0;\n    border-radius: @base-border-radius;\n    background: @grayMid;\n  }\n  \n\n/* Sidebar - Tag Drag */\n\n  .sidebar-tag-drag {\n  \tbackground: @white;\n  \tborder: 1px solid @gray;\n  \tborder-radius: 4px;\n  \tpadding: 5px 10px;\n  \tfont-size: 14px;\n  \tfont-weight: bold;\n    z-index: 9999;\n  }\n  \n"
  },
  {
    "path": "shared-data/default-theme/less/app/tablet.less",
    "content": "/* =============== Tablet Style =============== */\n@media only screen and (max-width: 1020px) {\n  .tablet-hide {\n    display: none !important;\n  }\n\n  /* Shrink the sidebar a bit, but keep it. */\n  #sidebar { width: 180px; }\n  #sidebar-wrapper { width: 179px; font-size: 0.7em; }\n  #content { min-width: 0; left: 180px; }\n\n  /* Apply the cozy values per default, with a smaller font. */\n  #sidebar ul li { margin-top: 2px; margin-bottom: 3px; padding-top: 2px; padding-bottom: 2px; }\n  #sidebar a.sidebar-tag { padding: 3px 0px; }\n  #sidebar a.sidebar-tag > span.name,\n  #sidebar a.sidebar-tag > span.notification,\n  #sidebar a.sidebar-tag span.icon { font-size: 16px; line-height: 18px; }\n  #sidebar span.name,\n  #sidebar span.notification { font-size: 14px; line-height: 16px; }\n  #sidebar-bottom a { font-size: 14px; line-height: 14px; }\n\n  #sidebar li.sidebar-subtag a.sidebar-tag span.icon,\n  #sidebar li.sidebar-subtag a.sidebar-tag span.notification,\n  #sidebar li.sidebar-subtag a.sidebar-tag span.name { font-size: 14px; line-height: 14px; }\n\n\n  /* Pile */\n  #pile-results { min-width: 300px; }\n  #pile-results td.people { width: 220px; }\n  div.bulk-actions-hints { position: fixed; top: -100px; }\n\n  #pile-results xtd.draggable { display: none; }\n  #pile-results td.avatar { padding-left: 4px; }\n}\n"
  },
  {
    "path": "shared-data/default-theme/less/app/tags.less",
    "content": "/* Tags */\n\n  .tag-card-name {\n    max-width: 175px;\n    display: block;\n    margin-bottom: 10px;\n    font-family: @mailpile-text-font-family-bold;\n    font-weight: normal;\n    font-size: 18px;\n    line-height: 18px;\n    letter-spacing: -.25px;\n  }\n\n  .tag-card-name:hover {\n    color: @blue;\n  }\n\n  .tag-card-label {\n    font-family: Helvetica, Arial, sans-serif;\n  }\n\n  .tag-card-details {\n    min-height: 65px;\n    clear: both;\n    font-size: 14px;\n    line-height: 14px;\n    color: @gray;\n  }\n\n\n/* Tags - Edit */\n\n  #tag-editor-icon {\n    font-size: 36px;\n    line-height: 36px;\n    padding: 5px;  \n  }\n\n  li.modal-tag-icon-option {\n    font-size: 36px;\n    line-height: 36px;\n    margin: 0px 15px 15px 0px;\n    padding: 5px;\n    border-radius: @base-border-radius;\n  }\n\n  li.modal-tag-icon-option:hover {\n    background-color: @grayMid;\n    cursor: pointer;\n  }\n\n\n  #tag-editor-label-color {\n    width: 48px;\n    height: 48px;\n    display: inline-block;\n    border-radius: @base-border-radius;\n  }\n  \n  a.modal-tag-color-option {\n    width: 48px;\n    height: 48px;\n    display: block;\n    margin: 0px 15px 15px 0px;\n    border-radius: @base-border-radius;\n    cursor: pointer;\n  }\n  \n  a.modal-tag-color-option:hover {\n    opacity:0.7;\n  }\n  "
  },
  {
    "path": "shared-data/default-theme/less/app/terminal.less",
    "content": "\n/* Terminal */\n\n#terminal_blanket {\n    position: fixed;\n    z-index: 500;\n    width: 100%;\n    height: 100%;\n    display: none;\n    background: none;\n    opacity: 0.0;\n}\n\n#terminal {\n    position: fixed;\n    z-index: 501;\n    display: none;\n    background: #000;\n    font-family: monospace;\n    font-size: 14px;\n    color: #eee;\n    width: 100%;\n\n    div.log,\n    div.output {\n        white-space: pre-wrap;\n        word-wrap: break-word;\n        position: relative;\n        bottom: 0px;\n        vertical-align: bottom;\n        background: none;\n        padding: 0px;\n        margin: 0px;\n        padding-bottom: 10px;\n        color: #eee;\n        border: 0px;\n    }\n    div.html_blob {\n        position: relative;\n        white-space: normal;\n        background: #ddd;\n        padding: 0;\n        margin: 5px 50px 15px 50px;\n        border: 2px solid #fff;\n    }\n    div.html_blob #content-view {\n        position: inherit;\n        color: #111;\n    }\n    div.html_blob #pile-speed {\n        display: none;\n    }\n\n    #console {\n        position: absolute;\n        top: 0px;\n        bottom: 0px;\n        left: 0px;\n        width: 100%;\n\n        #terminal_output {\n            display: block;\n            position: absolute;\n            top: 0px;\n            bottom: 25px;\n            left: 0px;\n            right: 0px;\n            padding: 6px;\n            overflow-y: scroll;\n            overflow-x: hidden;\n        }\n\n        #terminal_input {\n            height: 20px;\n            background: #000;\n            position: absolute;\n            border-top: 1px solid #ccc;\n            width: 100%;\n            padding: 3px;\n            padding-left: 6px;\n            padding-right: 6px;\n            bottom: 0px;\n\n            #terminal_halfsize_button {\n                display: none;\n            }\n\n            #terminal_fullsize_button {}\n\n            form {\n                display: inline-block;\n                width: 90%;\n            }\n\n            #prompt {\n                color: #8f9;\n            }\n\n            input {\n                outline: none;\n                display: inline-block;\n                width: 100%;\n                background: none;\n                border: none;\n                font-family: monospace;\n                color: #eee;\n                font-size: 15px;\n            }\n        }\n    }\n\n    div.log {\n        line-height: 16px;\n        color: #77e;\n        padding: 0 0 2px 0;\n        margin: 0;\n    }\n    div.log .ts {\n        color: #55c;\n    }\n}\n"
  },
  {
    "path": "shared-data/default-theme/less/app/thread.less",
    "content": "/* Thread */\n\n/* Thread Title */\n\n  #thread-title {\n    display: table;\n    text-align: center;\n    padding: 0;\n  }\n\n  #thread-title h1 {\n    display: inline-block;\n    font-family: @mailpile-text-font-family-bold;\n    font-size: 21px;\n    line-height: 24px;\n    color: rgb(77, 77, 77); //@grayDark;\n  }\n\n  #thread-title ul li {\n    margin: 0 10px;\n  }\n\n  #thread-title ul a {\n    color: @gray;\n    font-family: Helvetica, Arial, sans-serif;\n    font-size: 12px;\n    font-weight: normal;\n  }\n\n  #thread-title ul a:hover {\n    color: @grayDark;\n  }\n\n  #thread-title div.thread-draggable {\n    width: 12px;\n    height: 100%;\n    display: table-cell;\n    background: url('../../img/draggable-pattern.png'), rgba(255,255,255,1);\n    opacity: 0.3;\n    filter:alpha(opacity=30);\n  }\n\n  #thread-title div.thread-draggable:hover {\n    cursor: move;\n  }\n\n  #thread-title div.thread-details {\n    display: table-cell;\n  }\n\n\n/* Thread New */\n\n  .thread-snippet.new,\n  .thread-message.new {\n    background: @colorNew;\n  }\n\n  .thread-snippet.new:hover,\n  .thread-message.new:hover {\n    background: @colorExpand;\n  }\n\n  .thread-snippet.new a.datetime,\n  .thread-snippet.new a.datetime:visited,\n  .thread-message.new a.datetime,\n  .thread-message.new a.datetime:visited {\n    color: @grayDark;\n  }\n\n\n/* Thread Snippet - Preview of messages */\n\n  .thread-snippet {\n    background: @white;\n    border-bottom: 1px solid @gray;\n  }\n\n  .thread-snippet:hover {\n    background: @colorExpand;\n    cursor: pointer;\n  }\n\n  .thread-snippet:hover .feedback-expand {\n    display: block;\n  }\n\n\n/* Thread Notification - Non message items */\n\n  .thread-notification {\n    padding: 10px 15px;\n    background: @white;\n    border-bottom: 1px solid @gray;\n    color: @gray;\n  }\n\n  .thread-notification span.instruction {\n    display: none;\n  }\n\n  .thread-notification a {\n    width: 100%;\n    height: 100%;\n    display: block;\n    color: @gray;\n    font-weight: normal;\n  }\n\n  .thread-notification:hover {\n    background: @colorExpand;\n  }\n\n  .thread-notification:hover span.instruction {\n    display: inline;\n  }\n\n  .thread-notification:hover a {\n    color: @grayDark;\n  }\n\n  .thread-notification a:hover,\n  .thread-notification:hover a:hover {\n    color: @blue;\n  }\n\n\n/* Thread Message - View of full message */\n\n  .thread-message {\n    background: @white;\n    border-bottom: 1px solid @gray;\n  }\n\n  .thread-line-subject {\n\n  }\n\n\n/* Thread Message Attachments */\n\n  div.thread-message-attachments {\n    margin-top: 15px;\n    margin-left: 20px;\n    margin-bottom: 0px;\n  }\n\n  ul.thread-message-attachments {\n    margin-left: 0px;\n    margin-bottom: 0px;\n  }\n\n  ul.thread-message-attachments li {\n    margin-right: 15px;\n    margin-bottom: 15px;\n    padding: 0;\n  }\n\n\n/* Thread - Reply */\n\n  div.thread-reply {\n    border-bottom: 1px solid @gray;\n  }\n"
  },
  {
    "path": "shared-data/default-theme/less/app/tooltips.less",
    "content": "/* Tooltips - Global */\n\n  .qtip-tipped small {\n    display: block;\n    font-size: 11px;\n    font-weight: normal;\n    color: @gray;\n  }\n\n\n/* Tooltips - Crypto */\n\n  .qtip-thread-crypto {\n  \tborder: 1px solid @gray;\n  \tborder-radius: 4px;\n  \tbackground: @white;\n  \tpadding: 8px 10px;\n    box-shadow: 1px 1px 2px 0px @grayMid;  \t\n  }\n\n  .qtip-thread-crypto .qtip-content h4 {\n    text-align: center;\n    margin-top: 5px;\n    margin-bottom: 15px;\n  }\n\n  .qtip-thread-crypto .qtip-content h4 span {\n    margin-right: 5px;    \n  }\n\n  .qtip-thread-crypto .qtip-content p {\n    margin-bottom: 10px;\n    text-align: center;\n    font-size: 14px;\n    font-weight: normal;\n  \tfont-family: Helvetica, Arial, sans-serif;\n  \tline-height: 18px;    \n    color: @grayDark;\n  }\n\n  .qtip-thread-crypto .qtip-icon {\n  \tborder: 2px solid #285589;\n  \tbackground: #285589;\n  }\n\n  .qtip-thread-crypto .qtip-icon .ui-icon {\n  \tbackground-color: #FBFBFB;\n  \tcolor: #555;\n  }\n\n\n/* Tooltips - Contact Details */\n\n  .qtip-contact-details {\n  \tborder: 1px solid @gray;\n  \tborder-radius: 4px;\n  \tbackground: @white;\n  \tpadding: 5px 10px 0px 10px;\n    box-shadow:         1px 1px 2px 0px @grayMid;  \t\n  }\n\n  .qtip-contact-details .qtip-content {\n    width: 215px;\n    margin-top: 5px;\n  }\n\n\n/* Tooltips - Tag Details */\n\n  .qtip-tag-details {\n  \tborder: 1px solid @gray;\n  \tborder-radius: 4px;\n  \tbackground: @white;\n  \tpadding: 5px 10px 0px 10px;\n    box-shadow: 1px 1px 2px 0px @grayMid;  \t\n  }\n\n  .qtip-tag-details .qtip-content {\n    width: 170px;\n    margin-top: 5px;\n  }\n\n  .qtip-tag-details .qtip-content a {\n    display: inline-block;\n    margin-bottom: 10px;\n  }\n"
  },
  {
    "path": "shared-data/default-theme/less/app/topbar.less",
    "content": "/* Topbar */\n\n.topbar {\n  width: 100%;\n  height: 62px;\n  display: table;\n  position: fixed;\n  top: 0px;\n  left: 0px;\n  z-index: 100;\n  min-width: 800px;\n  border-bottom: 1px solid @gray;\n  box-sizing: border-box;\n  background: @grayLight;\n}\n\n.topbar-logo {\n  width: 67px;\n  display: table-cell;\n  box-sizing: border-box;\n  vertical-align: middle;\n  text-align: center;\n}\n.topbar-logo #logo-icon {\n  display: block;\n  margin: 0 10px 0 15px;\n  height: 37px;\n}\n.topbar-logo span#page-title-icon {\n  font-size: 37px;\n  line-height: 37px;\n}\n\n.topbar-logo-name {\n  position: relative;\n  width: 157px;\n  height: 40px;\n  display: table-cell;\n  box-sizing: border-box;\n  vertical-align: middle;\n  text-align: left;\n}\n.topbar-logo-name #logo-name {\n  display: block;\n  margin-right: 10px;\n}\n.topbar-logo-name span#page-title-text {\n  display: block;\n  max-width: 75%;\n  position: absolute;\n  font-family: @mailpile-text-font-family-bold;\n  font-weight: normal;\n  font-size: 12px;\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  margin: -7px 0 0 8px;\n  padding: 0;\n}\n\n.topbar-actions {\n  min-width: 630px;\n  display: table-cell;\n  box-sizing: border-box;\n  vertical-align: middle;\n}\n\n.form-search {\n  width: 350px;\n  height: 36px;\n  float: left;\n  margin-top: 5px;\n  margin-left: 15px;\n  border-radius: 5px;\n  border: 1px solid @gray;\n  position: relative;\n}\n\n.form-search input {\n  width: 282px;\n  height: 20px;\n  padding: 8px 12px;\n  float: left;\n  font: normal 18px @text-font-family-bold;\n  border: 0;\n  background: @white;\n  border-radius: 5px 0 0 5px;\n  color: @grayMid;\n}\n\n.form-search input:focus {\n  outline: 0;\n  background: #fff;\n  box-shadow: 0 0 1px @grayDark inset;\n  color: @grayDark;\n}\n\n.form-search input::-webkit-input-placeholder,\n.form-search input:-moz-placeholder,\n.form-search input:-ms-input-placeholder {\n  color: #999;\n  font-weight: normal;\n  font-style: italic;\n}\n\n.form-search .clear-search {\n  position: absolute;\n  right: 50px;\n  top: 11px;\n  cursor: pointer;\n\n  color: @grayMid;\n\n  &:hover {\n    color: @gray;\n  }\n}\n\n.form-search button {\n  overflow: visible;\n  position: relative;\n  float: right;\n  border: 0;\n  padding: 0;\n  cursor: pointer;\n  height: 36px;\n  width: 44px;\n  font: bold 18px/40px @text-font-family-bold;\n  color: @white;\n  background: @blue;\n  border-radius: 0 3px 3px 0;\n  text-shadow: 0 -1px 0 rgba(0, 0 ,0, .3);\n}\n\n.form-search button:hover {\n  background: darken(@blue, 5%);\n}\n\n.form-search button:active,\n.form-search button:focus {\n  background: darken(@blue, 15%);\n  outline: 0;\n}\n\n.form-search button::-moz-focus-inner { /* remove extra button spacing for Mozilla Firefox */\n  border: 0;\n  padding: 0;\n}\n\n.topbar-nav {\n  float: right;\n  position: relative;\n  top: 0px;\n  right: 20px;\n}\n\n.topbar-nav ul {\n  list-style: none;\n}\n\n.topbar-nav ul:after {\n  clear: both;\n}\n\n.topbar-nav > ul > li {\n  float: left;\n  margin-left: 15px;\n  text-align: center;\n}\n\n.topbar-nav > ul > li > a {\n  width: 32px;\n  height: 32px;\n  display: block;\n  margin: 6px 8px;\n  font-weight: normal;\n  color: @grayDark;\n  -webkit-transition-duration: 0.3s;\n  -moz-transition-duration: 0.3s;\n  transition-duration: 0.3s;\n}\n\n.topbar-nav > ul > li > a:hover {\n  color: @blue;\n  -webkit-transition-duration: 0.2s;\n  -moz-transition-duration: 0.2s;\n  transition-duration: 0.2s;\n}\n\n.topbar-nav > ul > li > a img {\n  height: 32px;\n}\n\n.topbar-nav > ul > li > a.donate:hover {\n  color: @red;\n}\n\n.topbar-nav > ul > li > a span.link-icon {\n  display: block;\n  font-size: 32px;\n  line-height: 32px;\n}\n\n.topbar-nav > ul > li.navigation-on {\n  background: @grayMid;\n  border-radius: @base-border-radius;\n}\n\n.topbar-nav > ul > li.navigation-on > a {\n  color: @grayDark;\n  cursor: default;\n}\n\n.topbar-nav > ul > li.navigation-on.nav-dropdown > a:hover {\n  cursor: pointer !important;\n}\n\n.topbar-nav > ul > li.nav-dropdown ul.dropdown-menu li {\n  display: block;\n  float: none;\n  text-align: left;\n}\n\n.topbar-nav .nav-search {\n  display: none;\n}\n"
  },
  {
    "path": "shared-data/default-theme/less/app/webfonts.less",
    "content": "/* #Fonts\n================================================== */\n\n/* @license\n * Mailpile font designed for Mailpile\n *\n * You may obtain a copy of the license at the URLs below.\n *\n * Webfont: Mailpile by Brennan Novak\n * URL: http://github.com/mailpile/fonts\n *\n * Mailpile font is licensed under the SIL Open Font License (OFL)\n * http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL\n *\n * © 2013 Mailpile\n*/\n\n@font-face {\n\tfont-family: 'Mailpile-300';\n\tsrc: url('../../webfonts/Mailpile-Normal.eot');\n\tsrc: url('../../webfonts/Mailpile-Normal.eot?#iefix') format('embedded-opentype'),\n\turl('../../webfonts/Mailpile-Normal.woff') format('font-woff'),\n\turl('../../webfonts/Mailpile-Normal.ttf') format('truetype'),\n\turl('../../webfonts/Mailpile-Normal.svg#wf') format('svg');\n}\n\n@font-face {\n\tfont-family: 'Mailpile-500';\n\tsrc: url('../../webfonts/Mailpile-500.eot');\n\tsrc: url('../../webfonts/Mailpile-500.eot?#iefix') format('embedded-opentype'),\n\turl('../../webfonts/Mailpile-500.woff') format('font-woff'),\n\turl('../../webfonts/Mailpile-500.ttf') format('truetype'),\n\turl('../../webfonts/Mailpile-500.svg#wf') format('svg');\n}\n\n@font-face {\n\tfont-family: 'Mailpile-700';\n\tsrc: url('../../webfonts/Mailpile-700.eot');\n\tsrc: url('../../webfonts/Mailpile-700.eot?#iefix') format('embedded-opentype'),\n\turl('../../webfonts/Mailpile-700.woff') format('font-woff'),\n\turl('../../webfonts/Mailpile-700.ttf') format('truetype'),\n\turl('../../webfonts/Mailpile-700.svg#wf') format('svg');\n}\n"
  },
  {
    "path": "shared-data/default-theme/less/config.less",
    "content": "/* Mailpile v0.1.0\n * config.less\n */\n\n/* Mailpile */\n\n  @mailpile-text-font-family: Mailpile-300, \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n\t@mailpile-text-font-family-semibold: Mailpile-500, \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n\t@mailpile-text-font-family-bold: Mailpile-700, \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n\n\n/* Colors */\n\n\t@white: #FFFFFF;\n\t@black: #333333;\n\n\t@grayLight: #E9E9E9;\n\t@grayMid: #CCCCCC;\n\t@gray: #B3B3B3;\n\t@grayDark: #4D4D4D;\n\n\t@blueLight: #85B2E8;\n\t@blue: #337FB2;\n\t@blueDark: darken(#337FB2, 15%);\n\t@greenLight: #A3CE73;\n\t@green: #4B9441;\n\n\t@yellow: #E9DB2F;\n\t@orange: #FBB03B;\n\t@orangeRed: #F15A24;\n\t@red: #BE1C21;\n\n\t@brown: #7D4837;\n\t@brownLight: #90746C;\n  @pink: #F08DCA;\n  @purple: #6A27A4;\n\n  @colorSelect: lighten(@yellow, 35%);\n  @colorSelectHover: lighten(@yellow, 30%);\n  @colorNew: lighten(@blueLight, 23%);\n  @colorExpand: lighten(@blueLight, 18%);\n\n\n/* Body */\n\n\t@body-background-color: #ffffff;\n\t@body-background-image: ~'';\n\t@body-background-repeat: ~'';\n\t@body-background-position: ~'';\n\n\n/* Container */\n\n\t@container-desktop-max-width: 100%;\n\t@container-desktop-width: 1025px;\n\t@container-tablet-width: 768px;\n\t@container-mobile-width: 420px;\n\n\n/* Margins, Paddings, and Spacings */\n\n\t@base-line-height: 20px;\n\t@base-margin: 20px;\n\t@base-margin-bottom: @base-margin * 1.5;\n\t@base-padding: 15px;\n\t@base-border-radius: 3px;\n\n\n/* Text */\n\n\t@text-font-size: 14px;\n\t@text-font-weight: normal;\n\t@text-font-family: \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n\t@text-font-family-italic: \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n\t@text-font-family-semibold: \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n\t@text-font-family-bold: \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n\t@text-line-height: 24px;\n\t@text-color: #333333;\n\t@text-color-light: #999999;\n\n\t@em-font-style: italic;\n\n\t@strong-font-family: @text-font-family-bold;\n\t@strong-font-weight: bold;\n\t@strong-color: inherit;\n\n\t@text-shadow-on-light: 1px 1px 1px #;\n\t@text-shadow-on-dark: 0 -1px 0 rgba(0, 0, 0, 0.4);\n\n\t@pre-margin-bottom: 20px;\n\t@pre-background: @grayLight;\n  @pre-color: @black;\n  @pre-padding: 4px 8px;\n  @pre-border: 1px solid @gray;\n  @pre-border-radius: 3px;\n  @pre-font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;\n  @pre-font-size: 14px;\n  @pre-font-weight: normal;\n\n  @code-font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;\n  @code-font-size: 14px;\n  @code-font-weight: normal;\n  @code-padding: 2px 5px;\n\n/* Links */\n\n\t@link-font-size: 14px;\n\t@link-font-wieght: bold;\n\t@link-color: @grayDark;\n\t@link-color-hover: @blue;\n\t@link-decoration: none;\n\t@link-decoration-hover: none;\n\n\n/* Headings */\n\n\t@headings-color: #333333;\n\t@headings-font-family: @mailpile-text-font-family-bold;\n\t@headings-font-weight: normal;\n\t@headings-letter-spacing: 0px;\n\t@headings-size-h1: 36px;\n\t@headings-size-h2: 30px;\n\t@headings-size-h3: 24px;\n\t@headings-size-h4: 21px;\n\t@headings-size-h5: 18px;\n\t@headings-size-h6: 14px;\n\t@headings-line-height: 1px;\n\t@headings-margin-bottom: 0px;\n\n\n/* Navigation */\n\n\t@navigation-background: #222222;\n\t@navigation-text: #EEEEEE;\n\t@navigation-text-hover: #AAAAAA;\n\t@navigation-text-dark: #818181;\n\t@navigation-elements: #AAAAAA;\n\t@navigation-shadows: #EEEEEE;\n\n\t@navigation-sub-background: #d9d9d9;\n\t@navigation-sub-text: #EEEEEE;\n\t@navigation-sub-text-hover: #AAAAAA;\n\t@navigation-sub-text-dark: #818181;\n\t@navigation-sub-elements: #AAAAAA;\n\t@navigation-sub-shadows: #EEEEEE;\n\n\n/* Lists */\n\n  @list-margin: 0 0 20px 0;\n\t@list-padding: 0px;\n  @list-font-size: @text-font-size;\n  @list-line-height: @text-font-size;\n\t@list-item-margin: 5px;\n\t@list-item-padding: 5px;\n\n\t@list-unordered-type: none outside;\n\t@list-ordered-type: none outside;\n\n  @list-item-horizontal-margin: 0px;\n\n\n/* Separators */\n\n\t@blockquote-font-size: 18px;\n\t@blockquote-font-style: italic;\n\t@blockquote-line-height: 24px;\n\t@blockquote-color: #666666;\n\n\t@hr-border-type: solid;\n\t@hr-border-color: @gray;\n\t@hr-border-width: 1px;\n\t@hr-border-margin: 35px 0;\n\n\n/* Buttons */\n\n\t.button-global(@font-family, @font-size, @font-weight, @margin, @padding) {\n\n\t\t// Font\n\t\tfont-family: @font-family !important;\n\t\tfont-size: @font-size !important;\n\t\tfont-weight: @font-weight !important;\n\t\tline-height: inherit;\n\t\ttext-decoration: none;\n\n\t\t// Display\n    margin: @margin;\n\t\tpadding: @padding;\n\t\tdisplay: inline-block;\n\t\tcursor: pointer;\n\t\tbox-sizing: border-box;\n\n\t\t// Select\n    user-select:none;\n\n    // Animate\n    transition-duration: 0.2s;\n\t}\n\n\t.button-colors(@color, @text-shadow, @border-color, @background) {\n\n\t\tcolor: @color !important;\n\t\ttext-shadow: @text-shadow;\n\n\t\tborder: 1px solid @border-color;\n\t\tborder-radius: 4px;\n\n    background: @background;\n    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4), 0 0px 0px rgba(0, 0, 0, 0.2);\n  }\n\n  .button-colors-hover(@border-color, @background) {\n    border-color: @border-color;\n    background-color: @background;\n\t}\n\n  .button-colors-active(@border-color, @background) {\n    border-color: @border-color;\n    background-color: @background;\n    box-shadow: inset 0 .17em .1em rgba(0, 0, 0, 0.3);\n  }\n\n  @btn-color: @white;\n  @btn-font-family: @mailpile-text-font-family-semibold;\n  @btn-font-size: 16px;\n  @btn-font-weight: 300;\n  @btn-margin: 0px;\n  @btn-padding: 5px 15px;\n\n  @btn-small-font-size: 12px;\n  @btn-small-font-weight: bold;\n  @btn-small-margin: @btn-margin;\n  @btn-small-padding: 5px 8px;\n\n\n/* Button - Primary */\n\n\t@btn-primary-color: @white;\n\t@btn-primary-text-shadow: @text-shadow-on-dark;\n\t@btn-primary-background: @blue;\n\t@btn-primary-background-hover: darken(@blue, 5%);\n  @btn-primary-background-active: darken(@blue, 5%);\n\t@btn-primary-border-color: darken(@blue, 10%);\n\t@btn-primary-border-color-hover: darken(@blue, 15%);\n  @btn-primary-border-color-active: darken(@blue, 15%);\n\n\n/* Button - Secondary */\n\n\t@btn-secondary-color: @white;\n\t@btn-secondary-text-shadow: @text-shadow-on-dark;\n\t@btn-secondary-background: @green;\n\t@btn-secondary-background-hover: darken(@green, 5%);\n  @btn-secondary-background-active: darken(@green, 5%);\n\t@btn-secondary-border-color: darken(@green, 10%);\n\t@btn-secondary-border-color-hover: darken(@green, 10%);\n  @btn-secondary-border-color-active: darken(@green, 10%);\n\n\n/* Button - Info */\n\n\t@btn-info-color: @black;\n\t@btn-info-text-shadow: 0px 0px 0px;\n\t@btn-info-background: @white;\n\t@btn-info-background-hover: darken(@grayLight, 5%);\n  @btn-info-background-active: darken(@grayLight, 5%);\n\t@btn-info-border-color: @grayMid;\n\t@btn-info-border-color-hover: @gray;\n  @btn-info-border-color-active: darken(@gray, 10%);\n\n\n/* Button - Alert */\n\n\t@btn-alert-color: @white;\n\t@btn-alert-text-shadow: @text-shadow-on-dark;\n\t@btn-alert-background: @orange;\n\t@btn-alert-background-hover: darken(@orange, 8%);\n\t@btn-alert-background-active: darken(@orange, 5%);\n\t@btn-alert-border-color: darken(@orange, 10%);\n\t@btn-alert-border-color-hover: darken(@orange, 15%);\n\t@btn-alert-border-color-active: darken(@orange, 12%);\n\n\n/* Button - Warning */\n\n\t@btn-warning-color: @white;\n\t@btn-warning-text-shadow: @text-shadow-on-dark;\n  @btn-warning-background: @red;\n\t@btn-warning-background-hover: darken(@red, 7%);\n  @btn-warning-background-active: darken(@red, 5%);\n  @btn-warning-border-color: darken(@red, 10%);\n\t@btn-warning-border-color-hover: darken(@red, 10%);\n  @btn-warning-border-color-active: darken(@red, 8%);\n\n\n/* Forms */\n\n\t@form-background: #FFFFFF;\n\t@form-color: #666666;\n\t@form-color-focus: #333333;\n\t@form-font-size: 16px;\n\t@form-font-family: \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n\n\t@form-border-size: 1px;\n\t@form-border-color: @gray;\n\t@form-border-radius: @base-border-radius;\n\t@form-border-color-focus: @grayDark;\n\n  @form-label-margin: 0 0 5px 0;\n  @form-label-font-size: @form-font-size;\n  @form-label-font-style: italic;\n  @form-label-font-weight: normal;\n\t@form-label-color: @grayDark;\n\n  @form-input-width: 210px;\n  @form-input-margin-bottom: @base-margin;\n  @form-input-padding: 10px 12px;\n  @form-input-focus: rgba(0, 0, 0,.4);\n\n  @form-textarea-height: 60px;\n\n\t@form-select-color: @grayDark;\n  @form-select-padding: 15px 20px;\n\n  @form-tiny: 75px;\n  @form-small: 135px;\n  @form-medium: 350px;\n  @form-large: 500px;\n  @form-full: 100%;\n\n\n/* Validation */\n\n  @validation-border: 1px solid;\n  @validation-font-style: italic;\n  @validation-success-color: @green;\n  @validation-warning-color: @orange;\n  @validation-error-color: @red;\n\n\n/* Tables */\n\n  @table-background-color: @white;\n  @table-hover-background-color: #F5F6DB;\n  @table-zebra-odd-background-color: #F9F9F9;\n  @table-zebra-even-background-color: #FEFEFE;\n\n  @table-th-padding: 5px 10px;\n  @table-th-background-color: @white;\n  @table-th-font-style: italic;\n  @table-th-font-size: 14px;\n\n  @table-margin: @base-margin 0;\n  @table-padding: 10px;\n\n  @table-border: 1px solid #ccc;\n  @table-border-radius: 5px;\n\n  @table-td-border-top: 1px solid #ccc;\n  @table-font-size: 16px;\n  @table-line-height: 16px;\n\n\n/* Grid */\n\n  @grid-row-margin-bottom: 20px;\n\t@grid-gutter: 1%;\n\t@grid-column-count: 16;\n\n\t@grid-tablet-width: 100%;\n\t@grid-mobile-width: 100%;\n\n\n/* Boxes */\n\n  @boxes-padding: 15px;\n  @boxes-header-font-size: 24px;\n  @boxes-paragraph-font-size: 14px;\n  @boxes-link-color: @gray;\n\n\n/* Rectangles */\n\n  @rectangles-padding: 10px;\n  @rectangles-header-font-size: 18px;\n  @rectangles-paragraph-font-size: 12px;\n  @rectangles-link-color: @gray;\n  @rectangles-height: 70px;\n\n\n/* Bootstrap */\n\n  //** Disabled cursor for form controls and buttons.\n  @cursor-disabled: not-allowed;\n\n  @caret-width-base:          4px;\n  @caret-width-large:         5px;\n\n  @zindex-navbar:            1000;\n  @zindex-dropdown:          1000;\n  @zindex-popover:           1010;\n  @zindex-tooltip:           1030;\n  @zindex-navbar-fixed:      1030;\n  @zindex-modal-background:  1040;\n  @zindex-modal:             1050;\n\n  @font-size-base: @text-font-size;\n  @font-size-small: 12px;\n\n  @border-radius-base: 4px;\n  @border-radius-large: 4px;\n\n  @line-height-base: @base-line-height;\n  @line-height-computed: @base-line-height;\n\n  @screen-sm:                 768px;\n  @screen-sm-min:             @screen-sm;\n  @screen-md-min:             1024px;\n\n  @grid-float-breakpoint: 768px;\n  @grid-float-breakpoint-max: (@grid-float-breakpoint - 1);\n\n\n/* Bootstrap - Dropdown */\n\n  @dropdown-bg:                    @white;\n  @dropdown-border:                rgba(0,0,0,.15);\n  @dropdown-fallback-border:       @grayMid;\n  @dropdown-divider-bg:            @grayMid;\n  @dropdown-link-color:            @grayDark;\n  @dropdown-link-hover-color:      @white;\n  @dropdown-link-hover-bg:         @grayDark;\n  @dropdown-link-active-color:     @red;\n  @dropdown-link-active-bg:        @grayDark;\n  @dropdown-link-disabled-color:   @grayLight;\n  @dropdown-header-color:          @grayLight;\n  @dropdown-caret-color:           @grayDark;\n\n\n/* Bootstrap - Modal */\n\n  @zindex-modal:                  1050;\n  @zindex-modal-background:       1040;\n\n  @modal-inner-padding:           20px;\n  @modal-title-padding:           15px;\n  @modal-title-line-height:       1.428571429;\n  @modal-content-bg:              @white;\n  @modal-content-border-color:    @grayMid;\n  @modal-content-fallback-border-color: @white;\n\n  @modal-backdrop-bg:             @black;\n  @modal-backdrop-opacity:        .5;\n  @modal-header-border-color:     @grayMid;\n  @modal-footer-border-color:     @modal-header-border-color;\n\n  @modal-lg:                      900px;\n  @modal-md:                      600px;\n  @modal-sm:                      300px;"
  },
  {
    "path": "shared-data/default-theme/less/default.less",
    "content": "  /* Mailpile - Default Theme\n*  Version 0.4.1\n*\t Designed and built by @brennannovak and others\n*/\n\n/* Global - Must be included before the config file incase you want specify custom fonts or backgrounds */\n@import \"app/webfonts.less\";\n@import \"app/icons.less\";\n\n/* Config - Change the settings in this file change the look and feel of your style */\n@import \"config.less\";\n\n/* Bower */\n@import (less) \"../../../bower_components/animate.css/animate.css\";\n@import \"../../../bower_components/less-elements-old/elements.less\";\n\n/* Rebar - these files reset and style all the basic HTML elements */\n@import \"../../../bower_components/rebar/less/rebar/base.less\";\n@import \"../../../bower_components/rebar/less/rebar/buttons.less\";\n@import \"../../../bower_components/rebar/less/rebar/clearing.less\";\n@import \"../../../bower_components/rebar/less/rebar/forms.less\";\n@import \"../../../bower_components/rebar/less/rebar/images.less\";\n@import \"../../../bower_components/rebar/less/rebar/links.less\";\n@import \"../../../bower_components/rebar/less/rebar/lists.less\";\n@import \"../../../bower_components/rebar/less/rebar/rectangles.less\";\n@import \"../../../bower_components/rebar/less/rebar/separators.less\";\n//@import \"../../bower_components/rebar/less/rebar/shapes.less\";\n@import \"../../../bower_components/rebar/less/rebar/tables.less\";\n@import \"../../../bower_components/rebar/less/rebar/typography.less\";\n@import \"../../../bower_components/rebar/less/rebar/validation.less\";\n\n/* Libraries */\n@import \"libraries/bootstrap.less\";\n@import (less) \"../../../bower_components/select2/select2.css\";\n@import (less) \"../../../bower_components/qtip2/basic/jquery.qtip.css\";\n@import \"libraries/dropdowns.less\";\n@import \"libraries/modals.less\";\n@import \"libraries/typeahead.less\";\n\n/* FIXME: This overrides some library defaults, not sure this is the\n *        right way to do this! -bre */\n@import \"app/library-override.less\";\n\n/* App */\n@import \"app/setup.less\";\n@import \"app/settings.less\";\n@import \"app/profiles.less\";\n@import \"app/screens.less\";\n@import \"app/login.less\";\n@import \"app/global.less\";\n@import \"app/helpers.less\";\n@import \"app/notifications.less\";\n@import \"app/navigation.less\";\n@import \"app/topbar.less\";\n@import \"app/sidebar.less\";\n@import \"app/attachments.less\";\n@import \"app/crypto.less\";\n@import \"app/compose.less\";\n@import \"app/contacts.less\";\n@import \"app/modals.less\";\n@import \"app/search.less\";\n@import \"app/pile.less\";\n@import \"app/thread.less\";\n@import \"app/message.less\";\n@import \"app/tags.less\";\n@import \"app/files.less\";\n@import \"app/tooltips.less\";\n@import \"app/tablet.less\";\n@import \"app/mobile.less\";\n@import \"app/terminal.less\";\n"
  },
  {
    "path": "shared-data/default-theme/less/libraries/bootstrap.less",
    "content": "// Horizontal dividers\n// -------------------------\n// Dividers (basically an hr) within dropdowns and nav lists\n.nav-divider(@color: #e5e5e5) {\n  height: 1px;\n  margin: ((@line-height-computed / 2) - 1) 0;\n  overflow: hidden;\n  background-color: @color;\n}\n\n\n// Reset filters for IE\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n.reset-filter() {\n  filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n//   supported browsers that have box shadow capabilities now support the\n//   standard `box-shadow` property.\n.box-shadow(@shadow) {\n  -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n          box-shadow: @shadow;\n}\n\n\n// Transitions\n.transition(@transition) {\n  -webkit-transition: @transition;\n          transition: @transition;\n}\n.transition-property(@transition-property) {\n  -webkit-transition-property: @transition-property;\n          transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n  -webkit-transition-delay: @transition-delay;\n          transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n  -webkit-transition-duration: @transition-duration;\n          transition-duration: @transition-duration;\n}\n.transition-transform(@transition) {\n  -webkit-transition: -webkit-transform @transition;\n     -moz-transition: -moz-transform @transition;\n       -o-transition: -o-transform @transition;\n          transition: transform @transition;\n}"
  },
  {
    "path": "shared-data/default-theme/less/libraries/dropdowns.less",
    "content": "//\n// Dropdown menus\n// --------------------------------------------------\n\n\n// Dropdown arrow/caret\n.caret {\n  display: inline-block;\n  width: 0;\n  height: 0;\n  margin-left: 2px;\n  vertical-align: middle;\n  border-top:   @caret-width-base solid;\n  border-right: @caret-width-base solid transparent;\n  border-left:  @caret-width-base solid transparent;\n}\n\n// The dropdown wrapper (div)\n.dropdown {\n  position: relative;\n}\n\n// Prevent the focus on the dropdown toggle when closing dropdowns\n.dropdown-toggle:focus {\n  outline: 0;\n}\n\n// The dropdown menu (ul)\n.dropdown-menu {\n  position: absolute;\n  top: 100%;\n  left: 0;\n  z-index: @zindex-dropdown;\n  display: none; // none by default, but block on \"open\" of the menu\n  float: left;\n  min-width: 160px;\n  padding: 5px 0;\n  margin: 2px 0 0; // override default ul\n  list-style: none;\n  font-size: @font-size-base;\n  text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer)\n  background-color: @dropdown-bg;\n  border: 1px solid @dropdown-fallback-border; // IE8 fallback\n  border: 1px solid @dropdown-border;\n  border-radius: @border-radius-base;\n  .box-shadow(0 6px 12px rgba(0,0,0,.175));\n  background-clip: padding-box;\n\n  // Aligns the dropdown menu to right\n  //\n  // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]`\n  &.pull-right {\n    right: 0;\n    left: auto;\n  }\n\n  // Dividers (basically an hr) within the dropdown\n  .divider {\n    .nav-divider(@dropdown-divider-bg);\n  }\n\n  // Links within the dropdown menu\n  > li > a {\n    display: block;\n    padding: 3px 20px;\n    clear: both;\n    font-weight: normal;\n    line-height: @line-height-base;\n    color: @dropdown-link-color;\n    white-space: nowrap; // prevent links from randomly breaking onto new lines\n  }\n}\n\n// Hover/Focus state\n.dropdown-menu > li > a {\n  &:hover,\n  &:focus {\n    text-decoration: none;\n    color: @dropdown-link-hover-color;\n    background-color: @dropdown-link-hover-bg;\n  }\n}\n\n// Active state\n.dropdown-menu > .active > a {\n  &,\n  &:hover,\n  &:focus {\n    color: @dropdown-link-active-color;\n    text-decoration: none;\n    outline: 0;\n    background-color: @dropdown-link-active-bg;\n  }\n}\n\n// Disabled state\n//\n// Gray out text and ensure the hover/focus state remains gray\n\n.dropdown-menu > .disabled > a {\n  &,\n  &:hover,\n  &:focus {\n    color: @dropdown-link-disabled-color;\n  }\n\n  // Nuke hover/focus effects\n  &:hover,\n  &:focus {\n    text-decoration: none;\n    background-color: transparent;\n    background-image: none; // Remove CSS gradient\n    .reset-filter();\n    cursor: @cursor-disabled;\n  }\n}\n\n// Open state for the dropdown\n.open {\n  // Show the menu\n  > .dropdown-menu {\n    display: block;\n  }\n\n  // Remove the outline when :focus is triggered\n  > a {\n    outline: 0;\n  }\n}\n\n// Menu positioning\n//\n// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown\n// menu with the parent.\n.dropdown-menu-right {\n  left: auto; // Reset the default from `.dropdown-menu`\n  right: 0;\n}\n// With v3, we enabled auto-flipping if you have a dropdown within a right\n// aligned nav component. To enable the undoing of that, we provide an override\n// to restore the default dropdown menu alignment.\n//\n// This is only for left-aligning a dropdown menu within a `.navbar-right` or\n// `.pull-right` nav component.\n.dropdown-menu-left {\n  left: 0;\n  right: auto;\n}\n\n// Dropdown section headers\n.dropdown-header {\n  display: block;\n  padding: 3px 20px;\n  font-size: @font-size-small;\n  line-height: @line-height-base;\n  color: @dropdown-header-color;\n  white-space: nowrap; // as with > li > a\n}\n\n// Backdrop to catch body clicks on mobile, etc.\n.dropdown-backdrop {\n  position: fixed;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  top: 0;\n  z-index: (@zindex-dropdown - 10);\n}\n\n// Right aligned dropdowns\n.pull-right > .dropdown-menu {\n  right: 0;\n  left: auto;\n}\n\n// Allow for dropdowns to go bottom up (aka, dropup-menu)\n//\n// Just add .dropup after the standard .dropdown class and you're set, bro.\n// TODO: abstract this so that the navbar fixed styles are not placed here?\n\n.dropup,\n.navbar-fixed-bottom .dropdown {\n  // Reverse the caret\n  .caret {\n    border-top: 0;\n    border-bottom: @caret-width-base solid;\n    content: \"\";\n  }\n  // Different positioning for bottom up menu\n  .dropdown-menu {\n    top: auto;\n    bottom: 100%;\n    margin-bottom: 1px;\n  }\n}\n\n\n// Component alignment\n//\n// Reiterate per navbar.less and the modified component alignment there.\n\n@media (min-width: @grid-float-breakpoint) {\n  .navbar-right {\n    .dropdown-menu {\n      .dropdown-menu-right();\n    }\n    // Necessary for overrides of the default right aligned menu.\n    // Will remove come v4 in all likelihood.\n    .dropdown-menu-left {\n      .dropdown-menu-left();\n    }\n  }\n}\n"
  },
  {
    "path": "shared-data/default-theme/less/libraries/modals.less",
    "content": "//\n// Modals\n// --------------------------------------------------\n\n// .modal-open      - body class for killing the scroll\n// .modal           - container to scroll within\n// .modal-dialog    - positioning shell for the actual modal\n// .modal-content   - actual modal w/ bg and corners and shit\n\n// Kill the scroll on the body\n.modal-open {\n  overflow: hidden;\n}\n\n// Container that the modal scrolls within\n.modal {\n  display: none;\n  overflow: hidden;\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: @zindex-modal;\n  -webkit-overflow-scrolling: touch;\n\n  // Prevent Chrome on Windows from adding a focus outline. For details, see\n  // https://github.com/twbs/bootstrap/pull/10951.\n  outline: 0;\n\n  // When fading in the modal, animate it to slide down\n  &.fade .modal-dialog {\n    .translate(0, -25%);\n    .transition-transform(~\"0.3s ease-out\");\n  }\n  &.in .modal-dialog { .translate(0, 0) }\n}\n.modal-open .modal {\n  overflow-x: hidden;\n  overflow-y: auto;\n}\n\n// Shell div to position the modal with bottom padding\n.modal-dialog {\n  position: relative;\n  width: auto;\n  margin: 10px;\n}\n\n// Actual modal\n.modal-content {\n  position: relative;\n  background-color: @modal-content-bg;\n  border: 1px solid @modal-content-fallback-border-color; //old browsers fallback (ie8 etc)\n  border: 1px solid @modal-content-border-color;\n  border-radius: @border-radius-large;\n  .box-shadow(0 3px 9px rgba(0,0,0,.5));\n  background-clip: padding-box;\n  // Remove focus outline from opened modal\n  outline: 0;\n}\n\n// Modal background\n.modal-backdrop {\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  background-color: @modal-backdrop-bg;\n  z-index: (@zindex-modal - 1);\n  // Fade for backdrop\n  &.fade { .opacity(0); }\n  &.in { .opacity(@modal-backdrop-opacity); }\n}\n\n// Modal header\n// Top section of the modal w/ title and dismiss\n.modal-header {\n  padding: @modal-title-padding;\n  border-bottom: 1px solid @modal-header-border-color;\n  min-height: (@modal-title-padding + @modal-title-line-height);\n}\n// Close icon\n.modal-header .close {\n  margin-top: -2px;\n  float: right;\n}\n\n// Title text within header\n.modal-title {\n  margin: 0;\n  line-height: @modal-title-line-height;\n}\n\n// Modal body\n// Where all modal content resides (sibling of .modal-header and .modal-footer)\n.modal-body {\n  position: relative;\n  padding: @modal-inner-padding;\n}\n\n// Footer (for actions)\n.modal-footer {\n  padding: @modal-inner-padding;\n  text-align: right; // right align buttons\n  border-top: 1px solid @modal-footer-border-color;\n  &:extend(.clearfix all); // clear it in case folks use .pull-* classes on buttons\n\n  // Properly space out buttons\n  .btn + .btn {\n    margin-left: 5px;\n    margin-bottom: 0; // account for input[type=\"submit\"] which gets the bottom margin like all other inputs\n  }\n  // but override that for button groups\n  .btn-group .btn + .btn {\n    margin-left: -1px;\n  }\n  // and override it for block buttons as well\n  .btn-block + .btn-block {\n    margin-left: 0;\n  }\n}\n\n// Measure scrollbar width for padding body during modal show/hide\n.modal-scrollbar-measure {\n  position: absolute;\n  top: -9999px;\n  width: 50px;\n  height: 50px;\n  overflow: scroll;\n}\n\n// Scale up the modal\n@media (min-width: @screen-sm-min) {\n  // Automatically set modal's width for larger viewports\n  .modal-dialog {\n    width: @modal-md;\n    margin: 30px auto;\n  }\n  .modal-content {\n    .box-shadow(0 5px 15px rgba(0,0,0,.5));\n  }\n\n  // Modal sizes\n  .modal-sm { width: @modal-sm; }\n}\n\n@media (min-width: @screen-md-min) {\n  .modal-lg { width: @modal-lg; }\n}\n"
  },
  {
    "path": "shared-data/default-theme/less/libraries/typeahead.less",
    "content": ".twitter-typeahead {\n  width: 282px;\n  float: left;\n}\n\n.tt-dropdown-menu {\n  width: 305px;\n  background: @white;\n  border-right: 1px solid @gray;\n  border-bottom: 1px solid @gray;\n  border-left: 1px solid @gray;\n  border-bottom-left-radius: (@base-border-radius * 2);\n  border-bottom-right-radius: (@base-border-radius * 2);\n  box-shadow: 1px 1px 2px @grayMid; \n}\n\n.tt-suggestions {\n\n}\n\n.tt-suggestion {\n  font-size: 14px;\n}\n\n.tt-suggestion .separator {\n  border-top: 1px solid @grayMid;\n}\n\n.tt-suggestion .helper {\n  color: @gray;\n}\n\n.tt-suggestion .avatar {\n  width: 24px;\n  border-radius: @base-border-radius;\n  margin-right: 5px;\n}\n\n.tt-suggestion p {\n  padding: 5px @base-padding;\n  margin: 0px;\n}\n\n.tt-cursor {\n  background: @grayMid;\n}"
  },
  {
    "path": "shared-data/default-theme/less/print.less",
    "content": "/**\n * Use a readable type for print styles\n */\nbody {\n  font-family: Georgia, serif;\n  background: none;\n  color: black;\n}\n\n#sidebar, #header {\n  display: none;\n}\n#content {\n  margin: 0;\n  padding: 0;\n  width: 100%;\n  bacckground: none;\n}\n.thread-item-text { clear: both }\n"
  },
  {
    "path": "shared-data/default-theme/theme.json",
    "content": "{\n  \"name\": \"Mailpile (Chelsea)\",\n  \"authors\": [\"The Mailpile Team <team@mailpile.is>\"],\n  \"navigation_link\": \"#4d4d4d\",\n  \"navigation_on_color\": \"#4d4d4d\",\n  \"icons\": [\n    \"icon-new\",\n    \"icon-compose\",\n    \"icon-mailsource\",\n    \"icon-inbox\",\n    \"icon-sent\",\n    \"icon-outbox\",\n    \"icon-spam\",\n    \"icon-trash\",\n    \"icon-archive\",\n    \"icon-tag\",\n    \"icon-tags\",\n    \"icon-home\",\n    \"icon-comment\",\n    \"icon-forum\",\n    \"icon-star\",\n    \"icon-donate\",\n    \"icon-dislike\",\n    \"icon-like\",\n    \"icon-news\",\n    \"icon-photos\",\n    \"icon-image\",\n    \"icon-video\",\n    \"icon-music\",\n    \"icon-themes\",\n    \"icon-links\",\n    \"icon-document\",\n    \"icon-text\",\n    \"icon-travel\",\n    \"icon-transit\",\n    \"icon-money\",\n    \"icon-receipts\",\n    \"icon-trophy\",\n    \"icon-calendar\",\n    \"icon-spreadsheet\",\n    \"icon-attachment\",\n    \"icon-user\",\n    \"icon-social\",\n    \"icon-groups\",\n    \"icon-graph\",\n    \"icon-force-graph\",\n    \"icon-list\",\n    \"icon-checkmark\",\n    \"icon-alerts\",\n    \"icon-lightbulb\",\n    \"icon-zip\",\n    \"icon-work\",\n    \"icon-rss\",\n    \"icon-robot\",\n    \"icon-code\",\n    \"icon-eye\",\n    \"icon-privacy\",\n    \"icon-lock-closed\",\n    \"icon-key\",\n    \"icon-map\",\n    \"icon-geopoint\",\n    \"icon-animals\",\n    \"icon-clock\",\n    \"icon-columns\",\n    \"icon-flashlight\",\n    \"icon-food\",\n    \"icon-help\",\n    \"icon-purchases\",\n    \"icon-upload\",\n    \"icon-download\"\n  ],\n  \"colors\": {\n    \"01-gray-mid\": \"#cccccc\",\n    \"02-gray\": \"#b3b3b3\",\n    \"03-gray-dark\": \"#4d4d4d\",\n    \"04-black\": \"#333333\",\n    \"05-blue-light\" : \"#85b2e8\",\n    \"06-blue\": \"#337fb2\",\n    \"07-green-light\": \"#a3ce73\",\n    \"08-green\": \"#4b9441\",\n    \"09-yellow\": \"#e9db2f\",\n    \"10-orange\": \"#fbb03b\",\n    \"11-orange-red\": \"#f15a24\",\n    \"12-red\": \"#be1c21\",\n    \"13-brown\": \"#7d4837\",\n    \"14-brown-light\": \"#90746c\",\n    \"15-pink\": \"#f08dca\",\n    \"16-purple\": \"#6a27a4\"\n  }\n}"
  },
  {
    "path": "shared-data/default-theme/webfonts/LICENSE",
    "content": "Copyright (c) 2015, Mailpile Ehf. (team@mailpile.is) with Reserved Font Name Mailpile.\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttp://scripts.sil.org/OFL\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "shared-data/default-theme/webfonts/index.html",
    "content": "Put your webfonts in this folder"
  },
  {
    "path": "shared-data/locale/README.md",
    "content": "Hello translators!\n==================\n\nTranslation happens in Transifex, not in our git repository.\n\nSee: https://www.transifex.com/projects/p/mailpile/ \n\nThank you!\n"
  },
  {
    "path": "shared-data/locale/mailpile.pot",
    "content": "# Translations template for mailpile.\n# Copyright (C) 2019 Mailpile ehf\n# This file is distributed under the same license as the mailpile project.\n# Mailpile Team <team@mailpile.is>, 2019.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: mailpile VERSION\\n\"\n\"Report-Msgid-Bugs-To: EMAIL@ADDRESS\\n\"\n\"POT-Creation-Date: 2019-08-16 17:28+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language-Team: LANGUAGE <LL@li.org>\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.4.0\\n\"\n\n#: mailpile/app.py:125\nmsgid \"Mailpile is unconfigured, please run `setup` or visit the web UI.\"\nmsgstr \"\"\n\n#: mailpile/app.py:131\nmsgid \"Interrupted. Press CTRL-D or type `quit` to quit.\"\nmsgstr \"\"\n\n#: mailpile/app.py:190\nmsgid \"Ran interactive shell\"\nmsgstr \"\"\n\n#: mailpile/app.py:204\nmsgid \"Did nothing much for a while\"\nmsgstr \"\"\n\n#: mailpile/app.py:246 mailpile/plugins/core.py:60\nmsgid \"Failed to decrypt configuration, please log in!\"\nmsgstr \"\"\n\n#: mailpile/auth.py:188\nmsgid \"Hello world, welcome!\"\nmsgstr \"\"\n\n#: mailpile/auth.py:198\nmsgid \"Invalid password, please try again\"\nmsgstr \"\"\n\n#: mailpile/auth.py:209\nmsgid \"Incorrect username or password\"\nmsgstr \"\"\n\n#: mailpile/auth.py:222 mailpile/plugins/backups.py:292\nmsgid \"Your password: \"\nmsgstr \"\"\n\n#: mailpile/auth.py:230 mailpile/commands.py:579 mailpile/plugins/gui.py:166\n#: shared-data/default-theme/html/auth/login/index.html:2\n#: shared-data/default-theme/html/auth/logout/index.html:2\nmsgid \"Please log in\"\nmsgstr \"\"\n\n#: mailpile/auth.py:255\nmsgid \"Goodbye!\"\nmsgstr \"\"\n\n#: mailpile/auth.py:259\nmsgid \"No session found!\"\nmsgstr \"\"\n\n#: mailpile/auth.py:440\nmsgid \"Enter your Mailpile password\"\nmsgstr \"\"\n\n#: mailpile/auth.py:443\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: mailpile/auth.py:454\nmsgid \"That key is managed by Mailpile, it cannot be changed directly.\"\nmsgstr \"\"\n\n#: mailpile/auth.py:456\nmsgid \"Protected secret\"\nmsgstr \"\"\n\n#: mailpile/auth.py:470\nmsgid \"Password incorrect! Try again?\"\nmsgstr \"\"\n\n#: mailpile/auth.py:471\nmsgid \"Incorrect password\"\nmsgstr \"\"\n\n#: mailpile/auth.py:512\nmsgid \"No password found\"\nmsgstr \"\"\n\n#: mailpile/auth.py:514\nmsgid \"Retrieved stored password\"\nmsgstr \"\"\n\n#: mailpile/auth.py:523\nmsgid \"Password forgotten!\"\nmsgstr \"\"\n\n#: mailpile/auth.py:529\nmsgid \"Password will never be stored\"\nmsgstr \"\"\n\n#: mailpile/auth.py:537\nmsgid \"Password remembered!\"\nmsgstr \"\"\n\n#: mailpile/auth.py:547\nmsgid \"The password has been stored temporarily\"\nmsgstr \"\"\n\n#: mailpile/auth.py:550\nmsgid \"Invalid password policy\"\nmsgstr \"\"\n\n#: mailpile/command_cache.py:143\nmsgid \"New results are available\"\nmsgstr \"\"\n\n#: mailpile/commands.py:109\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:164\nmsgid \"OK\"\nmsgstr \"\"\n\n#: mailpile/commands.py:109 mailpile/mail_source/__init__.py:354\n#: shared-data/default-theme/html/setup/oauth2/index.html:82\nmsgid \"Failed\"\nmsgstr \"\"\n\n#: mailpile/commands.py:235\nmsgid \"Failed to parse arguments\"\nmsgstr \"\"\n\n#: mailpile/commands.py:276\n#, python-format\nmsgid \"Cached result as %s\"\nmsgstr \"\"\n\n#: mailpile/commands.py:382 mailpile/commands.py:387\nmsgid \"No results to choose from!\"\nmsgstr \"\"\n\n#: mailpile/commands.py:394\n#, python-format\nmsgid \"No such ID: %s\"\nmsgstr \"\"\n\n#: mailpile/commands.py:400 mailpile/commands.py:407 mailpile/commands.py:413\n#, python-format\nmsgid \"What message is %s?\"\nmsgstr \"\"\n\n#: mailpile/commands.py:421\n#, python-format\nmsgid \"%s error: %s\"\nmsgstr \"\"\n\n#: mailpile/commands.py:525\nmsgid \"Generating result\"\nmsgstr \"\"\n\n#: mailpile/commands.py:570\n#, python-format\nmsgid \"Using pre-cached result object %s\"\nmsgstr \"\"\n\n#: mailpile/commands.py:581 mailpile/mail_source/__init__.py:219\n#: mailpile/plugins/gui.py:133\nmsgid \"Shutting down\"\nmsgstr \"\"\n\n#: mailpile/commands.py:718\n#, python-format\nmsgid \"Unknown command: %s\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:94\n#, python-format\nmsgid \"%(tls_version)s (%(bits)s bit %(algorithm)s)\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:100 mailpile/conn_brokers.py:212\nmsgid \"no encryption\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:195\n#, python-format\nmsgid \"Failed to connect to %s: %s\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:202\nmsgid \"the local network\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:204\nmsgid \"the Internet\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:206\n#, python-format\nmsgid \"Attempting to connect to %(host)s\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:208\n#, python-format\nmsgid \"Connected to %(host)s over %(network)s with %(encryption)s.\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:411\n#, python-format\nmsgid \"SOCKS error, %s\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:413\nmsgid \"timed out\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:414\nmsgid \"host unreachable\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:415 mailpile/conn_brokers.py:502\nmsgid \"connection refused\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:500\n#, python-format\nmsgid \"Tor error, %s\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:707\nmsgid \"No connection method found\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:741\nmsgid \"No network events recorded\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:744\nmsgid \"Listed recent network events\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:767\nmsgid \"No certificates found\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:772\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:7\nmsgid \"Examine TLS certificates\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:977\nmsgid \"Failed to fetch certificate\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:984\nmsgid \"Downloaded TLS certificates\"\nmsgstr \"\"\n\n#: mailpile/conn_brokers.py:987\nmsgid \"Failed to download TLS certificates\"\nmsgstr \"\"\n\n#: mailpile/httpd.py:307\nmsgid \"XMLRPC has been disabled for now.\"\nmsgstr \"\"\n\n#: mailpile/httpd.py:329\nmsgid \"OMG, input too big\"\nmsgstr \"\"\n\n#: mailpile/httpd.py:332\nmsgid \"Unknown content-type\"\nmsgstr \"\"\n\n#: mailpile/httpd.py:338 mailpile/httpd.py:516 mailpile/ui.py:413\nmsgid \"Internal Error\"\nmsgstr \"\"\n\n#: mailpile/httpd.py:378\nmsgid \"File not found (invalid path)\"\nmsgstr \"\"\n\n#: mailpile/httpd.py:509\nmsgid \"Access Denied\"\nmsgstr \"\"\n\n#: mailpile/i18n.py:145\n#, python-format\nmsgid \"Loaded language %s\"\nmsgstr \"\"\n\n#: mailpile/postinglist.py:44\n#, python-format\nmsgid \"Save PLC %s\"\nmsgstr \"\"\n\n#: mailpile/search.py:153 mailpile/search.py:170\nmsgid \"Loading metadata index...\"\nmsgstr \"\"\n\n#: mailpile/search.py:162\nmsgid \"Your metadata index is either too old, too new or corrupt!\"\nmsgstr \"\"\n\n#: mailpile/search.py:166\nmsgid \"Corrupt data in metadata index! Trying to cope...\"\nmsgstr \"\"\n\n#: mailpile/search.py:190\n#, python-format\nmsgid \"Metadata index not found: %s\"\nmsgstr \"\"\n\n#: mailpile/search.py:193\nmsgid \"Loading global posting list...\"\nmsgstr \"\"\n\n#: mailpile/search.py:202\n#, python-format\nmsgid \"Recovered! Wrote bad metadata to: %s\"\nmsgstr \"\"\n\n#: mailpile/search.py:218\n#, python-format\nmsgid \"Fixing %d messages in keyword index not in metadata.\"\nmsgstr \"\"\n\n#: mailpile/search.py:284\nmsgid \"Saving metadata index changes...\"\nmsgstr \"\"\n\n#: mailpile/search.py:306\nmsgid \"Saved metadata index changes\"\nmsgstr \"\"\n\n#: mailpile/search.py:323\nmsgid \"Saving metadata index...\"\nmsgstr \"\"\n\n#: mailpile/search.py:351\nmsgid \"Saved metadata index\"\nmsgstr \"\"\n\n#: mailpile/search.py:362\nmsgid \"Updating high level indexes\"\nmsgstr \"\"\n\n#: mailpile/search.py:374\n#, python-format\nmsgid \"Bogus line: %s\"\nmsgstr \"\"\n\n#: mailpile/search.py:460\n#, python-format\nmsgid \"%s: Skipped: %s\"\nmsgstr \"\"\n\n#: mailpile/search.py:463\n#, python-format\nmsgid \"%s: Checking: %s\"\nmsgstr \"\"\n\n#: mailpile/search.py:469\n#, python-format\nmsgid \"%s: Error opening: %s (%s)\"\nmsgstr \"\"\n\n#: mailpile/search.py:480\n#, python-format\nmsgid \"%s: No new mail in: %s\"\nmsgstr \"\"\n\n#: mailpile/search.py:484\n#, python-format\nmsgid \"%s: Reading your mail: %d%% (%d/%d message)\"\nmsgstr \"\"\n\n#: mailpile/search.py:485\n#, python-format\nmsgid \"%s: Reading your mail: %d%% (%d/%d messages)\"\nmsgstr \"\"\n\n#: mailpile/search.py:507\n#, python-format\nmsgid \"Rescan interrupted: %s\"\nmsgstr \"\"\n\n#: mailpile/search.py:577\n#, python-format\nmsgid \"%s: Indexed mailbox: ...%s (%d new, %d updated)\"\nmsgstr \"\"\n\n#: mailpile/search.py:775\nmsgid \"(processing message ...)\"\nmsgstr \"\"\n\n#: mailpile/search.py:1165\nmsgid \"(missing message)\"\nmsgstr \"\"\n\n#: mailpile/search.py:1274\n#, python-format\nmsgid \"=%s/%s has bogus HTML.\"\nmsgstr \"\"\n\n#: mailpile/search.py:1869\n#, python-format\nmsgid \"Searching for %s\"\nmsgstr \"\"\n\n#: mailpile/search.py:1908\n#, python-format\nmsgid \"Ignoring common word: %s\"\nmsgstr \"\"\n\n#: mailpile/search.py:2083\nmsgid \"Recovering from bogus sort, corrupt index?\"\nmsgstr \"\"\n\n#: mailpile/search.py:2085\nmsgid \"Please tell team@mailpile.is !\"\nmsgstr \"\"\n\n#: mailpile/search.py:2094\n#, python-format\nmsgid \"Unknown sort order: %s\"\nmsgstr \"\"\n\n#: mailpile/search.py:2099\nmsgid \"Sort failed, sorting badly. Partial index?\"\nmsgstr \"\"\n\n#: mailpile/search.py:2115\nmsgid \"Collapsing conversations...\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:834 mailpile/security.py:69\nmsgid \"Insufficient free disk space\"\nmsgstr \"\"\n\n#: mailpile/security.py:76 mailpile/security.py:86 mailpile/security.py:94\n#: mailpile/security.py:106\nmsgid \"In lockdown, doing nothing.\"\nmsgstr \"\"\n\n#: mailpile/security.py:619\nmsgid \"We trust ourselves\"\nmsgstr \"\"\n\n#: mailpile/security.py:651\nmsgid \"This sender's reputation is unknown\"\nmsgstr \"\"\n\n#: mailpile/security.py:718\nmsgid \"The digital signature is invalid\"\nmsgstr \"\"\n\n#: mailpile/security.py:720\nmsgid \"This person usually signs their mail\"\nmsgstr \"\"\n\n#: mailpile/security.py:722\nmsgid \"This was signed by an unexpected key\"\nmsgstr \"\"\n\n#: mailpile/security.py:724\nmsgid \"This was signed by an expired key\"\nmsgstr \"\"\n\n#: mailpile/security.py:726\nmsgid \"Good signature, we are happy\"\nmsgstr \"\"\n\n#: mailpile/security.py:728\nmsgid \"This came from an unexpected source\"\nmsgstr \"\"\n\n#: mailpile/security.py:730\nmsgid \"No problems detected.\"\nmsgstr \"\"\n\n#: mailpile/smtp_client.py:196\n#, python-format\nmsgid \"Sendmail: From %s (%s), to %s via %s\\n\"\nmsgstr \"\"\n\n#: mailpile/smtp_client.py:201\n#, python-format\nmsgid \"Sending via %s\"\nmsgstr \"\"\n\n#: mailpile/smtp_client.py:218\n#, python-format\nmsgid \"%s failed with exit code %d\"\nmsgstr \"\"\n\n#: mailpile/smtp_client.py:242\n#, python-format\nmsgid \"SMTP connection to: %s:%s as %s\\n\"\nmsgstr \"\"\n\n#: mailpile/smtp_client.py:261\n#, python-format\nmsgid \"Failed to connect to %s\"\nmsgstr \"\"\n\n#: mailpile/mail_source/imap.py:680 mailpile/mail_source/imap.py:736\n#: mailpile/mail_source/pop3.py:52 mailpile/smtp_client.py:282\nmsgid \"Failed to make a secure TLS connection\"\nmsgstr \"\"\n\n#: mailpile/mail_source/imap.py:699 mailpile/smtp_client.py:297\nmsgid \"Access denied by mail server\"\nmsgstr \"\"\n\n#: mailpile/smtp_client.py:308\nmsgid \"Bad character in username or password\"\nmsgstr \"\"\n\n#: mailpile/mail_source/imap.py:689 mailpile/mail_source/pop3.py:49\n#: mailpile/smtp_client.py:313\nmsgid \"Invalid username or password\"\nmsgstr \"\"\n\n#: mailpile/smtp_client.py:321\nmsgid \"Sender rejected by SMTP server\"\nmsgstr \"\"\n\n#: mailpile/smtp_client.py:329\n#, python-format\nmsgid \"Server rejected recipient: %s\"\nmsgstr \"\"\n\n#: mailpile/smtp_client.py:332\n#, python-format\nmsgid \"Server rejected DATA: %s %s\"\nmsgstr \"\"\n\n#: mailpile/smtp_client.py:344\nmsgid \"Error spooling mail\"\nmsgstr \"\"\n\n#: mailpile/smtp_client.py:352\n#, python-format\nmsgid \"Invalid route: %s\"\nmsgstr \"\"\n\n#: mailpile/smtp_client.py:363\nmsgid \"Preparing message…\"\nmsgstr \"\"\n\n#: mailpile/smtp_client.py:369\nmsgid \"Quitting\"\nmsgstr \"\"\n\n#: mailpile/ui.py:319\n#, python-format\nmsgid \"Elapsed: %.3fs (%s)\"\nmsgstr \"\"\n\n#: mailpile/ui.py:325\n#, python-format\nmsgid \"Elapsed: %.3fs\"\nmsgstr \"\"\n\n#: mailpile/ui.py:514 mailpile/ui.py:529\nmsgid \"Template not found\"\nmsgstr \"\"\n\n#: mailpile/ui.py:521 mailpile/ui.py:536\nmsgid \"Template error\"\nmsgstr \"\"\n\n#: mailpile/ui.py:551\n#, python-format\nmsgid \"Message %s is not editable\"\nmsgstr \"\"\n\n#: mailpile/ui.py:577\nmsgid \"Number of edited messages does not match!\"\nmsgstr \"\"\n\n#: mailpile/util.py:505\nmsgid \"false\"\nmsgstr \"\"\n\n#: mailpile/util.py:505\nmsgid \"no\"\nmsgstr \"\"\n\n#: mailpile/util.py:505\nmsgid \"off\"\nmsgstr \"\"\n\n#: mailpile/util.py:507\nmsgid \"true\"\nmsgstr \"\"\n\n#: mailpile/util.py:507\nmsgid \"yes\"\nmsgstr \"\"\n\n#: mailpile/util.py:507\nmsgid \"on\"\nmsgstr \"\"\n\n#: mailpile/util.py:665\nmsgid \"now\"\nmsgstr \"\"\n\n#: mailpile/util.py:667\n#, python-format\nmsgid \"%d mins\"\nmsgstr \"\"\n\n#: mailpile/util.py:669\n#, python-format\nmsgid \"%d hour\"\nmsgstr \"\"\n\n#: mailpile/util.py:671\n#, python-format\nmsgid \"%d hours\"\nmsgstr \"\"\n\n#: mailpile/util.py:681\nmsgid \"Monday\"\nmsgstr \"\"\n\n#: mailpile/util.py:681\nmsgid \"Mon\"\nmsgstr \"\"\n\n#: mailpile/util.py:681\nmsgid \"Tuesday\"\nmsgstr \"\"\n\n#: mailpile/util.py:681\nmsgid \"Tue\"\nmsgstr \"\"\n\n#: mailpile/util.py:682\nmsgid \"Wednesday\"\nmsgstr \"\"\n\n#: mailpile/util.py:682\nmsgid \"Wed\"\nmsgstr \"\"\n\n#: mailpile/util.py:682\nmsgid \"Thursday\"\nmsgstr \"\"\n\n#: mailpile/util.py:682\nmsgid \"Thu\"\nmsgstr \"\"\n\n#: mailpile/util.py:683\nmsgid \"Friday\"\nmsgstr \"\"\n\n#: mailpile/util.py:683\nmsgid \"Fri\"\nmsgstr \"\"\n\n#: mailpile/util.py:683\nmsgid \"Saturday\"\nmsgstr \"\"\n\n#: mailpile/util.py:683\nmsgid \"Sat\"\nmsgstr \"\"\n\n#: mailpile/util.py:684\nmsgid \"Sunday\"\nmsgstr \"\"\n\n#: mailpile/util.py:684\nmsgid \"Sun\"\nmsgstr \"\"\n\n#: mailpile/util.py:685\nmsgid \"January\"\nmsgstr \"\"\n\n#: mailpile/util.py:685\nmsgid \"Jan\"\nmsgstr \"\"\n\n#: mailpile/util.py:685\nmsgid \"February\"\nmsgstr \"\"\n\n#: mailpile/util.py:685\nmsgid \"Feb\"\nmsgstr \"\"\n\n#: mailpile/util.py:686\nmsgid \"March\"\nmsgstr \"\"\n\n#: mailpile/util.py:686\nmsgid \"Mar\"\nmsgstr \"\"\n\n#: mailpile/util.py:686\nmsgid \"April\"\nmsgstr \"\"\n\n#: mailpile/util.py:686\nmsgid \"Apr\"\nmsgstr \"\"\n\n#: mailpile/util.py:687\nmsgid \"May\"\nmsgstr \"\"\n\n#: mailpile/util.py:687\nmsgid \"June\"\nmsgstr \"\"\n\n#: mailpile/util.py:687\nmsgid \"Jun\"\nmsgstr \"\"\n\n#: mailpile/util.py:688\nmsgid \"July\"\nmsgstr \"\"\n\n#: mailpile/util.py:688\nmsgid \"Jul\"\nmsgstr \"\"\n\n#: mailpile/util.py:688\nmsgid \"August\"\nmsgstr \"\"\n\n#: mailpile/util.py:688\nmsgid \"Aug\"\nmsgstr \"\"\n\n#: mailpile/util.py:689\nmsgid \"September\"\nmsgstr \"\"\n\n#: mailpile/util.py:689\nmsgid \"Sep\"\nmsgstr \"\"\n\n#: mailpile/util.py:689\nmsgid \"October\"\nmsgstr \"\"\n\n#: mailpile/util.py:689\nmsgid \"Oct\"\nmsgstr \"\"\n\n#: mailpile/util.py:690\nmsgid \"November\"\nmsgstr \"\"\n\n#: mailpile/util.py:690\nmsgid \"Nov\"\nmsgstr \"\"\n\n#: mailpile/util.py:690\nmsgid \"December\"\nmsgstr \"\"\n\n#: mailpile/util.py:690\nmsgid \"Dec\"\nmsgstr \"\"\n\n#: mailpile/vcard.py:464\n#, python-format\nmsgid \"Line number %s is out of range\"\nmsgstr \"\"\n\n#: mailpile/vcard.py:1523\nmsgid \"Generating new vCards\"\nmsgstr \"\"\n\n#: mailpile/vcard.py:1530\n#, python-format\nmsgid \"Merging %s\"\nmsgstr \"\"\n\n#: mailpile/vcard.py:1562\n#, python-format\nmsgid \"Failed to merge vCard %s into %s\"\nmsgstr \"\"\n\n#: mailpile/vcard.py:1580\n#, python-format\nmsgid \"Failed to create new vCard for %s\"\nmsgstr \"\"\n\n#: mailpile/vcard.py:1588\n#, python-format\nmsgid \"Saving %d updated vCards\"\nmsgstr \"\"\n\n#: mailpile/vfs.py:320\nmsgid \"My Files\"\nmsgstr \"\"\n\n#: mailpile/vfs.py:336\nmsgid \"Unix mail spool\"\nmsgstr \"\"\n\n#: mailpile/vfs.py:387\nmsgid \"Mailpile VFS\"\nmsgstr \"\"\n\n#: mailpile/config/base.py:278\nmsgid \"Please override this method\"\nmsgstr \"\"\n\n#: mailpile/config/base.py:312\n#, python-format\nmsgid \"Only lists or dictionaries can contain dictionary values (key %s).\"\nmsgstr \"\"\n\n#: mailpile/config/base.py:323\n#, python-format\nmsgid \"Subsections must be immutable (key %s).\"\nmsgstr \"\"\n\n#: mailpile/config/base.py:332\n#, python-format\nmsgid \"Lists cannot have default values (key %s).\"\nmsgstr \"\"\n\n#: mailpile/config/base.py:341\n#, python-format\nmsgid \"Invalid type \\\"%s\\\" for key \\\"%s\\\" (value: %s)\"\nmsgstr \"\"\n\n#: mailpile/config/base.py:357\n#, python-format\nmsgid \"Invalid key for %s: %s\"\nmsgstr \"\"\n\n#: mailpile/config/base.py:388\n#, python-format\nmsgid \"Access denied to %s\"\nmsgstr \"\"\n\n#: mailpile/config/base.py:461\n#, python-format\nmsgid \"Modifying %s/%s is not allowed\"\nmsgstr \"\"\n\n#: mailpile/config/base.py:465 mailpile/config/base.py:478\n#, python-format\nmsgid \"Invalid value for %s/%s: %s\"\nmsgstr \"\"\n\n#: mailpile/config/base.py:481\n#, python-format\nmsgid \"Unknown constraint for %s/%s: %s\"\nmsgstr \"\"\n\n#: mailpile/config/base.py:710\nmsgid \"Cannot append to fixed dict\"\nmsgstr \"\"\n\n#: mailpile/config/base.py:771\n#, python-format\nmsgid \"Invalid (%s): section %s does not exist\"\nmsgstr \"\"\n\n#: mailpile/config/base.py:782\n#, python-format\nmsgid \"Invalid (%s): section %s, variable %s=%s\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:30\nmsgid \"Mailpile program version\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:31\nmsgid \"Location of Mailpile data\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:32\nmsgid \"Configuration timestamp\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:33\nmsgid \"Master symmetric encryption key\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:34\nmsgid \"Technical system settings\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:35\nmsgid \"Max files kept open at once\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:36\nmsgid \"Required free disk space (MB)\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:37\nmsgid \"History length (lines, <0=no save)\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:38\nmsgid \"Listening host for web UI\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:40\nmsgid \"Listening port for web UI\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:41\nmsgid \"HTTP path of web UI\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:42\nmsgid \"Disable HTTP authentication\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:43\nmsgid \"AJAX Request timeout\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:44\nmsgid \"Posting list target size in KB\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:45\nmsgid \"Max results we sort \\\"well\\\"\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:46\nmsgid \"Max length of metadata snippets\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:47\nmsgid \"Debugging flags\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:48\nmsgid \"Enabled experiments\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:49\nmsgid \"Host:port of PGP keyserver\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:51\nmsgid \"Override the home directory of GnuPG\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:53\nmsgid \"Override the default GPG binary path\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:55\nmsgid \"Local read/write Maildir\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:56\nmsgid \"Metadata index file\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:57\nmsgid \"Search index directory\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:58\nmsgid \"Mailboxes we index\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:59\nmsgid \"Plugins to load before login\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:61\nmsgid \"Plugins to load after login\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:63\nmsgid \"Locations of assorted data\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:64\nmsgid \"User interface theme\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:65\nmsgid \"Location of vCards\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:66\nmsgid \"Location of event log\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:68\nmsgid \"Demo mode, disallow changes\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:69\nmsgid \"A custom banner for the login page\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:70\nmsgid \"Proxy settings\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:71\nmsgid \"Proxy protocol\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:74\nmsgid \"Allow fallback to direct conns\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:75 mailpile/config/defaults.py:204\n#: mailpile/config/defaults.py:225 mailpile/spambayes/Options.py:1057\nmsgid \"User name\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:76 mailpile/config/defaults.py:205\n#: mailpile/config/defaults.py:226 mailpile/spambayes/Options.py:1062\n#: mailpile/spambayes/Options.py:1088 mailpile/spambayes/Options.py:1202\n#: shared-data/default-theme/html/settings/index.html:66\n#: shared-data/default-theme/html/settings/recipes.html:22\nmsgid \"Password\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:77 mailpile/config/defaults.py:208\n#: mailpile/config/defaults.py:228\nmsgid \"Host\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:78 mailpile/config/defaults.py:209\n#: mailpile/config/defaults.py:229 mailpile/spambayes/Options.py:965\n#: shared-data/default-theme/html/settings/recipes.html:26\nmsgid \"Port\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:79\nmsgid \"List of hosts to avoid proxying\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:82\nmsgid \"Tor settings\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:83\nmsgid \"Override the default Tor binary path\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:85\nmsgid \"Use shared system-wide Tor (not our own)\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:87\nmsgid \"Socks host\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:88\nmsgid \"Socks Port\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:89\nmsgid \"Control Port\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:90\nmsgid \"Control Password\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:93\nmsgid \"User preferences\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:94\n#: shared-data/default-theme/html/settings/preferences.html:49\nmsgid \"Search results per page\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:95\nmsgid \"Misc. data refresh frequency\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:96\n#: shared-data/default-theme/html/settings/preferences.html:149\nmsgid \"Open in browser on startup\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:97\n#: shared-data/default-theme/html/settings/preferences.html:151\nmsgid \"Automatically mark e-mail as read\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:99\nmsgid \"Download content from the web\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:101\nmsgid \"Use HTML5 sandboxes\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:102\nmsgid \"URLs to treat as attachments (regex)\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:104\nmsgid \"Accept weak crypto in messages older than this (unix time)\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:106\nmsgid \"Never display HTML from encrypted mail\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:108\nmsgid \"Never fetch web content from encrypted mail\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:110\n#: shared-data/default-theme/html/settings/preferences.html:155\nmsgid \"Use the local GnuPG agent\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:111\nmsgid \"Inline PGP signatures or attached\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:113\nmsgid \"Encrypt local data to ...\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:114\nmsgid \"Enable e-mail based public key distribution\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:116\n#: shared-data/default-theme/html/settings/preferences.html:156\nmsgid \"Wrap keys and signatures in helpful HTML\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:118\nmsgid \"Enable experimental anti-phishing heuristics \"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:120\nmsgid \"Key Trust Model\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:121\nmsgid \"Minimum number of signatures required\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:123\nmsgid \"Window of time (days) to evaluate trust\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:125\nmsgid \"Signed ratio (%) above which we expect sigs\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:127\nmsgid \"Ratio of key use (%) above which we trust key\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:129\nmsgid \"Consider key new below this ratio (%) of sigs\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:132\nmsgid \"Advertise PGP preferences in a header?\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:135\nmsgid \"Default encryption policy for outgoing mail\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:137\nmsgid \"Use inline PGP when possible\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:138\nmsgid \"Encrypt subjects by default\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:139\nmsgid \"Default sort order\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:140\nmsgid \"Key to use to scramble the index\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:141\nmsgid \"Make encrypted content searchable\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:143\nmsgid \"Encrypt locally stored mail\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:144\nmsgid \"Encrypt the local search index\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:145\nmsgid \"Encrypt the contact database\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:146\nmsgid \"Encrypt the event log\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:147\nmsgid \"Encrypt misc. local data\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:148\nmsgid \"Allow permanent deletion of e-mails\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:150\nmsgid \"Max fraction of source mail to delete per pass\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:156\nmsgid \"Command run before rescanning\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:157\nmsgid \"Default outgoing e-mail address\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:158 mailpile/config/defaults.py:162\nmsgid \"Default outgoing mail route\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:159\nmsgid \"Target line length, <40 disables reflow\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:161\nmsgid \"Always BCC self on outgoing mail\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:163\n#: shared-data/default-theme/html/settings/preferences.html:108\nmsgid \"User interface language\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:164\nmsgid \"vCard import/export settings\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:165\nmsgid \"vCard import settings\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:166\nmsgid \"vCard export settings\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:167\nmsgid \"vCard context helper settings\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:170\nmsgid \"Web Interface Preferences\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:171\n#: shared-data/default-theme/html/settings/preferences.html:118\nmsgid \"Enable keyboard short-cuts\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:172\n#: shared-data/default-theme/html/settings/preferences.html:148\nmsgid \"Enable developer-only features\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:173\nmsgid \"User completed setup experience\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:174\nmsgid \"Display density of interface\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:175\nmsgid \"Quote replies to messages\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:176\nmsgid \"Nag user to backup their key\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:177\nmsgid \"Collapsed subtags in sidebar\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:178\n#: shared-data/default-theme/html/settings/preferences.html:119\nmsgid \"Display donate link in topbar?\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:179\nmsgid \"Display HTML hints?\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:180\nmsgid \"Display crypto hints?\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:181\nmsgid \"Display reply hints?\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:182\nmsgid \"Display tagging hints?\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:183\nmsgid \"Display release notes?\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:185\nmsgid \"Credentials allowed to access Mailpile\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:186\nmsgid \"Salted and hashed password\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:188\nmsgid \"Secrets the user wants saved\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:189\nmsgid \"A secret\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:190\nmsgid \"Security policy\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:194\nmsgid \"Settings for TLS certificate validation\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:195\nmsgid \"Server hostname:port\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:196\nmsgid \"SHA256 of acceptable certs\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:197\nmsgid \"Use web certificate authorities\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:199\nmsgid \"Outgoing message routes\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:200\n#: shared-data/default-theme/html/settings/recipes.html:18\nmsgid \"Route name\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:201\nmsgid \"Messaging protocol\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:206 mailpile/config/defaults.py:227\nmsgid \"Authentication scheme\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:207\n#: shared-data/default-theme/html/profiles/account-form.html:460\nmsgid \"Shell command\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:211\nmsgid \"Incoming message sources\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:212\nmsgid \"Source name\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:213\nmsgid \"Profile this source belongs to\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:214\nmsgid \"Is this mail source enabled?\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:215\nmsgid \"Mail source protocol\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:222\nmsgid \"Shell command run before syncing\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:223\nmsgid \"Shell command run after syncing\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:224\nmsgid \"How frequently to check for mail\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:230\nmsgid \"Keep server connections alive\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:231\nmsgid \"Mailbox discovery policy\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:232\nmsgid \"Paths to watch for new mailboxes\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:233\nmsgid \"Default mailbox policy\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:236\nmsgid \"Copy mail to a local mailbox?\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:237\nmsgid \"Parent tag for mailbox tags\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:238\nmsgid \"Guess which local tags match\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:239\nmsgid \"Create a tag for each mailbox?\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:240\nmsgid \"Make tags visible by default?\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:241\nmsgid \"Is a potential source of new mail\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:242 mailpile/config/defaults.py:254\nmsgid \"Tags applied to messages\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:243\nmsgid \"Max mailboxes to add\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:245\nmsgid \"Mailboxes\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:246\nmsgid \"The name of this mailbox\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:247\nmsgid \"Mailbox source path\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:248\nmsgid \"Mailbox policy\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:251\nmsgid \"Local mailbox path\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:252\nmsgid \"Is a source of new mail\"\nmsgstr \"\"\n\n#: mailpile/config/defaults.py:253\nmsgid \"A tag representing this mailbox\"\nmsgstr \"\"\n\n#: mailpile/config/manager.py:153\n#, python-format\nmsgid \"Creating: %s\"\nmsgstr \"\"\n\n#: mailpile/config/manager.py:164\nmsgid \"Another Mailpile or program is using the profile directory\"\nmsgstr \"\"\n\n#: mailpile/config/manager.py:386\n#, python-format\nmsgid \"Loading plugin: %s\"\nmsgstr \"\"\n\n#: mailpile/config/manager.py:388\nmsgid \"Processing manifests\"\nmsgstr \"\"\n\n#: mailpile/config/manager.py:719\n#, python-format\nmsgid \"No such mailbox: %s\"\nmsgstr \"\"\n\n#: mailpile/config/manager.py:891\n#, python-format\nmsgid \"%s: Updating: %s\"\nmsgstr \"\"\n\n#: mailpile/config/manager.py:910\n#, python-format\nmsgid \"%s: Opening: %s (may take a while)\"\nmsgstr \"\"\n\n#: mailpile/config/manager.py:996\nmsgid \"Please enter your password\"\nmsgstr \"\"\n\n#: mailpile/config/manager.py:1017 mailpile/plugins/contacts.py:826\nmsgid \"Sent using Mailpile, Free Software from www.mailpile.is\"\nmsgstr \"\"\n\n#: mailpile/config/manager.py:1069\n#, python-format\nmsgid \"Route %s for %s does not exist.\"\nmsgstr \"\"\n\n#: mailpile/config/manager.py:1237\nmsgid \"Parent paths are not allowed\"\nmsgstr \"\"\n\n#: mailpile/config/manager.py:1310\n#, python-format\nmsgid \"Port %s:%s in use by another Mailpile or program\"\nmsgstr \"\"\n\n#: mailpile/config/validators.py:44\n#, python-format\nmsgid \"Invalid boolean: %s\"\nmsgstr \"\"\n\n#: mailpile/config/validators.py:68\n#, python-format\nmsgid \"Invalid URL slug: %s\"\nmsgstr \"\"\n\n#: mailpile/config/validators.py:92\n#, python-format\nmsgid \"Invalid message delivery protocol: %s\"\nmsgstr \"\"\n\n#: mailpile/config/validators.py:154\n#, python-format\nmsgid \"Invalid hostname: %s\"\nmsgstr \"\"\n\n#: mailpile/config/validators.py:201\n#, python-format\nmsgid \"File/directory does not exist: %s\"\nmsgstr \"\"\n\n#: mailpile/config/validators.py:242\n#, python-format\nmsgid \"Not a file: %s\"\nmsgstr \"\"\n\n#: mailpile/config/validators.py:262\n#, python-format\nmsgid \"Not a directory: %s\"\nmsgstr \"\"\n\n#: mailpile/config/validators.py:300\n#, python-format\nmsgid \"Not a valid url: %s\"\nmsgstr \"\"\n\n#: mailpile/config/validators.py:312\n#, python-format\nmsgid \"Not a valid e-mail: %s\"\nmsgstr \"\"\n\n#: mailpile/config/validators.py:348 mailpile/config/validators.py:350\n#: mailpile/config/validators.py:355\nmsgid \"Not a GPG key ID or fingerprint\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:47\nmsgid \"RSA\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:48\nmsgid \"RSA (encrypt only)\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:49\nmsgid \"RSA (sign only)\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:50\nmsgid \"ElGamal (encrypt only)\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:51\nmsgid \"DSA\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:52\nmsgid \"ElGamal (encrypt/sign) [COMPROMISED]\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:53\nmsgid \"EdDSA\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:54 mailpile/plugins/motd.py:167\n#: mailpile/www/jinjaextensions.py:338 mailpile/www/jinjaextensions.py:408\n#: mailpile/www/jinjaextensions.py:497\nmsgid \"Unknown\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:563\nmsgid \"Your PGP key is needed for decrypting.\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:565\nmsgid \"Your PGP key is needed for signing.\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:567\nmsgid \"Unlock your encryption key\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:589\nmsgid \"Checking GnuPG version\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:604\nmsgid \"Checking GnuPG home directory\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:613\nmsgid \"Checking GnuPG availability\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:819\n#, python-format\nmsgid \"Fetching GnuPG public key list (selectors=%s)\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:849\n#, python-format\nmsgid \"Fetching GnuPG secret key list (selectors=%s)\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:879\nmsgid \"Importing key to GnuPG key chain\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:961\n#, python-format\nmsgid \"Decrypting %d bytes of data\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:1243\n#, python-format\nmsgid \"Checking signature in %d bytes of data\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:1264\n#, python-format\nmsgid \"Encrypting %d bytes of data to %s\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:1268\n#, python-format\nmsgid \"Encrypting %d bytes of data with password\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:1313\n#, python-format\nmsgid \"Signing %d bytes of data with %s\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:1314 mailpile/crypto/gpgi.py:1327\nmsgid \"default\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:1326\n#, python-format\nmsgid \"Signing key %s with %s\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:1341\n#, python-format\nmsgid \"Downloading key %s from key servers\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:1385\n#, python-format\nmsgid \"Searching for key for %s in key servers\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:1421\n#, python-format\nmsgid \"Exporting keys %s from keychain\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:1545\n#, python-format\nmsgid \"Keys missing for %s\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:1549\n#, python-format\nmsgid \"Keys ambiguous for %s\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:1757\n#, python-format\nmsgid \"Creating a %(bits)s bit GnuPG key\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:1777\nmsgid \"Waiting to generate a PGP key.\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:1781\nmsgid \"Generating new PGP key.\"\nmsgstr \"\"\n\n#: mailpile/crypto/gpgi.py:1863\n#, python-format\nmsgid \"Unable to create a %(bits)s bit key, wrong GnuPG version\"\nmsgstr \"\"\n\n#: mailpile/crypto/mime.py:89\n#, python-format\nmsgid \"Decrypted: %s\"\nmsgstr \"\"\n\n#: mailpile/crypto/mime.py:409\nmsgid \"Subject unavailable\"\nmsgstr \"\"\n\n#: mailpile/crypto/mime.py:729\nmsgid \"Failed to sign message!\"\nmsgstr \"\"\n\n#: mailpile/crypto/mime.py:793\nmsgid \"Failed to encrypt message!\"\nmsgstr \"\"\n\n#: mailpile/index/base.py:201\nmsgid \"(unprocessed)\"\nmsgstr \"\"\n\n#: mailpile/index/base.py:203\nmsgid \"(ghost)\"\nmsgstr \"\"\n\n#: mailpile/index/base.py:205\nmsgid \"(deleted)\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:146 mailpile/mail_source/__init__.py:153\n#: shared-data/mailpile-gui/mailpile-gui.py:137\nmsgid \"Starting up\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:148 mailpile/mail_source/__init__.py:1069\nmsgid \"Disabled\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:155\nmsgid \"Mail Source\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:177 mailpile/mail_source/imap.py:634\n#: mailpile/mail_source/imap.py:869 mailpile/mail_source/pop3.py:24\n#: mailpile/mail_source/pop3.py:106\nmsgid \"Nothing is wrong\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:212 mailpile/plugins/core.py:90\n#, python-format\nmsgid \"Insufficient free space in %s\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:218\n#, python-format\nmsgid \"Interrupted: %s\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:275\nmsgid \"Pending\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:296\nmsgid \"Skipped\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:305\nmsgid \"Postponed\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:311\nmsgid \"Working ...\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:345\nmsgid \"Completed\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:348\n#, python-format\nmsgid \"Indexed %d\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:360\nmsgid \"Unchanged\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:363 mailpile/plugins/core.py:1949\n#: mailpile/www/jinjaextensions.py:350 mailpile/www/jinjaextensions.py:545\n#: shared-data/default-theme/html/jsapi/ui/notifications.js:362\nmsgid \"Error\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:368 mailpile/mail_source/__init__.py:371\nmsgid \"Internal error\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:379\n#, python-format\nmsgid \"Discovered %d mailboxes\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:382\n#, python-format\nmsgid \"Processed %d mailboxes\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:384\n#, python-format\nmsgid \"Failed to process %d\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:386\n#, python-format\nmsgid \"No new mail at %s\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:454\nmsgid \"Checking for new mailboxes\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:460\nmsgid \"Too many mailboxes found! Raise limits to continue.\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:488\n#, python-format\nmsgid \"Checking for new mailboxes in %s\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:646\n#, python-format\nmsgid \"Mail: %s\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:707\nmsgid \"Unnamed\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:770 mailpile/plugins/core.py:367\nmsgid \"Aborted\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:825\n#, python-format\nmsgid \"Should delete %d messages\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:843\nmsgid \"Deletion is disabled\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:881\n#, python-format\nmsgid \"Copying message: %s\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:983\nmsgid \"Copying\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:984\n#, python-format\nmsgid \"Copying up to %d e-mails from %s\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:996\n#: shared-data/default-theme/html/partials/hidden.html:209\nmsgid \"Working\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:997\n#, python-format\nmsgid \"Updating search engine for %s\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:1056\nmsgid \"Checking new credentials\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:1107\nmsgid \"Internal error!  Sleeping...\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:1118 mailpile/mail_source/__init__.py:1177\n#: mailpile/plugins/gui.py:58\n#: shared-data/default-theme/html/settings/index.html:137\nmsgid \"Shutdown\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:1167 mailpile/plugins/core.py:161\n#: mailpile/plugins/core.py:1607\nmsgid \"User aborted\"\nmsgstr \"\"\n\n#: mailpile/mail_source/__init__.py:1213\n#, python-format\nmsgid \"Unknown mail source protocol: %s\"\nmsgstr \"\"\n\n#: mailpile/mail_source/imap.py:429\nmsgid \"Failed to add message\"\nmsgstr \"\"\n\n#: mailpile/mail_source/imap.py:440\nmsgid \"Failed to remove message\"\nmsgstr \"\"\n\n#: mailpile/mail_source/imap.py:464\nmsgid \"Mailbox is out of sync\"\nmsgstr \"\"\n\n#: mailpile/mail_source/imap.py:497\n#, python-format\nmsgid \"Fetching chunk %d failed\"\nmsgstr \"\"\n\n#: mailpile/mail_source/imap.py:527\nmsgid \"Failed to list mailbox contents\"\nmsgstr \"\"\n\n#: mailpile/mail_source/imap.py:601\n#, python-format\nmsgid \"IMAP: %s\"\nmsgstr \"\"\n\n#: mailpile/mail_source/imap.py:603\n#, python-format\nmsgid \"IMAP: %s (not logged in)\"\nmsgstr \"\"\n\n#: mailpile/mail_source/imap.py:607\n#, python-format\nmsgid \"e-mail with ID %s\"\nmsgstr \"\"\n\n#: mailpile/mail_source/imap.py:609\nmsgid \"remote mailbox is inavailable\"\nmsgstr \"\"\n\n#: mailpile/mail_source/imap.py:732\nmsgid \"Connection timed out\"\nmsgstr \"\"\n\n#: mailpile/mail_source/imap.py:741\nmsgid \"An IMAP protocol error occurred\"\nmsgstr \"\"\n\n#: mailpile/mail_source/imap.py:745 mailpile/mail_source/pop3.py:55\nmsgid \"A network error occurred\"\nmsgstr \"\"\n\n#: mailpile/mail_source/imap.py:792\nmsgid \"Not connected to IMAP server.\"\nmsgstr \"\"\n\n#: mailpile/mail_source/imap.py:931\n#, python-format\nmsgid \"Connected to IMAP server %s\"\nmsgstr \"\"\n\n#: mailpile/mail_source/local.py:20 mailpile/plugins/contacts.py:860\nmsgid \"Local mail\"\nmsgstr \"\"\n\n#: mailpile/mail_source/local.py:60\n#, python-format\nmsgid \"Watching %d mbox mailboxes\"\nmsgstr \"\"\n\n#: mailpile/mail_source/pop3.py:109\n#, python-format\nmsgid \"Watching %d POP3 mailboxes\"\nmsgstr \"\"\n\n#: mailpile/mail_source/pop3.py:154 mailpile/plugins/setup_magic.py:67\nmsgid \"Inbox\"\nmsgstr \"\"\n\n#: mailpile/mailboxes/__init__.py:98 mailpile/mailboxes/mbox.py:68\nmsgid \"message not found in mailbox\"\nmsgstr \"\"\n\n#: mailpile/mailboxes/__init__.py:135 mailpile/mailboxes/mbox.py:182\n#, python-format\nmsgid \"Saving %s state to %s\"\nmsgstr \"\"\n\n#: mailpile/mailboxes/macmail.py:156\n#, python-format\nmsgid \"Mac Maildir %s\"\nmsgstr \"\"\n\n#: mailpile/mailboxes/macmail.py:159 mailpile/mailboxes/maildir.py:36\n#: mailpile/mailboxes/wervd.py:47\n#, python-format\nmsgid \"e-mail in file %s\"\nmsgstr \"\"\n\n#: mailpile/mailboxes/maildir.py:33\n#, python-format\nmsgid \"Maildir at %s\"\nmsgstr \"\"\n\n#: mailpile/mailboxes/mbox.py:61\n#, python-format\nmsgid \"Unix mbox at %s\"\nmsgstr \"\"\n\n#: mailpile/mailboxes/mbox.py:66\n#, python-format\nmsgid \"message at bytes %d..%d\"\nmsgstr \"\"\n\n#: mailpile/mailboxes/mbox.py:348 mailpile/mailutils/emails.py:934\nmsgid \"Message not found\"\nmsgstr \"\"\n\n#: mailpile/mailboxes/wervd.py:44\n#, python-format\nmsgid \"Mailpile mailbox at %s\"\nmsgstr \"\"\n\n#: mailpile/mailboxes/wervd.py:157\nmsgid \"Could not find a filename for the message.\"\nmsgstr \"\"\n\n#: mailpile/mailboxes/wervd.py:177\n#, python-format\nmsgid \"Invalid message type: %s\"\nmsgstr \"\"\n\n#: mailpile/mailboxes/wervd.py:180\nmsgid \"Mailbox messages are immutable\"\nmsgstr \"\"\n\n#: mailpile/mailutils/__init__.py:10\n#, python-format\nmsgid \"%s is too large to be a mailbox ID\"\nmsgstr \"\"\n\n#: mailpile/mailutils/emails.py:327\n#, python-format\nmsgid \"Unknown crypto policy: %s\"\nmsgstr \"\"\n\n#: mailpile/mailutils/emails.py:654 mailpile/mailutils/emails.py:732\n#: mailpile/mailutils/emails.py:1003 mailpile/mailutils/emails.py:1013\nmsgid \"Message or mailbox is read-only.\"\nmsgstr \"\"\n\n#: mailpile/mailutils/emails.py:1058\n#, python-format\nmsgid \"Extracted attachment %s\"\nmsgstr \"\"\n\n#: mailpile/mailutils/emails.py:1071\n#, python-format\nmsgid \"Wrote preview to: %s\"\nmsgstr \"\"\n\n#: mailpile/mailutils/emails.py:1073\nmsgid \"Failed to generate thumbnail\"\nmsgstr \"\"\n\n#: mailpile/mailutils/emails.py:1094\n#, python-format\nmsgid \"Wrote attachment to: %s\"\nmsgstr \"\"\n\n#: mailpile/mailutils/emails.py:1099\n#, python-format\nmsgid \"No attachments found for: %s\"\nmsgstr \"\"\n\n#: mailpile/mailutils/emails.py:1289\nmsgid \"[Binary data suppressed]\\n\"\nmsgstr \"\"\n\n#: mailpile/mailutils/html.py:105\nmsgid \"(Invalid HTML suppressed)\"\nmsgstr \"\"\n\n#: mailpile/mailutils/safe.py:137\n#, python-format\nmsgid \"=%s/%s using Received: instead of Date:\"\nmsgstr \"\"\n\n#: mailpile/mailutils/safe.py:145\n#, python-format\nmsgid \"=%s/%s has a bogus date\"\nmsgstr \"\"\n\n#: mailpile/plugins/__init__.py:757\n#, python-format\nmsgid \"Already registered: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/autotag.py:187\nmsgid \"Retraining SpamBayes autotaggers\"\nmsgstr \"\"\n\n#: mailpile/plugins/autotag.py:207\n#, python-format\nmsgid \"Have %d interesting %s messages\"\nmsgstr \"\"\n\n#: mailpile/plugins/autotag.py:275\n#, python-format\nmsgid \"Reading %s (%d/%d, %s=%s)\"\nmsgstr \"\"\n\n#: mailpile/plugins/autotag.py:292\n#, python-format\nmsgid \"Failed to process message at =%s\"\nmsgstr \"\"\n\n#: mailpile/plugins/autotag.py:299\n#, python-format\nmsgid \"Retrained SpamBayes auto-tagging for %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/autotag.py:324\nmsgid \"Periodically retrain autotagger (seconds)\"\nmsgstr \"\"\n\n#: mailpile/plugins/autotag.py:348\n#, python-format\nmsgid \"Unknown tag: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/autotag.py:364\n#, python-format\nmsgid \"Classified %d messages\"\nmsgstr \"\"\n\n#: mailpile/plugins/autotag.py:401\n#, python-format\nmsgid \"Auto-tagged %d messages\"\nmsgstr \"\"\n\n#: mailpile/plugins/backups.py:104\n#, python-format\nmsgid \"This is a backup of Mailpile v%(ver)s keys and configuration.\"\nmsgstr \"\"\n\n#: mailpile/plugins/backups.py:106\n#, python-format\nmsgid \"This backup was generated on: %(date)s.\"\nmsgstr \"\"\n\n#: mailpile/plugins/backups.py:107\nmsgid \"The contents of this file should be encrypted.\"\nmsgstr \"\"\n\n#: mailpile/plugins/backups.py:108\nmsgid \"The entire ZIP file must be uploaded during restoration.\"\nmsgstr \"\"\n\n#: mailpile/plugins/backups.py:353\nmsgid \"Backup restored\"\nmsgstr \"\"\n\n#: mailpile/plugins/backups.py:357\nmsgid \"Backup validated, restoration is possible\"\nmsgstr \"\"\n\n#: mailpile/plugins/backups.py:364\nmsgid \"Restore from backup\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:43\n#, python-format\nmsgid \"Sent: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:190\nmsgid \"Cannot update from multiple files\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:272\nmsgid \"Message is not editable\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:281 mailpile/plugins/compose.py:309\n#, python-format\nmsgid \"%d message(s) edited\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:283\n#, python-format\nmsgid \"%d message(s) created\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:304\nmsgid \"No messages!\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:311\n#, python-format\nmsgid \"%d message(s) unchanged\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:503\n#, python-format\nmsgid \"%s wrote:\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:516\n#, python-format\nmsgid \"Composing a reply from %(from)s to %(to)s, cc %(cc)s\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:518\n#, python-format\nmsgid \"Composing a reply from %(from)s to %(to)s\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:585\nmsgid \"You must configure a From address first.\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:594 mailpile/plugins/compose.py:700\nmsgid \"No message found\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:682\nmsgid \"Sorry, ephemeral messages cannot have attachments at this time.\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:740\nmsgid \"No files found\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:747 mailpile/plugins/compose.py:812\nmsgid \"No messages selected\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:762 mailpile/plugins/compose.py:828\n#, python-format\nmsgid \"Read-only message: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:764\n#, python-format\nmsgid \"Error attaching to %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:769\n#, python-format\nmsgid \"Attached %s to %d messages, failed %d\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:772\n#, python-format\nmsgid \"Attached %s to %d messages\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:830\n#, python-format\nmsgid \"Error removing from %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:834\n#, python-format\nmsgid \"Removed %s from %d messages, failed %d\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:837\n#, python-format\nmsgid \"Removed %s from %d messages\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:918\nmsgid \"Sending message\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:957\n#, python-format\nmsgid \"Could not send mail to %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:959\nmsgid \"Could not send mail\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:985\n#, python-format\nmsgid \"Sent %d messages\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:987\nmsgid \"Nothing was sent\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:1006 mailpile/plugins/compose.py:1092\nmsgid \"Nothing to do!\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:1010\nmsgid \"Failed to attach files\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:1014\nmsgid \"Cannot find message\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:1019\n#, python-format\nmsgid \"%d message(s) updated\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:1033\nmsgid \"Missing encryption keys\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:1038\nmsgid \"Could not encrypt message\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:1043\nmsgid \"Could not sign message\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:1087\n#, python-format\nmsgid \"Unthreaded %d messages\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:1090\n#, python-format\nmsgid \"Unthread %d messages\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:1107\nmsgid \"The index is not ready yet\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:1124\nmsgid \"Sending cancelled.\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:1132\nmsgid \"The outbox is empty\"\nmsgstr \"\"\n\n#: mailpile/plugins/compose.py:1136\nmsgid \"Delay between attempts to send mail\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:60\nmsgid \"(BASE64 ENCODED DATA)\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:157 mailpile/plugins/crypto_autocrypt.py:188\n#, python-format\nmsgid \"Found %d results\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:212\nmsgid \"Add contacts here!\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:283\n#, python-format\nmsgid \"Added %d contacts\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:312\n#, python-format\nmsgid \"No such contact: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:314\n#, python-format\nmsgid \"Removed contacts: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:317\nmsgid \"No contacts found\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:392\n#, python-format\nmsgid \"Added %d lines\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:402\n#, python-format\nmsgid \"Error adding lines to %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:447\n#, python-format\nmsgid \"Removed %d lines\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:457\n#, python-format\nmsgid \"Error removing lines from %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:525\n#, python-format\nmsgid \"Listed %d/%d results\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:623\n#, python-format\nmsgid \"Required parameter missing. Required parameters are: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:631\n#, python-format\nmsgid \"\"\n\"Unknown parameter passed to importer. Provided %s; but known parameters \"\n\"are: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:800\nmsgid \"Searched for addresses\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:873\n#, python-format\nmsgid \"Unhandled outgoing mail protocol: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:924\nmsgid \"Mail spool not found\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:927\n#, python-format\nmsgid \"Already configured: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:992\n#, python-format\nmsgid \"Unhandled incoming mail protocol: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:1003\n#, python-format\nmsgid \"The PGP key for %s is ready for use.\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:1006\nmsgid \"PGP key generation is complete\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:1016\nmsgid \"PGP key generation failed!\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:1302\nmsgid \"Account Updated!\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:1305\n#: shared-data/default-theme/html/profiles/edit/index.html:2\nmsgid \"Edit Account\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:1452\nmsgid \"Remove account\"\nmsgstr \"\"\n\n#: mailpile/plugins/contacts.py:1504\nmsgid \"Choosing from address\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:56\nmsgid \"Loaded metadata index\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:58\nmsgid \"Failed to load metadata index\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:102\nmsgid \"Rescanned vCards\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:106\nmsgid \"Rescanned mailboxes\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:128\n#, python-format\nmsgid \"Failed to reindex: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:135\n#, python-format\nmsgid \"Indexed %d messages\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:142\nmsgid \"Rescan already in progress\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:157\nmsgid \"Rescanned vCards and mailboxes\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:174 mailpile/plugins/core.py:258\n#: mailpile/plugins/core.py:324\n#, python-format\nmsgid \"Rescanning: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:183\n#, python-format\nmsgid \"Importing VCards from: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:202\n#, python-format\nmsgid \"Running: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:228\n#, python-format\nmsgid \"Rescan command returned %d\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:236\nmsgid \"Aborting rescan command\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:286\n#, python-format\nmsgid \"Rescanning: %s %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:305\n#, python-format\nmsgid \"Failed to rescan: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:347\nmsgid \"Nothing changed\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:365\nmsgid \"Optimized search engine\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:435\nmsgid \"Could not delete all messages\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:437\n#, python-format\nmsgid \"Deleted %d messages\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:478 shared-data/mailpile-gui/mailpile-gui.py:309\nmsgid \"Launching Mailpile\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:514\n#, python-format\nmsgid \"Moved the web server to %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:517\n#, python-format\nmsgid \"Started the web server on %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:520\nmsgid \"Failed to start the web server\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:550\nmsgid \"Performed shutdown tasks\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:565\n#, python-format\nmsgid \"Wrote PID to %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:585\nmsgid \"Rendered the page\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:612 mailpile/plugins/core.py:620\n#: mailpile/plugins/core.py:627 mailpile/plugins/core.py:633\n#: mailpile/plugins/core.py:642 mailpile/plugins/core.py:964\nmsgid \"Nothing Found\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:733\nmsgid \"Listed events, threads, and locks\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:791\nmsgid \"Displayed CRON schedule\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:828\nmsgid \"Memory corruption detected\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:841\nmsgid \"Your Mailpile is read-only!\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:875\nmsgid \"We are healthy!\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:900\nmsgid \"(no output)\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:932 mailpile/plugins/eventlog.py:210\n#: shared-data/contrib/hacks/hacks.py:96\nmsgid \"That was fun!\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:933\n#, python-format\nmsgid \"%s returned: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1011\n#, python-format\nmsgid \"Network error: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1017\n#, python-format\nmsgid \"Failed to list: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1038\n#, python-format\nmsgid \"Listed %d files or directories\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1062\n#, python-format\nmsgid \"Failed to change directories: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1087\n#, python-format\nmsgid \"That file already exists: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1105\n#, python-format\nmsgid \"Dumped to %s: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1108\n#, python-format\nmsgid \"Dumped: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1126\nmsgid \"Listed available translations\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1169\nmsgid \"WARNING: Any changes will be overwritten on login\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1190\n#, python-format\nmsgid \"Invalid section or variable: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1208 mailpile/plugins/core.py:1293\n#: mailpile/plugins/core.py:1349\nmsgid \"I refuse to change the master key!\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1250 mailpile/plugins/core.py:1310\nmsgid \"Updated your settings\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1363\nmsgid \"Reset to default values\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1454\nmsgid \"Invalid keys\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1461\nmsgid \"Displayed settings\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1542\n#, python-format\nmsgid \"Account not found: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1598\nmsgid \"Too many files\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1601\n#, python-format\nmsgid \"Failed to read: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1604\n#, python-format\nmsgid \"Not a mailbox: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1615\nmsgid \"Add and configure mailboxes\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1662\n#, python-format\nmsgid \"Configured %d mailboxes\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1667\nmsgid \"Nothing was added\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1693\n#, python-format\nmsgid \"Set output mode to: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1791 mailpile/plugins/core.py:1848\nmsgid \"Shutting down...\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1811\n#, python-format\nmsgid \"Will shutdown if idle for over %s seconds\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1870\n#, python-format\nmsgid \"The Web interface address is: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1873\nmsgid \"The Web interface is disabled, type `www` to turn it on.\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1879\nmsgid \"Type `help` for instructions or `quit` to quit.\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1881\nmsgid \"Long running operations can be aborted by pressing: <CTRL-C>\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1884\n#, python-format\nmsgid \"You can log in using the `%s` command.\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1887\nmsgid \"Check your web browser!\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1906\nmsgid \"Commands:\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1940\nmsgid \"Tags:  (use a tag as a command to display tagged messages)\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1978 mailpile/plugins/core.py:2006\nmsgid \"Displayed help\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:1983\nmsgid \"Unknown command\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:2039\nmsgid \"(subsection)\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:2056\nmsgid \"Displayed variables\"\nmsgstr \"\"\n\n#: mailpile/plugins/core.py:2081\nmsgid \"Displayed welcome message\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_autocrypt.py:176\n#: mailpile/plugins/crypto_gnupg.py:213 mailpile/plugins/search.py:733\n#: mailpile/plugins/vcard_gnupg.py:234\nmsgid \"No results\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_autocrypt.py:191\n#: mailpile/plugins/crypto_autocrypt.py:218\nmsgid \"Not found\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_autocrypt.py:216\n#, python-format\nmsgid \"Forgot %d recipients\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_autocrypt.py:265\n#, python-format\nmsgid \"Found %d peers\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:47\nmsgid \"\"\n\"This is a digital encryption key, which you can use to send\\n\"\n\"confidential messages to the owner, or to verify their\\n\"\n\"digital signatures. You can safely discard or ignore this\\n\"\n\"file if you do not use e-mail encryption or signatures.\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:52\nmsgid \"Generated by Mailpile and GnuPG\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:120\n#, python-format\nmsgid \"Encryption key for %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:122\nmsgid \"My encryption key\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:285\n#, python-format\nmsgid \"Imported %d keys\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:486\nmsgid \"Looks good!\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:488\nmsgid \"Proposed fixes:\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:496\nmsgid \"You need a new key!\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:497 mailpile/plugins/crypto_gnupg.py:506\n#: mailpile/plugins/crypto_gnupg.py:507 mailpile/plugins/crypto_gnupg.py:513\n#: mailpile/plugins/crypto_gnupg.py:523\n#, python-format\nmsgid \"Run: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:498\n#, python-format\nmsgid \"Answer the tool's questions: use RSA and RSA, %d bits or more\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:504\nmsgid \"Update the Mailpile config to use a good key:\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:505\nmsgid \"IMPORTANT: This MUST be done before disabling the key!\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:508\nmsgid \"This key's passphrase will be used to log in to Mailpile\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:512\nmsgid \"Revoke bad keys:\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:514\nmsgid \"Say yes to the first question, then follow the instructions\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:515\nmsgid \"A revocation certificate will be shown on screen\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:516\nmsgid \"Copy & paste that, save, and send to people who have the old key\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:517\n#, python-format\nmsgid \"You can search for %s to find such people\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:522\nmsgid \"Disable bad keys:\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:524 mailpile/plugins/crypto_gnupg.py:525\n#, python-format\nmsgid \"Type %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:556\n#, python-format\nmsgid \"%s: --- Disabled.\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:561\n#, python-format\nmsgid \"%s: --- Revoked.\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:565\n#, python-format\nmsgid \"%s: Bad: Expired on %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:569\n#, python-format\nmsgid \"%s: Bad: Key is useless\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:572\n#, python-format\nmsgid \"%s: Bad: Expires on %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:575\n#, python-format\nmsgid \"%s: Bad: Too small (%d bits)\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:582\n#, python-format\nmsgid \"%s: OK: %d bits, looks good!\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:589\nmsgid \"(optional)\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:601\n#, python-format\nmsgid \"%s: Mailpile config uses bad key\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:629\n#, python-format\nmsgid \"%(key)s: Bad key in profile %(fn)s <%(email)s> (%(profile)s)\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:635\n#, python-format\nmsgid \"No key for %(fn)s <%(email)s> (%(profile)s)\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_gnupg.py:648\n#, python-format\nmsgid \"Sanity checked: %d keys in GPG keyring, %d profiles\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_policy.py:47\n#, python-format\nmsgid \"Set crypto policy for %s to %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_policy.py:51\n#, python-format\nmsgid \"No vCard for email %s!\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_policy.py:59\nmsgid \"Please provide email address and policy!\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_policy.py:176\nmsgid \"Recipients have conflicting encryption policies.\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_policy.py:182\n#, python-format\nmsgid \"The encryption policy for these recipients is: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_policy.py:194 mailpile/plugins/crypto_policy.py:197\nmsgid \"This account does not have an encryption key.\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_policy.py:208\nmsgid \"Your policy is to always encrypt, but we do not have keys for everyone!\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_policy.py:211\nmsgid \"Some recipients require encryption, but we do not have keys for everyone!\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_policy.py:227\nmsgid \"We have keys for everyone!\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_policy.py:232\nmsgid \"Will not encrypt because historic data is insufficient.\"\nmsgstr \"\"\n\n#: mailpile/plugins/crypto_policy.py:235\nmsgid \"Cannot encrypt because we do not have keys for all recipients.\"\nmsgstr \"\"\n\n#: mailpile/plugins/eventlog.py:114\n#, python-format\nmsgid \"Found %d events\"\nmsgstr \"\"\n\n#: mailpile/plugins/eventlog.py:149\n#, python-format\nmsgid \"Canceled %d events\"\nmsgstr \"\"\n\n#: mailpile/plugins/eventlog.py:168\nmsgid \"Need an event ID!\"\nmsgstr \"\"\n\n#: mailpile/plugins/eventlog.py:180\n#, python-format\nmsgid \"Event %s is not undoable\"\nmsgstr \"\"\n\n#: mailpile/plugins/eventlog.py:182\n#, python-format\nmsgid \"Event %s not found\"\nmsgstr \"\"\n\n#: mailpile/plugins/eventlog.py:196\nmsgid \"Watching logs: Press CTRL-C to return to the CLI\"\nmsgstr \"\"\n\n#: mailpile/plugins/exporters.py:92\nmsgid \"Parent directory does not exist.\"\nmsgstr \"\"\n\n#: mailpile/plugins/exporters.py:94\nmsgid \"Is the disk full? Are permissions lacking?\"\nmsgstr \"\"\n\n#: mailpile/plugins/exporters.py:95\n#, python-format\nmsgid \"Failed to create mailbox: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/exporters.py:103\n#, python-format\nmsgid \"Exporting message =%s ...\"\nmsgstr \"\"\n\n#: mailpile/plugins/exporters.py:107\n#, python-format\nmsgid \"Message =%s is unreadable! Skipping.\"\nmsgstr \"\"\n\n#: mailpile/plugins/exporters.py:140\n#, python-format\nmsgid \"Exported %d messages to %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:55\nmsgid \"Connected\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:57\n#: shared-data/default-theme/html/settings/index.html:140\nmsgid \"Shutdown Mailpile\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:71 mailpile/plugins/setup_magic.py:1024\n#: mailpile/plugins/setup_magic.py:1086 mailpile/plugins/setup_magic.py:1132\n#: shared-data/default-theme/html/setup/welcome/index.html:2\n#: shared-data/default-theme/html/setup/welcome/index.html:5\nmsgid \"Welcome to Mailpile!\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:72\nmsgid \"Mailpile is now running on this computer.\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:81\nmsgid \"Brand new installation!\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:83\nmsgid \"This appears to be a new installation of Mailpile!\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:85\nmsgid \"You need to choose a language, password and privacy policy.\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:87 mailpile/plugins/gui.py:101\nmsgid \"To proceed, open Mailpile in your web browser.\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:88\nmsgid \"Get Started!\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:97\nmsgid \"Not logged in\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:98\nmsgid \"Your data is stored encrypted and is inaccessible until you log in.\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:102\nmsgid \"Log In\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:110\nmsgid \"Logging you in...\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:120\nmsgid \"You are logged in\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:121\nmsgid \"Mailpile can now process and display your e-mail.\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:125\nmsgid \"Open E-mail\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:163 mailpile/plugins/gui.py:166\nmsgid \"Mailpile\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:163\nmsgid \"New Installation\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:183\nmsgid \"Loaded metadata for {num} messages so far, please wait.\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:226\nmsgid \"No new mail, {num} messages total.\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:232\nmsgid \"{tagName}: {new} new messages ({num} unread)\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:237\nmsgid \"{tagName}: {num} unread messages\"\nmsgstr \"\"\n\n#: mailpile/plugins/gui.py:240\nmsgid \"You have {num} unread messages in {tags} tags\"\nmsgstr \"\"\n\n#: mailpile/plugins/html_magic.py:61\nmsgid \"Serving up API content\"\nmsgstr \"\"\n\n#: mailpile/plugins/html_magic.py:112\nmsgid \"Generated Javascript API\"\nmsgstr \"\"\n\n#: mailpile/plugins/html_magic.py:124\nmsgid \"Rendered Progressive Web App Data\"\nmsgstr \"\"\n\n#: mailpile/plugins/migrate.py:33\n#, python-format\nmsgid \"%(user)s on %(host)s\"\nmsgstr \"\"\n\n#: mailpile/plugins/migrate.py:42\n#, python-format\nmsgid \"Could not migrate route: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/migrate.py:217\n#, python-format\nmsgid \"Unknown migration: %s (available: %s)\"\nmsgstr \"\"\n\n#: mailpile/plugins/migrate.py:237\n#, python-format\nmsgid \"Performed %d migrations, failed %d.\"\nmsgstr \"\"\n\n#: mailpile/plugins/motd.py:44\nmsgid \"URL to the Message Of The Day\"\nmsgstr \"\"\n\n#: mailpile/plugins/motd.py:78\n#, python-format\nmsgid \"Unsupported URL for message of the day: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/motd.py:92 mailpile/plugins/motd.py:109\n#: mailpile/plugins/motd.py:136 mailpile/plugins/motd.py:167\n#: shared-data/default-theme/html/settings/privacy.html:195\nmsgid \"Message Of The Day\"\nmsgstr \"\"\n\n#: mailpile/plugins/motd.py:109\nmsgid \"Loaded\"\nmsgstr \"\"\n\n#: mailpile/plugins/motd.py:136\nmsgid \"Updated\"\nmsgstr \"\"\n\n#: mailpile/plugins/motd.py:152\nmsgid \"Mailpile update info unavailable\"\nmsgstr \"\"\n\n#: mailpile/plugins/motd.py:154\nmsgid \"Your Mailpile is up to date\"\nmsgstr \"\"\n\n#: mailpile/plugins/motd.py:156\n#, python-format\nmsgid \"An upgrade for Mailpile is available, version %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/oauth.py:252\nmsgid \"OAuth2 Authorization\"\nmsgstr \"\"\n\n#: mailpile/plugins/plugins.py:33\nmsgid \"Listed available plugins\"\nmsgstr \"\"\n\n#: mailpile/plugins/plugins.py:53\n#, python-format\nmsgid \"Already loaded: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/plugins.py:70\n#, python-format\nmsgid \"Failed to load plugin: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/plugins.py:74\n#, python-format\nmsgid \"Loaded plugins: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/plugins.py:94\n#, python-format\nmsgid \"Required plugins can not be disabled: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/plugins.py:97\n#, python-format\nmsgid \"Plugin not loaded: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/plugins.py:106\n#, python-format\nmsgid \"Disabled plugins: %s (restart required)\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:336\nmsgid \"Failed to read message {mail_key}, {mail_desc}.\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:337\nmsgid \"unknown error\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:339\nmsgid \"Failed to open message {mail_key}, {mail_desc}.\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:341\nmsgid \"Failed to open mailbox {mbox_key}\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:362\nmsgid \"Failed to load and parse message data.\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:365 mailpile/plugins/search.py:372\nmsgid \"Failed process message crypto (decrypt, etc).\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:368\nmsgid \"Failed to parse message.\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:375\nmsgid \"Failed to evalute sender trust\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:381\n#, python-format\nmsgid \"Failed to parse %s headers.\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:395\nmsgid \"Message may be corrupt!\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:436\n#, python-format\nmsgid \"Parsing metadata for %d results (full_threads=%s)\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:441\n#, python-format\nmsgid \"Search: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:731\nmsgid \"Unprintable results\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:819\n#, python-format\nmsgid \"Weird starting point: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:1021\n#, python-format\nmsgid \"Prepared %d search results (context=%s)\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:1023\n#, python-format\nmsgid \"Found %d results in %.3fs\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:1041\nmsgid \"You must perform a search before requesting the next page.\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:1044\nmsgid \"Displayed next page of results.\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:1060\nmsgid \"You must perform a search before requesting the previous page.\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:1063\nmsgid \"Displayed previous page of results.\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:1079\n#, python-format\nmsgid \"Changed sort order to %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:1136\n#, python-format\nmsgid \"Raw message: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:1164\nmsgid \"Displayed a single message\"\nmsgstr \"\"\n\n#: mailpile/plugins/search.py:1168\n#, python-format\nmsgid \"Displayed %d messages\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:58\n#: shared-data/default-theme/html/partials/tools_search.html:161\n#: shared-data/default-theme/html/profiles/index.html:58\nmsgid \"New\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:76\nmsgid \"Blank\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:88\nmsgid \"Drafts\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:99\nmsgid \"Outbox\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:109\nmsgid \"Sent\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:119\nmsgid \"Spam\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:127\nmsgid \"Ham\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:140\nmsgid \"Trash\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:159\nmsgid \"Photos\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:169\nmsgid \"Documents\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:183\nmsgid \"All Mail\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:205\nmsgid \"Disabling lockdown\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:221\n#, python-format\nmsgid \"Failed to create tag: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:264\nmsgid \"Created default tags\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:285\nmsgid \"Enabling SpamBayes autotagger\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:287\nmsgid \"Please install SpamBayes for super awesome spam filtering\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:293\nmsgid \"Enabling Gravatar image importer\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:296\nmsgid \"Enabling Libravatar image importer\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:302\nmsgid \"Importing contacts from GPG keyring\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:324\nmsgid \"Reenabling lockdown\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:354\nmsgid \"\"\n\"Unable to obfuscate search index without losing data. Not indexing \"\n\"encrypted mail.\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:360\nmsgid \"Obfuscating search index and enabling indexing of encrypted e-mail. Yay!\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:602\n#, python-format\nmsgid \"Checking ISPDB for %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:606\n#, python-format\nmsgid \"Found %s in ISPDB\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:649\n#, python-format\nmsgid \"Checking for autoconfig on %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:654\n#, python-format\nmsgid \"Found autoconfig on %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:729\n#, python-format\nmsgid \"Guessing settings for %(email)s\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:732\n#, python-format\nmsgid \"Found %d potential servers\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:737\nmsgid \"Probing for services...\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:756\n#, python-format\nmsgid \"Found %(service)s server on %(host)s:%(port)s\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:806\nmsgid \"Ran out of time, results may be incomplete\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:864\n#, python-format\nmsgid \"Probing %s, cleartext=%s\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:900\n#, python-format\nmsgid \"Testing %(protocol)4.4s on %(host)s:%(port)s with STARTTLS as %(username)s\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:904\n#, python-format\nmsgid \"Testing %(protocol)4.4s on %(host)s:%(port)s as %(username)s\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:908\nmsgid \"insecure\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:917\n#: shared-data/default-theme/html/setup/oauth2/index.html:77\nmsgid \"Success\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:923\nmsgid \"Protocol is OK\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:926\nmsgid \"Login failed\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:951\nmsgid \"Can only test settings for one account at a time\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:971\n#, python-format\nmsgid \"Found settings for %d addresses\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:974\nmsgid \"No settings found\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:1004\n#, python-format\nmsgid \"Invalid language: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:1176\nmsgid \"Unknown error\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:1185\nmsgid \"Route is working\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:1188\nmsgid \"Invalid command\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:1198\nmsgid \"Route is not working\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:1246\n#, python-format\nmsgid \"Failed to connect to Tor on %s:%s. Is it installed?\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:1268\nmsgid \"Successfully configured and enabled Tor!\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:1272\n#, python-format\nmsgid \"Failed to configure Tor on %s:%s. Is the network down?\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:1280\nmsgid \"Proxy settings have already been configured.\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:1346\n#, python-format\nmsgid \"Language set to: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:1351\nmsgid \"Choose a password for Mailpile: \"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:1353\nmsgid \"Confirm password: \"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:1360\nmsgid \"Passwords did not match! Please try again.\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:1368\nmsgid \"Performed initial Mailpile setup\"\nmsgstr \"\"\n\n#: mailpile/plugins/setup_magic.py:1372\nmsgid \"Entering setup flow\"\nmsgstr \"\"\n\n#: mailpile/plugins/smtp_server.py:25\nmsgid \"SMTP Daemon\"\nmsgstr \"\"\n\n#: mailpile/plugins/smtp_server.py:26\nmsgid \"Listening host for SMTP daemon\"\nmsgstr \"\"\n\n#: mailpile/plugins/smtp_server.py:27\nmsgid \"Listening port for SMTP daemon\"\nmsgstr \"\"\n\n#: mailpile/plugins/tags.py:237\nmsgid \"name\"\nmsgstr \"\"\n\n#: mailpile/plugins/tags.py:427 shared-data/contrib/datadig/datadig.py:37\n#: shared-data/contrib/hints/hints.py:185\nmsgid \"Nothing Happened\"\nmsgstr \"\"\n\n#: mailpile/plugins/tags.py:472\nmsgid \"Undid tagging operation\"\nmsgstr \"\"\n\n#: mailpile/plugins/tags.py:489\n#, python-format\nmsgid \"Scheduled %d messages for future tagging\"\nmsgstr \"\"\n\n#: mailpile/plugins/tags.py:552\nmsgid \"Add tags here!\"\nmsgstr \"\"\n\n#: mailpile/plugins/tags.py:599\n#, python-format\nmsgid \"Added %d tags\"\nmsgstr \"\"\n\n#: mailpile/plugins/tags.py:717\n#, python-format\nmsgid \"Listed %d tags\"\nmsgstr \"\"\n\n#: mailpile/plugins/tags.py:779\n#, python-format\nmsgid \"Deleted %d tags\"\nmsgstr \"\"\n\n#: mailpile/plugins/tags.py:849\n#, python-format\nmsgid \"Performed automation for %d tags\"\nmsgstr \"\"\n\n#: mailpile/plugins/tags.py:860\nmsgid \"Periodically perform tag automation (seconds)\"\nmsgstr \"\"\n\n#: mailpile/plugins/tags.py:961\nmsgid \"Need tag name\"\nmsgstr \"\"\n\n#: mailpile/plugins/tags.py:966\nmsgid \"Need flags and search terms or a hook\"\nmsgstr \"\"\n\n#: mailpile/plugins/tags.py:979\n#, python-format\nmsgid \"No such tag: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/tags.py:1013\nmsgid \"Added new filter\"\nmsgstr \"\"\n\n#: mailpile/plugins/tags.py:1042\n#, python-format\nmsgid \"Removed %d filter(s)\"\nmsgstr \"\"\n\n#: mailpile/plugins/vcard_gnupg.py:23\nmsgid \"Import contacts from GnuPG keyring\"\nmsgstr \"\"\n\n#: mailpile/plugins/vcard_gnupg.py:26 mailpile/plugins/vcard_gravatar.py:33\n#: mailpile/plugins/vcard_libravatar.py:30\nmsgid \"Enable this importer\"\nmsgstr \"\"\n\n#: mailpile/plugins/vcard_gnupg.py:27\nmsgid \"Location of keyring\"\nmsgstr \"\"\n\n#: mailpile/plugins/vcard_gnupg.py:251\n#, python-format\nmsgid \"Extracted %d vCards from GPG keychain\"\nmsgstr \"\"\n\n#: mailpile/plugins/vcard_gnupg.py:285\n#, python-format\nmsgid \"Imported %d vCards from GPG keychain\"\nmsgstr \"\"\n\n#: mailpile/plugins/vcard_gravatar.py:30\nmsgid \"Import contact info from a Gravatar server\"\nmsgstr \"\"\n\n#: mailpile/plugins/vcard_gravatar.py:34\n#: mailpile/plugins/vcard_libravatar.py:31\nmsgid \"Require anonymity for use\"\nmsgstr \"\"\n\n#: mailpile/plugins/vcard_gravatar.py:35\n#: mailpile/plugins/vcard_libravatar.py:32\nmsgid \"Minimum days between refreshing\"\nmsgstr \"\"\n\n#: mailpile/plugins/vcard_gravatar.py:36\n#: mailpile/plugins/vcard_libravatar.py:33\nmsgid \"Max batch size per update\"\nmsgstr \"\"\n\n#: mailpile/plugins/vcard_gravatar.py:37\n#: mailpile/plugins/vcard_libravatar.py:34\nmsgid \"Default thumbnail style\"\nmsgstr \"\"\n\n#: mailpile/plugins/vcard_gravatar.py:38\n#: mailpile/plugins/vcard_libravatar.py:35\nmsgid \"Preferred thumbnail rating\"\nmsgstr \"\"\n\n#: mailpile/plugins/vcard_gravatar.py:40\n#: mailpile/plugins/vcard_libravatar.py:37\nmsgid \"Preferred thumbnail size\"\nmsgstr \"\"\n\n#: mailpile/plugins/vcard_gravatar.py:41\nmsgid \"Gravatar server URL\"\nmsgstr \"\"\n\n#: mailpile/plugins/vcard_libravatar.py:27\nmsgid \"Import contact info from a Libravatar server\"\nmsgstr \"\"\n\n#: mailpile/plugins/vcard_libravatar.py:38\nmsgid \"Libravatar server URL\"\nmsgstr \"\"\n\n#: mailpile/plugins/vcard_mork.py:44\nmsgid \"Location of Mork database\"\nmsgstr \"\"\n\n#: mailpile/plugins/webterminal.py:30\nmsgid \"Created a session\"\nmsgstr \"\"\n\n#: mailpile/plugins/webterminal.py:51\nmsgid \"No SID supplied\"\nmsgstr \"\"\n\n#: mailpile/plugins/webterminal.py:55\nmsgid \"Ended a session\"\nmsgstr \"\"\n\n#: mailpile/plugins/webterminal.py:81\nmsgid \"No session ID supplied\"\nmsgstr \"\"\n\n#: mailpile/plugins/webterminal.py:83\nmsgid \"Unknown session ID\"\nmsgstr \"\"\n\n#: mailpile/plugins/webterminal.py:91\nmsgid \"Command disallowed\"\nmsgstr \"\"\n\n#: mailpile/plugins/webterminal.py:97\nmsgid \"Ran a command\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/__init__.py:36\nmsgid \"Encryption key is revoked\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/__init__.py:38\nmsgid \"Encryption key is disabled\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/__init__.py:40\nmsgid \"Encryption key has expired\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/__init__.py:42\nmsgid \"Encryption key has been imported and verified\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/__init__.py:60\nmsgid \"Encryption key has been imported\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/__init__.py:71\n#, python-format\nmsgid \"Signature seen on %d messages\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/__init__.py:79\nmsgid \"Encryption key is very strong\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/__init__.py:81\nmsgid \"Encryption key is strong\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/__init__.py:83\nmsgid \"Encryption key is good\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/__init__.py:85\nmsgid \"Encryption key is weak\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/__init__.py:108\nmsgid \"Anonymous\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/__init__.py:162\n#, python-format\nmsgid \"Searching for encryption keys in: %s\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/__init__.py:511\nmsgid \"Found encryption key in keychain\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/__init__.py:562\nmsgid \"Found encryption key in keyserver\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/__init__.py:677\nmsgid \"Found encryption key in keys.openpgp.org\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/email_keylookup.py:39\nmsgid \"E-mail keys\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/email_keylookup.py:55\nmsgid \"Found key in local e-mail\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/wkd.py:49\nmsgid \"Web Key Directory\"\nmsgstr \"\"\n\n#: mailpile/plugins/keylookup/wkd.py:64\nmsgid \"Found key in Web Key Directory\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:63\nmsgid \"Basic header tokenising\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:64\nmsgid \"\"\n\"If true, tokenizer.Tokenizer.tokenize_headers() will tokenize the\\n\"\n\"     contents of each header field just like the text of the message\\n\"\n\"     body, using the name of the header as a tag.  Tokens look like\\n\"\n\"     \\\"header:word\\\".  The basic approach is simple and effective, but \"\n\"also\\n\"\n\"     very sensitive to biases in the ham and spam collections.  For\\n\"\n\"     example, if the ham and spam were collected at different times,\\n\"\n\"     several headers with date/time information will become the best\\n\"\n\"     discriminators.  (Not just Date, but Received and X-From_.)\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:74\nmsgid \"Only basic header tokenising\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:75\nmsgid \"\"\n\"If true and basic_header_tokenize is also true, then\\n\"\n\"     basic_header_tokenize is the only action performed.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:79\nmsgid \"Basic headers to skip\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:80\nmsgid \"\"\n\"If basic_header_tokenize is true, then basic_header_skip is a set\\n\"\n\"     of headers that should be skipped.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:84\nmsgid \"Check application/octet-stream sections\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:85\nmsgid \"\"\n\"If true, the first few characters of application/octet-stream\\n\"\n\"     sections are used, undecoded.  What 'few' means is decided by\\n\"\n\"     octet_prefix_size.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:90\nmsgid \"Number of characters of octet stream to process\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:91\nmsgid \"\"\n\"The number of characters of the application/octet-stream sections\\n\"\n\"     to use, if check_octets is set to true.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:95\nmsgid \"Count runs of short 'words'\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:96\nmsgid \"\"\n\"(EXPERIMENTAL) If true, generate tokens based on max number of\\n\"\n\"     short word runs. Short words are anything of length < the\\n\"\n\"     skip_max_word_size option.  Normally they are skipped, but one \"\n\"common\\n\"\n\"     spam technique spells words like 'V I A G RA'.\\n\"\n\"     \"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:103\nmsgid \"Generate IP address tokens from hostnames\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:104\nmsgid \"\"\n\"(EXPERIMENTAL) Generate IP address tokens from hostnames.\\n\"\n\"     Requires PyDNS (http://pydns.sourceforge.net/).\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:108\nmsgid \"x-lookup_ip cache file location\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:109\nmsgid \"\"\n\"Tell SpamBayes where to cache IP address lookup information.\\n\"\n\"     Only comes into play if lookup_ip is enabled. The default\\n\"\n\"     (empty string) disables the file cache.  When caching is enabled,\\n\"\n\"     the cache file is stored using the same database type as the main\\n\"\n\"     token store (only dbm and zodb supported so far, zodb has problems,\\n\"\n\"     dbm is untested, hence the default).\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:117\nmsgid \"Generate image size tokens\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:118\nmsgid \"\"\n\"If true, generate tokens based on the sizes of\\n\"\n\"     embedded images.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:122\nmsgid \"Look inside images for text\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:123\nmsgid \"\"\n\"If true, generate tokens based on the\\n\"\n\"     (hopefully) text content contained in any images in each message.\\n\"\n\"     The current support is minimal, relies on the installation of\\n\"\n\"     an OCR 'engine' (see ocr_engine.)\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:129\nmsgid \"OCR engine to use\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:130\nmsgid \"\"\n\"The name of the OCR engine to use.  If empty, all\\n\"\n\"     supported engines will be checked to see if they are installed.\\n\"\n\"     Engines currently supported include ocrad\\n\"\n\"     (http://www.gnu.org/software/ocrad/ocrad.html) and gocr\\n\"\n\"     (http://jocr.sourceforge.net/download.html) and they require the\\n\"\n\"     appropriate executable be installed in either your PATH, or in the\\n\"\n\"     main spambayes directory.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:139\nmsgid \"Cache to speed up ocr.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:140\nmsgid \"\"\n\"If non-empty, names a file from which to read cached ocr info\\n\"\n\"     at start and to which to save that info at exit.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:144\nmsgid \"Scale factor to use with ocrad.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:145\nmsgid \"\"\n\"Specifies the scale factor to apply when running ocrad.  While\\n\"\n\"     you can specify a negative scale it probably won't help.  Scaling up\"\n\"\\n\"\n\"     by a factor of 2 or 3 seems to work well for the sort of spam images\"\n\"\\n\"\n\"     encountered by SpamBayes.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:151\nmsgid \"Charset to apply with ocrad.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:152\nmsgid \"\"\n\"Specifies the charset to use when running ocrad.  Valid values\\n\"\n\"     are 'ascii', 'iso-8859-9' and 'iso-8859-15'.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:156\nmsgid \"Max image size to try OCR-ing\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:157\nmsgid \"\"\n\"When crack_images is enabled, this specifies the largest\\n\"\n\"     image to try OCR on.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:161\nmsgid \"Count all header lines\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:162\nmsgid \"\"\n\"Generate tokens just counting the number of instances of each kind\\n\"\n\"     of header line, in a case-sensitive way.\\n\"\n\"\\n\"\n\"     Depending on data collection, some headers are not safe to count.\\n\"\n\"     For example, if ham is collected from a mailing list but spam from\\n\"\n\"     your regular inbox traffic, the presence of a header like List-Info\\n\"\n\"     will be a very strong ham clue, but a bogus one.  In that case, set\\n\"\n\"     count_all_header_lines to False, and adjust safe_headers instead.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:172\nmsgid \"Record header absence\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:173\nmsgid \"\"\n\"When True, generate a \\\"noheader:HEADERNAME\\\" token for each header\\n\"\n\"     in safe_headers (below) that *doesn't* appear in the headers.  This\\n\"\n\"     helped in various of Tim's python.org tests, but appeared to hurt a\\n\"\n\"     little in Anthony Baxter's tests.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:179\nmsgid \"Safe headers\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:186\nmsgid \"\"\n\"Like count_all_header_lines, but restricted to headers in this list.\\n\"\n\"     safe_headers is ignored when count_all_header_lines is true, unless\\n\"\n\"     record_header_absence is also true.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:191\nmsgid \"Mine the received headers\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:192\nmsgid \"\"\n\"A lot of clues can be gotten from IP addresses and names in\\n\"\n\"     Received: headers.  This can give spectacular results for bogus\\n\"\n\"     reasons if your corpora are from different sources.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:197\nmsgid \"Mine NNTP-Posting-Host headers\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:198\nmsgid \"\"\n\"Usenet is host to a lot of spam.  Usenet/Mailing list gateways\\n\"\n\"     can let it leak across.  Similar to mining received headers, we pick\"\n\"\\n\"\n\"     apart the IP address or host name in this header for clues.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:203\nmsgid \"Address headers to mine\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:205\nmsgid \"\"\n\"Mine the following address headers. If you have mixed source\\n\"\n\"     corpuses (as opposed to a mixed sauce walrus, which is delicious!)\\n\"\n\"     then you probably don't want to use 'to' or 'cc') Address headers \"\n\"will\\n\"\n\"     be decoded, and will generate charset tokens as well as the real\\n\"\n\"     address.  Others to consider: errors-to, ...\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:212\nmsgid \"Generate long skips\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:213\nmsgid \"\"\n\"If legitimate mail contains things that look like text to the\\n\"\n\"     tokenizer and turning turning off this option helps (perhaps binary\\n\"\n\"     attachments get 'defanged' by something upstream from this operation\"\n\"\\n\"\n\"     and thus look like text), this may help, and should be an alert that\"\n\"\\n\"\n\"     perhaps the tokenizer is broken.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:220\nmsgid \"Summarise email prefixes\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:221 mailpile/spambayes/Options.py:225\nmsgid \"Try to capitalize on mail sent to multiple similar addresses.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:224\nmsgid \"Summarise email suffixes\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:228\nmsgid \"Long skip trigger length\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:229\nmsgid \"\"\n\"Length of words that triggers 'long skips'. Longer than this\\n\"\n\"     triggers a skip.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:233\nmsgid \"Extract clues about url structure\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:234\nmsgid \"\"\n\"(EXPERIMENTAL) Note whether url contains non-standard port or\\n\"\n\"     user/password elements.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:238\nmsgid \"Extract URLs without http:// prefix\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:239\nmsgid \"\"\n\"(EXPERIMENTAL) Recognize 'www.python.org' or ftp.python.org as URLs\\n\"\n\"     instead of just long words.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:243\nmsgid \"Replace non-ascii characters\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:244\nmsgid \"\"\n\"If true, replace high-bit characters (ord(c) >= 128) and control\\n\"\n\"     characters with question marks.  This allows non-ASCII character\\n\"\n\"     strings to be identified with little training and small database\\n\"\n\"     burden.  It's appropriate only if your ham is plain 7-bit ASCII, or\\n\"\n\"     nearly so, so that the mere presence of non-ASCII character strings \"\n\"is\\n\"\n\"     known in advance to be a strong spam indicator.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:252\nmsgid \"Search for Habeas Headers\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:253\nmsgid \"\"\n\"(EXPERIMENTAL) If true, search for the habeas headers (see\\n\"\n\"     http://www.habeas.com). If they are present and correct, this should\"\n\"\\n\"\n\"     be a strong ham sign, if they are present and incorrect, this should\"\n\"\\n\"\n\"     be a strong spam sign.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:259\nmsgid \"Reduce Habeas Header Tokens to Single\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:260\nmsgid \"\"\n\"(EXPERIMENTAL) If SpamBayes is set to search for the Habeas\\n\"\n\"     headers, nine tokens are generated for messages with habeas headers.\"\n\"\\n\"\n\"     This should be fine, since messages with the headers should either \"\n\"be\\n\"\n\"     ham, or result in FN so that we can send them to habeas so they can\\n\"\n\"     be sued.  However, to reduce the strength of habeas headers, we \"\n\"offer\\n\"\n\"     the ability to reduce the nine tokens to one. (This option has no\\n\"\n\"     effect if 'Search for Habeas Headers' is False)\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:293\nmsgid \"Ham cutoff\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:294\nmsgid \"\"\n\"Spambayes gives each email message a spam probability between\\n\"\n\"     0 and 1. Emails below the Ham Cutoff probability are classified\\n\"\n\"     as Ham. Larger values will result in more messages being\\n\"\n\"     classified as ham, but with less certainty that all of them\\n\"\n\"     actually are ham. This value should be between 0 and 1,\\n\"\n\"     and should be smaller than the Spam Cutoff.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:302\nmsgid \"Spam cutoff\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:303\nmsgid \"\"\n\"Emails with a spam probability above the Spam Cutoff are\\n\"\n\"     classified as Spam - just like the Ham Cutoff but at the other\\n\"\n\"     end of the scale.  Messages that fall between the two values\\n\"\n\"     are classified as Unsure.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:313\nmsgid \"Number of buckets\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:314\nmsgid \"Number of buckets in histograms.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:317\nmsgid \"Show histograms\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:321\nmsgid \"Compute best cutoffs from histograms\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:322\nmsgid \"\"\n\"After the display of a ham+spam histogram pair, you can get a\\n\"\n\"     listing of all the cutoff values (coinciding with histogram bucket\\n\"\n\"     boundaries) that minimize:\\n\"\n\"         best_cutoff_fp_weight * (# false positives) +\\n\"\n\"         best_cutoff_fn_weight * (# false negatives) +\\n\"\n\"         best_cutoff_unsure_weight * (# unsure msgs)\\n\"\n\"\\n\"\n\"     This displays two cutoffs:  hamc and spamc, where\\n\"\n\"        0.0 <= hamc <= spamc <= 1.0\\n\"\n\"\\n\"\n\"     The idea is that if something scores < hamc, it's called ham; if\\n\"\n\"     something scores >= spamc, it's called spam; and everything else is\\n\"\n\"     called 'I am not sure' -- the middle ground.\\n\"\n\"\\n\"\n\"     Note:  You may wish to increase nbuckets, to give this scheme more \"\n\"cutoff\\n\"\n\"     values to analyze.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:340\nmsgid \"Best cutoff false positive weight\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:344\nmsgid \"Best cutoff false negative weight\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:348\nmsgid \"Best cutoff unsure weight\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:352\nmsgid \"Percentiles\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:353\n#, python-format\nmsgid \"\"\n\"Histogram analysis also displays percentiles.  For each percentile\\n\"\n\"     p in the list, the score S such that p% of all scores are <= S is\\n\"\n\"     given. Note that percentile 50 is the median, and is displayed \"\n\"(along\\n\"\n\"     with the min score and max score) independent of this option.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:360 mailpile/spambayes/Options.py:365\n#: mailpile/spambayes/Options.py:370 mailpile/spambayes/Options.py:375\nmsgid \"\"\n\"Display spam when show_spam_lo <= spamprob <= show_spam_hi and\\n\"\n\"     likewise for ham.  The defaults here do not show anything.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:379\nmsgid \"Show false positives\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:383\nmsgid \"Show false negatives\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:387\nmsgid \"Show unsure\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:391\nmsgid \"Show character limit\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:392\nmsgid \"\"\n\"The maximum # of characters to display for a msg displayed due to\\n\"\n\"     the show_xyz options above.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:396\nmsgid \"Save trained pickles\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:397\nmsgid \"\"\n\"If save_trained_pickles is true, Driver.train() saves a binary\\n\"\n\"     pickle of the classifier after training.  The file basename is given\"\n\"\\n\"\n\"     by pickle_basename, the extension is .pik, and increasing integers \"\n\"are\\n\"\n\"     appended to pickle_basename.  By default (if save_trained_pickles is\"\n\"\\n\"\n\"     true), the filenames are class1.pik, class2.pik, ...  If a file of\\n\"\n\"     that name already exists, it is overwritten.  pickle_basename is\\n\"\n\"     ignored when save_trained_pickles is false.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:406\nmsgid \"Pickle basename\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:410\nmsgid \"Save histogram pickles\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:411\nmsgid \"\"\n\"If save_histogram_pickles is true, Driver.train() saves a binary\\n\"\n\"     pickle of the spam and ham histogram for \\\"all test runs\\\". The file\"\n\"\\n\"\n\"     basename is given by pickle_basename, the suffix _spamhist.pik\\n\"\n\"     or _hamhist.pik is appended  to the basename.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:417\nmsgid \"Spam directories\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:418 mailpile/spambayes/Options.py:423\nmsgid \"\"\n\"default locations for timcv and timtest - these get the set number\\n\"\n\"     interpolated.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:422\nmsgid \"Ham directories\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:429\nmsgid \"Build each classifier from scratch\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:430\nmsgid \"\"\n\"A cross-validation driver takes N ham+spam sets, and builds N\\n\"\n\"     classifiers, training each on N-1 sets, and the predicting against \"\n\"the\\n\"\n\"     set not trained on.  By default, it does this in a clever way,\\n\"\n\"     learning *and* unlearning sets as it goes along, so that it never\\n\"\n\"     needs to train on N-1 sets in one gulp after the first time.  \"\n\"Setting\\n\"\n\"     this option true forces ''one gulp from-scratch'' training every \"\n\"time.\\n\"\n\"     There used to be a set of combining schemes that needed this, but \"\n\"now\\n\"\n\"     it is just in case you are paranoid <wink>.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:442\nmsgid \"Maximum number of extreme words\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:443\nmsgid \"\"\n\"The maximum number of extreme words to look at in a message, where\\n\"\n\"     \\\"extreme\\\" means with spam probability farthest away from 0.5.  150\"\n\"\\n\"\n\"     appears to work well across all corpora tested.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:448\nmsgid \"Unknown word probability\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:449\nmsgid \"\"\n\"These two control the prior assumption about word probabilities.\\n\"\n\"     unknown_word_prob is essentially the probability given to a word \"\n\"that\\n\"\n\"     has never been seen before.  Nobody has reported an improvement via\\n\"\n\"     moving it away from 1/2, although Tim has measured a mean spamprob \"\n\"of\\n\"\n\"     a bit over 0.5 (0.51-0.55) in 3 well-trained classifiers.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:456\nmsgid \"Unknown word strength\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:457\nmsgid \"\"\n\"This adjusts how much weight to give the prior\\n\"\n\"     assumption relative to the probabilities estimated by counting.  At \"\n\"0,\\n\"\n\"     the counting estimates are believed 100%, even to the extent of\\n\"\n\"     assigning certainty (0 or 1) to a word that has appeared in only ham\"\n\"\\n\"\n\"     or only spam.  This is a disaster.\\n\"\n\"\\n\"\n\"     As unknown_word_strength tends toward infinity, all probabilities\\n\"\n\"     tend toward unknown_word_prob.  All reports were that a value near \"\n\"0.4\\n\"\n\"     worked best, so this does not seem to be corpus-dependent.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:468\nmsgid \"Minimum probability strength\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:469\nmsgid \"\"\n\"When scoring a message, ignore all words with\\n\"\n\"     abs(word.spamprob - 0.5) < minimum_prob_strength.\\n\"\n\"     This may be a hack, but it has proved to reduce error rates in many\\n\"\n\"     tests.  0.1 appeared to work well across all corpora.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:475\nmsgid \"Use chi-squared combining\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:476\nmsgid \"\"\n\"For vectors of random, uniformly distributed probabilities,\\n\"\n\"     -2*sum(ln(p_i)) follows the chi-squared distribution with 2*n \"\n\"degrees\\n\"\n\"     of freedom.  This is the \\\"provably most-sensitive\\\" test the \"\n\"original\\n\"\n\"     scheme was monotonic with.  Getting closer to the theoretical basis\\n\"\n\"     appears to give an excellent combining method, usually very extreme \"\n\"in\\n\"\n\"     its judgment, yet finding a tiny (in # of msgs, spread across a huge\"\n\"\\n\"\n\"     range of scores) middle ground where lots of the mistakes live.  \"\n\"This\\n\"\n\"     is the best method so far. One systematic benefit is is immunity to\\n\"\n\"     \\\"cancellation disease\\\". One systematic drawback is sensitivity to\\n\"\n\"     *any* deviation from a uniform distribution, regardless of whether\\n\"\n\"     actually evidence of ham or spam. Rob Hooft alleviated that by\\n\"\n\"     combining the final S and H measures via (S-H+1)/2 instead of via\\n\"\n\"     S/(S+H)). In practice, it appears that setting ham_cutoff=0.05, and\\n\"\n\"     spam_cutoff=0.95, does well across test sets; while these cutoffs \"\n\"are\\n\"\n\"     rarely optimal, they get close to optimal.  With more training data,\"\n\"\\n\"\n\"     Tim has had good luck with ham_cutoff=0.30 and spam_cutoff=0.80 \"\n\"across\\n\"\n\"     three test data sets (original c.l.p data, his own email, and newer\\n\"\n\"     general python.org traffic).\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:496\nmsgid \"Use mixed uni/bi-grams scheme\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:497\nmsgid \"\"\n\"Generate both unigrams (words) and bigrams (pairs of\\n\"\n\"     words). However, extending an idea originally from Gary Robinson, \"\n\"the\\n\"\n\"     message is 'tiled' into non-overlapping unigrams and bigrams,\\n\"\n\"     approximating the strongest outcome over all possible tilings.\\n\"\n\"\\n\"\n\"     Note that to really test this option you need to retrain with it on,\"\n\"\\n\"\n\"     so that your database includes the bigrams - if you subsequently \"\n\"turn\\n\"\n\"     it off, these tokens will have no effect.  This option will at least\"\n\"\\n\"\n\"     double your database size given the same training data, and will\\n\"\n\"     probably at least triple it.\\n\"\n\"\\n\"\n\"     You may also wish to increase the max_discriminators (maximum number\"\n\"\\n\"\n\"     of extreme words) option if you enable this option, perhaps doubling\"\n\" or\\n\"\n\"     quadrupling it.  It's not yet clear.  Bigrams create many more \"\n\"hapaxes,\\n\"\n\"     and that seems to increase the brittleness of minimalist training\\n\"\n\"     regimes; increasing max_discriminators may help to soften that \"\n\"effect.\\n\"\n\"     OTOH, max_discriminators defaults to 150 in part because that makes \"\n\"it\\n\"\n\"     easy to prove that the chi-squared math is immune from numeric\\n\"\n\"     problems.  Increase it too much, and insane results will eventually\\n\"\n\"     result (including fatal floating-point exceptions on some boxes).\\n\"\n\"\\n\"\n\"     This option is experimental, and may be removed in a future release.\"\n\"\\n\"\n\"     We would appreciate feedback about it if you use it - email\\n\"\n\"     spambayes@python.org with your comments and results.\\n\"\n\"     \"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:526\nmsgid \"Train when filtering\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:527\nmsgid \"\"\n\"Train when filtering?  After filtering a message, hammie can then\\n\"\n\"     train itself on the judgement (ham or spam).  This can speed things \"\n\"up\\n\"\n\"     with a procmail-based solution.  If you do enable this, please make\\n\"\n\"     sure to retrain any mistakes.  Otherwise, your word database will\\n\"\n\"     slowly become useless.  Note that this option is only used by\\n\"\n\"     sb_filter, and will have no effect on sb_server's POP3 proxy, or\\n\"\n\"     the IMAP filter.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:545\nmsgid \"Database backend\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:546\nmsgid \"\"\n\"SpamBayes can use either a ZODB or dbm database (quick to score\\n\"\n\"     one message) or a pickle (quick to train on huge amounts of \"\n\"messages).\\n\"\n\"     There is also (experimental) ability to use a mySQL or PostgresSQL\\n\"\n\"     database.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:552\nmsgid \"Storage file name\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:553\nmsgid \"\"\n\"Spambayes builds a database of information that it gathers\\n\"\n\"     from incoming emails and from you, the user, to get better and\\n\"\n\"     better at classifying your email.  This option specifies the\\n\"\n\"     name of the database file.  If you don't give a full pathname,\\n\"\n\"     the name will be taken to be relative to the location of the\\n\"\n\"     most recent configuration file loaded.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:561\nmsgid \"Message information file name\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:562\nmsgid \"\"\n\"Spambayes builds a database of information about messages\\n\"\n\"     that it has already seen and trained or classified.  This\\n\"\n\"     database is used to ensure that these messages are not retrained\\n\"\n\"     or reclassified (unless specifically requested to).  This option\\n\"\n\"     specifies the name of the database file.  If you don't give a\\n\"\n\"     full pathname, the name will be taken to be relative to the location\"\n\"\\n\"\n\"     of the most recent configuration file loaded.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:571\nmsgid \"Use gzip\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:572\nmsgid \"Use gzip to compress the cache.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:575\nmsgid \"Days before cached messages expire\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:576\nmsgid \"\"\n\"Messages will be expired from the cache after this many days.\\n\"\n\"     After this time, you will no longer be able to train on these \"\n\"messages\\n\"\n\"     (note this does not affect the copy of the message that you have in\\n\"\n\"     your mail client).\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:582 mailpile/spambayes/Options.py:597\nmsgid \"Spam cache directory\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:583 mailpile/spambayes/Options.py:598\nmsgid \"\"\n\"Directory that SpamBayes should cache spam in.  If this does\\n\"\n\"     not exist, it will be created.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:587 mailpile/spambayes/Options.py:602\nmsgid \"Ham cache directory\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:588 mailpile/spambayes/Options.py:603\nmsgid \"\"\n\"Directory that SpamBayes should cache ham in.  If this does\\n\"\n\"     not exist, it will be created.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:592 mailpile/spambayes/Options.py:607\nmsgid \"Unknown cache directory\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:593 mailpile/spambayes/Options.py:608\nmsgid \"\"\n\"Directory that SpamBayes should cache unclassified messages in.\\n\"\n\"     If this does not exist, it will be created.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:612\nmsgid \"Cache messages\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:613\nmsgid \"\"\n\"You can disable the pop3proxy caching of messages.  This\\n\"\n\"     will make the proxy a bit faster, and make it use less space\\n\"\n\"     on your hard drive.  The proxy uses its cache for reviewing\\n\"\n\"     and training of messages, so if you disable caching you won't\\n\"\n\"     be able to do further training unless you re-enable it.\\n\"\n\"     Thus, you should only turn caching off when you are satisfied\\n\"\n\"     with the filtering that Spambayes is doing for you.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:622\nmsgid \"Suppress caching of bulk ham\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:623\nmsgid \"\"\n\"Where message caching is enabled, this option suppresses caching\\n\"\n\"     of messages which are classified as ham and marked as\\n\"\n\"     'Precedence: bulk' or 'Precedence: list'.  If you subscribe to a\\n\"\n\"     high-volume mailing list then your 'Review messages' page can be\\n\"\n\"     overwhelmed with list messages, making training a pain.  Once you've\"\n\"\\n\"\n\"     trained Spambayes on enough list traffic, you can use this option\\n\"\n\"     to prevent that traffic showing up in 'Review messages'.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:632\nmsgid \"Maximum size of cached messages\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:633\nmsgid \"\"\n\"Where message caching is enabled, this option suppresses caching\\n\"\n\"     of messages which are larger than this value (measured in bytes).\\n\"\n\"     If you receive a lot of messages that include large attachments\\n\"\n\"     (and are correctly classified), you may not wish to cache these.\\n\"\n\"     If you set this to zero (0), then this option will have no effect.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:649\nmsgid \"Classification header name\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:650\nmsgid \"\"\n\"Spambayes classifies each message by inserting a new header into\\n\"\n\"     the message.  This header can then be used by your email client\\n\"\n\"     (provided your client supports filtering) to move spam into a\\n\"\n\"     separate folder (recommended), delete it (not recommended), etc.\\n\"\n\"     This option specifies the name of the header that Spambayes inserts.\"\n\"\\n\"\n\"     The default value should work just fine, but you may change it to\\n\"\n\"     anything that you wish.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:661\nmsgid \"Spam disposition name\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:661 mailpile/spambayes/Options.py:773\n#: mailpile/spambayes/Options.py:778 mailpile/spambayes/Options.py:1021\n#: mailpile/spambayes/Options.py:1027 mailpile/spambayes/Options.py:1033\nmsgid \"spam\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:662\nmsgid \"\"\n\"The header that Spambayes inserts into each email has a name,\\n\"\n\"     (Classification header name, above), and a value.  If the classifier\"\n\"\\n\"\n\"     determines that this email is probably spam, it places a header \"\n\"named\\n\"\n\"     as above with a value as specified by this string.  The default\\n\"\n\"     value should work just fine, but you may change it to anything\\n\"\n\"     that you wish.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:670\nmsgid \"Ham disposition name\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:670 mailpile/spambayes/Options.py:773\n#: mailpile/spambayes/Options.py:778 mailpile/spambayes/Options.py:1021\n#: mailpile/spambayes/Options.py:1027 mailpile/spambayes/Options.py:1033\nmsgid \"ham\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:671\nmsgid \"As for Spam Designation, but for emails classified as Ham.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:674\nmsgid \"Unsure disposition name\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:674 mailpile/spambayes/Options.py:773\n#: mailpile/spambayes/Options.py:778\nmsgid \"unsure\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:675\nmsgid \"\"\n\"As for Spam/Ham Designation, but for emails which the\\n\"\n\"     classifer wasn't sure about (ie. the spam probability fell between\\n\"\n\"     the Ham and Spam Cutoffs).  Emails that have this classification\\n\"\n\"     should always be the subject of training.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:681\nmsgid \"Accuracy of reported score\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:682\nmsgid \"Accuracy of the score in the header in decimal digits.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:685\nmsgid \"Augment score with logarithm\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:686\nmsgid \"\"\n\"Set this option to augment scores of 1.00 or 0.00 by a\\n\"\n\"     logarithmic \\\"one-ness\\\" or \\\"zero-ness\\\" score (basically it shows \"\n\"the\\n\"\n\"     \\\"number of zeros\\\" or \\\"number of nines\\\" next to the score value).\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:691\nmsgid \"Add probability (score) header\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:692\nmsgid \"\"\n\"You can have Spambayes insert a header with the calculated spam\\n\"\n\"     probability into each mail.  If you can view headers with your\\n\"\n\"     mailer, then you can see this information, which can be interesting\\n\"\n\"     and even instructive if you're a serious SpamBayes junkie.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:698\nmsgid \"Probability (score) header name\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:702\nmsgid \"Add level header\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:703\nmsgid \"\"\n\"You can have spambayes insert a header with the calculated spam\\n\"\n\"     probability, expressed as a number of '*'s, into each mail (the more\"\n\"\\n\"\n\"     '*'s, the higher the probability it is spam). If your mailer\\n\"\n\"     supports it, you can use this information to fine tune your\\n\"\n\"     classification of ham/spam, ignoring the classification given.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:710\nmsgid \"Level header name\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:714\nmsgid \"Add evidence header\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:715\nmsgid \"\"\n\"You can have spambayes insert a header into mail, with the\\n\"\n\"     evidence that it used to classify that message (a collection of\\n\"\n\"     words with ham and spam probabilities).  If you can view headers\\n\"\n\"     with your mailer, then this may give you some insight as to why\\n\"\n\"     a particular message was scored in a particular way.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:722\nmsgid \"Evidence header name\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:726\nmsgid \"Spambayes id header name\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:730\nmsgid \"Add trained header\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:731\nmsgid \"\"\n\"sb_mboxtrain.py and sb_filter.py can add a header that details\\n\"\n\"     how a message was trained, which lets you keep track of it, and\\n\"\n\"     appropriately re-train messages.  However, if you would rather\\n\"\n\"     mboxtrain/sb_filter didn't rewrite the message files, you can \"\n\"disable\\n\"\n\"     this option.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:738\nmsgid \"Trained header name\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:739\nmsgid \"\"\n\"When training on a message, the name of the header to add with how\\n\"\n\"     it was trained\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:743\nmsgid \"Debug header cutoff\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:744\nmsgid \"\"\n\"The range of clues that are added to the \\\"debug\\\" header in the\\n\"\n\"     E-mail. All clues that have their probability smaller than this \"\n\"number,\\n\"\n\"     or larger than one minus this number are added to the header such \"\n\"that\\n\"\n\"     you can see why spambayes thinks this is ham/spam or why it is \"\n\"unsure.\\n\"\n\"     The default is to show all clues, but you can reduce that by setting\"\n\"\\n\"\n\"     showclue to a lower value, such as 0.1\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:752\nmsgid \"Add unique spambayes id\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:753\nmsgid \"\"\n\"If you wish to be able to find a specific message (via the 'find'\\n\"\n\"     box on the home page), or use the SMTP proxy to train using cached\\n\"\n\"     messages, you will need to know the unique id of each message.  This\"\n\"\\n\"\n\"     option adds this information to a header added to each message.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:759\nmsgid \"Notate to\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:760\nmsgid \"\"\n\"Some email clients (Outlook Express, for example) can only set up\\n\"\n\"     filtering rules on a limited set of headers.  These clients cannot\\n\"\n\"     test for the existence/value of an arbitrary header and filter mail\\n\"\n\"     based on that information.  To accommodate these kind of mail \"\n\"clients,\\n\"\n\"     you can add \\\"spam\\\", \\\"ham\\\", or \\\"unsure\\\" to the recipient list.\"\n\"  A\\n\"\n\"     filter rule can then use this to see if one of these words (followed\"\n\"\\n\"\n\"     by a comma) is in the recipient list, and route the mail to an\\n\"\n\"     appropriate folder, or take whatever other action is supported and\\n\"\n\"     appropriate for the mail classification.\\n\"\n\"\\n\"\n\"     As it interferes with replying, you may only wish to do this for\\n\"\n\"     spam messages; simply tick the boxes of the classifications take\\n\"\n\"     should be identified in this fashion.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:775\nmsgid \"Classify in subject: header\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:776\nmsgid \"\"\n\"This option will add the same information as 'Notate To',\\n\"\n\"     but to the start of the mail subject line.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:786 mailpile/spambayes/Options.py:844\n#: mailpile/spambayes/Options.py:921\nmsgid \"Remote Servers\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:787\nmsgid \"\"\n\"     The SpamBayes POP3 proxy intercepts incoming email and classifies it\"\n\"\\n\"\n\"     before sending it on to your email client.  You need to specify \"\n\"which\\n\"\n\"     POP3 server(s) and port(s) you wish it to connect to - a POP3 server\"\n\"\\n\"\n\"     address typically looks like 'pop3.myisp.net:110' where\\n\"\n\"     'pop3.myisp.net' is the name of the computer where the POP3 server \"\n\"runs\\n\"\n\"     and '110' is the port on which the POP3 server listens.  The other \"\n\"port\\n\"\n\"     you might find is '995', which is used for secure POP3.  If you use\\n\"\n\"     more than one server, simply separate their names with commas.  For\\n\"\n\"     example:  'pop3.myisp.net:110,pop.gmail.com:995'.  You can get\\n\"\n\"     these server names and port numbers from your existing email\\n\"\n\"     configuration, or from your ISP or system administrator.  If you are\"\n\"\\n\"\n\"     using Web-based email, you can't use the SpamBayes POP3 proxy \"\n\"(sorry!).\\n\"\n\"     In your email client's configuration, where you would normally put \"\n\"your\\n\"\n\"     POP3 server address, you should now put the address of the machine\\n\"\n\"     running SpamBayes.\\n\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:806 mailpile/spambayes/Options.py:862\n#: mailpile/spambayes/Options.py:935\nmsgid \"SpamBayes Ports\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:807\nmsgid \"\"\n\"     Each monitored POP3 server must be assigned to a different port in \"\n\"the\\n\"\n\"     SpamBayes POP3 proxy.  You need to configure your email client to\\n\"\n\"     connect to this port instead of the actual remote POP3 server.  If \"\n\"you\\n\"\n\"     don't know what port to use, try 8110 and go up from there.  If you\\n\"\n\"     have two servers, your list of listen ports might then be \"\n\"'8110,8111'.\\n\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:816\nmsgid \"Allowed remote POP3 connections\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:817\nmsgid \"\"\n\"Enter a list of trusted IPs, separated by commas. Remote POP\\n\"\n\"     connections from any of them will be allowed. You can trust any\\n\"\n\"     IP using a single '*' as field value. You can also trust ranges of\\n\"\n\"     IPs using the '*' character as a wildcard (for instance \"\n\"192.168.0.*).\\n\"\n\"     The localhost IP will always be trusted. Type 'localhost' in the\\n\"\n\"     field to trust this only address.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:825\nmsgid \"Retrieval timeout\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:826\nmsgid \"\"\n\"When proxying messages, time out after this length of time if\\n\"\n\"     all the headers have been received.  The rest of the mesasge will\\n\"\n\"     proxy straight through.  Some clients have a short timeout period,\\n\"\n\"     and will give up on waiting for the message if this is too long.\\n\"\n\"     Note that the shorter this is, the less of long messages will be\\n\"\n\"     used for classifications (i.e. results may be effected).\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:845\nmsgid \"\"\n\"Use of the SMTP proxy is optional - if you would rather just train\\n\"\n\"     via the web interface, or the pop3dnd or mboxtrain scripts, then you\"\n\"\\n\"\n\"     can safely leave this option blank.  The Spambayes SMTP proxy\\n\"\n\"     intercepts outgoing email - if you forward mail to one of the\\n\"\n\"     addresses below, it is examined for an id and the message\\n\"\n\"     corresponding to that id is trained as ham/spam.  All other mail is\\n\"\n\"     sent along to your outgoing mail server.  You need to specify which\\n\"\n\"     SMTP server(s) you wish it to intercept - a SMTP server address\\n\"\n\"     typically looks like \\\"smtp.myisp.net\\\".  If you use more than one\\n\"\n\"     server, simply separate their names with commas.  You can get these\\n\"\n\"     server names from your existing email configuration, or from your \"\n\"ISP\\n\"\n\"     or system administrator.  If you are using Web-based email, you \"\n\"can't\\n\"\n\"     use the Spambayes SMTP proxy (sorry!).  In your email client's\\n\"\n\"     configuration, where you would normally put your SMTP server \"\n\"address,\\n\"\n\"     you should now put the address of the machine running SpamBayes.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:863\nmsgid \"\"\n\"Each SMTP server that is being monitored must be assigned to a\\n\"\n\"     'port' in the Spambayes SMTP proxy.  This port must be different for\"\n\"\\n\"\n\"     each monitored server, and there must be a port for\\n\"\n\"     each monitored server.  Again, you need to configure your email\\n\"\n\"     client to use this port.  If there are multiple servers, you must\\n\"\n\"     specify the same number of ports as servers, separated by commas.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:871\nmsgid \"Allowed remote SMTP connections\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:872\nmsgid \"\"\n\"Enter a list of trusted IPs, separated by commas. Remote SMTP\\n\"\n\"     connections from any of them will be allowed. You can trust any\\n\"\n\"     IP using a single '*' as field value. You can also trust ranges of\\n\"\n\"     IPs using the '*' character as a wildcard (for instance \"\n\"192.168.0.*).\\n\"\n\"     The localhost IP will always be trusted. Type 'localhost' in the\\n\"\n\"     field to trust this only address.  Note that you can unwittingly\\n\"\n\"     turn a SMTP server into an open proxy if you open this up, as\\n\"\n\"     connections to the server will appear to be from your machine, even\\n\"\n\"     if they are from a remote machine *through* your machine, to the\\n\"\n\"     server.  We do not recommend opening this up fully (i.e. using '*').\"\n\"\\n\"\n\"     \"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:885\nmsgid \"Train as ham address\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:886\nmsgid \"\"\n\"When a message is received that you wish to train on (for example,\\n\"\n\"     one that was incorrectly classified), you need to forward or bounce\\n\"\n\"     it to one of two special addresses so that the SMTP proxy can \"\n\"identify\\n\"\n\"     it.  If you wish to train it as ham, forward or bounce it to this\\n\"\n\"     address.  You will want to use an address that is not\\n\"\n\"     a valid email address, like ham@nowhere.nothing.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:894\nmsgid \"Train as spam address\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:895\nmsgid \"\"\n\"As with Ham Address above, but the address that you need to forward\\n\"\n\"     or bounce mail that you wish to train as spam.  You will want to use\"\n\"\\n\"\n\"     an address that is not a valid email address, like\\n\"\n\"     spam@nowhere.nothing.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:901\nmsgid \"Lookup message in cache\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:902\nmsgid \"\"\n\"If this option is set, then the smtpproxy will attempt to\\n\"\n\"     look up the messages sent to it (for training) in the POP3 proxy \"\n\"cache\\n\"\n\"     or IMAP filter folders, and use that message as the training data.\\n\"\n\"     This avoids any problems where your mail client might change the\\n\"\n\"     message when forwarding, contaminating your training data.  If you \"\n\"can\\n\"\n\"     be sure that this won't occur, then the id-lookup can be avoided.\\n\"\n\"\\n\"\n\"     Note that Outlook Express users cannot use the lookup option \"\n\"(because\\n\"\n\"     of the way messages are forwarded), and so if they wish to use the\\n\"\n\"     SMTP proxy they must enable this option (but as messages are \"\n\"altered,\\n\"\n\"     may not get the best results, and this is not recommended).\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:922\nmsgid \"\"\n\"The SpamBayes IMAP4 proxy intercepts incoming email and classifies\\n\"\n\"     it before sending it on to your email client.  You need to specify\\n\"\n\"     which IMAP4 server(s) you wish it to intercept - a IMAP4 server\\n\"\n\"     address typically looks like \\\"mail.myisp.net\\\".  If you use more \"\n\"than\\n\"\n\"     one server, simply separate their names with commas.  You can get\\n\"\n\"     these server names from your existing email configuration, or from\\n\"\n\"     your ISP or system administrator.  If you are using Web-based email,\"\n\"\\n\"\n\"     you can't use the SpamBayes IMAP4 proxy (sorry!).  In your email\\n\"\n\"     client's configuration, where you would normally put your IMAP4 \"\n\"server\\n\"\n\"     address, you should now put the address of the machine running\\n\"\n\"     SpamBayes.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:936\nmsgid \"\"\n\"Each IMAP4 server that is being monitored must be assigned to a\\n\"\n\"     'port' in the SpamBayes IMAP4 proxy.  This port must be different \"\n\"for\\n\"\n\"     each monitored server, and there must be a port for each monitored\\n\"\n\"     server.  Again, you need to configure your email client to use this\\n\"\n\"     port.  If there are multiple servers, you must specify the same \"\n\"number\\n\"\n\"     of ports as servers, separated by commas. If you don't know what to\\n\"\n\"     use here, and you only have one server, try 143, or if that doesn't\\n\"\n\"     work, try 8143.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:946\nmsgid \"Allowed remote IMAP4 connections\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:947\nmsgid \"\"\n\"Enter a list of trusted IPs, separated by commas. Remote IMAP\\n\"\n\"     connections from any of them will be allowed. You can trust any\\n\"\n\"     IP using a single '*' as field value. You can also trust ranges of\\n\"\n\"     IPs using the '*' character as a wildcard (for instance \"\n\"192.168.0.*).\\n\"\n\"     The localhost IP will always be trusted. Type 'localhost' in the\\n\"\n\"     field to trust this only address.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:969\nmsgid \"Launch browser\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:970\nmsgid \"\"\n\"If this option is set, then whenever sb_server or sb_imapfilter is\\n\"\n\"     started the default web browser will be opened to the main web\\n\"\n\"     interface page.  Use of the -b switch when starting from the command\"\n\"\\n\"\n\"     line overrides this option.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:976\nmsgid \"Allowed remote UI connections\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:977\nmsgid \"\"\n\"Enter a list of trusted IPs, separated by commas. Remote\\n\"\n\"     connections from any of them will be allowed. You can trust any\\n\"\n\"     IP using a single '*' as field value. You can also trust ranges of\\n\"\n\"     IPs using the '*' character as a wildcard (for instance \"\n\"192.168.0.*).\\n\"\n\"     The localhost IP will always be trusted. Type 'localhost' in the\\n\"\n\"     field to trust this only address.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:985\nmsgid \"Headers to display in message review\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:986\nmsgid \"\"\n\"When reviewing messages via the web user interface, you are\\n\"\n\"     presented with various information about the message.  By default, \"\n\"you\\n\"\n\"     are shown the subject and who the message is from.  You can add \"\n\"other\\n\"\n\"     message headers to display, however, such as the address the message\"\n\"\\n\"\n\"     is to, or the date that the message was sent.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:993\nmsgid \"Display date received in message review\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:994\nmsgid \"\"\n\"When reviewing messages via the web user interface, you are\\n\"\n\"     presented with various information about the message.  If you set\\n\"\n\"     this option, you will be shown the date that the message was \"\n\"received.\\n\"\n\"     \"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1000\nmsgid \"Display score in message review\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1001\nmsgid \"\"\n\"When reviewing messages via the web user interface, you are\\n\"\n\"     presented with various information about the message.  If you\\n\"\n\"     set this option, this information will include the score that\\n\"\n\"     the message received when it was classified.  You might wish to\\n\"\n\"     see this purely out of curiousity, or you might wish to only\\n\"\n\"     train on messages that score towards the boundaries of the\\n\"\n\"     classification areas.  Note that in order to use this option,\\n\"\n\"     you must also enable the option to include the score in the\\n\"\n\"     message headers.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1012\nmsgid \"Display the advanced find query\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1013\nmsgid \"\"\n\"Present advanced options in the 'Word Query' box on the front page,\\n\"\n\"     including wildcard and regular expression searching.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1017\nmsgid \"Default training for ham\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1017 mailpile/spambayes/Options.py:1021\n#: mailpile/spambayes/Options.py:1023 mailpile/spambayes/Options.py:1027\n#: mailpile/spambayes/Options.py:1033\nmsgid \"discard\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1018\nmsgid \"\"\n\"When presented with the review list in the web interface,\\n\"\n\"     which button would you like checked by default when the message\\n\"\n\"     is classified as ham?\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1021 mailpile/spambayes/Options.py:1027\n#: mailpile/spambayes/Options.py:1029 mailpile/spambayes/Options.py:1033\nmsgid \"defer\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1023\nmsgid \"Default training for spam\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1024\nmsgid \"\"\n\"When presented with the review list in the web interface,\\n\"\n\"     which button would you like checked by default when the message\\n\"\n\"     is classified as spam?\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1029\nmsgid \"Default training for unsure\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1030\nmsgid \"\"\n\"When presented with the review list in the web interface,\\n\"\n\"     which button would you like checked by default when the message\\n\"\n\"     is classified as unsure?\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1035\nmsgid \"Ham Discard Level\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1036\nmsgid \"\"\n\"Hams scoring less than this percentage will default to being\\n\"\n\"     discarded in the training interface (they won't be trained). You'll\\n\"\n\"     need to turn off the 'Train when filtering' option, above, for this\\n\"\n\"     to have any effect\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1042\nmsgid \"Spam Discard Level\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1043\nmsgid \"\"\n\"Spams scoring more than this percentage will default to being\\n\"\n\"     discarded in the training interface (they won't be trained). You'll\\n\"\n\"     need to turn off the 'Train when filtering' option, above, for this\\n\"\n\"     to have any effect\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1049\nmsgid \"HTTP Authentication\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1050\nmsgid \"\"\n\"This option lets you choose the security level of the web interface.\\n\"\n\"     When selecting Basic or Digest, the user will be prompted a login \"\n\"and a\\n\"\n\"     password to access the web interface. The Basic option is faster, \"\n\"but\\n\"\n\"     transmits the password in clear on the network. The Digest option\\n\"\n\"     encrypts the password before transmission.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1058\nmsgid \"\"\n\"If you activated the HTTP authentication option, you can modify the\\n\"\n\"     authorized user name here.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1063\nmsgid \"\"\n\"If you activated the HTTP authentication option, you can modify the\\n\"\n\"     authorized user password here.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1067\nmsgid \"Rows per section\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1068\nmsgid \"Number of rows to display per ham/spam/unsure section.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1073\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:196\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:222\n#: shared-data/default-theme/html/settings/recipes.html:24\nmsgid \"Server\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1074\nmsgid \"\"\n\"These are the names and ports of the imap servers that store your\\n\"\n\"     mail, and which the imap filter will connect to - for example:\\n\"\n\"     mail.example.com or imap.example.com:143.  The default IMAP port is\\n\"\n\"     143 (or 993 if using SSL); if you connect via one of those ports, \"\n\"you\\n\"\n\"     can leave this blank. If you use more than one server, use a comma\\n\"\n\"     delimited list of the server:port values.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1082 mailpile/spambayes/Options.py:1198\n#: shared-data/default-theme/html/profiles/account-form.html:424\n#: shared-data/default-theme/html/profiles/account-form.html:528\nmsgid \"Username\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1083\nmsgid \"\"\n\"This is the id that you use to log into your imap server.  If your\\n\"\n\"     address is funkyguy@example.com, then your username is probably\\n\"\n\"     funkyguy.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1089\nmsgid \"\"\n\"That is that password that you use to log into your imap server.\\n\"\n\"     This will be stored in plain text in your configuration file, and if\"\n\"\\n\"\n\"     you have set the web user interface to allow remote connections, \"\n\"then\\n\"\n\"     it will be available for the whole world to see in plain text.  If\\n\"\n\"     I've just freaked you out, don't panic <wink>.  You can leave this\\n\"\n\"     blank and use the -p command line option to imapfilter.py and you \"\n\"will\\n\"\n\"     be prompted for your password.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1098\nmsgid \"Purge//Expunge\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1099\nmsgid \"\"\n\"Permanently remove *all* messages flagged with //Deleted on logout.\\n\"\n\"     If you do not know what this means, then please leave this as\\n\"\n\"     False.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1104\nmsgid \"Connect via a secure socket layer\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1105\nmsgid \"\"\n\"Use SSL to connect to the server. This allows spambayes to connect\\n\"\n\"     without sending the password in plain text.\\n\"\n\"\\n\"\n\"     Note that this does not check the server certificate at this point \"\n\"in\\n\"\n\"     time.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1112\nmsgid \"Folders to filter\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1113\nmsgid \"Comma delimited list of folders to be filtered\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1116\nmsgid \"Folder for unsure messages\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1120\nmsgid \"Folder for suspected spam\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1124\nmsgid \"Folder for ham messages\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1125\nmsgid \"\"\n\"If you leave this option blank, messages classified as ham will not\\n\"\n\"     be moved.  However, if you wish to have ham messages moved, you can\\n\"\n\"     select a folder here.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1130\nmsgid \"Folders with mail to be trained as ham\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1131\nmsgid \"\"\n\"Comma delimited list of folders that will be examined for messages\\n\"\n\"     to train as ham.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1135\nmsgid \"Folders with mail to be trained as spam\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1136\nmsgid \"\"\n\"Comma delimited list of folders that will be examined for messages\\n\"\n\"     to train as spam.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1140\nmsgid \"Folder to move trained spam to\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1141\nmsgid \"\"\n\"When training, all messages in the spam training folder(s) (above)\\n\"\n\"     are examined - if they are new, they are used to train, if not, they\"\n\"\\n\"\n\"     are ignored.  This examination does take time, however, so if speed\\n\"\n\"     is an issue for you, you may wish to move messages out of this \"\n\"folder\\n\"\n\"     once they have been trained (either to delete them or to a storage\\n\"\n\"     folder).  If a folder name is specified here, this will happen\\n\"\n\"     automatically.  Note that the filter is not yet clever enough to\\n\"\n\"     move the mail to different folders depending on which folder it\\n\"\n\"     was originally in - *all* messages will be moved to the same\\n\"\n\"     folder.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1153\nmsgid \"Folder to move trained ham to\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1154\nmsgid \"\"\n\"When training, all messages in the ham training folder(s) (above)\\n\"\n\"     are examined - if they are new, they are used to train, if not, they\"\n\"\\n\"\n\"     are ignored.  This examination does take time, however, so if speed\\n\"\n\"     is an issue for you, you may wish to move messages out of this \"\n\"folder\\n\"\n\"     once they have been trained (either to delete them or to a storage\\n\"\n\"     folder).  If a folder name is specified here, this will happen\\n\"\n\"     automatically.  Note that the filter is not yet clever enough to\\n\"\n\"     move the mail to different folders depending on which folder it\\n\"\n\"     was originally in - *all* messages will be moved to the same\\n\"\n\"     folder.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1199\nmsgid \"The username to use when logging into the SpamBayes IMAP server.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1203\nmsgid \"The password to use when logging into the SpamBayes IMAP server.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1206\nmsgid \"IMAP Listen Port\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1207\nmsgid \"The port to serve the SpamBayes IMAP server on.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1212\nmsgid \"Verbose\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1216\nmsgid \"Database storage type\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1217\nmsgid \"\"\n\"What DBM storage type should we use?  Must be best, db3hash,\\n\"\n\"     dbhash or gdbm.  Windows folk should steer clear of dbhash.  Default\"\n\"\\n\"\n\"     is \\\"best\\\", which will pick the best DBM type available on your\\n\"\n\"     platform.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1223\nmsgid \"HTTP Proxy Username\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1224\nmsgid \"\"\n\"The username to give to the HTTP proxy when required.  If a\\n\"\n\"     username is not necessary, simply leave blank.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1227\nmsgid \"HTTP Proxy Password\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1228\nmsgid \"\"\n\"The password to give to the HTTP proxy when required.  This is\\n\"\n\"     stored in clear text in your configuration file, so if that bothers\\n\"\n\"     you then don't do this.  You'll need to use a proxy that doesn't \"\n\"need\\n\"\n\"     authentication, or do without any SpamBayes HTTP activity.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1233\nmsgid \"HTTP Proxy Server\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1234\nmsgid \"\"\n\"If a spambayes application needs to use HTTP, it will try to do so\\n\"\n\"     through this proxy server.  The port defaults to 8080, or can be\\n\"\n\"     entered with the server:port form.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1239\nmsgid \"User Interface Language\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1240\nmsgid \"\"\n\"If possible, the user interface should use a language from this\\n\"\n\"     list (in order of preference).\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1245\nmsgid \"XML-RPC path\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1246\nmsgid \"The path to respond to.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1248\nmsgid \"XML-RPC host\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1249\nmsgid \"The host to listen on.\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1251\nmsgid \"XML-RPC port\"\nmsgstr \"\"\n\n#: mailpile/spambayes/Options.py:1252\nmsgid \"The port to listen on.\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:298\nmsgid \"unnamed\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:339\nmsgid \"There is something unknown or wrong with this signature\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:344\nmsgid \"Not Signed\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:345\nmsgid \"\"\n\"This data has no digital signature, which means it could have come from \"\n\"anyone, not necessarily the apparent sender\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:351\nmsgid \"There was a weird error with this digital signature\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:355 mailpile/www/jinjaextensions.py:550\nmsgid \"Mixed Error\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:356\nmsgid \"Parts of this message have a signature with a weird error\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:360\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:221\n#: shared-data/default-theme/html/partials/compose.html:100\nmsgid \"Unsigned\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:361\nmsgid \"\"\n\"This data has no digital signature, which means it could easily have been\"\n\" forged. This sender usually signs their messages, so be careful!\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:367\nmsgid \"Mixed Unsigned\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:368\nmsgid \"\"\n\"This message has no digital signature, which means it could easily have \"\n\"been forged. This sender usually signs their messages, so be careful!\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:374\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:375\nmsgid \"The digital signature was invalid or bad\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:379\nmsgid \"Mixed Invalid\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:380\nmsgid \"Parts of this message have a digital signature that is invalid or bad\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:385\nmsgid \"Revoked\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:386\nmsgid \"\"\n\"Watch out, the digital signature was made with a key that has been \"\n\"revoked - this is not a good thing\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:391\nmsgid \"Mixed Revoked\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:392\nmsgid \"\"\n\"Watch out, parts of this message were digitally signed with a key that \"\n\"has been revoked\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:397\nmsgid \"Expired\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:398\nmsgid \"The digital signature was made with an expired key\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:402\nmsgid \"Mixed Expired\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:403\nmsgid \"Parts of this message have a digital signature made with an expired key\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:409\nmsgid \"\"\n\"The digital signature was made with an unknown key, so we can not verify \"\n\"it\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:414\nmsgid \"Mixed Unknown\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:415\nmsgid \"\"\n\"Parts of this message have a signature made with an unknown key which we \"\n\"can not verify\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:420\nmsgid \"Changed\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:421\nmsgid \"The digital signature was made with an unexpected key. Be careful!\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:426\nmsgid \"Mixed Changed\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:427\nmsgid \"\"\n\"Parts of this message have a digital signature that was made with an \"\n\"unexpected key. Be careful!\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:432\nmsgid \"Unverified\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:433\nmsgid \"The signature was good but it came from a key that is not verified yet\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:438\nmsgid \"Mixed Unverified\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:439\nmsgid \"Parts of this message have an unverified signature\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:443\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:214\nmsgid \"Signed\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:444\nmsgid \"\"\n\"The digital signature is valid and was made with a key we have seen \"\n\"before. Looks good!\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:449\nmsgid \"Mixed Signed\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:450\nmsgid \"\"\n\"Parts of the message have a good digital signature, made with a key we \"\n\"have seen before.\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:455\nmsgid \"Verified\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:456\nmsgid \"The signature was good and came from a verified key, w00t!\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:460\nmsgid \"Mixed Verified\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:461\nmsgid \"Parts of the message have a verified signature, but other parts do not\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:498\nmsgid \"There is some unknown thing wrong with this encryption\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:503\nmsgid \"Not Encrypted\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:504\nmsgid \"\"\n\"This content was not encrypted. It could have been intercepted and read \"\n\"by an unauthorized party\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:509\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:258\nmsgid \"Encrypted\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:510\nmsgid \"This content was encrypted, great job being secure\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:514\nmsgid \"Mixed Encrypted\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:515\nmsgid \"Part of this message were encrypted, but other parts were not encrypted\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:520\nmsgid \"Locked Key\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:521\nmsgid \"You have the encryption key to decrypt this, but the key itself is locked.\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:526\nmsgid \"Mixed Locked Key\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:527\nmsgid \"\"\n\"Parts of the message could not be decrypted because your encryption key \"\n\"is locked.\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:532\nmsgid \"Missing Key\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:533\nmsgid \"\"\n\"You don't have the encryption key to decrypt this, perhaps it was \"\n\"encrypted to an old key you don't have anymore?\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:538\nmsgid \"Mixed Missing Key\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:539\nmsgid \"\"\n\"Parts of the message could not be decrypted because you are missing the \"\n\"private key. Perhaps it was encrypted to an old key you don't have \"\n\"anymore?\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:546\nmsgid \"We failed to decrypt and are unsure why.\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:551\nmsgid \"We failed to decrypt parts of this message and are unsure why\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:584 mailpile/www/jinjaextensions.py:589\nmsgid \"Automatic\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:585 mailpile/www/jinjaextensions.py:590\nmsgid \"\"\n\"Mailpile will intelligently try to guess and suggest the best security \"\n\"with the given contact\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:593\nmsgid \"Don't Sign or Encrypt\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:594\nmsgid \"Messages will not be encrypted nor signed by your encryption key\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:596\nmsgid \"Only Sign\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:597\nmsgid \"Messages will only be signed by your encryption key\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:599\nmsgid \"Only Encrypt\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:600\nmsgid \"Messages will only be encrypted but not signed by your encryption key\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:602\nmsgid \"Always Encrypt & Sign\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:603\nmsgid \"Messages will be both encrypted and signed by your encryption key\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:668\nmsgid \"\"\n\"Mailpile security tip: \\\\n\\\\n  Uh oh! This web site may be dangerous!\\\\n\"\n\"  Are you sure you want to continue?\\\\n\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:824 mailpile/www/jinjaextensions.py:832\nmsgid \"No Subject\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:872\n#, python-format\nmsgid \"and %d others\"\nmsgstr \"\"\n\n#: mailpile/www/jinjaextensions.py:1032\nmsgid \"No Fingerprint\"\nmsgstr \"\"\n\n#: shared-data/contrib/datadig/datadig.py:125\n#: shared-data/contrib/datadig/datadig.py:128\n#, python-format\nmsgid \"Digging into =%s\"\nmsgstr \"\"\n\n#: shared-data/contrib/datadig/datadig.py:136\n#, python-format\nmsgid \"Found %d rows in %d messages\"\nmsgstr \"\"\n\n#: shared-data/contrib/demos/demos.py:45\nmsgid \"Demo Contacts\"\nmsgstr \"\"\n\n#: shared-data/contrib/demos/demos.py:46\nmsgid \"This is the demo importer\"\nmsgstr \"\"\n\n#: shared-data/contrib/demos/demos.py:49\nmsgid \"Activate demo importer\"\nmsgstr \"\"\n\n#: shared-data/contrib/demos/demos.py:50\nmsgid \"Contact name\"\nmsgstr \"\"\n\n#: shared-data/contrib/demos/demos.py:51\nmsgid \"Contact email\"\nmsgstr \"\"\n\n#: shared-data/contrib/demos/demos.py:112\nmsgid \"I refuse to work with empty or gross data\"\nmsgstr \"\"\n\n#: shared-data/contrib/demos/demos.py:116\nmsgid \"I hashed your data for you, yay!\"\nmsgstr \"\"\n\n#: shared-data/contrib/forcegrapher/forcegrapher.py:80\nmsgid \"Generated graph view\"\nmsgstr \"\"\n\n#: shared-data/contrib/hacks/hacks.py:64\nmsgid \"Tried to fix metadata index\"\nmsgstr \"\"\n\n#: shared-data/contrib/hacks/hacks.py:150\nmsgid \"Displayed raw metadata\"\nmsgstr \"\"\n\n#: shared-data/contrib/hacks/hacks.py:194\nmsgid \"Edited raw metadata\"\nmsgstr \"\"\n\n#: shared-data/contrib/hacks/hacks.py:213\nmsgid \"Displayed message keywords\"\nmsgstr \"\"\n\n#: shared-data/contrib/hacks/hacks.py:235\nmsgid \"Displayed message HeaderPrint\"\nmsgstr \"\"\n\n#: shared-data/contrib/hints/hints.py:35\n#, python-format\nmsgid \"This is Mailpile version %s\"\nmsgstr \"\"\n\n#: shared-data/contrib/hints/hints.py:41\nmsgid \"Your Mailpile is configured to never delete e-mail\"\nmsgstr \"\"\n\n#: shared-data/contrib/hints/hints.py:46\nmsgid \"Mailpile has keyboard shortcuts!\"\nmsgstr \"\"\n\n#: shared-data/contrib/hints/hints.py:53\nmsgid \"Learn how to get the most out of Mailpile's spam filter\"\nmsgstr \"\"\n\n#: shared-data/contrib/hints/hints.py:58\nmsgid \"Rearrange your sidebar to organize how you see your e-mail\"\nmsgstr \"\"\n\n#: shared-data/contrib/hints/hints.py:64\nmsgid \"Learn how dragging and dropping tags works\"\nmsgstr \"\"\n\n#: shared-data/contrib/hints/hints.py:70\nmsgid \"Mailpile uses Gravatar thumbnails!\"\nmsgstr \"\"\n\n#: shared-data/contrib/hints/hints.py:77\nmsgid \"You really should make backups of your Mailpile\"\nmsgstr \"\"\n\n#: shared-data/contrib/hints/hints.py:83\nmsgid \"Mailpile can automatically tag or untag any kind of e-mail!\"\nmsgstr \"\"\n\n#: shared-data/contrib/hints/hints.py:129\nmsgid \"never\"\nmsgstr \"\"\n\n#: shared-data/contrib/hints/hints.py:138\n#: shared-data/default-theme/html/jsapi/ui/notifications.js:328\n#: shared-data/default-theme/html/profiles/account-form.html:831\nmsgid \"learn more\"\nmsgstr \"\"\n\n#: shared-data/contrib/hints/hints.py:140\n#: shared-data/contrib/hints/hints.py:188\nmsgid \"Did you know\"\nmsgstr \"\"\n\n#: shared-data/contrib/i18nhelper/i18nhelper.py:19\nmsgid \"Nothing recently translated\"\nmsgstr \"\"\n\n#: shared-data/contrib/print/print.py:48\n#, python-format\nmsgid \"Printing e-mail to %s\"\nmsgstr \"\"\n\n#: shared-data/contrib/print/print.py:64\n#, python-format\nmsgid \"Printed to %d files\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/auth/shared.html:18\nmsgid \"Log In With Your\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/auth/shared.html:24\n#: shared-data/default-theme/html/backup/restore/index.html:53\n#: shared-data/default-theme/html/backup/restore/index.html:54\n#: shared-data/default-theme/html/setup/password/index.html:2\nmsgid \"Mailpile Password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/auth/shared.html:28\nmsgid \"Oops, wrong password. Try again?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/auth/shared.html:30\nmsgid \"Last failed login:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/auth/shared.html:40\nmsgid \"You Have Been Logged Out!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/auth/shared.html:46\nmsgid \"Note\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/auth/shared.html:47\nmsgid \"Mailpile is still running and processing your e-mail in the background.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/auth/shared.html:48\nmsgid \"\"\n\"To disable Mailpile completely, press the Shutdown button on the settings\"\n\" page or in the desktop application window.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/backup/restore/index.html:2\n#: shared-data/default-theme/html/backup/restore/index.html:24\n#: shared-data/default-theme/html/setup/welcome/index.html:47\nmsgid \"Restore from a Backup\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/backup/restore/index.html:2\n#: shared-data/default-theme/html/help/index.html:18\n#: shared-data/default-theme/html/setup/password/index.html:2\nmsgid \"Setup\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/backup/restore/index.html:22\nmsgid \"Backup Archive is OK\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/backup/restore/index.html:31\nmsgid \"This backup was created by Mailpile version {ver} on {date}.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/backup/restore/index.html:37\nmsgid \"Restore encryption keys to shared GnuPG keychain\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/backup/restore/index.html:40\nmsgid \"Restore encryption keys to Mailpile-only keychain\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/backup/restore/index.html:43\nmsgid \"Do not restore GnuPG/PGP encryption keys\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/backup/restore/index.html:46\nmsgid \"Keep default Operating System settings (override backup)\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/backup/restore/index.html:50\nmsgid \"Enter your Mailpile Password to restore this configuration.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/backup/restore/index.html:58\nmsgid \"Restore\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/backup/restore/index.html:63\nmsgid \"\"\n\"You can restore a previous Mailpile configuration (keys, tags, etc.), \"\n\"provided you have a Mailpile Backup Archive available.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/backup/restore/index.html:69\nmsgid \"Upload a Backup Archive\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/backup/restore/index.html:79\nmsgid \"Please upload your Mailpile Backup Archive to continue.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/backup/restore/index.html:80\nmsgid \"It should have a name similar to: \"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/backup/restore/index.html:86\nmsgid \"Upload and Verify\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/backup/restore/index.html:110\nmsgid \"Back to Setup\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/browse/index.html:5\nmsgid \"Browsing folders and mailboxes\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/browse/index.html:7\nmsgid \"Browsing\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/browse/index.html:15\n#: shared-data/default-theme/html/partials/tag_add.html:19\n#: shared-data/default-theme/html/partials/tools_search.html:212\n#: shared-data/default-theme/html/tags/form.html:114\nmsgid \"Parent\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/browse/index.html:16\n#: shared-data/default-theme/html/partials/tools_search.html:213\nmsgid \"Open Parent Folder\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/browse/index.html:44\nmsgid \"Browse local or remote folders for mailboxes to add to your pile.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/browse/index.html:48\nmsgid \"These are mailboxes recognized by Mailpile.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/browse/index.html:49\nmsgid \"Click to view mailbox contents or change settings.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/browse/index.html:97\n#: shared-data/default-theme/html/logs/layout.html:8\n#: shared-data/default-theme/html/profiles/index.html:60\n#: shared-data/default-theme/html/settings/index.html:27\n#: shared-data/default-theme/html/settings/plugins.html:90\n#: shared-data/default-theme/html/settings/recipes.html:152\n#: shared-data/default-theme/html/settings/recipes.html:154\n#: shared-data/default-theme/html/settings/recipes.html:156\n#: shared-data/default-theme/html/settings/recipes.html:158\n#: shared-data/default-theme/html/settings/recipes.html:160\n#: shared-data/default-theme/html/tags/form.html:108\nmsgid \"Settings\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/index.html:2\n#: shared-data/default-theme/html/contacts/index.html:65\n#: shared-data/default-theme/html/partials/sidebar.html:21\n#: shared-data/default-theme/html/partials/tools_contacts.html:37\n#: shared-data/default-theme/html/partials/topbar.html:51\nmsgid \"Contacts\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/index.html:46\nmsgid \"No Contact Info\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/index.html:52\nmsgid \"No Contacts Found\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/index.html:53\nmsgid \"\"\n\"Don't worry, it's ok. Mailpile will start to automatically create \"\n\"contacts whenever you send or reply to messages.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/index.html:60\n#: shared-data/default-theme/html/search/prev_more_next.html:15\nmsgid \"Previous\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/index.html:63\n#: shared-data/default-theme/html/profiles/account-form.html:355\n#: shared-data/default-theme/html/profiles/account-form.html:478\n#: shared-data/default-theme/html/profiles/account-form.html:673\n#: shared-data/default-theme/html/search/prev_more_next.html:36\nmsgid \"Next\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/index.html:65\n#: shared-data/default-theme/html/search/prev_more_next.html:40\nmsgid \"of\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/index.html:65\n#: shared-data/default-theme/html/search/prev_more_next.html:42\nmsgid \"1 Conversation\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/index.html:65\nmsgid \"No results found\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/add/index.html:2\n#: shared-data/default-theme/html/jsapi/templates/modal-contact-add.html:6\n#: shared-data/default-theme/html/partials/search_item.html:88\n#: shared-data/default-theme/html/partials/tools_contacts.html:43\nmsgid \"Add Contact\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/add/index.html:8\n#: shared-data/default-theme/html/jsapi/templates/modal-add-tag.html:9\n#: shared-data/default-theme/html/jsapi/templates/modal-contact-add.html:10\n#: shared-data/default-theme/html/partials/hidden.html:45\n#: shared-data/default-theme/html/profiles/account-form.html:317\n#: shared-data/default-theme/html/settings/recipes.html:4\nmsgid \"Name\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/add/index.html:10\n#: shared-data/default-theme/html/jsapi/templates/modal-contact-add.html:12\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:33\n#: shared-data/default-theme/html/partials/hidden.html:47\n#: shared-data/default-theme/html/profiles/account-form.html:323\nmsgid \"E-mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/add/index.html:15\n#: shared-data/default-theme/html/jsapi/templates/modal-add-tag.html:11\n#: shared-data/default-theme/html/jsapi/templates/modal-add-tag.html:12\n#: shared-data/default-theme/html/jsapi/templates/modal-contact-add.html:14\n#: shared-data/default-theme/html/jsapi/templates/modal-contact-add.html:15\n#: shared-data/default-theme/html/jsapi/templates/modal-send-public-key.html:11\n#: shared-data/default-theme/html/jsapi/templates/modal-upload-key.html:15\n#: shared-data/default-theme/html/partials/hidden.html:52\n#: shared-data/default-theme/html/partials/sidebar.html:29\n#: shared-data/default-theme/html/partials/tag_add.html:37\n#: shared-data/default-theme/html/profiles/account-form.html:855\n#: shared-data/default-theme/html/tags/form.html:282\nmsgid \"Add\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/import/index.html:2\nmsgid \"Import Contacts\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/view/index.html:2\n#: shared-data/default-theme/html/contacts/view/index.html:9\n#: shared-data/default-theme/html/jsapi/crypto/find.js:23\n#: shared-data/default-theme/html/partials/search_item.html:166\nmsgid \"No Name\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/view/index.html:21\nmsgid \"Hey that's you\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/view/index.html:32\n#: shared-data/default-theme/html/message/draft/index.html:2\n#: shared-data/default-theme/html/partials/topbar.html:42\nmsgid \"Compose\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/view/index.html:33\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:103\n#: shared-data/default-theme/html/jsapi/templates/modal-search-keyservers.html:15\n#: shared-data/default-theme/html/partials/topbar.html:19\n#: shared-data/default-theme/html/partials/topbar.html:20\n#: shared-data/default-theme/html/search/default.html:65\nmsgid \"Search\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/view/index.html:41\nmsgid \"Security & Keys\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/view/index.html:84\nmsgid \"messages when communicating with\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/view/index.html:88\nmsgid \"\"\n\"You have no encryption keys for this contact. You need encryption keys in\"\n\" order to communicate securely.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/view/index.html:95\n#: shared-data/default-theme/html/jsapi/templates/modal-composer-encryption-helper.html:40\n#: shared-data/default-theme/html/jsapi/templates/modal-search-keyservers.html:8\n#: shared-data/default-theme/html/partials/tooltips.html:46\nmsgid \"Find Encryption Keys\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/view/index.html:121\n#: shared-data/default-theme/html/search/prev_more_next.html:40\n#: shared-data/default-theme/html/settings/preferences.html:58\n#: shared-data/default-theme/html/settings/preferences.html:59\n#: shared-data/default-theme/html/settings/preferences.html:62\nmsgid \"Conversations\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/contacts/view/index.html:147\nmsgid \"No Conversations\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:5\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:16\nmsgid \"Invalid TLS Certificate\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:21\nmsgid \"Show debug information\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:40\nmsgid \"The identity of the remote server ({H}) could not be verified.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:59\nmsgid \"SHA-256 Fingerprint\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:62\nmsgid \"Certificate Vitals\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:67\nmsgid \"TOFU failed; certificate hasn't been seen before.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:70\nmsgid \"TOFU is active; further validation is not required.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:73\nmsgid \"Has been seen on this server before.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:78\nmsgid \"Not valid for this server, issued to {S}.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:83\nmsgid \"Is valid for this server.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:88\nmsgid \"Issued and signed by {CA}.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:92\nmsgid \"Could not be validated against known Certificate Authorities.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:96\nmsgid \"Valid from {DATE1} until {DATE2}.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:104\nmsgid \"\"\n\"If the certificate cannot be verified, then there is no guarantee you are\"\n\" communicating with the right server.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:105\nmsgid \"Your account details or e-mail may be at risk if you proceed.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:108\nmsgid \"\"\n\"Some servers deliberately use certificates that cannot be verified; in \"\n\"such cases adding a security exception should be safe.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:109\nmsgid \"Ask your e-mail server administrator to be sure.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:117\nmsgid \"\"\n\"The information below could not be validated. It may be incorrect or \"\n\"forged.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:121\nmsgid \"Issued To\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:131\nmsgid \"Issued By\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:131\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:133\n#: shared-data/default-theme/html/partials/search_item.html:147\n#: shared-data/default-theme/html/partials/search_item.html:165\nmsgid \"unknown\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:133\nmsgid \"Apparently Issued By\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:143\nmsgid \"Raw PEM Certificate\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:150\nmsgid \"Current date appears to be {D}. Is the system clock correct?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:155\nmsgid \"\"\n\"If this certificate error is unsual, then adding a security exception is \"\n\"not recommended.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:162\nmsgid \"Try Again Later\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:173\nmsgid \"Show PEM\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:207\nmsgid \"TLS Certificates\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:211\nmsgid \"You can use this tool to examine the TLS certificates of remote servers.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:214\nmsgid \"\"\n\"TLS certificates are a form of digital identification, used to ensure you\"\n\" are communicating with the intended server and not an imposter.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:217\nmsgid \"\"\n\"If necessary, you can add security exceptions (TOFU: Trust on First Use) \"\n\"which will allow you to connect to a server even if it does not present a\"\n\" valid certificate.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:218\nmsgid \"\"\n\"When TOFU is in use, if the remote certificate ever changes the new one \"\n\"will be rejected until you add another exception.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/crypto/tls/getcert/index.html:234\nmsgid \"Known Certificates\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/filter/list/index.html:2\n#: shared-data/default-theme/html/partials/tools_tags.html:16\nmsgid \"Filters\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/filter/list/index.html:30\n#: shared-data/default-theme/html/partials/tools_search.html:222\n#: shared-data/default-theme/html/partials/tools_search.html:223\n#: shared-data/default-theme/html/partials/tooltips.html:9\n#: shared-data/default-theme/html/settings/recipes.html:48\n#: shared-data/default-theme/html/settings/recipes.html:71\nmsgid \"Edit\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/filter/list/index.html:31\n#: shared-data/default-theme/html/settings/recipes.html:49\n#: shared-data/default-theme/html/settings/recipes.html:72\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/filter/list/index.html:37\nmsgid \"No Filters Exist\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/filter/list/index.html:38\nmsgid \"\"\n\"Filters are clever rules that automatically apply tags, sort and \"\n\"otherwise handle your messages.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/help/bottom.html:2\nmsgid \"Hopefully this tip helped.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/help/bottom.html:3\nmsgid \"Let us know if something is missing or incorrect.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/help/bottom.html:4\nmsgid \"Or file an issue\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/help/index.html:2\n#: shared-data/default-theme/html/help/index.html:7\n#: shared-data/default-theme/html/page/gmail-2-step-verification/index.html:2\n#: shared-data/default-theme/html/page/gmail-2-step-verification/index.html:7\n#: shared-data/default-theme/html/page/gmail-access-non-google-accounts/index.html:2\n#: shared-data/default-theme/html/page/gmail-access-non-google-accounts/index.html:7\nmsgid \"Help\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/help/index.html:9\nmsgid \"Custom Searches\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/help/index.html:10\nmsgid \"\"\n\"Underneath the hood Mailpile is a high performance search engine that can\"\n\" answer complex questions.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/help/index.html:12\n#: shared-data/default-theme/html/jsapi/templates/modal-display-keybindings.html:5\nmsgid \"Keyboard Shortcuts\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/help/index.html:13\nmsgid \"\"\n\"Here will be a list of keyboard shortcuts / keybindings for all you cool \"\n\"cat power users out there!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/help/index.html:15\nmsgid \"Encryption & Security\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/help/index.html:16\nmsgid \"\"\n\"Are you new to all these terms like encryption and keys? Don't fret. We \"\n\"will do our best to help explain and educate you how to use your Mailpile\"\n\" in the most secure way possible.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/help/index.html:20\nmsgid \"Gmail's 2 Step App Passwords\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/help/index.html:21\n#: shared-data/default-theme/html/page/gmail-access-non-google-accounts/index.html:2\n#: shared-data/default-theme/html/page/gmail-access-non-google-accounts/index.html:10\nmsgid \"Enable Access for Non-Google Apps\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:33\nmsgid \"Search mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:34\nmsgid \"Compose e-mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:35\nmsgid \"Move selection down\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:36\nmsgid \"Extend selection down\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:37\nmsgid \"Move selection up\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:38\nmsgid \"Previous page of results\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:39\nmsgid \"Next page of results\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:40\nmsgid \"Open e-mail for reading\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:41\nmsgid \"Go to Drafts\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:42\nmsgid \"Go to Inbox\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:43\nmsgid \"Go to Outbox\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:44\nmsgid \"Go to Sent\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:45\nmsgid \"Go to Spam\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:46\nmsgid \"Go to Trash\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:47\nmsgid \"Go to All Mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:48\nmsgid \"Follow search hint\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:49\nmsgid \"Reply to e-mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:50\nmsgid \"Reply to many e-mails at once\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:52\nmsgid \"Forward one or more e-mails\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:54\nmsgid \"Mark as read\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:55\nmsgid \"Mark as unread\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:56\nmsgid \"Move to spam\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:57\nmsgid \"Archive e-mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:58\nmsgid \"Untag, remove e-mail from view\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:60\nmsgid \"Delete e-mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:61\nmsgid \"Undo last action\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:62\nmsgid \"Select all visible\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:63\nmsgid \"Select all matching search\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:65\nmsgid \"Deselect all\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:66\nmsgid \"Dismiss all notifications\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:68\nmsgid \"Account List\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:69\n#: shared-data/default-theme/html/layouts/content.html:57\n#: shared-data/default-theme/html/profiles/index.html:35\n#: shared-data/default-theme/html/profiles/index.html:174\n#: shared-data/default-theme/html/settings/index.html:61\n#: shared-data/default-theme/html/settings/privacy.html:7\n#: shared-data/default-theme/html/settings/privacy.html:24\nmsgid \"Security and Privacy Settings\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:104\nmsgid \"Show terminal (small).\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:180\nmsgid \"Check your network?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:181\nmsgid \"Restart the app?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/eventlog.js:275\n#: shared-data/default-theme/html/jsapi/index.js:231\n#: shared-data/default-theme/html/jsapi/ui/notifications.js:292\nmsgid \"Oops. Mailpile failed to complete your task.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:248\nmsgid \"Mailpile is unreachable.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/index.js:252\nmsgid \"Mailpile timed out...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/attachments.js:6\nmsgid \"\"\n\"There is still an attachment upload in progress. Are you sure you want to\"\n\" cancel the upload?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/attachments.js:158\nmsgid \"is\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/attachments.js:160\nmsgid \"Some people cannot receive such large e-mails.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/attachments.js:161\nmsgid \"Send it anyway?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/attachments.js:193\nmsgid \"Attachment upload failed: status\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/attachments.js:203\nmsgid \"Could not upload attachment because\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:137\nmsgid \"Neither signing nor encrypting.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:138\nmsgid \"Signing but not encrypting.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:139\nmsgid \"Encrypting but not signing.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:140\nmsgid \"Signing and encrypting.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:141\nmsgid \"Undefined state: \"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:205\nmsgid \"Verification Error\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:207\nmsgid \"Error accessing your encryption key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:212\nmsgid \"\"\n\"This message will be verifiable to recipients who have your encryption \"\n\"key. They will know it actually came from you :)\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:219\nmsgid \"\"\n\"This message will not be verifiable, recipients will have no way of \"\n\"knowing it actually came from you.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:256\nmsgid \"\"\n\"This message and attachments will be encrypted. The recipients & subject \"\n\"(metadata) will not\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:263\nmsgid \"\"\n\"This message cannot be encrypted because you do not have keys for one or \"\n\"more recipients\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:265\nmsgid \"Can Not Encrypt\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:270\nmsgid \"This message and metadata will not be encrypted\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:272\n#: shared-data/default-theme/html/partials/compose.html:97\n#: shared-data/default-theme/html/profiles/account-form.html:406\n#: shared-data/default-theme/html/profiles/account-form.html:501\n#: shared-data/default-theme/html/tags/form.html:116\nmsgid \"None\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:277\nmsgid \"There was an error prepping this message for encryption\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/crypto.js:279\nmsgid \"Error Encrypting\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/events.js:107\nmsgid \"Click OK to send the message anyway.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/events.js:114\n#: shared-data/default-theme/html/jsapi/compose/events.js:126\nmsgid \"Preparing to send...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/events.js:120\nmsgid \"Saving...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/compose/events.js:382\nmsgid \"Yay, Can Now Encrypt\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/crypto/find.js:24\nmsgid \"No Email\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/crypto/find.js:147\nmsgid \"Searching\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/crypto/import.js:83\nmsgid \"is too large:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/crypto/import.js:83\nmsgid \"You can not upload a key larger than 5 Megabytes.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/crypto/import.js:117\nmsgid \"Could not upload encryption key. Status:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/crypto/import.js:122\nmsgid \"Could not upload encryption key because\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/crypto/tooltips.js:9\nmsgid \"Click For Details\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/helpers.js:5\nmsgid \"What is an Encryption Key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/helpers.js:9\nmsgid \"What is Missing Encryption Key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:4\nmsgid \"Mail is being imported into your Mailpile\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:5\nmsgid \"Copying your mail, please do not be alarmed!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:6\nmsgid \"Copying mail. This could take a while\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:7\nmsgid \"Please be patient, we are copying mail as fast as possible\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:8\n#: shared-data/default-theme/html/jsapi/global/silly.js:45\nmsgid \"Wow, you have a lot of mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:9\nmsgid \"I hope you dont have a plane to catch or anything\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:10\nmsgid \"Copying mail. Put your arms above your head and whistle La Cucaracha\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:11\nmsgid \"Making a copy of your mail. Putting it in your inbox, all comfy like\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:14\nmsgid \"Damn kids.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:15\nmsgid \"RMS approves!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:16\nmsgid \"Formatting your C:\\\\ drive... (just kidding)\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:17\nmsgid \"Fortifying encryption shields\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:18\nmsgid \"Increasing entropy & scrambling bits\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:19\nmsgid \"Patching bugs...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:20\nmsgid \"Indexing kittens...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:21\nmsgid \"Indexing lovenotes\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:22\nmsgid \"Reticulating Splines\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:23\nmsgid \"Syntax error in line 45 of this e-mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:24\nmsgid \"Shoveling more coal into the server\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:25\nmsgid \"Calibrating flux capacitors\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:26\nmsgid \"A few bytes tried to escape, but we caught them\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:27\nmsgid \"Deterministically simulating future state\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:28\nmsgid \"Embiggening prototypes\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:29\nmsgid \"Resolving interdependence\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:30\nmsgid \"Spinning violently around the y-axis\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:31\nmsgid \"Locating additional gigapixels\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:32\nmsgid \"Initializing hamsters\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:33\nmsgid \"This is our world now... the world of the electron and the switch\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:34\nmsgid \"Fedexing all your spam to online advertisers...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:35\nmsgid \"Becoming self-aware\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:36\nmsgid \"Looking for heretofore unknown prime numbers\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:37\nmsgid \"Re-aligning satellite grid\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:38\nmsgid \"Re-routing bitstream\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:39\nmsgid \"Warming up particle accelerator\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:40\nmsgid \"Time is an illusion. Loading time doubly so\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:41\nmsgid \"Verifying local gravitational constant\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:42\nmsgid \"This server is powered by a lemon and two electrodes\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:46\nmsgid \"Mailpile has an advanced tagging system & search engine at its core\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:47\nmsgid \"Do you really need to keep all these Amazon Prime shipping confirmations?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:48\nmsgid \"I am pretty sure e-mail from ThinkGeeks 2011 X-mas sale can be deleted.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:49\nmsgid \"Do you really need an e-mail to know when you have been retweeted?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:50\nmsgid \"E-mail is the largest internet based social network on the planet\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:51\nmsgid \"Which other technology do you use that is 40 years old?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:52\nmsgid \"\"\n\"There are 2.5 billion e-mail users worldwide, that is double the amount \"\n\"of Facebook users!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:53\nmsgid \"Use Mailpile Tags to better organize and search your mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:54\nmsgid \"\"\n\"Remember getting your first e-mail address? Remember how it felt like \"\n\"something private?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:55\nmsgid \"\"\n\"E-mail is decentralized by design, this means no one company or \"\n\"government owns it!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:56\nmsgid \"Over 100 trillion e-mails are sent per year, wow!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:57\nmsgid \"\"\n\"E-mail is the most widely used communication protocol ever created by \"\n\"humans\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:58\nmsgid \"\"\n\"E-mail uses an open standard agreed upon by the entire world & owned by \"\n\"no one\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:61\nmsgid \"BCC-ing ALL THE SPIES!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:62\nmsgid \"Many powerful governments are conducting mass dragnet surveillance\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:63\nmsgid \"\"\n\"Most e-mail can be read by network operators as it travels through the \"\n\"internet\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:64\nmsgid \"\"\n\"Encryption ensures that your e-mails are only read by the intended \"\n\"recipient\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:65\nmsgid \"Unencrypted e-mail is more like sending a postcard than sending a letter\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:66\nmsgid \"Mailpile uses OpenPGP to encrypt and decrypt your messages securely\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:67\nmsgid \"All of your config settings & passwords are encrypted with AES-256\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:68\nmsgid \"Encrypting e-mails means your communication actually stays private\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:69\nmsgid \"The more encrypted e-mail you send, the better!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:70\nmsgid \"Make sure you print or save your keys & passwords somewhere secure\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:71\nmsgid \"Mailpile by default encrypts your search index!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:72\nmsgid \"The most common e-mail password is 123456, hopefully yours is different\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:75\nmsgid \"Good things come to those who wait\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:76\nmsgid \"Make Free Software and be happy\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:77\nmsgid \"Much of Mailpile was built in cafes in Reykjavík, Iceland\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:78\nmsgid \"Many Icelanders believe in elves and magical hidden people\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:79\nmsgid \"The founders of Mailpile first met in a public hot tub in Reykjavík\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:80\nmsgid \"We like volcanos, do you like volcanos?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:81\nmsgid \"A million hamsters are spinning their wheels right now\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:82\nmsgid \"Tapping earth for more geothermal energy\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:83\nmsgid \"Digging moat. Filling with alligators. Fortifying walls\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:84\nmsgid \"Crossing out swear words...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:85\nmsgid \"Compiling bullshit bingo grid...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:86\nmsgid \"Abandon all hope, ye who enter here\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:87\nmsgid \"Welcome to the nine circles of suffering\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:88\nmsgid \"Informing David Cameron of suspicious ac^H^H^H ... naaah :)\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:89\nmsgid \"Letting you wait for no apparent reason\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:90\nmsgid \"What are you wearing?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:91\nmsgid \"Go put the kettle on, this could be a while\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:92\nmsgid \"\"\n\"Go get a cup of tea and some biscuits. This will take approximately 4 \"\n\"custard creams worth of time\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:93\nmsgid \"Did you know humans share 50 percent of their DNA with a banana? Freaky\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:94\nmsgid \"Estimating chance of astroid hitting Earth\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:95\nmsgid \"Reading Terms of Service documents\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:96\nmsgid \"Catching up on shows on Netflix\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:97\nmsgid \"Oh, you have some very interesting old e-mails\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:98\nmsgid \"I think I better understand you now\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:99\nmsgid \"Your past is just a story you tell yourself\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:100\nmsgid \"Checking e-mails for stolen Winklevoss ideas\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:101\nmsgid \"Applying coupons...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:102\nmsgid \"Licking stamps...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:103\nmsgid \"Self potato\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:104\nmsgid \"Yum yum, that one was tasty\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:105\nmsgid \"\"\n\"Hey, there is some Nigerian prince here who wants to give you twenty \"\n\"million dollars...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:106\nmsgid \"How rude!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:107\nmsgid \"Now enhancing photos\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:108\nmsgid \"Backing up the entire Internet...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:109\nmsgid \"Really? You are still waiting?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:110\nmsgid \"Well... it sure is a beautiful day\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:111\nmsgid \"You should probably go outside or something\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:112\nmsgid \"Slacking off over here\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:113\nmsgid \"Doing nothing\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:114\nmsgid \"Making you wait for no reason\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:115\nmsgid \"Testing your patience\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:116\nmsgid \"Pay no attention to the man behind the curtain\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:117\nmsgid \"You are great just the way you are\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:118\nmsgid \"Warning: do not think of purple hippos\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:119\nmsgid \"Follow the white rabbit\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:120\nmsgid \"Wanna see how deep the rabbit hole goes?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:121\nmsgid \"Supplying monkeys with typewriters\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:122\nmsgid \"Waiting for Godot.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/global/silly.js:123\nmsgid \"Swapping time and space\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/message/events.js:63\nmsgid \"Plain Text\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/message/html-sandbox.js:280\nmsgid \"\"\n\"This message references images or other content from the web. Downloading\"\n\" and displaying these images may notify the sender that you have read the\"\n\" mail.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/message/html-sandbox.js:282\nmsgid \"Okay, display the images\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/message/html-sandbox.js:283\nmsgid \"Always display images from this sender\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/message/html-sandbox.js:284\nmsgid \"No, thanks!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/message/tooltips.js:63\n#: shared-data/default-theme/html/partials/pile_message.html:151\n#: shared-data/default-theme/html/partials/pile_message.html:201\n#: shared-data/default-theme/html/partials/search_item.html:196\nmsgid \"Download\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/message/ui.js:4\nmsgid \"Loading message...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/message/ui.js:10\nmsgid \"Could not retrieve message\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/search/bulk_actions.js:80\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:13\nmsgid \"Search for Similar E-mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/search/ui.js:134\nmsgid \"No Messages Selected\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/search/ui.js:155\nmsgid \"1 conversation\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/search/ui.js:160\nmsgid \"conversations\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/search/ui.js:163\nmsgid \"Moving\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/tags/events.js:107\nmsgid \"Are you sure you want to delete this tag?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/tags/events.js:108\nmsgid \"This action cannot be undone.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-add-tag.html:6\n#: shared-data/default-theme/html/partials/sidebar.html:27\n#: shared-data/default-theme/html/partials/tools_tags.html:10\n#: shared-data/default-theme/html/tags/add/index.html:2\n#: shared-data/default-theme/html/tags/add/index.html:4\nmsgid \"Add Tag\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-add-tag.html:10\n#: shared-data/default-theme/html/partials/tag_add.html:8\nmsgid \"Friends & Family\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-compose-quoted-reply.html:7\nmsgid \"Quoted Replies\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-compose-quoted-reply.html:12\nmsgid \"Would you like to disable quoted replies in all the messages you compose?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-compose-quoted-reply.html:13\nmsgid \"Disable Quoted Replies\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-compose-quoted-reply.html:14\n#: shared-data/default-theme/html/partials/compose.html:201\n#: shared-data/default-theme/html/partials/compose.html:202\n#: shared-data/default-theme/html/partials/pile_compose.html:170\n#: shared-data/default-theme/html/partials/pile_compose.html:171\n#: shared-data/default-theme/html/partials/tools_search.html:231\n#: shared-data/default-theme/html/profiles/edit/index.html:8\n#: shared-data/default-theme/html/settings/mailbox/index.html:89\n#: shared-data/default-theme/html/setup/oauth2/index.html:38\n#: shared-data/default-theme/html/tags/form.html:284\nmsgid \"Save\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-composer-encryption-helper.html:7\nmsgid \"Cannot Encrypt\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-composer-encryption-helper.html:15\nmsgid \"You are missing encryption keys for the following contacts\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-composer-encryption-helper.html:50\nmsgid \"Yourself!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-composer-encryption-helper.html:53\nmsgid \"\"\n\"You cannot send encrypted or signed mail without an encryption key of \"\n\"your own.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-composer-encryption-helper.html:54\nmsgid \"Check your account settings or send using a different profile.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-composer-encryption-helper.html:62\n#: shared-data/default-theme/html/partials/hidden.html:59\nmsgid \"Searching for encryption keys for:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-composer-encryption-helper.html:74\nmsgid \"Searching for encryption keys...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-composer-encryption-helper.html:88\nmsgid \"Send Unencrypted\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-composer-encryption-helper.html:92\nmsgid \"\"\n\"Tick this box to keep searching, even after keys have been found in \"\n\"preferred locations.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-composer-encryption-helper.html:93\nmsgid \"Search All Sources\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-composer-encryption-helper.html:98\n#: shared-data/default-theme/html/partials/hidden.html:151\nmsgid \"Try Again\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-display-keybindings.html:10\n#: shared-data/default-theme/html/settings/index.html:121\nmsgid \"Keys\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-display-keybindings.html:11\nmsgid \"Action\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:19\nmsgid \"Select which common message attributes you would like to search for.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:20\nmsgid \"The more attributes you select, the narrower the search.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:25\n#: shared-data/default-theme/html/partials/compose.html:31\n#: shared-data/default-theme/html/partials/compose.html:32\n#: shared-data/default-theme/html/partials/pile_compose.html:43\n#: shared-data/default-theme/html/partials/pile_compose.html:46\n#: shared-data/default-theme/html/partials/search_item.html:143\n#: shared-data/default-theme/html/partials/search_item.html:160\n#: shared-data/default-theme/html/partials/search_item.html:260\nmsgid \"Subject\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:29\n#: shared-data/default-theme/html/partials/hidden.html:20\n#: shared-data/default-theme/html/partials/search_item.html:74\n#: shared-data/default-theme/html/partials/search_item.html:144\n#: shared-data/default-theme/html/partials/search_item.html:161\nmsgid \"From\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:37\nmsgid \"Mailing list\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:42\nmsgid \"E-mail client\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:46\nmsgid \"Message structure\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:50\nmsgid \"Sender fingerprint\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:59\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:66\nmsgid \"Similar dates\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:59\nmsgid \"week\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:66\nmsgid \"weeks\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:72\nmsgid \"Has an image\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:78\nmsgid \"Has an attachment\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:85\nmsgid \"Has a digital signature\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:91\nmsgid \"Is encrypted\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-related-search.html:108\nmsgid \"Additional search terms\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-save-search.html:8\n#: shared-data/default-theme/html/jsapi/templates/modal-save-search.html:63\nmsgid \"Save Search\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-save-search.html:13\nmsgid \"Saved Searches appear as Tags in the sidebar.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-save-search.html:14\nmsgid \"\"\n\"New matching messages will be tagged automatically as they arrive, but \"\n\"you can also add or remove messages by hand.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-save-search.html:17\nmsgid \"Configure new Saved Search\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-save-search.html:22\n#: shared-data/default-theme/html/tags/form.html:33\nmsgid \"Change Icon\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-save-search.html:24\n#: shared-data/default-theme/html/tags/form.html:39\nmsgid \"Change Color\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-save-search.html:24\n#: shared-data/default-theme/html/tags/form.html:39\nmsgid \"Color\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-save-search.html:27\nmsgid \"Save as\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-save-search.html:28\n#: shared-data/default-theme/html/tags/form.html:45\nmsgid \"Tag Name\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-save-search.html:35\nmsgid \"Mark as Read\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-save-search.html:37\nmsgid \"Remove from Inbox\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-save-search.html:39\nmsgid \"Never send to Spam\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-save-search.html:45\n#: shared-data/default-theme/html/tags/form.html:133\nmsgid \"Choose an Icon\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-save-search.html:51\n#: shared-data/default-theme/html/tags/form.html:141\nmsgid \"Choose a Color\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-save-search.html:69\nmsgid \"Create new Saved Search\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-save-search.html:72\nmsgid \"Replace\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-save-search.html:80\n#: shared-data/default-theme/html/profiles/remove/index.html:96\n#: shared-data/default-theme/html/setup/oauth2/index.html:42\n#: shared-data/default-theme/html/setup/password/index.html:99\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-search-keyservers.html:13\nmsgid \"Enter Name or Email Address\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-search-keyservers.html:19\nmsgid \"Want to search for something else\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-search-keyservers.html:19\nmsgid \"click here\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-search-keyservers.html:24\nmsgid \"Searching for encryption keys for\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-send-public-key.html:5\nmsgid \"Send Encryption Keys\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-send-public-key.html:12\nmsgid \"Send Selected Keys\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-tag-picker.html:7\n#: shared-data/default-theme/html/partials/tag_add.html:7\n#: shared-data/default-theme/html/partials/tag_add.html:15\n#: shared-data/default-theme/html/tags/form.html:43\nmsgid \"Tag\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-tag-picker.html:7\n#: shared-data/default-theme/html/jsapi/templates/template-modal-tag-picker-item.html:9\n#: shared-data/default-theme/html/partials/tools_search.html:53\n#: shared-data/default-theme/html/settings/preferences.html:60\n#: shared-data/default-theme/html/settings/preferences.html:61\nmsgid \"Messages\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-tag-picker.html:13\n#: shared-data/default-theme/html/partials/tools_tags.html:4\n#: shared-data/default-theme/html/partials/topbar.html:54\n#: shared-data/default-theme/html/tags/index.html:2\n#: shared-data/default-theme/html/tags/index.html:46\n#: shared-data/default-theme/html/tags/sidebar.html:2\nmsgid \"Tags\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-tag-picker.html:21\n#: shared-data/default-theme/html/jsapi/templates/modal-tag-picker.html:22\nmsgid \"Remove\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-tag-picker.html:24\n#: shared-data/default-theme/html/jsapi/templates/modal-tag-picker.html:25\nmsgid \"Apply\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-upload-key.html:5\nmsgid \"Upload Encryption Key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-upload-key.html:9\nmsgid \"Something went wrong uploading encryption key. Try again?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-upload-key.html:14\nmsgid \"Select or Drag Encryption Key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-upload-key.html:14\nmsgid \"A file located on your computer usually ending in: .asc .key .pub\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-upload-key.html:16\nmsgid \"Select Encryption Key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-upload-key.html:18\nmsgid \"Unable to create uploader\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/templates/modal-upload-key.html:18\n#: shared-data/default-theme/html/partials/compose.html:78\n#: shared-data/default-theme/html/partials/pile_compose.html:94\nmsgid \"update your browser\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/notifications.js:79\nmsgid \"Password Required\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/notifications.js:170\nmsgid \"Found over (LIMIT) mailboxes\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/notifications.js:183\nmsgid \"continue adding more\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/notifications.js:247\nmsgid \"Working...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/notifications.js:329\nmsgid \"This may take some time!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/notifications.js:377\nmsgid \"edit settings\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/notifications.js:380\n#: shared-data/default-theme/html/jsapi/ui/notifications.js:430\nmsgid \"details\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/notifications.js:385\nmsgid \"please log in\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/notifications.js:396\n#: shared-data/default-theme/html/jsapi/ui/notifications.js:421\nmsgid \"grant access\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/selection.js:142\n#: shared-data/default-theme/html/partials/tools_search.html:191\nmsgid \"All\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/sidebar.js:96\n#: shared-data/default-theme/html/partials/hidden.html:20\nmsgid \"to\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:6\nmsgid \"Tagged 1 message\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:7\nmsgid \"Tagged (num) messages\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:8\nmsgid \"Untagged 1 message\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:9\nmsgid \"Untagged (num) messages\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:10\nmsgid \"Marked 1 message read\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:11\nmsgid \"Marked (num) messages read\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:12\nmsgid \"Marked 1 message unread\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:13\nmsgid \"Marked (num) messages unread\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:14\nmsgid \"Moved 1 message\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:15\nmsgid \"Moved (num) messages\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:16\nmsgid \"Archived 1 message\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:17\nmsgid \"Archived (num) messages\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:18\nmsgid \"Moved 1 message to trash\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:19\nmsgid \"Moved (num)  messages to trash\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:20\nmsgid \"Moved 1 message out of spam\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:21\nmsgid \"Moved (num)  messages out of spam\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:22\nmsgid \"Moved 1 message to spam\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:23\nmsgid \"Moved (num) messages to spam\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tagging.js:37\nmsgid \"Tagging...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/jsapi/ui/tooltips.js:49\nmsgid \"Compose to:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/layouts/content.html:42\nmsgid \"Uh oh! You have Javascript disabled. This will cause problems.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/layouts/content.html:45\nmsgid \"The AGPLv3 License\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/layouts/content.html:46\nmsgid \"Copyright\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/layouts/content.html:48\nmsgid \"Visit www.mailpile.is\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/layouts/content.html:51\nmsgid \"Credits and Thanks\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/layouts/content.html:52\nmsgid \"the community\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/layouts/content.html:54\n#: shared-data/default-theme/html/page/release-notes/index.html:9\n#, python-format\nmsgid \"Welcome to Mailpile %(version)s\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/layouts/content.html:55\nmsgid \"About\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/layouts/content.html:58\n#: shared-data/default-theme/html/profiles/index.html:34\n#: shared-data/default-theme/html/settings/index.html:58\nmsgid \"Privacy\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/layouts/full.html:10\n#: shared-data/default-theme/html/partials/topbar.html:3\nmsgid \"Somebody\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/layout.html:9\nmsgid \"Settings and Technical Tools\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/events/index.html:55\n#: shared-data/default-theme/html/logs/layout.html:14\n#: shared-data/default-theme/html/settings/index.html:81\nmsgid \"Event Log\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/events/index.html:2\n#: shared-data/default-theme/html/logs/layout.html:15\n#: shared-data/default-theme/html/settings/index.html:84\nmsgid \"Mailpile Event Log\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/layout.html:20\n#: shared-data/default-theme/html/settings/index.html:89\nmsgid \"Network\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/layout.html:21\n#: shared-data/default-theme/html/logs/network/index.html:3\n#: shared-data/default-theme/html/settings/index.html:92\nmsgid \"Recent Network Activity\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/layout.html:23\nmsgid \"Settings, logs, events, troubleshooting, ...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/events/index.html:37\nmsgid \"Undo\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/events/index.html:58\nmsgid \"Ongoing Events\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/events/index.html:62\nmsgid \"\"\n\"Ongoing events represent current actions taken by Mailpile on your \"\n\"behalf, such as watching a Mail Source for new mail or refreshing your \"\n\"contact database.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/events/index.html:66\nmsgid \"\"\n\"Browsing the event details can help with troubleshooting if Mailpile is \"\n\"not behaving as you expect.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/events/index.html:79\nmsgid \"No Events Found\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/events/index.html:96\nmsgid \"Completed Events\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/events/index.html:100\nmsgid \"\"\n\"The Event Log gives an overview over what has happened in your Mailpile \"\n\"recently.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/events/index.html:104\nmsgid \"Some events, such as Tagging operations, can be undone.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/network/index.html:7\nmsgid \"Network Activity\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/network/index.html:14\nmsgid \"Recent Activity\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/network/index.html:19\nmsgid \"\"\n\"Here you can see a log of recent network activity, both successful and \"\n\"failed attempts.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/network/index.html:24\n#, python-format\nmsgid \"You have a proxy (%(proxy)s) configured at %(host)s:%(port)s.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/network/index.html:32\nmsgid \"\"\n\"Proxy fallback is disabled, so if the proxy cannot connect to a given \"\n\"server, the connection will fail.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/network/index.html:34\nmsgid \"\"\n\"Some service providers block connections from Tor, so this may prevent \"\n\"you from accessing your mail or other data.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/network/index.html:39\nmsgid \"\"\n\"You can configure destinations to bypass the proxy in the Advanced \"\n\"section of Networking in your Privacy Policy.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/network/index.html:45\nmsgid \"\"\n\"Fall-back is enabled, so a direct connection will be made if Tor cannot \"\n\"connect.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/network/index.html:46\n#: shared-data/default-theme/html/logs/network/index.html:63\nmsgid \"\"\n\"This may leak your IP address and allow monitoring of which servers you \"\n\"communicate with.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/network/index.html:48\nmsgid \"\"\n\"Fall-back is enabled, so a direct connection will be made if the proxy \"\n\"cannot connect.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/network/index.html:55\nmsgid \"You are using Tor for unencrypted traffic.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/network/index.html:56\nmsgid \"\"\n\"This may allow exit-node operators to listen in or modify your \"\n\"communications.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/network/index.html:62\nmsgid \"You are not using Tor.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/logs/network/index.html:67\nmsgid \"Edit your Privacy Policy.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/bugs.html:2\nmsgid \"Reporting bugs\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/bugs.html:6\nmsgid \"Reporting Bugs\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/bugs.html:8\nmsgid \"Note:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/bugs.html:9\nmsgid \"\"\n\"These instructions are in English only, because we are currently unable \"\n\"to accept bug reports in other languages. Sorry!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/bugs.html:12\nmsgid \"Thank you for testing Mailpile!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/index.html:2\n#: shared-data/default-theme/html/page/contribute/index.html:6\nmsgid \"Help Mailpile Grow\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/index.html:8\nmsgid \"\"\n\"Mailpile is created by a diverse community of people and organizations \"\n\"who care about freedom and privacy.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/index.html:9\nmsgid \"Please join our community and contribute in any way you can!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/index.html:10\nmsgid \"Thank you!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/index.html:13\nmsgid \"Become a backer\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/index.html:15\nmsgid \"Donate\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/index.html:19\nmsgid \"\"\n\"Employing people to work on Mailpile full time is the fastest way to \"\n\"improve the software.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/index.html:20\nmsgid \"\"\n\"Although the software is (and will always be) free, salaries still cost \"\n\"money and your support matters.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/index.html:21\nmsgid \"\"\n\"If you contribute $23 USD or more, you get access to our community voting\"\n\" platform, where you can have a say in the future direction of the \"\n\"project:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/index.html:25\nmsgid \"Tell your friends\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/index.html:28\nmsgid \"If you like Mailpile, please tell people about it!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/index.html:29\nmsgid \"\"\n\"Tweet, share, link, write articles and blog posts, act out dramatic \"\n\"tutorials on Youtube, make movies...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/index.html:30\nmsgid \"\"\n\"Let us know what you create, so we can share the best content with the \"\n\"rest of the community.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/index.html:31\nmsgid \"We are:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/index.html:37\nmsgid \"Geek out!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/index.html:39\nmsgid \"If you like breaking things, you can find and report bugs.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/index.html:40\nmsgid \"If you are fluent in an interesting language, you can help translate.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/contribute/index.html:41\nmsgid \"If you are a hacker, you can contribute code.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/entropy/index.html:2\n#: shared-data/default-theme/html/page/entropy/index.html:7\nmsgid \"Increasing Entropy\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/entropy/index.html:10\nmsgid \"Mailpile needs true random numbers to generate good encryption keys.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/entropy/index.html:13\nmsgid \"\"\n\"Although your computer may be powerful, it may still have hard time \"\n\"finding enough randomness when generating keys.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/entropy/index.html:14\nmsgid \"The technical term used to describe the source of randomness is Entropy!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/entropy/index.html:17\nmsgid \"\"\n\"On the machine that is running Mailpile you can do a few things to \"\n\"increase the Entropy:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/entropy/index.html:20\nmsgid \"Move the mouse.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/entropy/index.html:21\nmsgid \"Perform other activity which requires using the keyboard.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/entropy/index.html:22\nmsgid \"Start a process that utilizes the disk(s).\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/entropy/index.html:23\nmsgid \"Browse the internet.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/entropy/index.html:27\nmsgid \"\"\n\"Because the computer cannot predict human activity these actions help the\"\n\" computer generate random numbers.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/gmail-2-step-verification/index.html:2\n#: shared-data/default-theme/html/page/gmail-2-step-verification/index.html:10\nmsgid \"Gmail's 2-Step App Passwords\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/multipile/index.html:2\n#: shared-data/default-theme/html/page/multipile/index.html:46\nmsgid \"Your Mailpile may not be running\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/multipile/index.html:27\nmsgid \"Launch Mailpile as ...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/multipile/index.html:32\n#: shared-data/default-theme/html/page/multipile/index.html:33\nmsgid \"Unix Username\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/multipile/index.html:37\n#: shared-data/default-theme/html/page/multipile/index.html:38\nmsgid \"Unix Password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/multipile/index.html:48\nmsgid \"\"\n\"To launch the app please enter your account details above or use a \"\n\"terminal (SSH) to start Mailpile manually.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/credits.html:2\n#: shared-data/default-theme/html/page/release-notes/credits.html:6\nmsgid \"Credits\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/credits.html:8\nmsgid \"Core Team\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/credits.html:10\nmsgid \"Papa Smurf\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/credits.html:11\nmsgid \"Security, community, code\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/credits.html:12\nmsgid \"UI and design\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/credits.html:15\nmsgid \"\"\n\"The Mailpile team would like to thank all the people who contributed\\n\"\n\"    money or volunteered their time to help create a truly free, secure\\n\"\n\"    e-mail client. You're all awesome!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/credits.html:22\nmsgid \"Community\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/credits.html:35\nmsgid \"Financial Backers\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/credits.html:43\nmsgid \"... and many more!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/credits.html:49\nmsgid \"Code Contributions\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/credits.html:57\nmsgid \"Translations\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:2\n#, python-format\nmsgid \"Mailpile %(version)s release notes\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:13\nmsgid \"Thank you for testing our release candidate!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:14\n#, python-format\nmsgid \"If no critical bugs are found, this will become version %(version)s.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:19\n#, python-format\nmsgid \"What is Mailpile %(version)s?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:21\nmsgid \"An e-mail client with a strong focus on privacy\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:22\nmsgid \"A tool for organizing and searching large volumes of e-mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:23\nmsgid \"An easy way to get started with PGP e-mail encryption\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:26\nmsgid \"What is Mailpile not?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:28\nmsgid \"A synchronizing IMAP client:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:29\nmsgid \"Local changes stay local, we don't tell the server\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:30\nmsgid \"A money-making enterprise\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:31\nmsgid \"A calendar\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:32\nmsgid \"Finished.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:35\nmsgid \"Help make it better.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:40\nmsgid \"That's it!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:41\nmsgid \"Read on if you've used one of the Mailpile Beta or GitHub releases.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:44\nmsgid \"What's new?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:46\nmsgid \"A better (shorter) setup process\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:47\nmsgid \"Support for Gmail OAuth 2.0 authentication\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:48\nmsgid \"Improved PGP support, including automatic importing of keys\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:49\n#, python-format\nmsgid \"\"\n\"Per-tag automatic bayesian classification, as <a target=_blank \"\n\"href=\\\"%(blog_url)s\\\">discussed on our blog</a>\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:51\nmsgid \"The ability to delete e-mail and accounts\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:52\nmsgid \"A more responsive (almost) mobile-friendly web interface\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:55\nmsgid \"What's fixed?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:57\n#, python-format\nmsgid \"\"\n\"See GitHub for <a target=_blank href=\\\"%(closed_url)s\\\">all closed \"\n\"issues</a> and <a target=_blank href=\\\"%(release_url)s\\\">issues closed \"\n\"for this release</a>\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:63\nmsgid \"What's still broken?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:65\nmsgid \"Mailpile is still rather slow and RAM-hungry\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:66\nmsgid \"IMAP synchronization is not yet implemented\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:67\n#, python-format\nmsgid \"\"\n\"The <a target=_blank href=\\\"%(roadmap_url)s\\\">Security Roadmap</a> is \"\n\"incomplete\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:70\n#, python-format\nmsgid \"Github has <a target=_blank href=\\\"%(issues_url)s\\\">many open issues</a>.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:72\nmsgid \"You can help.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/index.html:79\n#: shared-data/default-theme/html/partials/pile_email_hints.html:15\n#: shared-data/default-theme/html/partials/pile_email_hints.html:29\nmsgid \"OK, got it\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/license.html:9\nmsgid \"The Mailpile license is in English.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/page/release-notes/license.html:10\nmsgid \"Translations may be available on the FSF web site.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/compose.html:8\n#: shared-data/default-theme/html/partials/compose.html:14\n#: shared-data/default-theme/html/partials/compose.html:106\n#: shared-data/default-theme/html/partials/pile_compose.html:18\n#: shared-data/default-theme/html/partials/search_item.html:145\n#: shared-data/default-theme/html/partials/search_item.html:162\nmsgid \"To\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/compose.html:11\n#: shared-data/default-theme/html/partials/compose.html:17\n#: shared-data/default-theme/html/partials/compose.html:21\n#: shared-data/default-theme/html/partials/pile_compose.html:19\nmsgid \"Cc\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/compose.html:12\n#: shared-data/default-theme/html/partials/compose.html:24\n#: shared-data/default-theme/html/partials/compose.html:28\n#: shared-data/default-theme/html/partials/pile_compose.html:20\nmsgid \"Bcc\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/compose.html:19\nmsgid \"hide\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/compose.html:36\n#: shared-data/default-theme/html/partials/pile_compose.html:52\nmsgid \"Your Message...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/compose.html:77\n#: shared-data/default-theme/html/partials/pile_compose.html:93\nmsgid \"Add Attachment\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/compose.html:78\n#: shared-data/default-theme/html/partials/pile_compose.html:94\nmsgid \"Unable to add attachments\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/compose.html:82\n#: shared-data/default-theme/html/partials/pile_compose.html:98\nmsgid \"Attach your public encryption key to this message\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/compose.html:88\n#: shared-data/default-theme/html/partials/pile_compose.html:105\nmsgid \"Attach Key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/compose.html:106\nmsgid \"hide details\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/compose.html:109\n#: shared-data/default-theme/html/partials/pile_compose.html:32\nmsgid \"autosaving...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/compose.html:109\n#: shared-data/default-theme/html/partials/pile_compose.html:33\nmsgid \"error autosaving\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/compose.html:114\nmsgid \"Quote\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/compose.html:150\nmsgid \"Choose Sending Account\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/compose.html:200\n#: shared-data/default-theme/html/partials/pile_compose.html:11\nmsgid \"Move Draft to Trash\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/compose.html:207\n#: shared-data/default-theme/html/partials/compose.html:208\n#: shared-data/default-theme/html/partials/pile_compose.html:173\n#: shared-data/default-theme/html/partials/pile_compose.html:174\nmsgid \"Send\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/error_message_missing.html:3\nmsgid \"Message Not Found\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/error_message_missing.html:20\nmsgid \"Display technical details.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/error_message_missing.html:32\nmsgid \"We were unable to find the message you requested, perhaps it was deleted?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/error_message_missing.html:37\n#, python-format\nmsgid \"\"\n\"If you think this is a bug, please <a href=\\\"%(url)s\\\" \"\n\"target=\\\"_blank\\\">file a report</a> including the technical details \"\n\"above.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/error_message_missing.html:47\nmsgid \"DESCRIBE YOUR PROBLEM HERE\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/errors_content.html:3\nmsgid \"Contact Missing\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/errors_content.html:4\nmsgid \"\"\n\"We were unable to find the contact you requested, perhaps it was typed \"\n\"(or linked to) incorrectly?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/errors_content.html:6\nmsgid \"Tag Missing\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/errors_content.html:7\nmsgid \"\"\n\"We were unable to find the tag you requested, perhaps it was typed (or \"\n\"linked to) incorrectly?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/errors_content.html:9\nmsgid \"Oops, You've Found a Quirk\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/errors_content.html:10\nmsgid \"\"\n\"There's something happening here. What it is ain't exactly clear. \"\n\"Everybody look what's going down.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/errors_content.html:12\n#, python-format\nmsgid \"\"\n\"If you think this is a bug, please file a <a href=\\\"%(url)s\\\" \"\n\"target=\\\"_blank\\\">bug report</a>\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/helpers.html:14\n#: shared-data/default-theme/html/partials/helpers.html:15\nmsgid \"Don't Show\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/helpers.html:45\n#: shared-data/default-theme/html/partials/hidden.html:88\nmsgid \"Contact Info\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/helpers.html:55\n#: shared-data/default-theme/html/partials/hidden.html:101\nmsgid \"Score\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/helpers.html:60\n#: shared-data/default-theme/html/partials/hidden.html:107\nmsgid \"Details\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/helpers.html:61\n#: shared-data/default-theme/html/partials/hidden.html:108\nmsgid \"Strength\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/helpers.html:62\n#: shared-data/default-theme/html/partials/hidden.html:109\nmsgid \"Algorithm\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/helpers.html:63\n#: shared-data/default-theme/html/partials/hidden.html:110\n#: shared-data/default-theme/html/partials/hidden.html:178\nmsgid \"Created\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/helpers.html:77\nmsgid \"Use This Key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/helpers.html:78\nmsgid \"Don't Use This Key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:20\nmsgid \"Start Date\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:20\nmsgid \"Present\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:21\nmsgid \"With\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:21\nmsgid \"No Tags\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:22\nmsgid \"In\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:22\nmsgid \"All Groups\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:23\nmsgid \"And\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:23\nmsgid \"All Contacts\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:63\nmsgid \"No encryption keys found matching:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:67\nmsgid \"Make sure your internet connection is working\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:123\nmsgid \"Don't use this key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:128\nmsgid \"This key is available for use.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:137\nmsgid \"Import Key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:150\nmsgid \"Importing Failed\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:153\nmsgid \"Importing Encryption Key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:156\nmsgid \"Uploading:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:158\nmsgid \"This may take a few moments...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:177\nmsgid \"Key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:179\nmsgid \"Key Size\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:180\nmsgid \"Key Type\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:188\nmsgid \"Show Hidden Encryption Keys\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:191\nmsgid \"Hidden keys are either revoked or expired and should not be used\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:237\nmsgid \"Draft was deleted\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:237\nmsgid \"click to reload\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:255\nmsgid \"do it now?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:257\nmsgid \"A mistake?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:257\nmsgid \"undo\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:266\nmsgid \"Encryption Key from\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:266\nmsgid \"Import Encryption Key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/hidden.html:267\nmsgid \"Show Encryption Key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_email_hints.html:7\nmsgid \"This message has HTML formatted content.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_email_hints.html:8\nmsgid \"For security reasons, Mailpile will by default only display plain text.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_email_hints.html:10\nmsgid \"\"\n\"You can change display modes by clicking the icons to the right of the \"\n\"Subject line.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_email_hints.html:23\nmsgid \"\"\n\"Usually when you reply to a message, all participants in the conversation\"\n\" will be sent a copy of your reply.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_email_hints.html:25\nmsgid \"\"\n\"If you want to send a private reply, only to the author of this message, \"\n\"mouse over their name and click the reply icon that appears.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_message.html:77\nmsgid \"Blocked attachments, replies, and links.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_message.html:78\nmsgid \"Attachments and replies are blocked.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_message.html:79\nmsgid \"Links are blocked.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_message.html:91\nmsgid \"Remove from spam\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_message.html:92\n#: shared-data/default-theme/html/partials/pile_message.html:96\n#: shared-data/default-theme/html/partials/pile_message.html:102\nmsgid \"Show risky content\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_message.html:97\n#: shared-data/default-theme/html/partials/pile_message.html:103\nmsgid \"Send to spam\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_message.html:101\nmsgid \"Add to contacts\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_message.html:189\nmsgid \"Unknown Text Part\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_message.html:207\nmsgid \"HTML rendering requires Javascript, sorry!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_message.html:216\nmsgid \"Message content is empty\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_message.html:229\n#: shared-data/default-theme/html/partials/pile_message.html:257\nmsgid \"Untitled\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_message.html:299\nmsgid \"Reply\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_message.html:300\nmsgid \"Forward\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_threading.html:3\nmsgid \"Conversation\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/pile_threading.html:43\nmsgid \"Show entire conversation\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/search_item.html:53\nmsgid \"Previous Conversation\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/search_item.html:59\nmsgid \"Close\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/search_item.html:62\nmsgid \"Next Conversation\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/search_item.html:91\nmsgid \"Private Reply\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/search_item.html:224\nmsgid \"Download from the web\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/search_item.html:239\nmsgid \"Oops, no attachments found!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/search_item.html:240\nmsgid \"A bug?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/search_item.html:241\nmsgid \"Click to view the message.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/search_item.html:274\nmsgid \"Display HTML formatted message content\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/search_item.html:278\nmsgid \"Display plain-text message content\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/search_item.html:296\nmsgid \"Switch to full conversation view\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/search_item.html:299\nmsgid \"Display message source code\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/search_item.html:302\nmsgid \"Display technical message data as JSON\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/sidebar.html:15\nmsgid \"Remove tags\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/sidebar.html:32\nmsgid \"Done\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/sidebar.html:33\nmsgid \"Edit Sidebar\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/sidebar.html:35\nmsgid \"Organize\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tag_add.html:3\nmsgid \"Add New Tag\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tag_add.html:10\nmsgid \"Slug\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tag_add.html:11\nmsgid \"friends-family\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tag_add.html:13\n#: shared-data/default-theme/html/partials/tools_search.html:66\nmsgid \"Display\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tag_add.html:16\nmsgid \"Archive\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tag_add.html:21\nmsgid \"none\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tag_add.html:29\nmsgid \"Template\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tag_add.html:31\nmsgid \"Default\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tag_add.html:34\nmsgid \"Search Terms\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/thread_message_cryptofail.html:12\nmsgid \"Send Your Encryption Key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/thread_message_cryptofail.html:16\nmsgid \"Password incorrect?  Try again!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/thread_message_cryptofail.html:21\nmsgid \"Decrypt Message\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_contacts.html:3\nmsgid \"Search and import encryption keys\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_contacts.html:4\nmsgid \"Import Encryption Keys\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_contacts.html:9\nmsgid \"Search for Key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_contacts.html:14\nmsgid \"Upload Key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_contacts.html:20\nmsgid \"Import from URL\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_contacts.html:27\nmsgid \"What's This?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_contacts.html:38\nmsgid \"View your contacts\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_contacts.html:44\nmsgid \"Add a new contact\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_default.html:6\nmsgid \"Click item or checkbox to select\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_default.html:85\nmsgid \"conversations selected\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_default.html:86\nmsgid \"conversations selected, click to select all matching this search\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_default.html:87\nmsgid \"Entire search selected, click to undo\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:31\nmsgid \"Order\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:38\nmsgid \"Freshness\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:43\n#: shared-data/default-theme/html/settings/preferences.html:58\n#: shared-data/default-theme/html/settings/preferences.html:60\nmsgid \"Newest First\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:48\n#: shared-data/default-theme/html/settings/preferences.html:59\n#: shared-data/default-theme/html/settings/preferences.html:61\nmsgid \"Oldest First\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:58\n#: shared-data/default-theme/html/settings/preferences.html:62\nmsgid \"Unsorted\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:70\n#: shared-data/default-theme/html/settings/preferences.html:80\nmsgid \"Snug\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:74\n#: shared-data/default-theme/html/settings/preferences.html:79\nmsgid \"Cozy\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:78\n#: shared-data/default-theme/html/settings/preferences.html:78\nmsgid \"Comfy\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:91\n#, python-format\nmsgid \"Toggle %(tag_name)s\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:94\n#, python-format\nmsgid \"Move to %(tag_name)s\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:117\nmsgid \"Move to Spam\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:126\n#: shared-data/default-theme/html/tags/form.html:266\nmsgid \"Move to Trash\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:135\nmsgid \"Untag Selection\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:144\nmsgid \"Archive Selection\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:144\nmsgid \"untag completely\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:162\nmsgid \"Unread messages\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:172\nmsgid \"Images\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:173\nmsgid \"Photos and images in mail matching this search\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:181\nmsgid \"Attachments\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:182\nmsgid \"Attachments in mail matching this search\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:192\nmsgid \"All messages\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:200\nmsgid \"List\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:201\nmsgid \"List view\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:232\nmsgid \"Save the results of this search to a new tag\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:240\n#: shared-data/default-theme/html/partials/topbar.html:65\nmsgid \"Home\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_search.html:241\n#: shared-data/default-theme/html/profiles/index.html:52\nmsgid \"E-mail Accounts\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_tags.html:5\nmsgid \"View all tags\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_tags.html:11\nmsgid \"Create a new tag\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_tags.html:17\nmsgid \"All current filters\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_tags.html:24\nmsgid \"Type\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tools_tags.html:25\nmsgid \"Show Tag by type\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tooltips.html:10\n#: shared-data/default-theme/html/tags/edit.html:8\nmsgid \"Edit Tag\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tooltips.html:11\nmsgid \"Remove Tag\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/tooltips.html:44\nmsgid \"Show Encryption Keys\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/topbar.html:3\n#, python-format\nmsgid \"%(name)s's Mailpile is version %(version)s with %(size)s messages\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/topbar.html:37\nmsgid \"How to report bugs\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/topbar.html:38\nmsgid \"Report Bugs\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/topbar.html:71\nmsgid \"Contribute\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/topbar.html:76\nmsgid \"Settings and Tools\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/topbar.html:80\nmsgid \"Logout\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/partials/topbar.html:95\nmsgid \"Notifications\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:149\nmsgid \"You need at least an e-mail address to proceed.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:154\nmsgid \"Detecting settings for: \"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:298\nmsgid \"Basic Details\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:300\nmsgid \"At least a name and e-mail are required!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:310\nmsgid \"Existing Account\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:311\nmsgid \"New Address\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:335\nmsgid \"Signature\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:336\nmsgid \"Everyone needs a unique, witty signature!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:344\nmsgid \"Add custom signature\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:351\nmsgid \"Detect settings\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:362\nmsgid \"Auto-detecting settings\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:366\nmsgid \"Connecting over Tor, this may take a while.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:377\nmsgid \"Failed to detect settings, manual configuration required\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:383\n#: shared-data/default-theme/html/settings/privacy.html:188\nmsgid \"Troubleshoot recent Network Activity.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:388\nmsgid \"Failed to log in, check the username and password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:393\nmsgid \"Sending Mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:405\nmsgid \"Local\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:414\n#: shared-data/default-theme/html/profiles/account-form.html:514\nmsgid \"Host name\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:418\n#: shared-data/default-theme/html/profiles/account-form.html:520\nmsgid \"Port number\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:423\n#: shared-data/default-theme/html/profiles/account-form.html:527\nmsgid \"copied\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:431\n#: shared-data/default-theme/html/profiles/account-form.html:536\nmsgid \"Authentication\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:446\n#: shared-data/default-theme/html/profiles/account-form.html:550\nmsgid \"Forget password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:455\nmsgid \"Send mail using local Unix tools.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:456\n#: shared-data/default-theme/html/profiles/account-form.html:592\nmsgid \"Use this setting if you have a working mail server on this machine.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:463\nmsgid \"Leave blank to auto-detect\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:471\nmsgid \"No outgoing mail for this account.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:485\nmsgid \"Receiving Mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:499\nmsgid \"Mail spool\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:500\n#: shared-data/default-theme/html/profiles/remove/index.html:51\nmsgid \"Local files\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:557\nmsgid \"Leave mail on server\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:563\nmsgid \"Copy all mail and add to search engine\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:575\nmsgid \"Require STARTTLS encryption\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:581\n#: shared-data/default-theme/html/profiles/account-form.html:694\nmsgid \"Enable IMAP\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:591\nmsgid \"Receive mail from local Unix mail spool.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:600\n#: shared-data/default-theme/html/settings/mailbox/index.html:75\n#: shared-data/default-theme/html/tags/form.html:177\nmsgid \"Copy mail to Mailpile secure storage\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:606\nmsgid \"Delete from Unix mail spool\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:606\nmsgid \"after copying\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:620\nmsgid \"Choose a protocol for the new mail source...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:622\nmsgid \"No incoming mail for this account.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:631\nmsgid \"\"\n\"Use this setting if you would like Mailpile to read e-mails already \"\n\"downloaded by Thunderbird, Mac Mail or another local application on this \"\n\"machine.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:634\nmsgid \"Use the Browse tool to import local mailboxes later on.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:648\nmsgid \"Enable this mail source\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:670\nmsgid \"Add New\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:679\nmsgid \"\"\n\"Mailpile may not be able to access your mail unless you log on to your \"\n\"account and enable IMAP and/or POP3.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:682\nmsgid \"Important\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:687\nmsgid \"Without this, some providers will even mistake Mailpile for an intruder!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:700\nmsgid \"Got it\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:706\nmsgid \"Security and Privacy\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:712\nmsgid \"Encryption key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:716\nmsgid \"Create a new Autocrypt Level 1.1 compatible key (Ed25519)\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:732\nmsgid \"Create a new 4096 bit RSA key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:732\nmsgid \"Legacy, strong\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:733\nmsgid \"Create a new 3072 bit RSA key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:733\nmsgid \"Autocrypt Level 1\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:735\nmsgid \"Disable encryption for this account\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:741\nmsgid \"Show too many encryption settings\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:753\nmsgid \"Best-effort: Encrypt and/or sign mail whenever possible\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:761\nmsgid \"Always digitally sign outgoing mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:769\nmsgid \"Always encrypt (warn when sending unencrypted mail)\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:777\nmsgid \"Minimize metadata (may make mail unreadable)\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:785\nmsgid \"Prefer compatibility; avoid PGP/MIME (makes mail ugly)\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:794\nmsgid \"Prefer PGP/MIME\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:804\nmsgid \"Signal a preference for encrypted mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:812\nmsgid \"Signal a preference for signed mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:820\nmsgid \"Signal a preference for un-signed, un-encrypted mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:829\nmsgid \"Use Autocrypt to exchange encryption keys\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/account-form.html:837\nmsgid \"Use attachments to exchange encryption keys\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:9\n#: shared-data/default-theme/html/profiles/index.html:47\nmsgid \"Welcome Home\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:17\nmsgid \"Browse\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:18\nmsgid \"Browse Files and Mailboxes\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/add/index.html:2\n#: shared-data/default-theme/html/profiles/index.html:24\n#: shared-data/default-theme/html/profiles/index.html:186\nmsgid \"Add Account\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/add/index.html:6\n#: shared-data/default-theme/html/profiles/index.html:25\n#: shared-data/default-theme/html/profiles/index.html:184\nmsgid \"Create a new Account\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:27\nmsgid \"Configure your accounts, profiles and other settings\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:37\nmsgid \"\"\n\"You can configure accounts and profiles once you have reviewed your \"\n\"privacy settings.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:56\nmsgid \"Your name\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:57\nmsgid \"E-mail address\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:59\nmsgid \"Total\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:68\nmsgid \"Still loading profiles and contacts, please wait...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:123\nmsgid \"Configure Incoming Mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:132\n#: shared-data/default-theme/html/profiles/index.html:133\nmsgid \"Incoming Mail:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:139\nmsgid \"Outgoing Mail Settings\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:145\nmsgid \"Security Settings\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:153\nmsgid \"Edit Profile\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:160\n#: shared-data/default-theme/html/profiles/remove/index.html:2\n#: shared-data/default-theme/html/profiles/remove/index.html:6\n#: shared-data/default-theme/html/profiles/remove/index.html:99\nmsgid \"Remove Account\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:173\nmsgid \"Please review your privacy settings before adding any accounts.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:176\nmsgid \"Privacy & Security\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:182\nmsgid \"You have not configured any accounts yet.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:195\n#, python-format\nmsgid \"Message Of The Day, %(date)s\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/index.html:206\n#: shared-data/default-theme/html/settings/index.html:21\n#, python-format\nmsgid \"This is Mailpile version %(version)s\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/edit/index.html:6\nmsgid \"Edit your Account\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/remove/index.html:10\nmsgid \"You are about to remove this account from your Mailpile\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/remove/index.html:42\nmsgid \"Unix shell\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/remove/index.html:63\nmsgid \"{TOTAL} e-mails\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/remove/index.html:71\nmsgid \"Delete Account Tags\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/remove/index.html:74\nmsgid \"Delete Encryption Keys\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/remove/index.html:78\nmsgid \"Move E-mail to Trash\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/remove/index.html:82\nmsgid \"\"\n\"By default this will remove: account details, OAuth credentials, saved \"\n\"passwords, linked mail sources, and route settings.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/remove/index.html:83\nmsgid \"\"\n\"This only affects local data, e-mail and settings on remote mail servers \"\n\"will not be modifed.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/remove/index.html:87\nmsgid \"\"\n\"If you also delete the encryption keys, you may be unable to read old \"\n\"encrypted e-mail.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/remove/index.html:91\n#: shared-data/default-theme/html/settings/plugins.html:61\n#: shared-data/default-theme/html/settings/preferences.html:141\nmsgid \"Be careful!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/profiles/remove/index.html:92\nmsgid \"This operation can not be undone.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/default.html:33\nmsgid \"Auto-tagging is enabled.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/default.html:39\nmsgid \"Messages will be permanently deleted after {days} days.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/default.html:45\nmsgid \"Messages will be moved to Trash after {days} days.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/default.html:49\nmsgid \"Automation is enabled. Consult tag settings for details.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/default.html:53\nmsgid \"Add and remove messages to train the system.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/default.html:115\n#: shared-data/default-theme/html/search/default.html:133\nmsgid \"Nothing Happened.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/default.html:116\nmsgid \"Usually, matching e-mails would be listed here.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/default.html:118\nmsgid \"You need to create an account and add some mail first!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/default.html:123\nmsgid \"Manage your Accounts\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/default.html:129\nmsgid \"No Spam Found, Hooray!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/default.html:131\nmsgid \"Inbox zero? Impressive!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/default.html:134\nmsgid \"It seems your Mailpile does not contain any messages for the search\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/default.html:139\nmsgid \"Here are some other options for you\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/default.html:143\nmsgid \"Browse for mailboxes\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/default.html:148\nmsgid \"Compose a message\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/default.html:155\nmsgid \"Search Tips & Tricks\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/default.html:164\n#: shared-data/default-theme/html/search/photos.html:51\nmsgid \"Hrm, We Could Not Find Anything\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/drafts.html:1\nmsgid \"Your unfinished messages.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/outbox.html:1\nmsgid \"Messages that are ready to send.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/prev_more_next.html:26\nmsgid \"More\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/prev_more_next.html:47\n#, python-format\nmsgid \"\"\n\"Searched <strong>%(number)s</strong> messages in \"\n\"<strong>%(elapsed)s</strong> seconds.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/prev_more_next.html:49\nmsgid \"Vroom!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/search/sent.html:1\nmsgid \"These messages have been sent.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/index.html:7\n#: shared-data/default-theme/html/settings/index.html:15\nmsgid \"Settings & Tools\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/index.html:32\n#: shared-data/default-theme/html/settings/preferences.html:7\n#: shared-data/default-theme/html/settings/preferences.html:17\n#: shared-data/default-theme/html/settings/recipes.html:89\n#: shared-data/default-theme/html/settings/recipes.html:156\nmsgid \"Preferences\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/index.html:35\nmsgid \"Your Preferences\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/index.html:40\n#: shared-data/default-theme/html/settings/plugins.html:7\n#: shared-data/default-theme/html/settings/plugins.html:13\nmsgid \"Plugins\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/index.html:43\nmsgid \"Plugins and Addons\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/index.html:69\nmsgid \"Change Your Mailpile Password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/index.html:76\nmsgid \"Tools\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/index.html:97\nmsgid \"Backup\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/index.html:100\nmsgid \"Backup Your Settings and Keys\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/index.html:105\nmsgid \"TLS Certs\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/index.html:108\nmsgid \"TLS Certificate Tool\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/index.html:113\nmsgid \"Passwords\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/index.html:116\n#: shared-data/default-theme/html/settings/set/password/index.html:14\nmsgid \"Account Password Management\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/index.html:124\n#: shared-data/default-theme/html/settings/set/password/keys.html:21\nmsgid \"Encryption Key Management\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/index.html:129\nmsgid \"CLI\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/index.html:132\nmsgid \"Command Line Interface\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/index.html:151\nmsgid \"Are you sure you want to shutdown Mailpile?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/index.html:154\nmsgid \"Shutting down ...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/plugins.html:20\n#: shared-data/default-theme/html/settings/preferences.html:24\n#: shared-data/default-theme/html/settings/privacy.html:41\nmsgid \"Your settings have been saved.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/plugins.html:47\nmsgid \"This plugin is currently enabled!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/plugins.html:52\nmsgid \"This plugin is disabled.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/plugins.html:56\nmsgid \"Author\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/plugins.html:62\nmsgid \"This plugin is meant for developers and may be incomplete or dangerous.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/plugins.html:74\nmsgid \"Plugin will be disabled on restart.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/plugins.html:77\nmsgid \"Restart Now\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/plugins.html:84\nmsgid \"Disable:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/plugins.html:99\nmsgid \"Enable\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/plugins.html:131\nmsgid \"Are you sure you want to restart Mailpile?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/plugins.html:136\nmsgid \"Restarting ...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:30\nmsgid \"Searches and Tags\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:34\nmsgid \"Here you can customize how search results and tag views are displayed.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:35\nmsgid \"\"\n\"The ordering and grouping options determine whether messages are grouped \"\n\"together into conversations or not, and which results are sorted to the \"\n\"top of the list.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:39\nmsgid \"\"\n\"The display density determines how tightly the results are packed \"\n\"together on your screen.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:64\nmsgid \"Custom\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:67\nmsgid \"Ordering and grouping\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:76\nmsgid \"Display density\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:89\nmsgid \"User Interface\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:93\nmsgid \"Mailpile is translated to many languages.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:94\nmsgid \"\"\n\"The translation only applies to the application, not the e-mails \"\n\"themselves.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:98\nmsgid \"Note that some translations may be incomplete.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:102\nmsgid \"Help translate Mailpile\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:126\n#: shared-data/default-theme/html/settings/preferences.html:165\n#: shared-data/default-theme/html/settings/privacy.html:347\n#: shared-data/default-theme/html/settings/privacy.html:383\n#: shared-data/default-theme/html/settings/privacy.html:394\n#: shared-data/default-theme/html/settings/privacy.html:405\n#: shared-data/default-theme/html/settings/privacy.html:416\n#: shared-data/default-theme/html/settings/privacy.html:425\n#: shared-data/default-theme/html/settings/privacy.html:438\nmsgid \"Off\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:128\n#: shared-data/default-theme/html/settings/preferences.html:167\n#: shared-data/default-theme/html/settings/privacy.html:350\n#: shared-data/default-theme/html/settings/privacy.html:386\n#: shared-data/default-theme/html/settings/privacy.html:388\n#: shared-data/default-theme/html/settings/privacy.html:397\n#: shared-data/default-theme/html/settings/privacy.html:399\n#: shared-data/default-theme/html/settings/privacy.html:408\n#: shared-data/default-theme/html/settings/privacy.html:410\n#: shared-data/default-theme/html/settings/privacy.html:419\n#: shared-data/default-theme/html/settings/privacy.html:428\n#: shared-data/default-theme/html/settings/privacy.html:442\nmsgid \"On\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:137\nmsgid \"Danger Zone\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:142\nmsgid \"\"\n\"These are technical settings which may interfere with normal use of \"\n\"Mailpile.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:143\nmsgid \"If Mailpile came with a warranty, changing these would invalidate it.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:150\nmsgid \"Always BCC self when sending mail\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/preferences.html:176\n#: shared-data/default-theme/html/settings/privacy.html:36\n#: shared-data/default-theme/html/settings/privacy.html:482\nmsgid \"Save Settings\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:31\nmsgid \"Please review and save your settings.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:32\nmsgid \"Some features may not be enabled until you have saved.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:45\nmsgid \"Choose your own privacy policy!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:51\nmsgid \"Networking Privacy\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:55\nmsgid \"Tor is a community-run system for making anonymous network connections.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:56\nmsgid \"\"\n\"It masks your IP address and location, thus preventing many forms of \"\n\"surveillance and tracking.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:60\nmsgid \"Using Tor may be considered suspicious or even illegal in some countries.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:61\nmsgid \"\"\n\"Since the Tor network is run by volunteers, bad actors can (and do) \"\n\"volunteer to run exit-nodes so they can listen in on traffic; as a result\"\n\" Tor is not suitable for accessing the unencrypted web.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:65\nmsgid \"Visit the Tor project's web-site\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:69\nmsgid \"proxy\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:72\nmsgid \"When sending and receiving mail, or downloading from the web:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:78\n#, python-format\nmsgid \"Custom proxy setting (%(proto)s on %(host)s:%(port)s)\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:87\nmsgid \"Prefer anonymous Tor networking for encrypted connections.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:88\n#: shared-data/default-theme/html/settings/privacy.html:117\n#: shared-data/default-theme/html/settings/privacy.html:216\n#: shared-data/default-theme/html/settings/privacy.html:284\n#: shared-data/default-theme/html/settings/privacy.html:293\n#: shared-data/default-theme/html/settings/privacy.html:388\n#: shared-data/default-theme/html/settings/privacy.html:399\n#: shared-data/default-theme/html/settings/privacy.html:410\nmsgid \"Recommended setting\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:94\nmsgid \"Prefer anonymous Tor networking for all connections.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:99\nmsgid \"Use shared operating system proxy settings.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:103\n#, python-format\nmsgid \"Do not use %(proxy)s.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:109\n#: shared-data/default-theme/html/settings/recipes.html:102\nmsgid \"Advanced Settings\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:115\n#, python-format\nmsgid \"Fall back to direct networking if connecting over %(proxy)s fails.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:123\nmsgid \"Disable direct networking connections completely.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:127\n#, python-format\nmsgid \"Never use %(proxy)s for these hosts:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:146\nmsgid \"Tor is currently disabled.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:150\nmsgid \"The Tor application could not be found on your system, please install it!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:153\nmsgid \"To enable Tor, click the button below.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:178\nmsgid \"Enable Tor\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:183\nmsgid \"\"\n\"Without Tor, Mailpile can not prevent service providers and network \"\n\"operators from monitoring or tracking your IP address and communications.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:199\nmsgid \"\"\n\"The Mailpile Team publishes updates to notify users of available upgrades\"\n\" and potential security vulnerabilities in Mailpile.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:203\nmsgid \"\"\n\"Update subscriptions help the Mailpile Team keep track of how many people\"\n\" use Mailpile, what operating systems are in use, and which languages \"\n\"users speak.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:204\n#: shared-data/default-theme/html/settings/privacy.html:266\nmsgid \"If Tor is not installed, this may leak your IP address.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:208\nmsgid \"Consult the Mailpile wiki for more details\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:215\nmsgid \"Subscribe to Message Of The Day updates from the Mailpile Team.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:223\nmsgid \"Only download updates anonymously over Tor.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:228\nmsgid \"\"\n\"Generic updates only, over Tor - keeps all details about your setup \"\n\"private.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:234\nmsgid \"Download generic updates only, to keep details about your setup private.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:241\nmsgid \"Keep your custom settings:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:247\nmsgid \"Disable the Message Of The Day.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:250\n#: shared-data/default-theme/html/settings/privacy.html:303\nmsgid \"Tor will be used to protect your IP address, if it is available.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:256\nmsgid \"Third Party Content\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:260\nmsgid \"Mailpile can download content from the web to augment your mail.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:261\nmsgid \"\"\n\"This includes user photos from Gravatar, key material from key servers, \"\n\"and potentially other sources.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:265\nmsgid \"\"\n\"This may leak information about your address book and use of Mailpile to \"\n\"the providers of these services.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:269\nmsgid \"Learn more about:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:282\nmsgid \"Enable downloading of third party content from the web.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:292\nmsgid \"Only download third party content anonymously over Tor.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:300\nmsgid \"Do not download third party content.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:309\nmsgid \"Securing Your Data\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:314\nmsgid \"Your Mailpile stores data here:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:319\nmsgid \"Mailpile can encrypt your e-mail, search engine and settings.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:320\nmsgid \"This protects your privacy, even if your computer gets lost or stolen.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:324\nmsgid \"\"\n\"Encryption makes it harder to migrate your data to another e-mail client,\"\n\" slows things down and may increase the odds of data loss.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:325\nmsgid \"Losing your encryption key becomes equivalent to losing the data.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:329\nmsgid \"Download a backup of current settings and keys.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:334\nmsgid \"\"\n\"Also keep in mind that the security of your data depends entirely on the \"\n\"strength of your password.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:337\nmsgid \"You can change your Mailpile Password here.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:351\nmsgid \"Allow deletion of e-mail from servers and mailboxes.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:387\nmsgid \"Encrypt the contact database.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:398\nmsgid \"Encrypt the system event log.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:409\nmsgid \"Encrypt other (miscellaneous) data.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:420\nmsgid \"Encrypt locally stored e-mail.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:429\nmsgid \"Strongly encrypt the local search index (slow).\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:443\nmsgid \"Use shared GnuPG keychain for PGP encryption keys.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:447\nmsgid \"Notes:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:448\nmsgid \"\"\n\"Changing encryption settings will only affect data created or edited from\"\n\" now on.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:452\nmsgid \"\"\n\"Backups are protected by the same password and encryption as the \"\n\"configuration.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:454\nmsgid \"\"\n\"The configuration is always kept encrypted, because it may contain \"\n\"passwords.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:455\nmsgid \"\"\n\"The search index is always at least partially encrypted because it is so \"\n\"sensitive.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:463\n#: shared-data/default-theme/html/settings/privacy.html:470\nmsgid \"WARNING\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:464\nmsgid \"Changing GnuPG keychains may prevent decryption of old e-mail!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/privacy.html:466\nmsgid \"\"\n\"You will need to manually copy your PGP keys from one keychain to the \"\n\"other.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/recipes.html:5\nmsgid \"Chelsea Manning\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/recipes.html:6\nmsgid \"Email\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/recipes.html:7\nmsgid \"chelsea@email.com\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/recipes.html:8\nmsgid \"Delivery route\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/recipes.html:19\nmsgid \"my delivery route\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/recipes.html:20\nmsgid \"Login\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/recipes.html:21\nmsgid \"username@smtp.mailserver.org\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/recipes.html:25\nmsgid \"smtp.mailserver.org\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/recipes.html:39\n#: shared-data/default-theme/html/settings/recipes.html:118\n#: shared-data/default-theme/html/settings/recipes.html:124\nmsgid \"Add Profile\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/recipes.html:40\nmsgid \"Your Profiles\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/recipes.html:45\nmsgid \"Address:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/recipes.html:46\nmsgid \"Route:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/recipes.html:63\n#: shared-data/default-theme/html/settings/recipes.html:137\n#: shared-data/default-theme/html/settings/recipes.html:143\nmsgid \"Add Route\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/recipes.html:64\n#: shared-data/default-theme/html/settings/recipes.html:154\nmsgid \"Routes\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/recipes.html:75\nmsgid \"Edit Route\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/recipes.html:90\nmsgid \"Permit browser notifications\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/recipes.html:152\nmsgid \"Profiles\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/recipes.html:158\nmsgid \"Advanced\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/mailbox/index.html:16\nmsgid \"Search Folder\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/mailbox/index.html:22\nmsgid \"Mailbox Settings\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/mailbox/index.html:44\nmsgid \"Archives\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/mailbox/index.html:69\n#: shared-data/default-theme/html/tags/form.html:164\nmsgid \"Add messages to search engine\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/mailbox/index.html:87\nmsgid \"Configure Mailboxes\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/index.html:2\nmsgid \"Changed Settings\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:28\nmsgid \"Remote Accounts Using Passwords\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:47\nmsgid \"This account is used for receiving and sending e-mail.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:49\nmsgid \"This account is used for receiving e-mail.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:51\nmsgid \"This account is used for sending e-mail.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:57\n#: shared-data/default-theme/html/settings/set/password/keys.html:67\nmsgid \"Managed by Mailpile\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:59\n#: shared-data/default-theme/html/settings/set/password/keys.html:69\nmsgid \"Unlocked, password remembered\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:61\n#: shared-data/default-theme/html/settings/set/password/keys.html:71\nmsgid \"Unlocked\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:63\n#: shared-data/default-theme/html/settings/set/password/keys.html:73\nmsgid \"Locked\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:67\nmsgid \"Sends e-mail via. {H}\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:70\nmsgid \"Downloads e-mail from {H}\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:91\n#: shared-data/default-theme/html/settings/set/password/keys.html:137\nmsgid \"The password is:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:102\nmsgid \"Account Password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:105\n#: shared-data/default-theme/html/settings/set/password/keys.html:151\nmsgid \"Your Mailpile Password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:122\n#: shared-data/default-theme/html/settings/set/password/index.html:125\n#: shared-data/default-theme/html/settings/set/password/index.html:126\n#: shared-data/default-theme/html/settings/set/password/index.html:127\nmsgid \"Unlock Account\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:123\nmsgid \"Lock Account and Forget Password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:125\n#: shared-data/default-theme/html/settings/set/password/keys.html:171\nmsgid \"10 minutes\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:126\n#: shared-data/default-theme/html/settings/set/password/keys.html:172\nmsgid \"1 hour\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:127\n#: shared-data/default-theme/html/settings/set/password/keys.html:173\nmsgid \"12 hours\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:129\nmsgid \"Unlock Account and Remember Password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:130\nmsgid \"Display Remembered Account Password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:139\nmsgid \"Unlocking an account allows Mailpile to use it to send and receive e-mail.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:141\nmsgid \"By default, unlocked accounts stay accessible until you restart Mailpile.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:144\nmsgid \"\"\n\"If Mailpile remembers the password, you will not have to unlock the \"\n\"account again.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:150\nmsgid \"Locked accounts can not be used to send or receive e-mail.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:155\n#: shared-data/default-theme/html/settings/set/password/keys.html:208\nmsgid \"Please authenticate using your Mailpile password.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:278\nmsgid \"No Saved Passwords Here!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/index.html:280\nmsgid \"Hopefully that is a good thing...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:31\nmsgid \"Keys on Your GnuPG Key-Chain\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:47\nmsgid \"Key Fingerprint\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:58\nmsgid \"This key may be used to decrypt incoming e-mail.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:60\nmsgid \"This key is used for decrypting and signing e-mail.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:63\nmsgid \"{B} bit {T} key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:78\nmsgid \"Valid from {D1} to {D2}.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:82\nmsgid \"Valid from {D}.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:88\nmsgid \"Identities on key: {N}\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:91\nmsgid \"show all\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:96\nmsgid \"ID on key:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:105\nmsgid \"Not currently linked with any accounts.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:110\nmsgid \"Account\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:112\nmsgid \"Edit account security settings\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:148\nmsgid \"Key Password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:168\n#: shared-data/default-theme/html/settings/set/password/keys.html:171\n#: shared-data/default-theme/html/settings/set/password/keys.html:172\n#: shared-data/default-theme/html/settings/set/password/keys.html:173\nmsgid \"Unlock Encryption Key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:169\nmsgid \"Lock Encryption Key\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:175\nmsgid \"Unlock Key and Remember Password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:176\nmsgid \"Display Remembered Key Password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:192\nmsgid \"\"\n\"Unlocking a key allows Mailpile to use it for decryption and creating \"\n\"digital signatures.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:194\nmsgid \"By default, unlocked keys stay accessible until you restart Mailpile.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:197\nmsgid \"\"\n\"If Mailpile remembers the password, you will not have to unlock the key \"\n\"again.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:203\nmsgid \"Locked keys can not be used for decryption or digital signatures.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:325\nmsgid \"No Keys Here!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/settings/set/password/keys.html:327\nmsgid \"Encryption keys can be created during the account creation process.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/oauth2/index.html:2\n#: shared-data/default-theme/html/setup/oauth2/index.html:11\nmsgid \"Grant Access\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/oauth2/index.html:14\nmsgid \"\"\n\"In order for Mailpile to process your e-mail, you need to grant \"\n\"permission for the application to access your e-mail account.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/oauth2/index.html:17\nmsgid \"The authorization process will take place in a new window (or tab).\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/oauth2/index.html:19\nmsgid \"You will need to copy and paste an access code into the form below.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/oauth2/index.html:23\nmsgid \"Paste your access code here!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/oauth2/index.html:27\nmsgid \"If given a choice, be sure to grant access to:\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/oauth2/index.html:34\n#: shared-data/default-theme/html/setup/password/index.html:36\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/oauth2/index.html:79\nmsgid \"Mailpile should now be able to access your account.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/oauth2/index.html:84\nmsgid \"Oh no, something went wrong.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/oauth2/index.html:85\nmsgid \"Try again later?\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/oauth2/index.html:92\nmsgid \"Close Window\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:15\nmsgid \"Mailpile Secured!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:17\nmsgid \"Access to your Mailpile will now require this password.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:20\nmsgid \"\"\n\"All settings and downloaded mail will be encrypted, so even if someone \"\n\"steals your laptop they will not have access to your e-mail.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:23\nmsgid \"Start using Mailpile\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:34\nmsgid \"Change your Mailpile Password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:41\nmsgid \"Password incorrect! Try again...\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:43\nmsgid \"Your current Mailpile Password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:47\n#: shared-data/default-theme/html/setup/password/index.html:105\nmsgid \"Click to generate new secure password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:49\nmsgid \"Your new Mailpile Password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:55\nmsgid \"Choose your Mailpile Password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:58\nmsgid \"\"\n\"Your Mailpile Password should be something really hard to guess but \"\n\"memorable.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:59\nmsgid \"\"\n\"This password will be used to unlock your Mailpile settings, keys and \"\n\"accounts.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:64\nmsgid \"\"\n\"We have made a secure recommendation, but you still need to type it \"\n\"yourself!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:67\nmsgid \"The passwords don't match!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:70\nmsgid \"That password is too short. This is important!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:73\nmsgid \"That password is a bit short!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:74\nmsgid \"We recommend using a phrase of at least four memorable words.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:86\nmsgid \"Confirm your Mailpile Password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:94\nmsgid \"Set Mailpile Password\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/password/index.html:107\nmsgid \"Suggest\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/welcome/index.html:22\nmsgid \"You're about to experience secure e-mail like never before!\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/welcome/index.html:25\nmsgid \"Uh oh! You have JavaScript disabled. This will cause problems.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/welcome/index.html:31\nmsgid \"Select Language\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/setup/welcome/index.html:43\nmsgid \"Begin\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/edit.html:6\nmsgid \"Edit: \"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:26\nmsgid \"Tag Settings\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:50\nmsgid \"Show in top of sidebar\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:52\nmsgid \"Show in sidebar\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:54\nmsgid \"Hide from sidebar\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:58\nmsgid \"Show in search results\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:60\nmsgid \"Hide from search results\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:69\nmsgid \"Technical Settings\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:76\nmsgid \"Behave as category\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:78\nmsgid \"Behave as attribute\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:82\nmsgid \"Display in toolbar\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:84\nmsgid \"Hide from toolbar\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:91\nmsgid \"Keyword\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:97\n#: shared-data/default-theme/html/tags/index.html:23\nmsgid \"Subtags\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:152\nmsgid \"Mailbox settings\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:168\nmsgid \"Stop adding messages to search engine\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:172\nmsgid \"Use default mail source policy\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:214\nmsgid \"Automation\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:219\nmsgid \"Enable to tag messages automatically, like spam.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:220\nmsgid \"Tag or un-tag mail by hand to train the system.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:221\nmsgid \"Tagging\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:224\nmsgid \"Auto-tagging is always enabled for this tag.\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:231\nmsgid \"Automatic tagging\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:237\nmsgid \"Tag by hand\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:245\nmsgid \"Untagging\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:247\nmsgid \"Never\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:248\nmsgid \"After 1 day\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:249\nmsgid \"After 2 days\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:250\nmsgid \"After 3 days\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:251\nmsgid \"After 1 week\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:252\nmsgid \"After 2 weeks\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:253\nmsgid \"After 1 month\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:254\nmsgid \"After 3 months\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:256\n#, python-format\nmsgid \"After %(days)s days\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:262\n#, python-format\nmsgid \"Remove from %(name)s\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:264\nmsgid \"Move to Inbox\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:268\nmsgid \"Delete e-mails\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/form.html:289\nmsgid \"Delete Tag\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/index.html:35\nmsgid \"Priority Tags\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/index.html:64\nmsgid \"Archived\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/index.html:75\nmsgid \"Hide Archived\"\nmsgstr \"\"\n\n#: shared-data/default-theme/html/tags/index.html:75\nmsgid \"Show Archived\"\nmsgstr \"\"\n\n#: shared-data/mailpile-gui/mailpile-gui.py:112\nmsgid \"Mailpile is starting up\"\nmsgstr \"\"\n\n#: shared-data/mailpile-gui/mailpile-gui.py:113\nmsgid \"Patience is a virtue...\"\nmsgstr \"\"\n\n#: shared-data/mailpile-gui/mailpile-gui.py:117\nmsgid \"You are not logged in\"\nmsgstr \"\"\n\n#: shared-data/mailpile-gui/mailpile-gui.py:125\nmsgid \"Open Webmail\"\nmsgstr \"\"\n\n#: shared-data/mailpile-gui/mailpile-gui.py:130\nmsgid \"Quit\"\nmsgstr \"\"\n\n#: shared-data/mailpile-gui/mailpile-gui.py:143\nmsgid \"Show Status Window\"\nmsgstr \"\"\n\n#: shared-data/mailpile-gui/mailpile-gui.py:149\nmsgid \"Open in Web Browser\"\nmsgstr \"\"\n\n#: shared-data/mailpile-gui/mailpile-gui.py:155\nmsgid \"Open in Terminal\"\nmsgstr \"\"\n\n#: shared-data/mailpile-gui/mailpile-gui.py:168\nmsgid \"Quit GUI\"\nmsgstr \"\"\n\n#: shared-data/mailpile-gui/mailpile-gui.py:300\nmsgid \"Connecting to Mailpile\"\nmsgstr \"\"\n\n#: shared-data/mailpile-gui/mailpile-gui.py:302\nmsgid \"Failed to connect to Mailpile!\"\nmsgstr \"\"\n\n#: shared-data/mailpile-gui/mailpile-gui.py:311\nmsgid \"Failed to launch Mailpile!\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "shared-data/mailpile-gui/icons-osx/mk_icons.sh",
    "content": "#!/bin/bash\ncd ../icons-dark\nfor a in *.png; do\n    convert $a -resize 19x19 ../icons-osx/$a\ndone\n"
  },
  {
    "path": "shared-data/mailpile-gui/mailpile-gui.py",
    "content": "#!/usr/bin/python2.7\n#\n# This is a basic GUI launcher for Mailpile.\n#\n# It relies on gui-o-matic for the actual GUI, the logic here is used to\n# figure out if we need to launch a new Mailpile or if we can connect to\n# one that is already running in the background.\n#\n# The script can also be run with --script as an argument to simply\n# output the gui-o-matic launch sequence.\n#\n# It also supports --profile=... and --home=... for selecting alternate\n# Mailpile data directories.\n#\n# Note: Most of the GUI behaviours are defined in `mailpile.plugins.gui`.\n#       The logic here is just enough to configure our windows and display\n#       a splash-screen. Arguably, more of this logic should be moved\n#       into `mailpile.plugins.gui` so everything is in one place.\n#\nfrom __future__ import print_function\nimport copy\nimport fasteners\nimport json\nimport os\nimport sys\nimport subprocess\nimport threading\nfrom cStringIO import StringIO\n\nfrom mailpile.config.base import ConfigDict\nfrom mailpile.config.defaults import CONFIG_RULES\nfrom mailpile.config.paths import DEFAULT_WORKDIR, DEFAULT_SHARED_DATADIR\nfrom mailpile.config.paths import LOCK_PATHS\nfrom mailpile.i18n import ActivateTranslation\nfrom mailpile.i18n import gettext as _\nfrom mailpile.security import GetUserSecret\n\n\nAPPDIR = os.path.abspath(os.path.dirname(__file__))\nif not os.path.exists(os.path.join(APPDIR, 'media', 'splash.jpg')):\n    APPDIR = os.path.abspath(os.path.join(\n        DEFAULT_SHARED_DATADIR(), 'mailpile-gui'))\n\nMEDIA_PATH = os.path.join(APPDIR, 'media')\nICONS_PATH = os.path.join(APPDIR, 'icons-%(theme)s')\n\nMAILPILE_HOME_IMAGE   = os.path.join(MEDIA_PATH, 'background.jpg')\nMAILPILE_SPLASH_IMAGE = os.path.join(MEDIA_PATH, 'splash.jpg')\n\n\ndef SPLASH_SCREEN(state, message):\n    return {\n        \"background\": MAILPILE_SPLASH_IMAGE,\n        \"width\": 396,\n        \"height\": 594,\n        \"message_y\": 0.80,\n        \"progress_bar\": True,\n        \"message\": message}\n\n\ndef BASIC_GUI_CONFIGURATION(state):\n    mailpile_home = state.base_url\n    mailpile_quit = state.base_url + 'quitquitquit'\n    oib_checked = True if state.pub_config.prefs.open_in_browser else False\n    return {\n        \"app_name\": \"Mailpile\",\n        \"app_icon\": \"image:logo\",\n        \"images\": {\n            \"logo\":       os.path.join(MEDIA_PATH, 'logo-color.png'),\n            \"new-setup\":  os.path.join(MEDIA_PATH, 'new-setup.png'),\n            \"logged-in\":  os.path.join(MEDIA_PATH, 'lock-open.png'),\n            \"logged-out\": os.path.join(MEDIA_PATH, 'lock-closed.png'),\n            \"ra-on\":      os.path.join(MEDIA_PATH, 'remote-access-on.png'),\n            \"ra-off\":     os.path.join(MEDIA_PATH, 'remote-access-off.png'),\n            \"startup\":    os.path.join(ICONS_PATH, 'startup.png'),\n            \"normal\":     os.path.join(ICONS_PATH, 'normal.png'),\n            \"attention\":  os.path.join(ICONS_PATH, 'attention.png'),\n            \"working\":    os.path.join(ICONS_PATH, 'working.png'),\n            \"shutdown\":   os.path.join(ICONS_PATH, 'shutdown.png')},\n        \"font_styles\": {\n            \"title\": {\n                \"family\": \"normal\",\n                \"points\": 18,\n                \"bold\": True\n            },\n            \"details\": {\n                \"points\": 10\n            },\n            \"splash\": {\n                \"points\": 16\n            },\n            \"buttons\": {\n                \"points\": 16\n            },\n            \"notification_title\": {\n                \"points\": 1,\n            },\n            \"notification_details\": {\n                \"italic\": True\n            }\n        },\n        \"main_window\": {\n            \"show\": False,\n            \"close_quits\": False,\n            \"width\": 540,\n            \"height\": 300,\n            \"background\": MAILPILE_HOME_IMAGE,\n            \"initial_notification\": '',\n            \"status_displays\": [{\n                \"id\": \"mailpile\",\n                \"icon\": \"image:logo\",\n                \"title\": _(\"Mailpile is starting up\"),\n                \"details\": _(\"Patience is a virtue...\")\n            },{\n                \"id\": \"logged-in\",\n                \"icon\": \"image:logged-out\",\n                \"title\": _(\"You are not logged in\"),\n            },{\n                \"id\": \"notification\",\n                \"title\": \"\"\n            }],\n            \"action_items\": [{\n                \"id\": \"open\",\n                \"position\": \"first\",\n                \"label\": _(\"Open Webmail\"),\n                \"op\": \"show_url\",\n                \"args\": [mailpile_home]\n            },{\n                \"id\": \"quit_button\",\n                \"label\": _(\"Quit\"),\n                \"position\": \"last\",\n                \"op\": \"quit\"}]},\n        \"indicator\": {\n            \"initial_status\": \"startup\",\n            \"menu_items\": [{\n                \"id\": \"notification\",\n                \"label\": _(\"Starting up\"),\n                \"sensitive\": False\n            },{\n                \"separator\": True\n            },{\n                \"id\": \"main\",\n                \"label\": _(\"Show Status Window\"),\n                \"sensitive\": False,\n                \"op\": \"show_main_window\",\n                \"args\": [],\n            },{\n                \"id\": \"browse\",\n                \"label\": _(\"Open in Web Browser\"),\n                \"sensitive\": False,\n                \"op\": \"show_url\",\n                \"args\": [mailpile_home]\n            },{\n                \"id\": \"screen\",\n                \"label\": _(\"Open in Terminal\"),\n                \"sensitive\": True,\n                \"op\": \"terminal\",\n                \"args\": {\n                    \"command\": \"screen -r -x mailpile\",\n                    \"title\": \"mailpile\"}\n            },{\n                \"separator\": True\n            },{\n                \"id\": \"quit\",\n                \"op\": \"quit\",\n                \"sensitive\": True,\n                \"args\": [],\n                \"label\": _(\"Quit GUI\")}]}}\n\n\nclass MailpileState(object):\n    def __init__(self):\n        self.base_url = 'http://localhost:33411/'\n        self.workdir = DEFAULT_WORKDIR()\n        self.secret = GetUserSecret(self.workdir)\n        self.pub_config = None\n        self.is_running = None\n\n    def check_if_running(self):\n        # FIXME: This is rather slow. We should refactor upstream to speed\n        #        up or include our own custom parser if that is infeasible.\n        wd_lock_path = LOCK_PATHS()[1]\n        wd_lock = fasteners.InterProcessLock(wd_lock_path)\n        try:\n            if wd_lock.acquire(blocking=False):\n                wd_lock.release()\n                return False\n            else:\n                return True\n        except (OSError, IOError):\n            return False\n\n    def _load_public_config(self):\n        self.pub_config = ConfigDict(_rules=CONFIG_RULES)\n        try:\n            conffile = os.path.join(self.workdir, 'mailpile.rc')\n            with open(conffile) as fd:\n                self.pub_config.parse_config(None, fd.read(), source=conffile)\n            self.base_url = 'http://%s:%s%s/' % (\n                self.pub_config.sys.http_host,\n                self.pub_config.sys.http_port,\n                self.pub_config.sys.http_path)\n        except:\n            import traceback\n            sys.stderr.write(traceback.format_exc())\n\n    def discover(self, argv):\n        self._load_public_config()\n        self.is_running = self.check_if_running()\n        self.http_port = self.pub_config.sys.http_port\n\n        # Check if we have a screen session?\n\n        return self\n\n\ndef GenerateConfig(state):\n    \"\"\"Generate the basic gui-o-matic window configuration.\"\"\"\n    config = BASIC_GUI_CONFIGURATION(state)\n    return json.dumps(config, indent=2)\n\n\ndef LocateMailpile(trust_os_path=False):\n    \"\"\"\n    Locate the main mailpile launcher script\n    \"\"\"\n    # Locate mailpile's root script, searching upward from our script\n    # location. We do this BEFORE checking the system PATH, to increase\n    # our odds of finding a mailpile script from the same bundle as this\n    # particualr mailpile-gui.py (there might be more than one?).\n    directory = APPDIR\n    while not trust_os_path:\n        for sub in ('scripts', 'bin'):\n            mailpile_path = os.path.join(directory, sub, 'mailpile')\n            if os.path.exists(mailpile_path):\n                return mailpile_path\n\n        parts = os.path.split(directory)\n        if parts[0] == directory:\n            break\n        else:\n            directory = parts[0]\n\n    # Finally, check if the Mailpile script is on the system PATH.\n    # Note: Turns out os.defpath and $PATH are not the same thing.\n    for directory in (\n            os.getenv('PATH', '').split(os.pathsep) +\n            os.defpath.split(os.pathsep)):\n        if directory:\n            mailpile_path = os.path.join(directory, 'mailpile')\n            if os.path.exists(mailpile_path):\n                return mailpile_path\n\n    raise OSError('Cannot locate mailpile launcher script!')\n\n\ndef MailpileInvocation(trust_os_path=False):\n    \"\"\"\n    Return an appropriately formated string for invoking mailpile.\n\n    Really this is a container for platform specific behavior\n    \"\"\"\n    parts = []\n    common_opts = [\n        '--set=\"prefs.open_in_browser=false\"',\n        '--gui=%PORT% ' ]\n\n    mailpile_loc = LocateMailpile(trust_os_path=trust_os_path)\n    if os.name == 'nt':\n        parts.append('\"{}\"'.format(sys.executable))\n        parts.append('\"{}\"'.format(mailpile_loc))\n        parts.extend(common_opts)\n        parts.append('--www=')\n        parts.append('--wait')\n    else:\n        # FIXME: This should launch a screen session using the\n        #        same concepts as multipile's mailpile-admin.\n        parts.append('screen -S mailpile -d -m \"{}\"'.format(mailpile_loc))\n        parts.extend(common_opts)\n        parts.append('--interact')\n\n    return ' '.join(parts)\n\n\ndef GenerateBootstrap(state, trust_os_path=False):\n    \"\"\"\n    Generate the gui-o-matic bootstrap sequence.\n\n    Once this sequence completes, either we have failed and will die,\n    or Mailpile (specifically `mailpile.plugins.gui`) will take over and\n    start sending gui-o-matic commands to update the UI.\n    \"\"\"\n    bootstrap = [\"OK LISTEN\"]\n\n    if state.is_running:\n        # If Mailpile is running already, connect and ask it to talk to us.\n        bootstrap += [\n            \"show_main_window {}\",\n            \"notify_user %s\" % json.dumps({\n                'message': _(\"Connecting to Mailpile\")}),\n            \"set_next_error_message %s\" % json.dumps({\n                'message': _(\"Failed to connect to Mailpile!\")}),\n            \"OK LISTEN HTTP: \" + (\n                '%sgui/%s/watch/%%PORT%%/' % (state.base_url, state.secret))]\n    else:\n        # If Mailpile is not running already, launch it in a screen session.\n        bootstrap += [\n            \"show_splash_screen %s\" % json.dumps(\n                SPLASH_SCREEN(state, _(\"Launching Mailpile\"))),\n            \"set_next_error_message %s\" % json.dumps({\n                'message': _(\"Failed to launch Mailpile!\")}),\n            \"OK LISTEN TCP: \" + MailpileInvocation(trust_os_path) ]\n\n    return '\\n'.join(bootstrap)\n\n\ndef Main(argv):\n    set_profile = set_home = trust_os_path = False\n    for arg in argv:\n        if arg.startswith('--profile='):\n            os.environ['MAILPILE_PROFILE'] = arg.split('=', 1)[-1]\n            if 'MAILPILE_HOME' in os.environ:\n                del os.environ['MAILPILE_HOME']\n            set_profile = True\n        elif arg.startswith('--home='):\n            os.environ['MAILPILE_HOME'] = arg.split('=', 1)[-1]\n            if 'MAILPILE_PROFILE' in os.environ:\n                del os.environ['MAILPILE_PROFILE']\n            set_home = True\n        elif arg == '--trust-os-path':\n            trust_os_path = True\n    if set_home and set_profile:\n        raise ValueError('Please only use one of --home and --profile')\n\n    state = MailpileState().discover(argv)\n    ActivateTranslation(None, state.pub_config, None)\n\n    script = [\n        GenerateConfig(state),\n        GenerateBootstrap(state, trust_os_path=trust_os_path)]\n\n    if '--script' in argv:\n        print('\\n'.join(script))\n\n    else:\n        # FIXME: We shouldn't need to do this, refactoring upstream\n        #        to pull in less weird stuff would make sense.\n        from mailpile.safe_popen import MakePopenUnsafe\n        MakePopenUnsafe()\n\n        from gui_o_matic.control import GUIPipeControl\n        gpc = GUIPipeControl(StringIO('\\n'.join(script) + '\\n'))\n        gpc.bootstrap(dry_run=('--compile' in argv))\n\n\nif __name__ == \"__main__\":\n    Main(sys.argv)\n"
  },
  {
    "path": "shared-data/mailpile-gui/mailpile.desktop",
    "content": "[Desktop Entry]\nVersion=1.0\nName=Mailpile\nGenericName=E-mail Client\nX-GNOME-FullName=Mailpile Personal Web-mail Client\nComment=A privacy-focused e-mail client with a web-based user interface\nType=Application\nExec=mailpile-gui.py\nTryExec=mailpile-gui.py\nTerminal=false\nCategories=Network;Email;GTK;\nStartupNotify=true\nIcon=mailpile\n\n# FIXME:\n#\n#   The following is an example of extra sections that could be added\n#   if we detected that the user is making use of the (barely documented)\n#   Mailpile \"profiles\". On Linux this would probably mean adding an\n#   action for creating a new profile, which then installs an enhanced\n#   mailpile.desktop file to ~/.share/local/applications/mailpile.desktop\n#   to override the system-wide one.\n#\n#Actions=Dogfood;\n#\n#[Desktop Action Dogfood]\n#Name=Mailpile: Dogfood\n#Exec=mailpile-gui.py --profile=Dogfood\n"
  },
  {
    "path": "shared-data/multipile/README.md",
    "content": "## Multipile: multi-user Mailpile\n\nHere you can find tools to simplify administration of a smallish multi-user\nMailpile installation.\n\n\n### Requirements and Prerequisites\n\n   * Mailpile, Apache 2+, sudo and screen\n   * Each user who wants to use Mailpile, exists as a Unix user\n   * You *really* should configure your server to use TLS-encryption!\n\n\n### How does it work?\n\nOnce configured, `https://your-server/mailpile/` should present you with a\nlog-in page.\n\nApache is configured to proxy incoming traffic to/from running Mailpile\nprocesses, mapping usernames to localhost port numbers. If a user's Mailpile is\nnot running, the user is presented with a log-in page that requests a username,\nwhich is in turn passed to `mailpile-admin.py` (running as a CGI) which\nlaunches a much simpler (=secure?) script named `mailpile-launcher.py` via\n`sudo`.\n\nThe launcher will attempt to launch the user's Mailpile in a background screen\nsession owned by that user. The launcher will only launch Mailpiles for users\nthat have used the app at least once in the past (their Mailpile data directory\nmust exist). The recommended way to configure new users is using the\n`mailpile-admin.py` tool as described below.\n\n\n### Using mailpile-admin.py\n\nThe tool `mailpile-admin.py` does most of the heavy lifting.\n\nThe admin can use this tool manually to list, add, remove and otherwise manage\nMailpile installations:\n\n    ## Enable and start Mailpile for the user frank\n    $ sudo ./mailpile-admin.py --user frank --start\n    ...\n\n    ## Stop the running Mailpile for bob\n    $ sudo ./mailpile-admin.py --user bob --stop\n    ...\n\n    ## Delete all a user's Mailpile data! Requires adding the --force argument.\n    $ sudo ./mailpile-admin.py --user bob --delete\n\n    ## List running or configured Mailpile instances\n    $ sudo ./mailpile-admin.py --list\n    ...\n    \n\nThe tool can also be used to (try to) automatically configure Apache2 to serve\nMailpile on the `/mailpile/` path of the default VHost.\n\n    ## Configure Apache for use with multi-user Mailpile\n    $ sudo ./mailpile-admin.py --configure-apache\n    \nIf you have installed the `mailpile-apache2` Debian package, this will already have\nbeen done for you.\n\n\n### Mini-FAQ\n\nQ. Where can I find the `mailpile-admin.py` tool? It's not on my path?  \nA. The Debian package installs Multipile in `/usr/share/mailpile/multipile`\n\nQ. Why doesn't my Mailpile start up when I enter my username?  \nA. Your admin probably needs to run: `mailpile-admin.py --start --user YOU`\n\nQ. Why not nginx or some other better web-server?  \nA. Nobody has contributed the necessary recipies yet! Please do!\n\nQ. I had already run Mailpile manually, how to I migrate to Multipile?\n\n   1. Install Multipile: `apt install mailpile-apache2`\n   2. Launch your personal Mailpile, leave it running\n   3. Run `sudo mailpile-admin.py --list`, your Mailpile *should* be listed\n   4. If so, run `sudo mailpile-admin.py --discover --configure-apache-usermap`\n   5. In your Mailpile CLI, run `www http://127.0.0.1:PORT/mailpile/USER/`,\n      with PORT and USER matching the values shown in step 2 above. Careful, the\n      trailing slash IS important.\n\nQ. Can't I just edit the usermap (rewritemap) by hand?  \nA. Sure! It should be here: `/var/lib/mailpile/apache/usermap.txt`\n\n"
  },
  {
    "path": "shared-data/multipile/mailpile-admin.py",
    "content": "#!/usr/bin/env python2.7\n#\n# This is the Mailpile admin tool! It can do these things:\n#\n#  - Configure Apache for use with Mailpile (multi-user, proxying)\n#  - Start or stop a user's Mailpile (in a screen session)\n#  - Function as a CGI script to start Mailpile and reconfigure Apache\n#\nfrom __future__ import print_function\nimport argparse\nimport cgi\nimport ConfigParser\nimport copy\nimport distutils.spawn\nimport getpass\nimport json\nimport os\nimport pwd\nimport random\nimport re\nimport socket\nimport subprocess\nimport sys\nimport time\n\n\n# Default paths\nDEFAULT_CONFIG_FILE = '/etc/mailpile/multipile.rc'\nDEFAULT_CONFIG_SECTION = 'Multipile'\nMAILPILE_PIDS_PATH = \"/var/lib/mailpile/pids\"\nAPACHE_DEFAULT_WEBROOT = \"/mailpile\"\nAPACHE_REWRITEMAP_PATH = \"/var/lib/mailpile/apache/usermap.txt\"\nAPACHE_SUDOERS_PATH = \"/etc/sudoers.d/mailpile-apache\"\n\n\nMAILPILE_STOP_SCRIPT = [\n    # Ask it to shut down nicely, remove pid-file if not running.\n    'kill \"%(pid)s\" || (rm -f \"%(pidfile)s\"; false)',\n    'sleep 10',\n    # Remove pid-file iff shutdown succeeded.\n    'kill \"%(pid)s\" || (rm -f \"%(pidfile)s\"; true)']\n\nMAILPILE_FORCE_STOP_SCRIPT = [\n    # If still running, wait 20 more seconds and then force things.\n    'kill -INT \"%(pid)s\" && (sleep 20; kill -9 \"%(pid)s\") || true',\n    # Clean up!\n    'rm -f \"%(pidfile)s\"']\n\nMAILPILE_START_SCRIPT = [\n    # We start Mailpile in a screen session named \"mailpile\"\n    'screen -S mailpile -d -m \"%(mailpile)s\"'\n        ' \"--www=%(host)s:%(port)s%(path)s\"'\n        ' \"--idlequit=%(idlequit)s\"'\n        ' \"--pid=%(pidfile)s\"'\n        ' --interact']\n\nMAILPILE_LAUNCH_SCRIPT = [\n    'sudo %(mailpile-launcher)s %(user)s'\n        ' %(idlequit)s %(host)s:%(port)s%(path)s']\n\nMAILPILE_DELETE_SCRIPT = [\n    'rm -rf ~%(user)s/.local/share/Mailpile/default']\n\n\nCONFIGURE_APACHE_SCRIPT = [\n    '\"%(packager)s\" install screen sudo',\n    '\"%(a2enmod)s\" headers rewrite proxy proxy_http cgi',\n    'mkdir -p /var/lib/mailpile/apache/ /var/lib/mailpile/pids/',\n    'touch /var/lib/mailpile/apache/usermap.txt',\n    'touch %(multipile-www)s/admin.cgi',\n    'chmod +x %(multipile-www)s/admin.cgi',\n    '\"%(a2enconf)s\" mailpile',\n    '\"%(apache2ctl)s\" restart']\n\nFIX_PERMS_SCRIPT = [\n    'chown -R %(apache-user)s:%(apache-group)s /var/lib/mailpile/apache',\n    'chmod go+rwxt /var/lib/mailpile/pids',]\n\n\n# This is the Apache config template\nAPACHE_CONFIG_TEMPLATE = \"\"\"\\\n#\n# This is the Mailpile multi-user Apache config\n#\nAlias \"%(webroot)s/default-theme\" \"%(mailpile-theme)s\"\nAlias \"%(webroot)s\" \"%(multipile-www)s\"\n\nRewriteEngine On\nRewriteMap mailpile_u2hp \"txt:%(rewritemap)s\"\n\n<Directory \"%(mailpile-theme)s\">\n    Require all granted\n</Directory>\n<Directory \"%(multipile-www)s\">\n    AllowOverride All\n    Options FollowSymLinks ExecCGI\n    AddHandler cgi-script .cgi\n    LogLevel alert rewrite:trace8\n    Require all granted\n\n    # Show a helpful error if we're incorrectly configured\n    RewriteCond ${mailpile_u2hp:apache_map_test} !=ok\n    RewriteCond %%{REQUEST_URI} !.*/apache-broken.html$\n    RewriteRule .* %(webroot)s/apache-broken.html [L,R=302,E=nolcache:1]\n\n    # Redirect users\n    RewriteCond %%{REQUEST_FILENAME} !-f\n    RewriteRule ^([^/]+)(/.*) http://${mailpile_u2hp:$1}%(webroot)s/$1$2 [L,P,QSA]\n\n    # Redirect any proxy errors or 404 errors to our login page\n    ErrorDocument 503 %(webroot)s/not-running.html\n    ErrorDocument 502 %(webroot)s/not-running.html\n    ErrorDocument 404 %(webroot)s/not-running.html\n    RewriteRule ^not-running.html %(webroot)s/ [L,R=302,E=nolcache:1]\n\n    # Avoid caching our error pages\n    Header always set Cache-Control \"no-store, no-cache, must-revalidate\" env=nocache\n    Header always set Expires \"Thu, 01 Jan 1970 00:00:00 GMT\" env=nocache\n</Directory>\n\"\"\"\n\n# This is what allows Apache to launch Mailpile on behalf of other users.\nAPACHE_SUDOERS_TEMPLATE = \"\"\"\\\nwww-data\\tALL = NOPASSWD: %(mailpile-launcher)s\n\"\"\"\n\n\n# This is needed to ensure that we run mailpile-admin with the\n# right python interpreter\nCGI_SCRIPT_TEMPLATE = \"\"\"\\\n#!/bin/bash\nexec %(interpreter)s \"$(dirname $0)\"/../mailpile-admin.py \"$@\"\n\"\"\"\n\n\n# We prefer rewritemaps whenever possible!\nAPACHE_REWRITEMAP_LINE = \"%(user)s %(host)s:%(port)s\"\nAPACHE_REWRITEMAP_TEMPLATE = \"\"\"\\\n##\n## usermap.txt - User map to mailpile port\n##\n\n%(rewriterules)s\napache_map_test ok\n\n## EOF\n\"\"\"\n\n\ndef _escape(string):\n    return json.dumps(unicode(string).encode('utf-8'))[1:-1]\n\n\ndef _escaped(idict):\n    return dict((k, _escape(v)) for k, v in idict.iteritems())\n\n\ndef app_arguments_config_arg(ap):\n    ap.add_argument(\n        '--config', default='', help='Path to a configuration file')\n\n\ndef app_arguments():\n    ap = argparse.ArgumentParser(\n        description=\"Mailpile administration and system integration tool\")\n\n    ga = ap.add_mutually_exclusive_group(required=True)\n    ga.add_argument(\n        '--list', action='store_true',\n        help='List running Mailpiles')\n    ga.add_argument(\n        '--start', action='store_true',\n        help='Launch new Mailpile in a screen session')\n    ga.add_argument(\n        '--launch', action='store_true',\n        help='Launch exiting Mailpile in a screen session')\n    ga.add_argument(\n        '--stop', action='store_true',\n        help='Stop a running Mailpile')\n    ga.add_argument(\n        '--delete', action='store_true',\n        help='Delete a user\\'s Mailpile data (requires --force)')\n    ga.add_argument(\n        '--configure-apache', action='store_true',\n        help='Configure Apache for use with Mailpile (run with sudo)')\n    ga.add_argument(\n        '--configure-apache-usermap', action='store_true',\n        help='Update the Apache user/rewrite map (run with sudo)')\n    ga.add_argument(\n        '--generate-apache-config', action='store_true',\n        help='Print the apache config')\n    ga.add_argument(\n        '--generate-apache-sudoers', action='store_true',\n        help='Print the apache sudoers config')\n    ga.add_argument(\n        '--generate-apache-usermap', action='store_true',\n        help='Prints a rewritemap file (use --blank for an empty one)')\n\n    app_arguments_config_arg(ap)\n    ap.add_argument('--force', action='store_true',\n        help='With --stop, will kill -9 a running Mailpile')\n    ap.add_argument('--user', default=None,\n        help='Choose user, for use with --stop and --start')\n    ap.add_argument('--port', default=None,\n        help='Choose port, for use with --stop and --start')\n    ap.add_argument('--host', default='localhost',\n        help='Choose host, for use with --stop and --start')\n    ap.add_argument('--idlequit', default=(7*24*3600),\n        help='Mailpile shutdown after idling this many seconds')\n    ap.add_argument('--webroot', default=APACHE_DEFAULT_WEBROOT,\n        help='Parent web directory for Mailpile instances')\n    ap.add_argument('--mailpile', default=None,\n        help='Path to the Mailpile app itself')\n    ap.add_argument('--mailpile-share', default=None,\n        help='Location of Mailpile shared data')\n    ap.add_argument('--mailpile-theme', default=None,\n        help='Location of Mailpile theme files')\n    ap.add_argument('--multipile-www', default=None,\n        help='Location of Mailpile/Multipile files')\n    ap.add_argument('--apache-sudoers', default=APACHE_SUDOERS_PATH,\n        help='Sudoers config: path to Mailpile/Apache sudoers file')\n    ap.add_argument('--rewritemap', default=APACHE_REWRITEMAP_PATH,\n        help='Apache config: path to rewrite-map file')\n    ap.add_argument('--blank', action='store_true',\n        help='Apache config: blank slate; ignore running Mailpiles')\n    ap.add_argument('--discover', action='store_true',\n        help='Apache config: discover running Mailpiles')\n    ap.add_argument('--packager', default=None,\n        help='Apache config: OS packaging tool (apt-get)')\n    ap.add_argument('--interpreter', default=None,\n        help='Python interpreter: python interpreter to use')\n    ap.add_argument('--a2enmod', default=None,\n        help='Apache config: path to a2enmod utility')\n    ap.add_argument('--a2enconf', default=None,\n        help='Apache config: path to a2enmod utility')\n    ap.add_argument('--apache2ctl', default=None,\n        help='Apache config: path to apache2ctl utility')\n    ap.add_argument('--apache-user', default=None,\n        help='Apache config: Apache process unix username')\n    ap.add_argument('--apache-group', default=None,\n        help='Apache config: Apache process unix group')\n    ap.add_argument('--apache-confs', default=None,\n        help='Apache config: /etc/apache2/conf-available/ ?')\n\n    return ap\n\n\ndef usage(ap, reason, code=3):\n    print('error: %s' % reason)\n    ap.print_usage()\n    sys.exit(code)\n\n\ndef parse_config(app_args,\n                 conf_parsed=None,\n                 config=DEFAULT_CONFIG_FILE,\n                 section=DEFAULT_CONFIG_SECTION):\n    conf_file = config\n    if config:\n        if os.path.exists(conf_file):\n            config = ConfigParser.SafeConfigParser()\n            config.read([conf_file])\n            app_args.set_defaults(**dict(config.items(section)))\n        elif conf_parsed and conf_parsed.config:\n            usage(app_args, 'Config file not found: %s' % conf_file)\n\n\ndef parse_arguments_and_config(app_args,\n                               config=DEFAULT_CONFIG_FILE,\n                               section=DEFAULT_CONFIG_SECTION):\n    # We create a separate parser just to check for --config\n    conf_parser = argparse.ArgumentParser(add_help=False)\n    app_arguments_config_arg(conf_parser)\n    conf_parsed, unused_rest = conf_parser.parse_known_args()\n\n    # Okay, if we have a config file, parse it!\n    parse_config(app_args, conf_parsed,\n                 config=conf_parsed.config or config,\n                 section=section)\n\n    return app_args.parse_args()\n\n\ndef _parse_ps():\n    ps = subprocess.Popen(['ps', 'auxw'], stdout=subprocess.PIPE)\n    ps_re = re.compile('^(\\S+)\\s+(\\d+)\\s+\\S+\\s+\\S+\\s+\\S+\\s+(\\S+)'\n                       '.*\\s(?:(?:python[\\d\\.]*|pypy) +)?'\n                       '(?:\\S+/)?(mailpile)(?:\\s+|$)')\n    for line in ps.communicate()[0].splitlines():\n        m = re.match(ps_re, line)\n        if m:\n            yield (m.group(1), m.group(2), m.group(3), m.group(4))\n\n\ndef _parse_netstat():\n    ns = subprocess.Popen(['netstat', '-ant', '--program'],\n                          stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n    ns_re = re.compile('^tcp\\s+\\S+\\s+\\S+\\s+(\\S+:\\d+)\\s+(\\S+:.)'\n                       '\\s+.*?\\s(\\d+)\\/(\\S+)\\s*$')\n    for line in ns.communicate()[0].splitlines():\n        m = re.match(ns_re, line)\n        if m:\n            lhp, rhp, pid, proc = m.group(1), m.group(2), m.group(3), m.group(4)\n            yield lhp, rhp, pid, proc\n\n\ndef _get_random_port():\n    ns = _parse_netstat()\n    for tries in range(0, 100):\n       port = '%s' % random.randint(34110, 64110)\n       cport = ':' + port\n       for lhp, rhp, pid, proc in ns:\n           if lhp.endswith(cport):\n               port = None\n               break\n       if port:\n           return port\n    assert(not 'All the ports appear taken!')\n\n\ndef get_mailpile_shared_datadir():\n    # IMPORTANT: This code is duplicated in mailpile/config.py.\n    #            If it needs changing please change both places!\n    #\n    # Why? The code is duplicated here, so when running in CGI mode\n    # we don't have to load & parse the full Mailpile app just to\n    # find this path.\n    #\n    env_share = os.getenv('MAILPILE_SHARED')\n    if env_share is not None:\n        return env_share\n\n    # Check if we are running in a virtual env\n    # http://stackoverflow.com/questions/1871549/python-determine-if-running-inside-virtualenv\n    # We must also check that we are installed in the virtual env,\n    # not just that we are running in a virtual env.\n    if (hasattr(sys, 'real_prefix') or hasattr(sys, 'base_prefix')) and __file__.startswith(sys.prefix):\n        return os.path.join(sys.prefix, 'share', 'mailpile')\n\n    # Check if we've been installed to /usr/local (or equivalent)\n    usr_local = os.path.join(sys.prefix, 'local')\n    if __file__.startswith(usr_local):\n        return os.path.join(usr_local, 'share', 'mailpile')\n\n    # Check if we are in /usr/ (sys.prefix)\n    if __file__.startswith(sys.prefix):\n        return os.path.join(sys.prefix, 'share', 'mailpile')\n\n    # Else assume dev mode, source tree layout\n    # NOTE: This differs from mailpile/config.py!\n    return os.path.realpath(os.path.join(os.path.dirname(__file__), '..'))\n\n\ndef find_mailpile_executable():\n    mailpile = distutils.spawn.find_executable(\n        'mailpile',\n        os.path.join(sys.prefix, 'bin') + ':' + os.environ.get('PATH')\n    )\n\n    if mailpile:\n        return mailpile\n\n    # NOTE: mp_root is only correct when running from source!\n    mp_root = os.path.join(os.path.join(os.path.dirname(__file__), '..', '..'))\n    mp_root = os.path.realpath(mp_root)\n    return os.path.join(mp_root, 'mp')\n\n\ndef get_os_settings(args):\n    # FIXME: Detect OS, choose settings; these are the Ubuntu/Debian defaults.\n\n    mp_share = args.mailpile_share or get_mailpile_shared_datadir()\n\n    return {\n        'packager': args.packager or 'apt-get',\n        'interpreter': args.interpreter or sys.executable,\n        'a2enmod': args.a2enmod or 'a2enmod',\n        'a2enconf': args.a2enconf or 'a2enconf',\n        'apache2ctl': args.apache2ctl or 'apache2ctl',\n        'apache-user': args.apache_user or 'www-data',\n        'apache-group': args.apache_group or 'www-data',\n        'apache-confs': args.apache_confs or '/etc/apache2/conf-available',\n        'webroot': args.webroot,\n        'rewritemap': args.rewritemap,\n        'apache-sudoers': args.apache_sudoers,\n        'mailpile': args.mailpile or find_mailpile_executable(),\n        'mailpile-launcher': os.path.join(mp_share,\n                                          'multipile', 'mailpile-launcher.py'),\n        'mailpile-admin': os.path.realpath(sys.argv[0]),\n        'mailpile-theme': (args.mailpile_theme\n                           or os.path.join(mp_share, 'default-theme')),\n        'multipile-www': (args.multipile_www\n                          or os.path.join(mp_share, 'multipile', 'www'))}\n\n\ndef get_user_settings(args, user=None, mailpiles=None):\n    settings = get_os_settings(args)\n    user = user or pwd.getpwuid(os.getuid()).pw_name\n    assert('.' not in user and '/' not in user)\n    pidfile = os.path.join(MAILPILE_PIDS_PATH, user + '.pid')\n\n    port = args.port\n    if mailpiles and not port:\n        ports = [int(m[2]) for m in mailpiles.values() if m[0] == user]\n        if ports:\n            port = '%s' % min(ports)\n    if not port:\n        port = _get_random_port()\n\n    return {\n        'user': user,\n        'mailpile': settings['mailpile'],\n        'mailpile-launcher': settings['mailpile-launcher'],\n        'host': '127.0.0.1',\n        'port': port,\n        'path': ('%s/%s/' % (args.webroot, user)).replace('//', '/'),\n        'pidfile': pidfile,\n        'idlequit': args.idlequit,\n        'pid': os.path.exists(pidfile) and open(pidfile, 'r').read().strip()}\n\n\ndef discover_mailpiles(mailpiles=None):\n    mailpiles = mailpiles if (mailpiles is not None) else {}\n\n    # Check the process table for running Mailpiles\n    processes = {}\n    for username, pid, rss, proc in _parse_ps():\n        processes[pid] = [username, proc, rss]\n\n    # Add the listening host:port details from netstat\n    for listening_hostport, rhp, pid, proc in _parse_netstat():\n        if pid in processes:\n            processes[pid].append(listening_hostport)\n\n    for pid, details in processes.iteritems():\n        username, proc, rss, listening = (details[0], details[1],\n                                          details[2], details[3:])\n        if listening:\n            hostport = sorted(listening)[0]\n            host, port = hostport.split(':')\n            if hostport not in mailpiles:\n                mailpiles[hostport] = (username, host, port, False, pid, rss)\n            else:\n                mailpiles[hostport][4] = pid\n                mailpiles[hostport][5] = rss\n\n    return mailpiles\n\n\ndef _rewritemap(mailpiles):\n    rules = []\n    added = {}\n    count = 1\n    for hostport, details in mailpiles.iteritems():\n        user, host, port = details[0:3]\n        suffix = ''\n        if user in added:\n            print('WARNING: User %s has multiple Mailpiles!' % user)\n            suffix = '.%d' % (added[user] + 1)\n\n        rules.append(\n            APACHE_REWRITEMAP_LINE % {\n                'user': _escape(user)+suffix,\n                'host': host,\n                'port': port})\n        added[user] = added.get(user, 0) + 1\n\n    return '\\n'.join(rules)\n\n\ndef parse_rewritemap(args, os_settings, mailpiles=None):\n    mailpiles = mailpiles if (mailpiles is not None) else {}\n    try:\n        parse = re.compile('^(?P<user>[^#]+) (?P<host>[^:]+):(?P<port>.+)')\n        with open(args.rewritemap, 'r') as fd:\n            for line in fd:\n                m = re.match(parse, line)\n                if m:\n                    user = m.group('user')\n                    host = m.group('host')\n                    port = m.group('port')\n                    mailpiles['%s:%s' % (host, port)] = [\n                        user, host, port, True, None, None]\n    except (OSError, IOError, KeyError) as err:\n        print('WARNING: %s' % err)\n    return mailpiles\n\n\ndef save_rewritemap(args, os_settings, mailpiles):\n    with open(args.rewritemap + '.new', 'w') as fd:\n        os_settings['rewriterules'] = _rewritemap(mailpiles)\n        fd.write(APACHE_REWRITEMAP_TEMPLATE % os_settings)\n\n    if os.path.exists(args.rewritemap):\n        os.remove(args.rewritemap)\n\n    os.rename(args.rewritemap + '.new', args.rewritemap)\n\n\ndef parse_usermap(args, os_settings, mailpiles=None):\n    return parse_rewritemap(args, os_settings, mailpiles=mailpiles)\n\n\ndef save_usermap(args, os_settings, mailpiles):\n    return save_rewritemap(args, os_settings, mailpiles)\n\n\ndef save_cgi(os_settings):\n    with open(os.path.join(os_settings['multipile-www'], 'admin.cgi'), 'w') as fd:\n        fd.write(CGI_SCRIPT_TEMPLATE % os_settings)\n\n\ndef save_apache_sudoers(os_settings):\n    with open(os_settings['apache-sudoers'], 'w') as fd:\n        fd.write(APACHE_SUDOERS_TEMPLATE % os_settings)\n\n\ndef run_script(args, settings, script):\n    for line in script:\n        line = line % _escaped(settings)\n        print('==> %s' % line)\n        rv = os.system(line)\n        if 0 != rv:\n            print('==[ FAILED! Exit code: %s ]==' % rv)\n            return\n    print('===[ SUCCESS! ]===')\n\n\ndef _get_mailpiles(args, os_settings, discover=False):\n    mailpiles = {}\n    if not args.blank:\n        parse_usermap(args, os_settings, mailpiles=mailpiles)\n        if args.discover or discover:\n            discover_mailpiles(mailpiles=mailpiles)\n    return mailpiles\n\n\ndef list_mailpiles(args):\n    os_settings = get_os_settings(args)\n    mailpiles = _get_mailpiles(args, os_settings, discover=True)\n    fmt =  '%-8.8s %6.6s %6.6s %-6.6s %5.5s %s'\n    user_counts = {}\n    print(fmt % ('USER', 'PID', 'RSS', 'ACCESS', 'PORT', 'URL'))\n    for hostport in sorted(mailpiles.keys()):\n        user, host, port, in_usermap, pid, rss = mailpiles[hostport]\n        user_counts[user] = user_counts.get(user, 0) + 1\n        status = []\n        if in_usermap:\n            url = 'http://%s%s/%s/' % (socket.gethostname(),\n                                      os_settings['webroot'], user)\n        else:\n            url = 'http://%s:%s/' % (host, port)\n        print(fmt % (\n            user, pid or '?', rss or '',\n            'apache' if in_usermap else 'direct', port, url))\n\ndef generate_apache_usermap(app_args, args):\n    mailpiles = _get_mailpiles(args, get_os_settings(args))\n    print(APACHE_REWRITEMAP_TEMPLATE % {'rewriterules': _rewritemap(mailpiles)})\n\ndef generate_apache_sudoers(app_args, args):\n    print(APACHE_SUDOERS_TEMPLATE % get_os_settings(args))\n\ndef generate_apache_config(app_args, args):\n    print (APACHE_CONFIG_TEMPLATE % get_os_settings(args))\n\ndef configure_apache(app_args, args):\n    if os.getuid() == 0:\n        os_settings = get_os_settings(args)\n        with open(os.path.join(os_settings['apache-confs'], 'mailpile.conf'),\n                  'w') as fd:\n            fd.write(APACHE_CONFIG_TEMPLATE % os_settings)\n\n        run_script(args, os_settings, CONFIGURE_APACHE_SCRIPT)\n        save_cgi(os_settings)\n        save_apache_sudoers(os_settings)\n        save_usermap(args, os_settings, _get_mailpiles(args, os_settings))\n        run_script(args, os_settings, FIX_PERMS_SCRIPT)\n    else:\n        usage(app_args, 'Please run this as root!')\n\ndef configure_apache_usermap(app_args, args):\n    if os.getuid() == 0:\n        os_settings = get_os_settings(args)\n        save_usermap(args, os_settings, _get_mailpiles(args, os_settings))\n    else:\n        usage(app_args, 'Please run this as root!')\n\n\ndef start_mailpile(app_args, args):\n    os_settings = get_os_settings(args)\n    mailpiles = parse_usermap(args, os_settings)\n    user_settings = get_user_settings(args, user=args.user, mailpiles=mailpiles)\n    assert(re.match('^[0-9]+$', user_settings['port']) is not None)\n    assert(re.match('^[a-z0-9\\.]+$', user_settings['host']) is not None)\n    if args.user:\n        command = '%s \"%s\" --start --port=\"%s\" --host=\"%s\"' % (\n            _escape(os_settings['interpreter']),\n            _escape(os_settings['mailpile-admin']),\n            _escape(user_settings['port']),\n            _escape(user_settings['host']))\n        script = ['sudo -iHu \"%(user)s\" -- ' + command]\n    else:\n        script = MAILPILE_START_SCRIPT\n\n    if script:\n        run_script(args, user_settings, script)\n\n    if args.user:\n        hostport = '%s:%s' % (user_settings['host'], user_settings['port'])\n        mailpiles[hostport] = (user_settings['user'],\n                               user_settings['host'],\n                               user_settings['port'],\n                               False, None, None)\n        save_usermap(args, os_settings, mailpiles)\n        run_script(args, os_settings, FIX_PERMS_SCRIPT)\n\n\ndef launch_mailpile(app_args, args):\n    assert(args.user)\n    os_settings = get_os_settings(args)\n    mailpiles = parse_usermap(args, os_settings)\n    user_settings = get_user_settings(args, user=args.user, mailpiles=mailpiles)\n    run_script(args, user_settings, MAILPILE_LAUNCH_SCRIPT)\n\n\ndef stop_mailpile(app_args, args):\n    user_settings = get_user_settings(args, user=args.user)\n    if not user_settings.get('pid'):\n        usage(app_args, 'No PID found, cannot stop Mailpile', code=0)\n\n    script = MAILPILE_STOP_SCRIPT\n    if args.force:\n        script += MAILPILE_FORCE_STOP_SCRIPT\n\n    run_script(args, user_settings, script)\n\n\ndef delete_mailpile(app_args, args):\n    user_settings = get_user_settings(args, user=args.user)\n    if user_settings.get('pid'):\n        usage(app_args, 'PID found, please stop Mailpile first', code=0)\n    if not args.force:\n        usage(app_args, 'This command is scary, use --force if sure', code=0)\n\n    run_script(args, user_settings, MAILPILE_DELETE_SCRIPT)\n\n\ndef main():\n    app_args = app_arguments()\n    parsed_args = parse_arguments_and_config(app_args)\n\n    if parsed_args.list:\n        list_mailpiles(parsed_args)\n\n    elif parsed_args.configure_apache:\n        configure_apache(app_args, parsed_args)\n\n    elif parsed_args.configure_apache_usermap:\n        configure_apache_usermap(app_args, parsed_args)\n\n    elif parsed_args.generate_apache_config:\n        generate_apache_config(app_args, parsed_args)\n\n    elif parsed_args.generate_apache_sudoers:\n        generate_apache_sudoers(app_args, parsed_args)\n\n    elif parsed_args.generate_apache_usermap:\n        generate_apache_usermap(app_args, parsed_args)\n\n    elif parsed_args.start:\n        start_mailpile(app_args, parsed_args)\n\n    elif parsed_args.launch:\n        launch_mailpile(app_args, parsed_args)\n\n    elif parsed_args.stop:\n        stop_mailpile(app_args, parsed_args)\n\n    elif parsed_args.delete:\n        delete_mailpile(app_args, parsed_args)\n\n\ndef handle_cgi_post():\n    app_args = app_arguments()\n    parse_config(app_args)\n    try:\n        request = cgi.FieldStorage()\n        username = request.getfirst('username').split('@')[0]\n\n        # Sanity checks; these will raise on invalid/missing username\n        assert(username)\n        pwd.getpwnam(username)\n\n        # Generate argument and settings objects for use below\n        parsed_args = app_args.parse_args(['--launch', '--user', username])\n        settings = get_os_settings(parsed_args)\n\n        # Send headers now, so output doesn't confuse Apache\n        print('Location: %s/%s/' % (settings['webroot'], username))\n        print('Expires: 0')\n        print()\n\n        # Launch Mailpile?\n        rv = launch_mailpile(app_args, parsed_args)\n\n        time.sleep(5)\n    except:\n        parsed_args = app_args.parse_args(['--launch'])\n        settings = get_os_settings(parsed_args)\n        print('Location: %s/?error=yes' % settings['webroot'])\n        print('Expires: 0')\n        print()\n\n\nif __name__ == \"__main__\":\n    if os.getenv('REQUEST_METHOD') == 'POST':\n        assert(len(sys.argv) == 1)\n        handle_cgi_post()\n    else:\n        main()\n"
  },
  {
    "path": "shared-data/multipile/mailpile-launcher.py",
    "content": "#!/usr/bin/python\n#\n# IMPORTANT: This script runs as root and is invoked by the web server via sudo.\n#            So it's pretty security-sensitive: simple is better than clever!\n#\nfrom __future__ import print_function\nDOC=\"\"\"\\\n\nThis is a script to launch Mailpile as a specific user.\nThe user must already have Mailpile configured.\n\nUsage: mailpile-launcher.py USERNAME IDLE-QUIT-SECONDS URL\n\n\"\"\"\nimport os\nimport pwd\nimport re\nimport sys\nfrom fasteners import InterProcessLock\n\n\n# FIXME: Hard-coding this stuff is lame. But KISS is good.\nMAILPILE_PIDS_PATH = \"/var/lib/mailpile/pids\"\nMAILPILE_HOME_PATH = '%s/.local/share/Mailpile/default/'\nMAILPILE_WORK_LOCK = 'workdir-lock'\n\n\n\ndef usage(code, msg=''):\n    print(DOC, msg, \"\\n\")\n    sys.exit(code)\n\n\nif __name__ == '__main__':\n    if len(sys.argv) != 4:\n        usage(1)\n\n    mailpile_user = sys.argv[1]\n    idlequit = int(sys.argv[2])\n    url = sys.argv[3]\n\n    if not mailpile_user or mailpile_user == 'root':\n        usage(2, \"Please specify a (non-root) user to launch Mailpile.\")\n    if not re.match(r'^[a-zA-Z0-9\\._-]+$', mailpile_user):\n        usage(2, \"That is a strange looking username.\")\n    mailpile_user = pwd.getpwnam(mailpile_user)\n    if not mailpile_user:\n        usage(2, \"Please specify a (non-root) user to launch Mailpile.\")\n    if not re.match(r'^[a-zA-Z0-9\\.:/]+$', url):\n        usage(2, \"That is a strange looking URL.\")\n\n    mailpile_home = MAILPILE_HOME_PATH % mailpile_user.pw_dir\n    if not os.path.exists(mailpile_home):\n        usage(3, \"That user has never run Mailpile. Aborting.\")\n\n    mp_lockfile = os.path.join(mailpile_home, MAILPILE_WORK_LOCK)\n    mp_lock = InterProcessLock(mp_lockfile)\n    if not mp_lock.acquire(blocking=False):\n        # We are happy with this result, don't raise an error.\n        sys.stderr.write(\n            \"Mailpile is already running for that user. Doing Nothing.\\n\")\n    else:\n        # We will release the lock on exec(), but make sure the user owns\n        # the lockfile and will be able to take over.\n        os.chown(mp_lockfile, mailpile_user.pw_uid, mailpile_user.pw_gid)\n        os.execv('/bin/su',\n            ['/bin/su', '-', mailpile_user.pw_name, '-c', (\n                'screen -S mailpile -d -m '\n                'mailpile --idlequit=%d --pid=%s/%s.pid --www=%s --interact'\n                ) % (idlequit, MAILPILE_PIDS_PATH, mailpile_user.pw_name, url)])\n"
  },
  {
    "path": "shared-data/multipile/multipile.rc.sample",
    "content": "# Hello admin!\n#\n# This is an example of a configuration for the \"multipile\", multi-user\n# mailpile system.\n#\n# If you install this file into /etc/mailpile/multipile.rc, the contents will\n# be used as defaults by mailpile-admin.py, both when running as a CGI and\n# when used by the admin directly. This is useful for:\n#\n#   1. Changing the Mailpile idle-quit timeout\n#   2. Moving Mailpile to an alternate URL path\n#   3. Correcting mailpile-admin.py's defaults for your system\n#   4. Other Stuff\n#\n# Any of the CLI arguments (see mailpile-admin.py --help) can be given\n# new defaults in the Multipile section here below. Note dashes in argument\n# names must be converted to underscores.\n#\n# Unrecognized values are silently ignored.\n#\n\n[Multipile]\nidlequit = 900\nwebroot = /mulepale\napache_user = www-data\napache_group = root\n"
  },
  {
    "path": "shared-data/multipile/www/apache-broken.html",
    "content": "<h1>Oops, Apache RewiteMap is Broken</h1>\n<ul>\n  <li>Your Apache does not appear to be respecting the <tt>RewriteMap</tt>\n      configured by <tt>mailpile-admin.py</tt>.\n  <li>This probable means this <tt>VirtualHost</tt> has <tt>RewriteEngine On</tt>\n      for its own purposes.\n  <li>To inherit the Mailpile rewrite rules, add this to your virtual host config:\n      <tt>RewriteOptions Inherit</tt>\n  <li>Restart Apache: <tt>apache2ctl restart</tt>\n</ul>\n<p>\n  Good luck! :-)\n</p>\n<p>\n  <a href=\".\">Return to the login page</a>\n</p>\n"
  },
  {
    "path": "shared-data/multipile/www/index.html",
    "content": "<!doctype html>\n<!--[if lt IE 7]><html class=\"no-js ie6 oldie\" lang=\"en\"> <![endif]-->\n<!--[if IE 7]><html class=\"no-js ie7 oldie\" lang=\"en\"> <![endif]-->\n<!--[if IE 8]><html class=\"no-js ie8 oldie\" lang=\"en\"> <![endif]-->\n<!--[if gt IE 8]><!--> <html class=\"no-js\" lang=\"en\"> <!--<![endif]-->\n<head>\n  <title>Your Mailpile may not be running</title>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n  <meta name=\"referrer\" content=\"no-referrer\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\n  <link rel=\"stylesheet\" href=\"default-theme/css/default.css?ts=0.4.5\" />\n\n  <!-- Apple Icons -->\n  <link rel=\"apple-touch-icon\" sizes=\"57x57\" href=\"default-theme/img/apple-touch-icon.png\" />\n  <link rel=\"apple-touch-icon\" sizes=\"72x72\" href=\"default-theme/img/apple-touch-icon-72x72.png\" />\n  <link rel=\"apple-touch-icon\" sizes=\"114x114\" href=\"default-theme/img/apple-touch-icon-114x114.png\" />\n\n  <!-- Favicon -->\n  <link rel=\"shortcut icon\" id=\"basic-favicon\" href=\"default-theme/img/favicon.png\" />\n  <link rel=\"icon\" id=\"basic-favicon\" type=\"image/png\" href=\"default-theme/img/favicon.png\" />\n\n  <script src=\"default-theme/js/libraries.min.js\"></script>\n\n</head>\n<body><div id=\"login\">\n  <div id=\"login-left\" class=\"animated\"></div>\n  <div id=\"login-right\" class=\"animated\"></div>\n</div>\n\n<div id=\"login-logo\" class=\"animated\">\n<svg version=\"1.1\" id=\"logo-icon\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\" width=\"51.359px\" height=\"36.152px\" viewBox=\"0 0 51.359 36.152\" enable-background=\"new 0 0 51.359 36.152\" xml:space=\"preserve\">\n\t<g id=\"logo-bluemail\">\n\t\t<polygon fill=\"#337FB2\" points=\"30.845,12.283 29.104,1.644 23.619,8.464 20.684,11.224 24.706,15.172 29.83,18.932 29.34,19.619 25.846,17.665 19.871,11.907 16.647,15.924 11.939,13.392 10.562,16.32 5.347,23.443 4.34,22.793 9.494,15.707 11.289,12.473 6.505,10.245 0.663,6.159 1.218,13.629 3.142,24.806 13.08,25.266 24.38,22.949 30.398,22.161 31.59,20.342\"/>\n\t\t<polygon fill=\"#337FB2\" points=\"12.166,11.295 16.285,14.103 18.747,11.096 22.805,7.276 28.932,0.152 16.013,1.814 7.158,3.18 0.451,4.672 7.049,8.999\"/>\n\t</g>\n\t<g id=\"logo-redmail\">\n\t\t<polygon fill=\"#BE1C21\" points=\"37.792,30.277 38.955,23.488 34.223,26.465 31.873,27.522 33.48,30.744 35.794,34.083 35.355,34.396 33.651,32.488 31.239,27.765 28.455,29.521 26.139,27.01 24.697,28.492 20.063,31.711 19.59,31.107 24.18,27.899 25.937,26.319 23.511,23.978 21.343,24.521 19.612,24.922 18.447,32.074 24.356,34.422 31.664,35.375 35.465,36.152 36.563,35.301\"/>\n\t\t<polygon fill=\"#BE1C21\" points=\"26.711,25.791 28.617,28.346 30.729,27.041 33.977,25.578 39.162,22.551 32.186,21.152 31.029,22.9 27.28,23.32 25.385,23.489 24.48,23.721\"/>\n\t</g>\n\t<path id=\"logo-greenmail\" fill=\"#4B9441\" d=\"M48.834,1.655L41.45,2.97l-7.405,1.542l-2.729,0.433l1.311,7.074l1.351,6.243l4.976-1.134l7.71-1.351l4.696-1.044l-1.01-4.361L48.834,1.655z M44.243,12.998l0.073,0.423l-2.605,0.403l-5.567,1.019l-0.079-0.463l-0.113-0.472l2.204-0.486l0.03-0.005l3.516-0.531l2.488-0.343L44.243,12.998z M36,12.321v-0.36l2.67-0.582l2.711-0.494L42.66,10.6l1.12-0.211l0.087,0.769l-1.402,0.317l-3.99,0.682l-2.521,0.56L36,12.321z M43.418,8.727l0.096,0.434L40.75,9.694l-2.992,0.457l-2.437,0.434l-0.22-0.808l5.709-1.049l2.569-0.423L43.418,8.727z M48.141,5.422l-2.325,0.365l-0.326-1.106l-0.15-1.333l1.067-0.134l1.337-0.275l0.319,1.245L48.141,5.422z\"/>\n</svg><svg version=\"1.1\" id=\"logo-name\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\" width=\"116.926px\" height=\"29.117px\" viewBox=\"0 0 116.926 29.117\" enable-background=\"new 0 0 116.926 29.117\" xml:space=\"preserve\">\n  <g>\n  \t<path fill=\"#4E4E4E\" d=\"M1.984,10.559c0-0.48-0.257-0.704-0.704-0.704H0V6.399h3.744c1.439,0,2.144,0.672,2.144,1.824v0.48c0,0.352-0.032,0.704-0.032,0.704H5.92c0.768-1.6,2.72-3.392,5.279-3.392c2.464,0,4.063,1.152,4.768,3.359h0.064c0.863-1.76,2.976-3.359,5.567-3.359c3.392,0,5.344,1.92,5.344,6.208v6.367c0,0.449,0.256,0.705,0.704,0.705h1.247v3.424h-3.839c-1.536,0-2.176-0.641-2.176-2.176v-7.552c0-1.856-0.353-3.264-2.24-3.264c-2.016,0-3.456,1.696-3.904,3.744c-0.191,0.641-0.256,1.312-0.256,2.08v7.168h-4.063v-9.728c0-1.76-0.256-3.264-2.208-3.264c-2.048,0-3.424,1.696-3.936,3.776c-0.16,0.64-0.256,1.312-0.256,2.048v7.168H1.984V10.559z\"/>\n  \t<path fill=\"#4E4E4E\" d=\"M40.255,12.479h0.513v-0.128c0-2.336-0.929-3.2-3.008-3.2c-0.736,0-2.177,0.192-2.177,1.088v0.864h-3.775V9.375c0-3.04,4.288-3.359,5.983-3.359c5.439,0,7.04,2.848,7.04,6.496v6.079c0,0.449,0.256,0.705,0.704,0.705h1.279v3.424h-3.647c-1.504,0-2.08-0.832-2.08-1.793c0-0.416,0.032-0.703,0.032-0.703h-0.064c0,0-1.247,2.879-4.928,2.879c-2.911,0-5.567-1.823-5.567-5.087C30.56,12.895,37.6,12.479,40.255,12.479z M37.119,19.935c2.176,0,3.712-2.304,3.712-4.288v-0.384h-0.704c-2.111,0-5.472,0.288-5.472,2.56C34.655,18.91,35.455,19.935,37.119,19.935z\"/>\n  \t<path fill=\"#4E4E4E\" d=\"M50.208,10.559c0-0.48-0.257-0.704-0.704-0.704h-1.28V6.399h3.872c1.504,0,2.144,0.672,2.144,2.176V18.59c0,0.449,0.256,0.705,0.704,0.705h1.28v3.424h-3.872c-1.504,0-2.144-0.641-2.144-2.176V10.559z M50.399,0h3.52v3.744h-3.52V0z\"/>\n  \t<path fill=\"#4E4E4E\" d=\"M59.552,4.16c0-0.48-0.256-0.704-0.704-0.704h-1.279V0h3.871c1.504,0,2.176,0.672,2.176,2.176V18.59c0,0.449,0.257,0.705,0.704,0.705h1.248v3.424h-3.84c-1.535,0-2.176-0.641-2.176-2.176V4.16z\"/>\n  \t<path fill=\"#4E4E4E\" d=\"M69.087,9.215c0-0.448-0.257-0.704-0.704-0.704h-1.344V6.624h2.207c1.376,0,1.92,0.576,1.92,1.696c0,0.64-0.031,1.088-0.031,1.088h0.063c0,0,1.28-3.168,5.536-3.168c4.319,0,7.007,3.456,7.007,8.448c0,5.087-3.039,8.414-7.199,8.414c-3.936,0-5.279-3.039-5.279-3.039h-0.064c0,0,0.064,0.576,0.064,1.408v7.646h-2.176V9.215zM76.318,21.183c2.848,0,5.184-2.399,5.184-6.495c0-3.937-2.08-6.464-5.088-6.464c-2.688,0-5.216,1.92-5.216,6.496C71.198,17.951,72.958,21.183,76.318,21.183z\"/>\n  \t<path fill=\"#4E4E4E\" d=\"M88.03,9.215c0-0.448-0.256-0.704-0.703-0.704h-1.345V6.624h2.272c1.376,0,1.952,0.576,1.952,1.952v11.552c0,0.479,0.256,0.703,0.703,0.703h1.344v1.889h-2.271c-1.376,0-1.952-0.576-1.952-1.952V9.215z M87.967,0.224h2.111v2.751h-2.111V0.224z\"/>\n  \t<path fill=\"#4E4E4E\" d=\"M95.871,2.816c0-0.448-0.256-0.704-0.704-0.704h-1.344V0.224h2.271c1.376,0,1.952,0.576,1.952,1.952v17.951c0,0.479,0.256,0.703,0.704,0.703h1.344v1.889h-2.271c-1.376,0-1.952-0.576-1.952-1.952V2.816z\"/>\n  \t<path fill=\"#4E4E4E\" d=\"M110.174,6.239c4.448,0,6.752,3.424,6.752,7.424c0,0.384-0.063,1.088-0.063,1.088h-12.352c0.064,4.063,2.912,6.399,6.239,6.399c2.944,0,4.832-1.984,4.832-1.984l1.056,1.633c0,0-2.336,2.303-5.983,2.303c-4.768,0-8.415-3.455-8.415-8.414C102.239,9.407,105.854,6.239,110.174,6.239z M114.686,13.023c-0.128-3.328-2.176-4.959-4.543-4.959c-2.656,0-4.992,1.728-5.536,4.959H114.686z\"/>\n  </g>\n</svg></div>\n\n<!--\n<div id=\"login-messages\" class=\"animated text-center\">\nDecrypting Index, Messages, and Contacts\n</div>\n-->\n\n<div id=\"login-vault-lock\" class=\"vault-lock-outer animated\">\n  <div class=\"vault-lock-inner animated\">\n    <div class=\"vault-lock icon-groups animated\"></div>\n  </div>\n</div>\n\n<div id=\"login-details\" class=\"animated\">\n  <div class=\"form-text\">Launch Mailpile as ...</div>\n  <form method=\"POST\" action='admin.cgi'\n        id=\"form-login\" class=\"clearfix animated\">\n   <div class='form-login'>\n    <input id=\"login-passphrase\" type=\"text\" name=\"username\"\n           autocomplete=\"off\" tabindex=1 alt=\"username\"\n           placeholder=\"username\">\n    <button type=\"submit\" class=\"submit\">\n      <span class=\"icon-arrow-right\"></span>\n    </button>\n   </div>\n  </form>\n\n  <div class=\"logged-out-message not-running\" style=\"margin-top: 50x\">\n    <p>Your Mailpile may not be running</p>\n    <p style='font-weight: normal; text-align: left;'>\n      To launch the app, input your username or e-mail.\n    </p>\n    <p style='font-weight: normal; text-align: left;'>\n      <b>Note:</b> If you have never used Mailpile on this machine\n      before, the administrator (you?) may first need to\n      <a target=_blank style=\"color: white;\"\n         href=\"https://github.com/mailpile/Mailpile/tree/master/shared-data/multipile\">\n      activate it for this account (click for details)</a>.\n    </p>\n    <noscript>\n      <p>\n        <b>WARNING</b>\n        <br>You have Javascript disabled. This breaks things!\n      </p>\n    </noscript>\n  </div>\n\n</div>\n\n<script>\n$(document).ready(function() {\n\n  var height = $(window).height() - 16;\n  $('#content-wide').css({'height': height, 'margin-top': '0px'});\n  $('#login').height(height);\n  $('#login-left').height(height);\n  $('#login-right').height(height);\n\n  $('#login-passphrase').focus();\n});\n\n// Login Form is submitted\n$(document).on('submit', '#form-login', function(e) {\n\n  // Details\n  $('#login-details').addClass('bounceOutDown');\n\n  // Lock\n  setTimeout(function() {\n    $('#login-vault-lock').find('div.vault-lock').addClass('fadeOut');\n  }, 250);\n\n  setTimeout(function() {\n    $('#login-vault-lock').addClass('bounceOutUp');\n  }, 500);\n\n  // Panels\n  setTimeout(function() {\n    $('#login-left').addClass('bounceOutLeft');\n    $('#login-right').addClass('bounceOutRight');\n  }, 850);\n\n  // Loading\n  setTimeout(function() {\n    $('#login-logo .logo-name').addClass('fadeOut');\n    $('#login-logo').css({'left': '40%'});\n  }, 1000);\n\n  setInterval(function() {\n    $(\"#logo-bluemail\").fadeOut(2000);\n    $(\"#logo-redmail\").hide(2000);\n    $(\"#logo-greenmail\").hide(3000);\n    $(\"#logo-bluemail\").fadeIn(2000);\n    $(\"#logo-greenmail\").fadeIn(4000);\n    $(\"#logo-redmail\").fadeIn(6000);\n  }, 1000);\n\n});\n</script>\n</body>\n</html>\n"
  },
  {
    "path": "shared-data/multipile/www/not-running.html",
    "content": "<h1>Mailpile Is Not Here (yet)</h1>\n<p>\n  Note: If you can see this, that means your Mailpile's Apache integration is\n        not working properly. Oops!\n</p>\n"
  },
  {
    "path": "test-requirements.txt",
    "content": "selenium>=2.40.0\nmock>=1.0.1\ncoverage\nnose\ntox\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nenvlist = py27\n\n[testenv]\nVIRTUAL_ENV={envdir}\ndeps = -r{toxinidir}/requirements.txt\n       -r{toxinidir}/test-requirements.txt\ncommands =\n    nosetests {toxinidir}/mailpile/tests --with-coverage --cover-package=mailpile\n    python ./scripts/mailpile-test.py\n"
  }
]