├── docs
├── manhtml
│ └── .lock
├── rfcs
│ ├── README.md
│ ├── rfc2088.IMAP4_non_synchronizing_literals.txt
│ ├── rfc2061.compatibility_IMAP4-IMAP2bis.txt
│ ├── rfc1733.models_in_IMAP4.txt
│ ├── rfc2177.IMAP4_IDLE_command.txt
│ ├── rfc3691.IMAP_UNSELECT_command.txt
│ └── rfc2087.IMAP4_QUOTA_extension.txt
├── doc-src
│ ├── index.rst
│ ├── ui.rst
│ ├── repository.rst
│ ├── dco.rst
│ ├── API.rst
│ └── conf.py
├── build-uploads.sh
├── Makefile
├── offlineimapui.txt
├── website-doc.sh
└── offlineimap.known_issues.txt
├── test
├── __init__.py
├── tests
│ ├── __init__.py
│ ├── test_00_globals.py
│ ├── test_02_MappedIMAP.py
│ ├── test_00_imaputil.py
│ └── test_01_basic.py
├── .gitignore
├── credentials.conf.sample
├── README
└── OLItest
│ ├── __init__.py
│ └── globals.py
├── offlineimap
├── utils
│ ├── __init__.py
│ ├── stacktrace.py
│ ├── const.py
│ └── distro.py
├── folder
│ └── __init__.py
├── globals.py
├── __init__.py
├── version.py
├── ui
│ ├── __init__.py
│ ├── debuglock.py
│ ├── Noninteractive.py
│ ├── TTY.py
│ └── Machine.py
├── repository
│ ├── GmailMaildir.py
│ ├── __init__.py
│ ├── Gmail.py
│ └── LocalStatus.py
├── error.py
├── emailutil.py
├── localeval.py
├── virtual_imaplib2.py
└── threadutil.py
├── tests
├── .gitignore
├── requirements.txt
└── create_conf_file.py
├── setup.cfg
├── contrib
├── store-pw-with-gpg
│ ├── passwords-gmail.txt
│ ├── README.md
│ ├── offlineimaprc.sample
│ └── gpg-pw.py
├── systemd
│ ├── offlineimap-oneshot.timer
│ ├── offlineimap-oneshot@.timer
│ ├── offlineimap.service
│ ├── offlineimap@.service
│ ├── offlineimap-oneshot.service
│ ├── offlineimap-oneshot@.service
│ └── README.md
├── README.md
├── internet-urllib3.py
├── upcoming.py
└── tested-by.py
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
└── CODEOWNERS
├── requirements.txt
├── .coveragerc
├── .gitignore
├── offlineimap.conf.minimal
├── MANIFEST.in
├── snapcraft.yaml
├── CODE_OF_CONDUCT.md
├── bin
└── offlineimap
├── offlineimap.py
├── Changelog.maint.md
├── MAINTAINERS.rst
├── Makefile
├── scripts
└── get-repository.sh
├── setup.py
├── .travis.yml
├── TODO.rst
├── CONTRIBUTING.rst
└── README.md
/docs/manhtml/.lock:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/offlineimap/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 |
3 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 |
2 | [metadata]
3 | description-file = README.md
4 |
--------------------------------------------------------------------------------
/tests/requirements.txt:
--------------------------------------------------------------------------------
1 | pytest
2 | pytest-cov
3 | coverage
4 | codecov
5 |
--------------------------------------------------------------------------------
/test/.gitignore:
--------------------------------------------------------------------------------
1 | credentials.conf
2 | tmp_*
3 | *.pyc
4 | OLItest/*.pyc
5 | tests/*.pyc
6 |
--------------------------------------------------------------------------------
/offlineimap/folder/__init__.py:
--------------------------------------------------------------------------------
1 | from . import Base, Gmail, IMAP, Maildir, LocalStatus, UIDMaps
2 |
3 |
--------------------------------------------------------------------------------
/contrib/store-pw-with-gpg/passwords-gmail.txt:
--------------------------------------------------------------------------------
1 | account1@gmail.com password1
2 | account2@gmail.com password2
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | open_collective: offlineimap-organization
4 |
--------------------------------------------------------------------------------
/docs/rfcs/README.md:
--------------------------------------------------------------------------------
1 |
2 | All RFCs related to IMAP.
3 |
4 | TODO: Add a brief introduction here to introduce the most important RFCs.
5 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # Minimal requirements defined in setup.py
2 | -e .
3 | # Extra "optional" requirements
4 | gssapi[kerberos]
5 | portalocker[cygwin]
6 |
--------------------------------------------------------------------------------
/contrib/systemd/offlineimap-oneshot.timer:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Offlineimap Query Timer
3 |
4 | [Timer]
5 | OnBootSec=1m
6 | OnUnitInactiveSec=15m
7 |
8 | [Install]
9 | WantedBy=default.target
10 |
--------------------------------------------------------------------------------
/contrib/systemd/offlineimap-oneshot@.timer:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Offlineimap Query Timer for account %i
3 |
4 | [Timer]
5 | OnBootSec=1m
6 | OnUnitInactiveSec=15m
7 |
8 | [Install]
9 | WantedBy=default.target
10 |
--------------------------------------------------------------------------------
/contrib/systemd/offlineimap.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Offlineimap Service
3 | Documentation=man:offlineimap(1)
4 |
5 | [Service]
6 | ExecStart=/usr/bin/offlineimap -u basic
7 | Restart=on-failure
8 | RestartSec=60
9 |
10 | [Install]
11 | WantedBy=default.target
12 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | branch = True
3 | source = offlineimap
4 |
5 | [report]
6 | exclude_lines =
7 | if self.debug:
8 | pragma: no cover
9 | raise NotImplementedError
10 | if __name__ == .__main__.:
11 | ignore_errors = True
12 | omit =
13 | tests/*
14 | test/*
15 |
--------------------------------------------------------------------------------
/contrib/systemd/offlineimap@.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Offlineimap Service for account %i
3 | Documentation=man:offlineimap(1)
4 |
5 | [Service]
6 | ExecStart=/usr/bin/offlineimap -a %i -u basic
7 | Restart=on-failure
8 | RestartSec=60
9 |
10 | [Install]
11 | WantedBy=default.target
12 |
--------------------------------------------------------------------------------
/test/credentials.conf.sample:
--------------------------------------------------------------------------------
1 | [Repository IMAP]
2 | type = IMAP
3 | remotehost = localhost
4 | ssl = no
5 | #sslcacertfile =
6 | #cert_fingerprint =
7 | remoteuser = user@domain
8 | remotepass = SeKr3t
9 |
10 | [Repository Gmail]
11 | type = Gmail
12 | remoteuser = user@domain
13 | remotepass = SeKr3t
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Backups.
2 | .*.swp
3 | .*.swo
4 | *~
5 | # websites.
6 | /website/
7 | /wiki/
8 | # Generated files.
9 | *.html
10 | *.css
11 | /docs/dev-doc/
12 | /build/
13 | *.pyc
14 | offlineimap.1
15 | offlineimapui.7
16 | # Editors/IDEs
17 | tags
18 | # Generated conf files for Travis-CI tests.
19 | oli-travis.conf
20 |
--------------------------------------------------------------------------------
/contrib/systemd/offlineimap-oneshot.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Offlineimap Service (oneshot)
3 | Documentation=man:offlineimap(1)
4 |
5 | [Service]
6 | Type=oneshot
7 | ExecStart=/usr/bin/offlineimap -o -u basic
8 | # Give 120 seconds for offlineimap to gracefully stop before hard killing it:
9 | TimeoutStopSec=120
10 |
11 | [Install]
12 | WantedBy=mail.target
13 |
--------------------------------------------------------------------------------
/contrib/README.md:
--------------------------------------------------------------------------------
1 |
2 | README
3 | ======
4 |
5 | **This "./contrib" directory is where users share their own scripts and tools.**
6 |
7 | Everything here is submitted and maintained *by the users for the users*. You're
8 | welcome to add your own stuff. There is no barrier on your contributions here.
9 | We think it's expected to find contributions of various quality.
10 |
--------------------------------------------------------------------------------
/contrib/systemd/offlineimap-oneshot@.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Offlineimap Service for account %i (oneshot)
3 | Documentation=man:offlineimap(1)
4 |
5 | [Service]
6 | Type=oneshot
7 | ExecStart=/usr/bin/offlineimap -o -a %i -u basic
8 | # Give 120 seconds for offlineimap to gracefully stop before hard killing it.
9 | TimeoutStopSec=120
10 |
11 | [Install]
12 | WantedBy=default.target
13 |
--------------------------------------------------------------------------------
/offlineimap/globals.py:
--------------------------------------------------------------------------------
1 | # Copyright 2013-2016 Eygene A. Ryabinkin & contributors.
2 | #
3 | # Module that holds various global objects.
4 |
5 | from offlineimap.utils import const
6 |
7 | # Holds command-line options for OfflineIMAP.
8 | options = const.ConstProxy()
9 |
10 | def set_options(source):
11 | """Sets the source for options variable."""
12 |
13 | options.set_source(source)
14 |
--------------------------------------------------------------------------------
/offlineimap.conf.minimal:
--------------------------------------------------------------------------------
1 | # Sample minimal config file. Copy this to ~/.offlineimaprc and edit to
2 | # get started fast.
3 |
4 | [general]
5 | accounts = Test
6 |
7 | [Account Test]
8 | localrepository = Local
9 | remoterepository = Remote
10 |
11 | [Repository Local]
12 | type = Maildir
13 | localfolders = ~/Test
14 |
15 | [Repository Remote]
16 | type = IMAP
17 | remotehost = examplehost
18 | remoteuser = jgoerzen
19 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | global-exclude .gitignore .git *.bak *.orig *.rej
2 | include setup.py
3 | include COPYING
4 | include Changelog*
5 | include MAINTAINERS
6 | include MANIFEST.in
7 | include Makefile
8 | include README.md
9 | include offlineimap.conf*
10 | include offlineimap.py
11 | recursive-include contrib *
12 | recursive-include offlineimap *.py
13 | recursive-include bin *
14 | recursive-include docs *
15 | recursive-include test *
16 | prune docs/rfcs
17 |
--------------------------------------------------------------------------------
/snapcraft.yaml:
--------------------------------------------------------------------------------
1 | name: offlineimap
2 | version: git
3 | summary: OfflineIMAP
4 | description: |
5 | OfflineIMAP is software that downloads your email mailbox(es) as local
6 | Maildirs. OfflineIMAP will synchronize both sides via IMAP.
7 |
8 | grade: devel
9 | confinement: devmode
10 |
11 | apps:
12 | offlineimap:
13 | command: bin/offlineimap
14 |
15 | parts:
16 | offlineimap:
17 | plugin: python
18 | python-version: python2
19 | source: .
20 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 |
2 | # Realistic Code of Conduct
3 |
4 | 1. We mostly care about making our softwares better.
5 |
6 | 2. Everybody is free to decide how to contribute.
7 |
8 | 3. We believe in free speech. Everyone's entitled to their opinion.
9 |
10 | 4. Feel offended? This might be very well-deserved.
11 |
12 | 5. We don't need a code of conduct imposed on us, thanks.
13 |
14 | 6. Ignoring this Realistic Code of Conduct is welcome.
15 |
16 |
19 |
--------------------------------------------------------------------------------
/docs/doc-src/index.rst:
--------------------------------------------------------------------------------
1 | .. OfflineImap documentation master file
2 | .. _OfflineIMAP: http://www.offlineimap.org
3 |
4 |
5 | Welcome to OfflineIMAP's developer documentation
6 | ================================================
7 |
8 | **License**
9 | :doc:`dco` (dco)
10 |
11 | **Documented APIs**
12 |
13 | .. toctree::
14 | API
15 | repository
16 | ui
17 |
18 |
19 | .. moduleauthor:: John Goerzen, and many others. See AUTHORS and the git history for a full list.
20 |
21 | :License: This module is covered under the GNU GPL v2 (or later).
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 | #### General informations
3 |
4 | - system/distribution (with version):
5 | - offlineimap version (`offlineimap -V`):
6 | - Python version:
7 | - server name or domain:
8 | - CLI options:
9 |
10 | #### Configuration file offlineimaprc
11 |
12 | ```
13 | REMOVE PRIVATE DATA.
14 | ```
15 |
16 | #### pythonfile (if any)
17 |
18 | ```
19 | REMOVE PRIVATE DATA.
20 | ```
21 |
22 |
23 | #### Logs, error
24 |
25 | ```
26 | REMOVE PRIVATE DATA.
27 | ```
28 |
29 | #### Steps to reproduce the error
30 |
31 | -
32 | -
33 |
34 |
--------------------------------------------------------------------------------
/contrib/internet-urllib3.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import urllib3
4 | import certifi
5 |
6 | def isInternetConnected(url="www.ietf.org"):
7 | result = False
8 | http = urllib3.PoolManager(
9 | cert_reqs='CERT_REQUIRED', # Force certificate check.
10 | ca_certs=certifi.where(), # Path to the Certifi bundle.
11 | )
12 | try:
13 | r = http.request('HEAD', 'https://' + url)
14 | result = True
15 | except Exception as e: # urllib3.exceptions.SSLError
16 | result = False
17 | return result
18 |
19 | print isInternetConnected()
20 |
--------------------------------------------------------------------------------
/offlineimap/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = ['OfflineImap']
2 |
3 | from offlineimap.version import (
4 | __productname__,
5 | __version__,
6 | __copyright__,
7 | __author__,
8 | __author_email__,
9 | __description__,
10 | __license__,
11 | __bigcopyright__,
12 | __homepage__,
13 | banner
14 | )
15 |
16 | from offlineimap.error import OfflineImapError
17 | # put this last, so we don't run into circular dependencies using
18 | # e.g. offlineimap.__version__.
19 | from offlineimap.init import OfflineImap
20 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | > This v1.1 template stands in `.github/`.
2 |
3 | ### This PR
4 |
5 | > Add character x `[x]`.
6 |
7 | - [ ] I've read the [DCO](http://www.offlineimap.org/doc/dco.html).
8 | - [ ] I've read the [Coding Guidelines](http://www.offlineimap.org/doc/CodingGuidelines.html)
9 | - [ ] The relevant informations about the changes stands in the commit message, not here in the message of the pull request.
10 | - [ ] Code changes follow the style of the files they change.
11 | - [ ] Code is tested (provide details).
12 |
13 | ### References
14 |
15 | - Issue #no_space
16 |
17 | ### Additional information
18 |
19 |
20 |
--------------------------------------------------------------------------------
/offlineimap/version.py:
--------------------------------------------------------------------------------
1 | __productname__ = 'OfflineIMAP'
2 | # Expecting trailing "-rcN" or "" for stable releases.
3 | __version__ = "7.3.4"
4 | __copyright__ = "Copyright 2002-2021 John Goerzen & contributors"
5 | __author__ = "John Goerzen"
6 | __author_email__= "offlineimap-project@lists.alioth.debian.org"
7 | __description__ = "Disconnected Universal IMAP Mail Synchronization/Reader Support"
8 | __license__ = "Licensed under the GNU GPL v2 or any later version (with an OpenSSL exception)"
9 | __bigcopyright__ = """%(__productname__)s %(__version__)s
10 | %(__license__)s""" % locals()
11 | __homepage__ = "http://www.offlineimap.org"
12 |
13 | banner = __bigcopyright__
14 |
--------------------------------------------------------------------------------
/offlineimap/utils/stacktrace.py:
--------------------------------------------------------------------------------
1 | # Copyright 2013 Eygene A. Ryabinkin
2 | # Functions to perform stack tracing (for multithreaded programs
3 | # as well as for single-threaded ones).
4 |
5 | import sys
6 | import threading
7 | import traceback
8 |
9 |
10 | def dump(out):
11 | """ Dumps current stack trace into I/O object 'out' """
12 | id2name = {}
13 | for th in threading.enumerate():
14 | id2name[th.ident] = th.name
15 | n = 0
16 | for i, stack in sys._current_frames().items():
17 | out.write ("\n# Thread #%d (id=%d), %s\n" % \
18 | (n, i, id2name[i]))
19 | n = n + 1
20 | for f, lno, name, line in traceback.extract_stack (stack):
21 | out.write ('File: "%s", line %d, in %s' % \
22 | (f, lno, name))
23 | if line:
24 | out.write (" %s" % (line.strip()))
25 | out.write ("\n")
26 |
--------------------------------------------------------------------------------
/test/README:
--------------------------------------------------------------------------------
1 | Documentation for the OfflineImap Test suite.
2 |
3 | How to run the tests
4 | ====================
5 |
6 | - Copy the credentials.conf.sample to credentials.conf and insert
7 | credentials for an IMAP account and a Gmail account. Delete the Gmail
8 | section if you don't have a Gmail account. Do note, that the tests
9 | will change the account and upload/delete/modify it's contents and
10 | folder structure. So don't use a real used account here...
11 |
12 | - go to the top level dir (one above this one) and execute:
13 | 'python setup.py test'
14 |
15 | System requirements
16 | ===================
17 |
18 | This test suite depend on python>=2.7 to run out of the box. If you want to run this with python 2.6 you will need to install the backport from http://pypi.python.org/pypi/unittest2 instead.
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # This is a comment.
2 | # Each line is a file pattern followed by one or more owners.
3 |
4 | # These owners will be the default owners for everything in the repo.
5 | # Unless a later match takes precedence, @global-owner1 and @global-owner2
6 | # will be requested for review when someone opens a pull request.
7 | #* @global-owner1 @global-owner2
8 |
9 | # Order is important; the last matching pattern takes the most precedence.
10 | # When someone opens a pull request that only modifies JS files, only @js-owner
11 | # and not the global owner(s) will be requested for a review.
12 | #*.js @js-owner
13 |
14 | # You can also use email addresses if you prefer. They'll be used to look up
15 | # users just like we do for commit author emails.
16 | #docs/* docs@example.com
17 |
18 |
19 | * @chris001
20 |
--------------------------------------------------------------------------------
/docs/build-uploads.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # vim: expandtab ts=2 :
4 |
5 | WEBSITE_UPLOADS='./website/_uploads'
6 |
7 | while true
8 | do
9 | test -d .git && break
10 | cd ..
11 | done
12 |
13 | set -e
14 |
15 | echo "make clean"
16 | make clean >/dev/null
17 | echo "make targz"
18 | make targz >/dev/null
19 |
20 | # Defined in the root Makefile.
21 | version="$(./offlineimap.py --version)"
22 | abbrev="$(git log --format='%h' HEAD~1..)"
23 | targz="../offlineimap-v${version}-${abbrev}.tar.gz"
24 |
25 | filename="offlineimap-v${version}.tar.gz"
26 |
27 | mv -v "$targz" "${WEBSITE_UPLOADS}/${filename}"
28 | cd "$WEBSITE_UPLOADS"
29 | for digest in sha1 sha256 sha512
30 | do
31 | target="${filename}.${digest}"
32 | echo "Adding digest ${WEBSITE_UPLOADS}/${target}"
33 | "${digest}sum" "$filename" > "$target"
34 | done
35 |
--------------------------------------------------------------------------------
/docs/doc-src/ui.rst:
--------------------------------------------------------------------------------
1 | :mod:`offlineimap.ui` -- A flexible logging system
2 | --------------------------------------------------------
3 |
4 | .. currentmodule:: offlineimap.ui
5 |
6 | OfflineImap has various ui systems, that can be selected. They offer various
7 | functionalities. They must implement all functions that the
8 | :class:`offlineimap.ui.UIBase` offers. Early on, the ui must be set using
9 | :meth:`getglobalui`
10 |
11 | .. automethod:: offlineimap.ui.setglobalui
12 | .. automethod:: offlineimap.ui.getglobalui
13 |
14 | Base UI plugin
15 | ^^^^^^^^^^^^^^^^^^^^^^^^^^
16 |
17 | .. autoclass:: offlineimap.ui.UIBase.UIBase
18 | :members:
19 | :inherited-members:
20 |
21 | .. .. note:: :meth:`foo`
22 | .. .. attribute:: Database.MODE
23 |
24 | Defines constants that are used as the mode in which to open a database.
25 |
26 | MODE.READ_ONLY
27 | Open the database in read-only mode
28 |
29 | MODE.READ_WRITE
30 | Open the database in read-write mode
31 |
--------------------------------------------------------------------------------
/bin/offlineimap:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python2
2 | # Startup from system-wide installation
3 | # Copyright (C) 2002-2018 John Goerzen & contributors
4 | #
5 | # This program is free software; you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation; either version 2 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program; if not, write to the Free Software
17 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 |
19 | from offlineimap import OfflineImap
20 |
21 | oi = OfflineImap()
22 | oi.run()
23 |
--------------------------------------------------------------------------------
/offlineimap.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python2
2 | # Startup from single-user installation
3 | # Copyright (C) 2002-2018 John Goerzen & contributors
4 | #
5 | # This program is free software; you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation; either version 2 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program; if not, write to the Free Software
17 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 |
19 | import os
20 | import sys
21 |
22 | from offlineimap import OfflineImap
23 |
24 | oi = OfflineImap()
25 | oi.run()
26 |
--------------------------------------------------------------------------------
/contrib/systemd/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Integrating OfflineIMAP into systemd
4 | author: Ben Boeckel
5 | date: 2015-03-22
6 | contributors: Abdo Roig-Maranges, benutzer193, Hugo Osvaldo Barrera
7 | updated: 2017-06-01
8 | ---
9 |
10 |
11 |
12 |
13 | ## Systemd units
14 |
15 | These unit files are meant to be used in the user session. You may drop them
16 | into `/etc/systemd/user` or `${XDG_DATA_HOME}/systemd/user` followed by
17 | `systemctl --user daemon-reload` to have systemd aware of the unit files.
18 |
19 | These files are meant to be triggered either manually using `systemctl --user
20 | start offlineimap.service` or by enabling the timer unit using `systemctl --user
21 | enable offlineimap-oneshot.timer`. Additionally, specific accounts may be
22 | triggered by using `offlineimap@myaccount.timer` or
23 | `offlineimap-oneshot@myaccount.service`.
24 |
25 | If the defaults provided by these units doesn't suit your setup, any of the
26 | values may be overridden by using `systemctl --user edit offlineimap.service`.
27 | This'll prevent having to copy-and-edit the original file.
28 |
--------------------------------------------------------------------------------
/Changelog.maint.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Changelog of the stable branch
4 | ---
5 |
6 | * The following excerpt is only usefull when rendered in the website.
7 | {:toc}
8 |
9 | This is the Changelog of the maintenance branch.
10 |
11 | **NOTE FROM THE MAINTAINER:**
12 |
13 | This branch comes almost as-is. With no URGENT requirements to update this
14 | branch (e.g. big security fix), it is left behind.
15 | If anyone volunteers to maintain it and backport patches, let us know!
16 |
17 |
18 | ### OfflineIMAP v6.7.0.3 (2016-07-26)
19 |
20 | #### Bug Fixes
21 |
22 | * sqlite: properly serialize operations on the database files
23 |
24 |
25 | ### OfflineIMAP v6.7.0.2 (2016-07-22)
26 |
27 | #### Bug Fixes
28 |
29 | * sqlite: close the database when no more threads need connection.
30 |
31 |
32 | ### OfflineIMAP v6.7.0.1 (2016-06-08)
33 |
34 | #### Bug Fixes
35 |
36 | * Correctly open and close sqlite databases.
37 |
38 |
39 | ### OfflineIMAP v6.3.2.1 (2011-03-23)
40 |
41 | #### Bug Fixes
42 |
43 | * Sanity checks for SSL cacertfile configuration.
44 | * Fix regression (UIBase is no more).
45 | * Make profiling mode really enforce single-threading.
46 |
--------------------------------------------------------------------------------
/offlineimap/utils/const.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2013-2014 Eygene A. Ryabinkin and contributors
2 | #
3 | # Collection of classes that implement const-like behaviour
4 | # for various objects.
5 |
6 | import copy
7 |
8 | class ConstProxy(object):
9 | """Implements read-only access to a given object
10 | that can be attached to each instance only once."""
11 |
12 | def __init__(self):
13 | self.__dict__['__source'] = None
14 |
15 |
16 | def __getattr__(self, name):
17 | src = self.__dict__['__source']
18 | if src == None:
19 | raise ValueError("using non-initialized ConstProxy() object")
20 | return copy.deepcopy(getattr(src, name))
21 |
22 |
23 | def __setattr__(self, name, value):
24 | raise AttributeError("tried to set '%s' to '%s' for constant object"% \
25 | (name, value))
26 |
27 |
28 | def __delattr__(self, name):
29 | raise RuntimeError("tried to delete field '%s' from constant object"% \
30 | (name))
31 |
32 |
33 | def set_source(self, source):
34 | """ Sets source object for this instance. """
35 | if (self.__dict__['__source'] != None):
36 | raise ValueError("source object is already set")
37 | self.__dict__['__source'] = source
38 |
--------------------------------------------------------------------------------
/test/tests/test_00_globals.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright 2013 Eygene A. Ryabinkin
3 |
4 | from offlineimap import globals
5 | import unittest
6 |
7 | class Opt:
8 | def __init__(self):
9 | self.one = "baz"
10 | self.two = 42
11 | self.three = True
12 |
13 |
14 | class TestOfflineimapGlobals(unittest.TestCase):
15 |
16 | @classmethod
17 | def setUpClass(klass):
18 | klass.o = Opt()
19 | globals.set_options (klass.o)
20 |
21 | def test_initial_state(self):
22 | for k in self.o.__dict__.keys():
23 | self.assertTrue(getattr(self.o, k) ==
24 | getattr(globals.options, k))
25 |
26 | def test_object_changes(self):
27 | self.o.one = "one"
28 | self.o.two = 119
29 | self.o.three = False
30 | return self.test_initial_state()
31 |
32 | def test_modification(self):
33 | with self.assertRaises(AttributeError):
34 | globals.options.two = True
35 |
36 | def test_deletion(self):
37 | with self.assertRaises(RuntimeError):
38 | del globals.options.three
39 |
40 | def test_nonexistent_key(self):
41 | with self.assertRaises(AttributeError):
42 | a = globals.options.nosuchoption
43 |
44 | def test_double_init(self):
45 | with self.assertRaises(ValueError):
46 | globals.set_options (True)
47 |
48 |
49 | if __name__ == "__main__":
50 | suite = unittest.TestLoader().loadTestsFromTestCase(TestOfflineimapGlobals)
51 | unittest.TextTestRunner(verbosity=2).run(suite)
52 |
--------------------------------------------------------------------------------
/offlineimap/ui/__init__.py:
--------------------------------------------------------------------------------
1 | # UI module
2 | # Copyright (C) 2010-2011 Sebastian Spaeth & contributors
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 | from offlineimap.ui.UIBase import getglobalui, setglobalui
19 | from offlineimap.ui import TTY, Noninteractive, Machine
20 |
21 | UI_LIST = {'ttyui': TTY.TTYUI,
22 | 'basic': Noninteractive.Basic,
23 | 'quiet': Noninteractive.Quiet,
24 | 'syslog': Noninteractive.Syslog,
25 | 'machineui': Machine.MachineUI}
26 |
27 | # add Blinkenlights UI if it imports correctly (curses installed)
28 | try:
29 | from offlineimap.ui import Curses
30 | UI_LIST['blinkenlights'] = Curses.Blinkenlights
31 | except ImportError:
32 | pass
33 |
--------------------------------------------------------------------------------
/contrib/store-pw-with-gpg/README.md:
--------------------------------------------------------------------------------
1 | # gpg-offlineimap
2 |
3 | Python bindings for offlineimap to use gpg instead of storing cleartext passwords
4 |
5 | Author: Lorenzo G.
6 | [GitHub](https://github.com/lorenzog/gpg-offlineimap)
7 |
8 | ## Quickstart
9 |
10 | Requirements: a working GPG set-up. Ideally with gpg-agent. Should work
11 | out of the box on most modern Linux desktop environments.
12 |
13 | 1. Enable IMAP in gmail (if you have two factor authentication, you
14 | need to create an app-specific password)
15 |
16 | 2. Create a directory `~/Mail`
17 |
18 | 3. In `~/Mail`, create a password file `passwords-gmail.txt`. Format:
19 | `account@gmail.com password`. Look at the example file in this
20 | directory.
21 |
22 | 4. **ENCRYPT** the file: `gpg -e passwords-gmail.txt`. It should create
23 | a file `passwords-gmail.txt.gpg`. Check you can decrypt it: `gpg -d
24 | passwords-gmail.txt.gpg`: it will ask you for your GPG password and
25 | show it to you.
26 |
27 | 5. Use the file `offlineimaprc.sample` as a sample for your own
28 | `.offlineimaprc`; edit it by following the comments. Minimal items
29 | to configure: the `remoteuser` field and the `pythonfile` parameter
30 | pointing at the `offlineimap.py` file in this directory.
31 |
32 | 6. Run it: `offlineimap`. It should ask you for your GPG passphrase to
33 | decrypt the password file.
34 |
35 | 7. If all works well, delete the cleartext password file.
36 |
37 |
38 |
--------------------------------------------------------------------------------
/offlineimap/repository/GmailMaildir.py:
--------------------------------------------------------------------------------
1 | # Maildir repository support
2 | # Copyright (C) 2002-2015 John Goerzen & contributors
3 | #
4 | #
5 | # This program is free software; you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation; either version 2 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program; if not, write to the Free Software
17 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 |
19 | from offlineimap.repository.Maildir import MaildirRepository
20 | from offlineimap.folder.GmailMaildir import GmailMaildirFolder
21 |
22 | class GmailMaildirRepository(MaildirRepository):
23 | def __init__(self, reposname, account):
24 | """Initialize a MaildirRepository object. Takes a path name
25 | to the directory holding all the Maildir directories."""
26 |
27 | super(GmailMaildirRepository, self).__init__(reposname, account)
28 |
29 | def getfoldertype(self):
30 | return GmailMaildirFolder
31 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # This program is free software under the terms of the GNU General Public
2 | # License. See the COPYING file which must come with this package.
3 |
4 | SOURCES = $(wildcard *.rst)
5 | HTML_TARGETS = $(patsubst %.rst,%.html,$(SOURCES))
6 |
7 | RM = rm
8 | RST2HTML=`type rst2html >/dev/null 2>&1 && echo rst2html || echo rst2html.py`
9 | RST2MAN=`type rst2man >/dev/null 2>&1 && echo rst2man || echo rst2man.py`
10 | SPHINXBUILD = python2 -msphinx
11 |
12 | docs: man api
13 |
14 | html: $(HTML_TARGETS)
15 |
16 | $(HTML_TARGETS): %.html : %.rst
17 | $(RST2HTML) $? $@
18 |
19 | manhtml: offlineimap.html offlineimapui.html
20 |
21 | offlineimap.html: offlineimap.txt offlineimap.known_issues.txt
22 | a2x -v -d manpage -D manhtml -f xhtml $<
23 |
24 | offlineimapui.html: offlineimapui.txt
25 | a2x -v -d manpage -D manhtml -f xhtml $<
26 |
27 |
28 | man: offlineimap.1 offlineimapui.7
29 |
30 | offlineimap.1: offlineimap.txt offlineimap.known_issues.txt
31 | a2x -v -d manpage -f manpage $<
32 |
33 | offlineimapui.7: offlineimapui.txt
34 | a2x -v -d manpage -f manpage $<
35 |
36 | api:
37 | $(SPHINXBUILD) -b html -d html/doctrees doc-src html
38 |
39 | websitedoc:
40 | ./website-doc.sh releases
41 | ./website-doc.sh api
42 | ./website-doc.sh html
43 | ./website-doc.sh contrib
44 |
45 | clean:
46 | $(RM) -f $(HTML_TARGETS)
47 | $(RM) -f offlineimap.1
48 | $(RM) -f offlineimap.7
49 | $(RM) -f manhtml/*
50 | $(RM) -rf html/*
51 | -find . -name '*.html' -exec rm -f {} \;
52 |
53 | .PHONY: clean doc
54 |
--------------------------------------------------------------------------------
/test/OLItest/__init__.py:
--------------------------------------------------------------------------------
1 | # OfflineImap test library
2 | # Copyright (C) 2012- Sebastian Spaeth & contributors
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 | __all__ = ['OLITestLib', 'TextTestRunner','TestLoader']
19 |
20 | __productname__ = 'OfflineIMAP Test suite'
21 | __version__ = '0'
22 | __copyright__ = "Copyright 2012- Sebastian Spaeth & contributors"
23 | __author__ = 'Sebastian Spaeth'
24 | __author_email__= 'Sebastian@SSpaeth.de'
25 | __description__ = 'Moo'
26 | __license__ = "Licensed under the GNU GPL v2+ (v2 or any later version)"
27 | __homepage__ = "http://www.offlineimap.org"
28 | banner = """%(__productname__)s %(__version__)s
29 | %(__license__)s""" % locals()
30 |
31 | import unittest
32 | from unittest import TestLoader, TextTestRunner
33 | from .globals import default_conf
34 | from .TestRunner import OLITestLib
35 |
--------------------------------------------------------------------------------
/test/OLItest/globals.py:
--------------------------------------------------------------------------------
1 | #Constants, that don't rely on anything else in the module
2 | # Copyright (C) 2012- Sebastian Spaeth & contributors
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 | try:
18 | from cStringIO import StringIO
19 | except ImportError: #python3
20 | from io import StringIO
21 |
22 | default_conf=StringIO("""[general]
23 | #will be set automatically
24 | metadata =
25 | accounts = test
26 | ui = quiet
27 |
28 | [Account test]
29 | localrepository = Maildir
30 | remoterepository = IMAP
31 |
32 | [Repository Maildir]
33 | Type = Maildir
34 | # will be set automatically during tests
35 | localfolders =
36 |
37 | [Repository IMAP]
38 | type=IMAP
39 | # Don't hammer the server with too many connection attempts:
40 | maxconnections=1
41 | folderfilter= lambda f: f.startswith('INBOX.OLItest') or f.startswith('INBOX/OLItest')
42 | """)
43 |
--------------------------------------------------------------------------------
/offlineimap/error.py:
--------------------------------------------------------------------------------
1 | class OfflineImapError(Exception):
2 | """An Error during offlineimap synchronization"""
3 |
4 | class ERROR:
5 | """Severity level of an Exception
6 |
7 | * **MESSAGE**: Abort the current message, but continue with folder
8 | * **FOLDER_RETRY**: Error syncing folder, but do retry
9 | * **FOLDER**: Abort folder sync, but continue with next folder
10 | * **REPO**: Abort repository sync, continue with next account
11 | * **CRITICAL**: Immediately exit offlineimap
12 | """
13 |
14 | MESSAGE, FOLDER_RETRY, FOLDER, REPO, CRITICAL = 0, 10, 15, 20, 30
15 |
16 | def __init__(self, reason, severity, errcode=None):
17 | """
18 | :param reason: Human readable string suitable for logging
19 |
20 | :param severity: denoting which operations should be
21 | aborted. E.g. a ERROR.MESSAGE can occur on a faulty
22 | message, but a ERROR.REPO occurs when the server is
23 | offline.
24 |
25 | :param errcode: optional number denoting a predefined error
26 | situation (which let's us exit with a predefined exit
27 | value). So far, no errcodes have been defined yet.
28 |
29 | :type severity: OfflineImapError.ERROR value"""
30 |
31 | self.errcode = errcode
32 | self.severity = severity
33 |
34 | # 'reason' is stored in the Exception().args tuple.
35 | super(OfflineImapError, self).__init__(reason)
36 |
37 | @property
38 | def reason(self):
39 | return self.args[0]
40 |
--------------------------------------------------------------------------------
/offlineimap/emailutil.py:
--------------------------------------------------------------------------------
1 | # Some useful functions to extract data out of emails
2 | # Copyright (C) 2002-2015 John Goerzen & contributors
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 | import email
19 | from email.parser import Parser as MailParser
20 |
21 | def get_message_date(content, header='Date'):
22 | """Parses mail and returns resulting timestamp.
23 |
24 | :param header: the header to extract date from;
25 | :returns: timestamp or `None` in the case of failure.
26 | """
27 |
28 | message = MailParser().parsestr(content, True)
29 | dateheader = message.get(header)
30 | # parsedate_tz returns a 10-tuple that can be passed to mktime_tz
31 | # Will be None if missing or not in a valid format. Note that
32 | # indexes 6, 7, and 8 of the result tuple are not usable.
33 | datetuple = email.utils.parsedate_tz(dateheader)
34 | if datetuple is None:
35 | return None
36 | return email.utils.mktime_tz(datetuple)
37 |
--------------------------------------------------------------------------------
/offlineimap/localeval.py:
--------------------------------------------------------------------------------
1 | """Eval python code with global namespace of a python source file."""
2 |
3 | # Copyright (C) 2002-2016 John Goerzen & contributors
4 | #
5 | # This program is free software; you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation; either version 2 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program; if not, write to the Free Software
17 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 |
19 | import imp
20 | try:
21 | import errno
22 | except:
23 | pass
24 |
25 | class LocalEval(object):
26 | """Here is a powerfull but very dangerous option, of course."""
27 |
28 | def __init__(self, path=None):
29 | self.namespace = {}
30 |
31 | if path is not None:
32 | # FIXME: limit opening files owned by current user with rights set
33 | # to fixed mode 644.
34 | foo = open(path, 'r')
35 | module = imp.load_module(
36 | '',
37 | foo,
38 | path,
39 | ('', 'r', imp.PY_SOURCE))
40 | for attr in dir(module):
41 | self.namespace[attr] = getattr(module, attr)
42 |
43 | def eval(self, text, namespace=None):
44 | names = {}
45 | names.update(self.namespace)
46 | if namespace is not None:
47 | names.update(namespace)
48 | return eval(text, names)
49 |
--------------------------------------------------------------------------------
/offlineimap/ui/debuglock.py:
--------------------------------------------------------------------------------
1 | # Locking debugging code -- temporary
2 | # Copyright (C) 2003-2015 John Goerzen & contributors
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 | from threading import Lock, currentThread
19 | import traceback
20 | logfile = open("/tmp/logfile", "wt")
21 | loglock = Lock()
22 |
23 | class DebuggingLock:
24 | def __init__(self, name):
25 | self.lock = Lock()
26 | self.name = name
27 |
28 | def acquire(self, blocking = 1):
29 | self.print_tb("Acquire lock")
30 | self.lock.acquire(blocking)
31 | self.logmsg("===== %s: Thread %s acquired lock\n"%
32 | (self.name, currentThread().getName()))
33 |
34 | def release(self):
35 | self.print_tb("Release lock")
36 | self.lock.release()
37 |
38 | def logmsg(self, msg):
39 | loglock.acquire()
40 | logfile.write(msg + "\n")
41 | logfile.flush()
42 | loglock.release()
43 |
44 | def print_tb(self, msg):
45 | self.logmsg(".... %s: Thread %s attempting to %s\n"% \
46 | (self.name, currentThread().getName(), msg) + \
47 | "\n".join(traceback.format_list(traceback.extract_stack())))
48 |
49 |
50 |
--------------------------------------------------------------------------------
/offlineimap/ui/Noninteractive.py:
--------------------------------------------------------------------------------
1 | # Noninteractive UI
2 | # Copyright (C) 2002-2016 John Goerzen & contributors.
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 | import logging
19 |
20 | import offlineimap
21 | from offlineimap.ui.UIBase import UIBase
22 |
23 | class Basic(UIBase):
24 | """'Basic' simply sets log level to INFO."""
25 |
26 | def __init__(self, config, loglevel = logging.INFO):
27 | return super(Basic, self).__init__(config, loglevel)
28 |
29 | class Quiet(UIBase):
30 | """'Quiet' simply sets log level to WARNING"""
31 | def __init__(self, config, loglevel = logging.WARNING):
32 | return super(Quiet, self).__init__(config, loglevel)
33 |
34 | class Syslog(UIBase):
35 | """'Syslog' sets log level to INFO and outputs to syslog instead of stdout"""
36 | def __init__(self, config, loglevel = logging.INFO):
37 | return super(Syslog, self).__init__(config, loglevel)
38 |
39 | def setup_consolehandler(self):
40 | # create syslog handler
41 | ch = logging.handlers.SysLogHandler('/dev/log')
42 | # create formatter and add it to the handlers
43 | self.formatter = logging.Formatter("%(message)s")
44 | ch.setFormatter(self.formatter)
45 | # add the handlers to the logger
46 | self.logger.addHandler(ch)
47 | self.logger.info(offlineimap.banner)
48 | return ch
49 |
50 | def setup_sysloghandler(self):
51 | pass # Do not honor -s (log to syslog) CLI option.
52 |
--------------------------------------------------------------------------------
/offlineimap/virtual_imaplib2.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2016-2016 Nicolas Sebrecht & contributors
2 | #
3 | # This program is free software; you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation; either version 2 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program; if not, write to the Free Software
15 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16 |
17 | """
18 |
19 | The virtual imaplib2 takes care to import the correct imaplib2 library. Any
20 | internal use of imaplib2 everywhere else in offlineimap must be done through
21 | this virtual_imaplib2 or we might go into troubles.
22 |
23 | """
24 |
25 | DESC = None
26 |
27 | _SUPPORTED_RELEASE = 2
28 | _SUPPORTED_REVISION = 57
29 |
30 | try:
31 | # Try any imaplib2 in PYTHONPATH first. This allows both maintainers of
32 | # distributions and developers to not work with the bundled imaplib2.
33 | from imaplib2 import *
34 | import imaplib2 as imaplib
35 |
36 | if (int(imaplib.__release__) < _SUPPORTED_RELEASE or
37 | int(imaplib.__revision__) < _SUPPORTED_REVISION):
38 | raise ImportError("The provided imaplib2 version '%s' is not supported"%
39 | imaplib.__version__)
40 | DESC = "system"
41 | except (ImportError, NameError) as e:
42 | try:
43 | from offlineimap.bundled_imaplib2 import *
44 | import offlineimap.bundled_imaplib2 as imaplib
45 |
46 | DESC = "bundled"
47 | except:
48 | print("Error while trying to import system imaplib2: %s"% e)
49 | raise
50 |
51 | # Upstream won't expose those literals to avoid erasing them with "import *" in
52 | # case they exist.
53 | __version__ = imaplib.__version__
54 | __release__ = imaplib.__release__
55 | __revision__ = imaplib.__revision__
56 |
--------------------------------------------------------------------------------
/MAINTAINERS.rst:
--------------------------------------------------------------------------------
1 | .. -*- coding: utf-8 -*-
2 |
3 | Contacts
4 | ========
5 |
6 | - Abdó Roig-Maranges
7 | - email: abdo.roig at gmail.com
8 | - github: aroig
9 |
10 | - Ben Boeckel
11 | - email: mathstuf at gmail.com
12 | - github: mathstuf
13 |
14 | - benutzer193
15 | - email: registerbn at gmail.com
16 | - github: benutzer193
17 |
18 | - Chris Coleman
19 | - email: chris at espacenetworks.com
20 | - github: chris001
21 |
22 | - Darshit Shah
23 | - email: darnir at gmail.com
24 | - github: darnir
25 |
26 | - Eygene Ryabinkin
27 | - email: rea at freebsd.org
28 | - github: konvpalto
29 | - other: FreeBSD maintainer
30 |
31 | - Igor Almeida
32 | - email: igor.contato at gmail.com
33 | - github: igoralmeida
34 |
35 | - Ilias Tsitsimpis
36 | - email: i.tsitsimpis at gmail.com
37 | - github: iliastsi
38 | - other: Debian maintainer
39 |
40 | - "J"
41 | - email: offlineimap at 927589452.de
42 | - github: 927589452
43 | - other: FreeBSD user
44 |
45 | - Łukasz Żarnowiecki
46 | - email: dolohow at outlook.com
47 | - github: dolohow
48 |
49 | - Nicolas Sebrecht
50 | - email: nicolas.s-dev at laposte.net
51 | - github: nicolas33
52 | - system: Linux
53 |
54 | - Remi Locherer
55 | - email: remi.locherer at relo.ch
56 | - system: OpenBSD maintainer
57 |
58 | - Sebastian Spaeth
59 | - email: sebastian at sspaeth.de
60 | - github: spaetz
61 | - other: left the project but still responding
62 |
63 |
64 | Testers
65 | =======
66 |
67 | - Abdó Roig-Maranges
68 | - Ben Boeckel
69 | - Chris Coleman
70 | - Darshit Shah
71 | - Eygene Ryabinkin
72 | - Igor Almeida
73 | - Ilias Tsitsimpis
74 | - "J"
75 | - Łukasz Żarnowiecki
76 | - Nicolas Sebrecht
77 | - Remi Locherer
78 |
79 |
80 | Maintainers
81 | ===========
82 |
83 | - Eygene Ryabinkin
84 | - Sebastian Spaeth
85 | - Nicolas Sebrecht
86 | - Chris Coleman
87 |
88 |
89 | Github
90 | ------
91 |
92 | - Eygene Ryabinkin
93 | - Sebastian Spaeth
94 | - Nicolas Sebrecht
95 |
96 |
97 | Mailing List
98 | ------------
99 |
100 | - Eygene Ryabinkin
101 | - Sebastian Spaeth
102 | - Nicolas Sebrecht
103 |
104 |
105 | Twitter
106 | -------
107 |
108 | - Nicolas Sebrecht
109 |
110 |
111 | Pypi
112 | ----
113 |
114 | - Nicolas Sebrecht
115 | - Sebastian Spaeth
116 |
--------------------------------------------------------------------------------
/contrib/upcoming.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | """
4 |
5 | Put into Public Domain, by Nicolas Sebrecht.
6 |
7 | Produce the "upcoming release" notes.
8 |
9 | """
10 |
11 | from os import system
12 |
13 | from helpers import (
14 | MAILING_LIST, CACHEDIR, EDITOR, Testers, Git, OfflineimapInfo, User
15 | )
16 |
17 |
18 |
19 | UPCOMING_FILE = "{}/upcoming.txt".format(CACHEDIR)
20 | UPCOMING_HEADER = "{}/upcoming-header.txt".format(CACHEDIR)
21 |
22 | # Header is like:
23 | #
24 | #Message-Id: <{messageId}>
25 | #Date: {date}
26 | #From: {name} <{email}>
27 | #To: {mailinglist}
28 | #Cc: {ccList}
29 | #Subject: [ANNOUNCE] upcoming offlineimap v{expectedVersion}
30 | #
31 | ## Notes
32 | #
33 | #I think it's time for a new release.
34 | #
35 | #I aim to make the new release in one week, approximately. If you'd like more
36 | #time, please let me know. ,-)
37 | #
38 | #Please, send me a mail to confirm it works for you. This will be written in the
39 | #release notes and the git logs.
40 | #
41 | #
42 | ## Authors
43 | #
44 |
45 |
46 | if __name__ == '__main__':
47 | offlineimapInfo = OfflineimapInfo()
48 |
49 | print("Will read headers from {}".format(UPCOMING_HEADER))
50 | Git.chdirToRepositoryTopLevel()
51 | oVersion = offlineimapInfo.getVersion()
52 | ccList = Testers.listTestersInTeam()
53 | authors = Git.getAuthorsList(oVersion)
54 | for author in authors:
55 | email = author.getEmail()
56 | if email not in ccList:
57 | ccList.append(email)
58 |
59 | with open(UPCOMING_FILE, 'w') as upcoming, \
60 | open(UPCOMING_HEADER, 'r') as fd_header:
61 | header = {}
62 |
63 | header['messageId'] = Git.buildMessageId()
64 | header['date'] = Git.buildDate()
65 | header['name'], header['email'] = Git.getLocalUser()
66 | header['mailinglist'] = MAILING_LIST
67 | header['expectedVersion'] = User.request("Expected new version?")
68 | header['ccList'] = ", ".join(ccList)
69 |
70 | upcoming.write(fd_header.read().format(**header).lstrip())
71 | upcoming.write(Git.getShortlog(oVersion))
72 |
73 | upcoming.write("\n\n# Diffstat\n\n")
74 | upcoming.write(Git.getDiffstat(oVersion))
75 | upcoming.write("\n\n\n-- \n{}\n".format(Git.getLocalUser()[0]))
76 |
77 | system("{} {}".format(EDITOR, UPCOMING_FILE))
78 | print("{} written".format(UPCOMING_FILE))
79 |
--------------------------------------------------------------------------------
/contrib/store-pw-with-gpg/offlineimaprc.sample:
--------------------------------------------------------------------------------
1 | [general]
2 | # GPG quirks, leave unconfigured
3 | ui = ttyui
4 | # you can use any name as long as it matches the 'account1, 'account2' in the rest
5 | # of the file
6 | accounts = account1, account2
7 | # this is where the `gpg-pw.py` file is on disk
8 | pythonfile=~/where/is/the/file/gpg-pw.py
9 | fsync = False
10 |
11 | # you can call this any way you like
12 | [Account account1]
13 | localrepository = account1-local
14 | remoterepository = account1-remote
15 | # no need to touch this
16 | status_backend = sqlite
17 |
18 | [Account account2]
19 | localrepository = account2-local
20 | remoterepository = account2-remote
21 | status_backend = sqlite
22 |
23 | # thi sis a gmail account
24 | [Repository account1-local]
25 | type = Maildir
26 | # create with maildirmake or by hand by creating cur, new, tmp
27 | localfolders = ~/Mail/Mailboxes/account1
28 | # standard Gmail stuff
29 | nametrans = lambda folder: { 'drafts': '[Gmail]/Drafts',
30 | 'sent': '[Gmail]/Sent mail',
31 | 'flagged': '[Gmail]/Starred',
32 | 'trash': '[Gmail]/Trash',
33 | 'archive': '[Gmail]/All Mail'
34 | }.get(folder, folder)
35 |
36 | [Repository account1-remote]
37 | maxconnections = 1
38 | type = Gmail
39 | ssl=yes
40 | # for osx, you might need to download the certs by hand
41 | #sslcacertfile=~/Mail/certs.pem
42 | #sslcacertfile=~/Mail/imap.gmail.com.pem
43 | # sslcacertfile=/etc/ssl/cert.pem
44 |
45 | # or use Linux's standard certs
46 | sslcacertfile=/etc/ssl/certs/ca-certificates.crt
47 | # your account
48 | remoteuser = account1@gmail.com
49 | remotepasseval = get_pass(account="account1@gmail.com", server="imap.gmail.com", passwd_file="passwords-gmail.txt.gpg")
50 | realdelete = no
51 | createfolders = no
52 | nametrans = lambda folder: {'[Gmail]/Drafts': 'drafts',
53 | '[Gmail]/Sent Mail': 'sent',
54 | '[Gmail]/Starred': 'star',
55 | '[Gmail]/Trash': 'trash',
56 | '[Gmail]/All Mail': 'archive',
57 | }.get(folder, folder)
58 | folderfilter = lambda folder: folder not in ['[Gmail]/Trash',
59 | '[Gmail]/Spam',
60 | ]
61 |
62 | [Repository account2-remote]
63 | # copy the stanza above, change the 'account' parameter of get_pass, etc.
64 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2002 - 2018 John Goerzen & contributors.
2 | #
3 | # This program is free software; you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation; either version 2 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program; if not, write to the Free Software
15 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16 |
17 | # Warning: VERSION, ABBREV and TARGZ are used in docs/build-uploads.sh.
18 | VERSION=$(shell ./offlineimap.py --version)
19 | ABBREV=$(shell git log --format='%h' HEAD~1..)
20 | TARGZ=offlineimap-v$(VERSION)-$(ABBREV)
21 | SHELL=/bin/bash
22 | RST2HTML=`type rst2html >/dev/null 2>&1 && echo rst2html || echo rst2html.py`
23 |
24 | all: build
25 |
26 | build:
27 | python2 setup.py build
28 | @echo
29 | @echo "Build process finished, run 'python2 setup.py install' to install" \
30 | "or 'python2 setup.py --help' for more information".
31 |
32 | clean:
33 | -python2 setup.py clean --all
34 | -rm -f bin/offlineimapc 2>/dev/null
35 | -find . -name '*.pyc' -exec rm -f {} \;
36 | -find . -name '*.pygc' -exec rm -f {} \;
37 | -find . -name '*.class' -exec rm -f {} \;
38 | -find . -name '.cache*' -exec rm -f {} \;
39 | -find . -type d -name '__pycache__' -exec rm -rf {} \;
40 | -rm -f manpage.links manpage.refs 2>/dev/null
41 | -find . -name auth -exec rm -vf {}/password {}/username \;
42 | -$(MAKE) -C docs clean
43 |
44 | .PHONY: docs
45 | docs:
46 | @$(MAKE) -C docs
47 |
48 | websitedoc:
49 | @$(MAKE) -C websitedoc
50 |
51 | targz: ../$(TARGZ)
52 | ../$(TARGZ):
53 | cd .. && tar -zhcv --transform s,^offlineimap,offlineimap-v$(VERSION), -f $(TARGZ).tar.gz --exclude '.*.swp' --exclude '.*.swo' --exclude '*.pyc' --exclude '__pycache__' offlineimap/{bin,Changelog.md,Changelog.maint.md,contrib,CONTRIBUTING.rst,COPYING,docs,MAINTAINERS.rst,Makefile,MANIFEST.in,offlineimap,offlineimap.conf,offlineimap.conf.minimal,offlineimap.py,README.md,requirements.txt,scripts,setup.cfg,setup.py,snapcraft.yaml,test,tests,TODO.rst}
54 |
55 | rpm: targz
56 | cd .. && sudo rpmbuild -ta $(TARGZ)
57 |
--------------------------------------------------------------------------------
/docs/doc-src/repository.rst:
--------------------------------------------------------------------------------
1 | .. currentmodule:: offlineimap.repository
2 |
3 | :mod:`offlineimap.repository` -- Email repositories
4 | ------------------------------------------------------------
5 |
6 | A derivative of class
7 | :class:`Base.BaseRepository` represents an email
8 | repository depending on the type of storage, possible options are:
9 |
10 | * :class:`IMAPRepository`,
11 | * :class:`MappedIMAPRepository`
12 | * :class:`GmailRepository`,
13 | * :class:`MaildirRepository`, or
14 | * :class:`LocalStatusRepository`.
15 |
16 | Which class you need depends on your account
17 | configuration. The helper class :class:`offlineimap.repository.Repository` is
18 | an *autoloader*, that returns the correct class depending
19 | on your configuration. So when you want to instanciate a new
20 | :mod:`offlineimap.repository`, you will mostly do it through this class.
21 |
22 | .. autoclass:: offlineimap.repository.Repository
23 | :members:
24 | :inherited-members:
25 |
26 |
27 |
28 | :mod:`offlineimap.repository.Base.BaseRepository` -- Representation of a mail repository
29 | ------------------------------------------------------------------------------------------
30 | .. autoclass:: offlineimap.repository.Base.BaseRepository
31 | :members:
32 | :inherited-members:
33 | :undoc-members:
34 |
35 | .. .. note:: :meth:`foo`
36 | .. .. attribute:: Database.MODE
37 |
38 | Defines constants that are used as the mode in which to open a database.
39 |
40 | MODE.READ_ONLY
41 | Open the database in read-only mode
42 |
43 | MODE.READ_WRITE
44 | Open the database in read-write mode
45 |
46 | .. autoclass:: offlineimap.repository.IMAPRepository
47 | .. autoclass:: offlineimap.repository.MappedIMAPRepository
48 | .. autoclass:: offlineimap.repository.GmailRepository
49 | .. autoclass:: offlineimap.repository.MaildirRepository
50 | .. autoclass:: offlineimap.repository.LocalStatusRepository
51 |
52 | :mod:`offlineimap.folder` -- Basic representation of a local or remote Mail folder
53 | ---------------------------------------------------------------------------------------------------------
54 |
55 | .. autoclass:: offlineimap.folder.Base.BaseFolder
56 | :members:
57 | :inherited-members:
58 | :undoc-members:
59 |
60 | .. .. attribute:: Database.MODE
61 |
62 | Defines constants that are used as the mode in which to open a database.
63 |
64 | MODE.READ_ONLY
65 | Open the database in read-only mode
66 |
67 | MODE.READ_WRITE
68 | Open the database in read-write mode
69 |
--------------------------------------------------------------------------------
/scripts/get-repository.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # Licence: this file is in the public domain.
4 | #
5 | # Download and configure the repositories of the website or wiki.
6 |
7 | repository=$1
8 | github_remote=$2
9 |
10 | #
11 | # TODO
12 | #
13 | final_note () {
14 | cat <
20 | $ cd ./$1
21 | $ git remote add myfork https://github.com//.git
22 | EOF
23 | }
24 |
25 | setup () {
26 | target_dir=$1
27 | remote_url=$2
28 |
29 | # Adjust $PWD if necessary.
30 | test -d scripts || cd ..
31 | if test ! -d scripts
32 | then
33 | echo "cannot figure the correct workdir..."
34 | exit 2
35 | fi
36 |
37 | if test -d $target_dir
38 | then
39 | echo "Directory '$target_dir' already exists..."
40 | exit 3
41 | fi
42 |
43 | git clone "${remote_url}.git" "$1"
44 | echo ''
45 | if test $? -gt 0
46 | then
47 | echo "Cannot fork $remote_url to $1"
48 | exit 4
49 | fi
50 | }
51 |
52 | configure_website () {
53 | renderer='./render.sh'
54 |
55 | echo "Found Github username: '$1'"
56 | echo "If it's wrong, please fix the script ./website/render.sh"
57 |
58 | cd ./website
59 | if test $? -eq 0
60 | then
61 | sed -r -i -e "s,{{USERNAME}},$1," "$renderer"
62 | cd ..
63 | else
64 | echo "ERROR: could not enter ./website. (?)"
65 | fi
66 | }
67 |
68 | configure_wiki () {
69 | : # noop
70 | }
71 |
72 | test n$github_remote = 'n' && github_remote='origin'
73 |
74 | # Get Github username.
75 | #offlineimap_url="$(git config --local --get remote.origin.url)"
76 | offlineimap_url="$(git config --local --get remote.nicolas33.url)"
77 | username=$(echo $offlineimap_url | sed -r -e 's,.*github.com.([^/]+)/.*,\1,')
78 |
79 |
80 | case n$repository in
81 | nwebsite)
82 | upstream=https://github.com/OfflineIMAP/offlineimap.github.io
83 | setup website "$upstream"
84 | configure_website "$username"
85 | final_note website "$upstream"
86 | ;;
87 | nwiki)
88 | upstream=https://github.com/OfflineIMAP/offlineimap.wiki
89 | setup wiki "$upstream"
90 | configure_wiki
91 | final_note wiki "$upstream"
92 | ;;
93 | *)
94 | cat <]
96 |
97 | : The name of the Git repository of YOUR fork at Github.
98 | Default: origin
99 | EOF
100 | exit 1
101 | ;;
102 | esac
103 |
104 |
--------------------------------------------------------------------------------
/docs/doc-src/dco.rst:
--------------------------------------------------------------------------------
1 | .. _dco
2 |
3 | Developer's Certificate of Origin
4 | =================================
5 |
6 | v1.1::
7 |
8 | By making a contribution to this project, I certify that:
9 |
10 | (a) The contribution was created in whole or in part by me and I
11 | have the right to submit it under the open source license
12 | indicated in the file; or
13 |
14 | (b) The contribution is based upon previous work that, to the best
15 | of my knowledge, is covered under an appropriate open source
16 | license and I have the right under that license to submit that
17 | work with modifications, whether created in whole or in part
18 | by me, under the same open source license (unless I am
19 | permitted to submit under a different license), as indicated
20 | in the file; or
21 |
22 | (c) The contribution was provided directly to me by some other
23 | person who certified (a), (b) or (c) and I have not modified
24 | it.
25 |
26 | (d) I understand and agree that this project and the contribution
27 | are public and that a record of the contribution (including all
28 | personal information I submit with it, including my sign-off) is
29 | maintained indefinitely and may be redistributed consistent with
30 | this project or the open source license(s) involved.
31 |
32 |
33 | Then, you just add a line saying::
34 |
35 | Signed-off-by: Random J Developer
36 |
37 | This line can be automatically added by git if you run the git-commit command
38 | with the ``-s`` option. Signing can made be afterword with ``--amend -s``.
39 |
40 | Notice that you can place your own ``Signed-off-by:`` line when forwarding
41 | somebody else's patch with the above rules for D-C-O. Indeed you are encouraged
42 | to do so. Do not forget to place an in-body ``From:`` line at the beginning to
43 | properly attribute the change to its true author (see above).
44 |
45 | Also notice that a real name is used in the ``Signed-off-by:`` line. Please
46 | don't hide your real name.
47 |
48 | If you like, you can put extra tags at the end:
49 |
50 | Reported-by
51 | is used to to credit someone who found the bug that the patch attempts to fix.
52 |
53 | Acked-by
54 | says that the person who is more familiar with the area the patch attempts to
55 | modify liked the patch.
56 |
57 | Reviewed-by
58 | unlike the other tags, can only be offered by the reviewer and means that she
59 | is completely satisfied that the patch is ready for application. It is
60 | usually offered only after a detailed review.
61 |
62 | Tested-by
63 | is used to indicate that the person applied the patch and found it to have the
64 | desired effect.
65 |
66 | You can also create your own tag or use one that's in common usage such as
67 | ``Thanks-to:``, ``Based-on-patch-by:``, or ``Mentored-by:``.
68 |
69 |
70 |
--------------------------------------------------------------------------------
/docs/doc-src/API.rst:
--------------------------------------------------------------------------------
1 | .. OfflineImap API documentation
2 |
3 | .. currentmodule:: offlineimap
4 |
5 | .. _API docs:
6 |
7 | :mod:`offlineimap's` API documentation
8 | ======================================
9 |
10 | Within :mod:`offlineimap`, the classes :class:`OfflineImap` provides the
11 | high-level functionality. The rest of the classes should usually not needed to
12 | be touched by the user. Email repositories are represented by a
13 | :class:`offlineimap.repository.Base.BaseRepository` or derivatives (see
14 | :mod:`offlineimap.repository` for details). A folder within a repository is
15 | represented by a :class:`offlineimap.folder.Base.BaseFolder` or any derivative
16 | from :mod:`offlineimap.folder`.
17 |
18 | This page contains the main API overview of OfflineImap |release|.
19 |
20 | OfflineImap can be imported as::
21 |
22 | from offlineimap import OfflineImap
23 |
24 |
25 | :mod:`offlineimap` -- The OfflineImap module
26 | =============================================
27 |
28 | .. module:: offlineimap
29 |
30 | .. autoclass:: offlineimap.OfflineImap(cmdline_opts = None)
31 | :members:
32 | :inherited-members:
33 | :undoc-members:
34 | :private-members:
35 |
36 |
37 | :class:`offlineimap.account`
38 | ============================
39 |
40 | An :class:`accounts.Account` connects two email repositories that are to be
41 | synced. It comes in two flavors, normal and syncable.
42 |
43 | .. autoclass:: offlineimap.accounts.Account
44 |
45 | .. autoclass:: offlineimap.accounts.SyncableAccount
46 | :members:
47 | :inherited-members:
48 |
49 | .. autodata:: ui
50 |
51 | Contains the current :mod:`offlineimap.ui`, and can be used for logging etc.
52 |
53 | :exc:`OfflineImapError` -- A Notmuch execution error
54 | --------------------------------------------------------
55 |
56 | .. autoexception:: offlineimap.error.OfflineImapError
57 | :members:
58 |
59 | This exception inherits directly from :exc:`Exception` and is raised
60 | on errors during the offlineimap execution. It has an attribute
61 | `severity` that denotes the severity level of the error.
62 |
63 |
64 | :mod:`offlineimap.globals` -- module with global variables
65 | ==========================================================
66 |
67 | Module `offlineimap.globals` provides the read-only storage
68 | for the global variables.
69 |
70 | All exported module attributes can be set manually, but this practice
71 | is highly discouraged and shouldn't be used.
72 | However, attributes of all stored variables can only be read, write
73 | access to them is denied.
74 |
75 | Currently, we have only :attr:`options` attribute that holds
76 | command-line options as returned by OptionParser.
77 | The value of :attr:`options` must be set by :func:`set_options`
78 | prior to its first use.
79 |
80 | .. automodule:: offlineimap.globals
81 | :members:
82 |
83 | .. data:: options
84 |
85 | You can access the values of stored options using the usual
86 | syntax, offlineimap.globals.options.
87 |
--------------------------------------------------------------------------------
/test/tests/test_02_MappedIMAP.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2012- Sebastian Spaeth & contributors
2 | #
3 | # This program is free software; you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation; either version 2 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program; if not, write to the Free Software
15 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16 | import random
17 | import unittest
18 | import logging
19 | import os, sys
20 | from test.OLItest import OLITestLib
21 |
22 | # Things need to be setup first, usually setup.py initializes everything.
23 | # but if e.g. called from command line, we take care of default values here:
24 | if not OLITestLib.cred_file:
25 | OLITestLib(cred_file='./test/credentials.conf', cmd='./offlineimap.py')
26 |
27 |
28 | def setUpModule():
29 | logging.info("Set Up test module %s" % __name__)
30 | tdir = OLITestLib.create_test_dir(suffix=__name__)
31 |
32 | def tearDownModule():
33 | logging.info("Tear Down test module")
34 | OLITestLib.delete_test_dir()
35 |
36 | #Stuff that can be used
37 | #self.assertEqual(self.seq, range(10))
38 | # should raise an exception for an immutable sequence
39 | #self.assertRaises(TypeError, random.shuffle, (1,2,3))
40 | #self.assertTrue(element in self.seq)
41 | #self.assertFalse(element in self.seq)
42 |
43 | class TestBasicFunctions(unittest.TestCase):
44 | #@classmethod
45 | #def setUpClass(cls):
46 | #This is run before all tests in this class
47 | # cls._connection = createExpensiveConnectionObject()
48 |
49 | #@classmethod
50 | #This is run after all tests in this class
51 | #def tearDownClass(cls):
52 | # cls._connection.destroy()
53 |
54 | # This will be run before each test
55 | #def setUp(self):
56 | # self.seq = range(10)
57 |
58 | def test_01_MappedImap(self):
59 | """Tests if a MappedIMAP sync can be invoked without exceptions
60 |
61 | Cleans existing remote test folders. Then syncs all "OLItest*
62 | (specified in the default config) to our local IMAP (Gmail). The
63 | result should be 0 folders and 0 mails."""
64 | pass #TODO
65 | #OLITestLib.delete_remote_testfolders()
66 | #code, res = OLITestLib.run_OLI()
67 | #self.assertEqual(res, "")
68 | #boxes, mails = OLITestLib.count_maildir_mails('')
69 | #self.assertTrue((boxes, mails)==(0,0), msg="Expected 0 folders and 0"
70 | # "mails, but sync led to {} folders and {} mails".format(
71 | # boxes, mails))
72 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # $Id: setup.py,v 1.1 2002/06/21 18:10:49 jgoerzen Exp $
4 |
5 | # IMAP synchronization
6 | # Module: installer
7 | # COPYRIGHT #
8 | # Copyright (C) 2002 - 2020 John Goerzen & contributors
9 | #
10 | # This program is free software; you can redistribute it and/or modify
11 | # it under the terms of the GNU General Public License as published by
12 | # the Free Software Foundation; either version 2 of the License, or
13 | # (at your option) any later version.
14 | #
15 | # This program is distributed in the hope that it will be useful,
16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 | # GNU General Public License for more details.
19 | #
20 | # You should have received a copy of the GNU General Public License
21 | # along with this program; if not, write to the Free Software
22 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
23 |
24 | import os
25 | from distutils.core import setup, Command
26 | import logging
27 |
28 | from os import path
29 | here = path.abspath(path.dirname(__file__))
30 |
31 | # load __version__, __doc__, __author_, ...
32 | exec(open(path.join(here, 'offlineimap', 'version.py')).read())
33 |
34 | class TestCommand(Command):
35 | """runs the OLI testsuite"""
36 | description = """Runs the test suite. In order to execute only a single
37 | test, you could also issue e.g. 'python -m unittest
38 | test.tests.test_01_basic.TestBasicFunctions.test_01_olistartup' on the
39 | command line."""
40 | user_options = []
41 |
42 | def initialize_options(self):
43 | pass
44 |
45 | def finalize_options(self):
46 | pass
47 |
48 | def run(self):
49 | # Import the test classes here instead of at the begin of the module
50 | # to avoid an implicit dependency of the 'offlineimap' module
51 | # in the setup.py (which may run *before* offlineimap is installed)
52 | from test.OLItest import TextTestRunner, TestLoader, OLITestLib
53 |
54 | logging.basicConfig(format='%(message)s')
55 | # set credentials and OfflineImap command to be executed:
56 | OLITestLib(cred_file='./test/credentials.conf', cmd='./offlineimap.py')
57 | suite = TestLoader().discover('./test/tests')
58 | #TODO: failfast does not seem to exist in python2.6?
59 | TextTestRunner(verbosity=2,failfast=True).run(suite)
60 |
61 | reqs = [
62 | 'six',
63 | 'rfc6555'
64 | ]
65 |
66 | setup(name = "offlineimap",
67 | version = __version__,
68 | description = __description__,
69 | long_description = __description__,
70 | author = __author__,
71 | author_email = __author_email__,
72 | url = __homepage__,
73 | packages = ['offlineimap', 'offlineimap.folder',
74 | 'offlineimap.repository', 'offlineimap.ui',
75 | 'offlineimap.utils'],
76 | scripts = ['bin/offlineimap'],
77 | license = __copyright__ + \
78 | ", Licensed under the GPL version 2",
79 | cmdclass = { 'test': TestCommand},
80 | install_requires = reqs
81 | )
82 |
83 |
--------------------------------------------------------------------------------
/offlineimap/utils/distro.py:
--------------------------------------------------------------------------------
1 | # Copyright 2006-2018 Eygene A. Ryabinkin & contributors.
2 | #
3 | # Module that supports distribution-specific functions.
4 |
5 | import platform
6 | import os
7 |
8 |
9 | # Each dictionary value is either string or some iterable.
10 | #
11 | # For the former we will just return the value, for an iterable
12 | # we will walk through the values and will return the first
13 | # one that corresponds to the existing file.
14 | __DEF_OS_LOCATIONS = {
15 | 'freebsd': '/usr/local/share/certs/ca-root-nss.crt',
16 | 'openbsd': '/etc/ssl/cert.pem',
17 | 'dragonfly': '/etc/ssl/cert.pem',
18 | 'darwin': [
19 | # MacPorts, port curl-ca-bundle
20 | '/opt/local/share/curl/curl-ca-bundle.crt',
21 | # homebrew, package openssl
22 | '/usr/local/etc/openssl/cert.pem',
23 | ],
24 | 'linux-ubuntu': '/etc/ssl/certs/ca-certificates.crt',
25 | 'linux-debian': '/etc/ssl/certs/ca-certificates.crt',
26 | 'linux-gentoo': '/etc/ssl/certs/ca-certificates.crt',
27 | 'linux-fedora': '/etc/pki/tls/certs/ca-bundle.crt',
28 | 'linux-redhat': '/etc/pki/tls/certs/ca-bundle.crt',
29 | 'linux-suse': '/etc/ssl/ca-bundle.pem',
30 | 'linux-opensuse': '/etc/ssl/ca-bundle.pem',
31 | 'linux-arch': '/etc/ssl/certs/ca-certificates.crt',
32 | }
33 |
34 |
35 | def get_os_name():
36 | """
37 | Finds out OS name. For non-Linux system it will be just a plain
38 | OS name (like FreeBSD), for Linux it will be "linux-",
39 | where is the name of the distribution, as returned by
40 | the first component of platform.linux_distribution.
41 |
42 | Return value will be all-lowercase to avoid confusion about
43 | proper name capitalisation.
44 |
45 | """
46 | OS = platform.system().lower()
47 |
48 | if OS.startswith('linux'):
49 | DISTRO = platform.linux_distribution()[0]
50 | if DISTRO:
51 | OS = OS + "-%s" % DISTRO.split()[0].lower()
52 | if os.path.exists('/etc/arch-release'):
53 | OS = "linux-arch"
54 |
55 | return OS
56 |
57 | def get_os_sslcertfile_searchpath():
58 | """Returns search path for CA bundle for the current OS.
59 |
60 | We will return an iterable even if configuration has just
61 | a single value: it is easier for our callers to be sure
62 | that they can iterate over result.
63 |
64 | Returned value of None means that there is no search path
65 | at all.
66 | """
67 |
68 | OS = get_os_name()
69 |
70 | l = None
71 | if OS in __DEF_OS_LOCATIONS:
72 | l = __DEF_OS_LOCATIONS[OS]
73 | if not hasattr(l, '__iter__'):
74 | l = (l, )
75 | return l
76 |
77 |
78 | def get_os_sslcertfile():
79 | """
80 | Finds out the location for the distribution-specific
81 | CA certificate file bundle.
82 |
83 | Returns the location of the file or None if there is
84 | no known CA certificate file or all known locations
85 | correspond to non-existing filesystem objects.
86 | """
87 |
88 | l = get_os_sslcertfile_searchpath()
89 | if l == None:
90 | return None
91 |
92 | for f in l:
93 | assert (type(f) == type(""))
94 | if os.path.exists(f) and \
95 | (os.path.isfile(f) or os.path.islink(f)):
96 | return f
97 |
98 | return None
99 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - '2.7'
4 | notifications:
5 | webhooks:
6 | urls:
7 | - https://webhooks.gitter.im/e/975e807e0314c9fa189c
8 | on_success: always # options: [always|never|change] default: always
9 | on_failure: always # options: [always|never|change] default: always
10 | on_start: never
11 | os:
12 | - linux
13 | env:
14 | global:
15 | - secure: jehlvkFxQbkvr73A0z3HGNC/knZQPKcaXLf6nByGpNE0ZTQKF7Y5KkNfeTcw4st7L7KuRZ1S/1bFtpMXTaplE6G0OtIEC4//SM+z+Dnadn2OY6wHiaapwZmmqDC5qVvcXPdmz/wTRsdrJSGLb2l6kEb91vRGbCCfHHf6Z2cF71U=
16 | - secure: kWdmWAFK4qrA73ONz1X8CJdHSER3bCBXjLfYHYEEMPCZep21bTITUXIfZBlSNN1888SQtYksuloRJmvj7xiY/hf/4lyWiqM3RgWQ+YptJMVOQX+Gara6vm4nGntKQwaXgZF2YHSh+NYwQm1VY6m0n1ye/vfOIJnYfgGTk5qAZYU=
17 | - secure: MzytYRX6HxgBj6Q3efkACTtDed8ZYO+P6UJrDA9IDtvffi8fAFb+wkQtKJrdcvMXNOap6fPe4c0EVGjgL5hFxmgC8yAh5t2YK7OhstAtq0ptKFlOcU24/drrkqoq040sAM/4Lc0nQCvYpz7bH370jzZl69rpbQWttwQR0i1e3Gw=
18 | - secure: RWvIOHSiv2kt6cfZR7MEueiAmC61bWMXAtgsC6gKq1u3BfENfqSBTA/heIy+nlu7AXK1b6hPMZDCHWK09Zz6Klkd9xZ1gkE/AARWseoo9UWgGjmfvqng1S6qpESeX2GnZGR9CuBXTPGhtbYLgtNlxAo+6uZLolz2utW2XNk3Z/Y=
19 | - secure: spivQv+vSJhE+ttn/Z6tANaINqiMSaJSucRqtoXR7PtioVDTOTmmL01Ja6dXuo8Ua5iVFtpZPDzqVpntQLKtjcywSK2zWnC9qbZYDfENr1/yIvfbSRjGeseq0eoY+fFp67FGZV4mIasdC3LOB0lRGOyrsX787fNKVQ8ZH0CRz0o=
20 | - secure: ZcY0TvTQnRCdoFkdbJPfDJJNx91tViwbpiOBkxNEa3u0RN48xkZkii35kNVBaEcVZHcT9C81ctHk4QX+plBkCsoj5GDf25scgcv1j9R9UoN/rIkmyTu1Znmc+3UQ2J+EnGLWVn5xJ7yT/l9NZeLfNbULQRjttwT4j2MBGxezgdM=
21 | matrix:
22 | - OUTLOOK_AUTH=PLAIN GMAIL_AUTH=XOAUTH2
23 | - OUTLOOK_AUTH=LOGIN GMAIL_AUTH=XOAUTH2
24 | matrix:
25 | include:
26 | - os: osx
27 | language: generic
28 | env: PYTHON=2.7.14 OUTLOOK_AUTH=PLAIN GMAIL_AUTH=XOAUTH2
29 | - os: osx
30 | language: generic
31 | env: PYTHON=2.7.14 OUTLOOK_AUTH=LOGIN GMAIL_AUTH=XOAUTH2
32 | allow_failures:
33 | - os: osx
34 | cache: pip
35 | before_install:
36 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update && brew install openssl readline;
37 | fi
38 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export OSX_BREW_SSLCACERTFILE="/usr/local/etc/openssl/cert.pem";
39 | fi
40 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew outdated pyenv || brew upgrade pyenv;
41 | fi
42 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install pyenv-virtualenv; fi
43 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pyenv install $PYTHON; fi
44 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export PYENV_VERSION="${PYTHON}"; fi
45 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export PATH="/Users/travis/.pyenv/shims:${PATH}";
46 | fi
47 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pyenv virtualenv $PYTHON myvenv; fi
48 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pyenv versions; fi
49 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then python --version; fi
50 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pyenv version; fi
51 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then python --version; fi
52 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then python -m pip install -U pip; fi
53 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then python -m easy_install -U setuptools;
54 | fi
55 | install:
56 | - pip install -r requirements.txt
57 | - pip install -r tests/requirements.txt
58 | - export PATH=$PATH:.
59 | - python tests/create_conf_file.py
60 | script:
61 | - "./offlineimap.py -c ./oli-travis.conf"
62 | - codecov
63 |
--------------------------------------------------------------------------------
/contrib/store-pw-with-gpg/gpg-pw.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # Originally taken from: http://stevelosh.com/blog/2012/10/the-homely-mutt/
3 | # by Steve Losh
4 | # Modified by Lorenzo Grespan on Jan, 2014
5 |
6 | import re
7 | import subprocess
8 | from sys import argv
9 | import logging
10 | from os.path import expanduser
11 | import unittest
12 | import os
13 | import sys
14 |
15 | logging.basicConfig(level=logging.INFO)
16 |
17 |
18 | DEFAULT_PASSWORDS_FILE = os.path.join(
19 | os.path.expanduser('~/Mail'),
20 | 'passwords.gpg')
21 |
22 |
23 | def get_keychain_pass(account=None, server=None):
24 | '''Mac OSX keychain password extraction'''
25 | params = {
26 | 'security': '/usr/bin/security',
27 | 'command': 'find-internet-password',
28 | 'account': account,
29 | 'server': server,
30 | 'keychain': expanduser('~') + '/Library/Keychains/login.keychain',
31 | }
32 | command = ("%(security)s -v %(command)s"
33 | " -g -a %(account)s -s %(server)s %(keychain)s" % params)
34 | output = subprocess.check_output(
35 | command, shell=True, stderr=subprocess.STDOUT)
36 | outtext = [l for l in output.splitlines()
37 | if l.startswith('password: ')][0]
38 | return find_password(outtext)
39 |
40 |
41 | def find_password(text):
42 | '''Helper method for osx password extraction'''
43 | # a non-capturing group
44 | r = re.match(r'password: (?:0x[A-F0-9]+ )?"(.*)"', text)
45 | if r:
46 | return r.group(1)
47 | else:
48 | logging.warn("Not found")
49 | return None
50 |
51 |
52 | def get_gpg_pass(account, storage):
53 | '''GPG method'''
54 | command = ("gpg", "-d", storage)
55 | # get attention
56 | print '\a' # BEL
57 | output = subprocess.check_output(command)
58 | # p = subprocess.Popen(command, stdout=subprocess.PIPE)
59 | # output, err = p.communicate()
60 | for line in output.split('\n'):
61 | r = re.match(r'{} ([a-zA-Z0-9]+)'.format(account), line)
62 | if r:
63 | return r.group(1)
64 | return None
65 |
66 |
67 | def get_pass(account=None, server=None, passwd_file=None):
68 | '''Main method'''
69 | if not passwd_file:
70 | storage = DEFAULT_PASSWORDS_FILE
71 | else:
72 | storage = os.path.join(
73 | os.path.expanduser('~/Mail'),
74 | passwd_file)
75 | if os.path.exists('/usr/bin/security'):
76 | return get_keychain_pass(account, server)
77 | if os.path.exists(storage):
78 | logging.info("Using {}".format(storage))
79 | return get_gpg_pass(account, storage)
80 | else:
81 | logging.warn("No password file found")
82 | sys.exit(1)
83 | return None
84 |
85 |
86 | # test with: python -m unittest
87 | # really basic tests.. nothing to see. move along
88 | class Tester(unittest.TestCase):
89 | def testMatchSimple(self):
90 | text = 'password: "exampleonetimepass "'
91 | self.assertTrue(find_password(text))
92 |
93 | def testMatchComplex(self):
94 | text = r'password: 0x74676D62646D736B646970766C66696B0A "anotherexamplepass\012"'
95 | self.assertTrue(find_password(text))
96 |
97 |
98 | if __name__ == "__main__":
99 | print get_pass(argv[1], argv[2], argv[3])
100 |
--------------------------------------------------------------------------------
/offlineimap/repository/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2002-2016 John Goerzen & contributors.
2 | #
3 | # This program is free software; you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation; either version 2 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program; if not, write to the Free Software
15 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16 |
17 | import six
18 | from sys import exc_info
19 |
20 | try:
21 | from configparser import NoSectionError
22 | except ImportError: #python2
23 | from ConfigParser import NoSectionError
24 |
25 | from offlineimap.repository.IMAP import IMAPRepository, MappedIMAPRepository
26 | from offlineimap.repository.Gmail import GmailRepository
27 | from offlineimap.repository.Maildir import MaildirRepository
28 | from offlineimap.repository.GmailMaildir import GmailMaildirRepository
29 | from offlineimap.repository.LocalStatus import LocalStatusRepository
30 | from offlineimap.error import OfflineImapError
31 |
32 |
33 | class Repository(object):
34 | """Abstract class that returns the correct Repository type
35 | instance based on 'account' and 'reqtype', e.g. a
36 | class:`ImapRepository` instance."""
37 |
38 | def __new__(cls, account, reqtype):
39 | """
40 | :param account: :class:`Account`
41 | :param regtype: 'remote', 'local', or 'status'"""
42 |
43 | if reqtype == 'remote':
44 | name = account.getconf('remoterepository')
45 | # We don't support Maildirs on the remote side.
46 | typemap = {'IMAP': IMAPRepository,
47 | 'Gmail': GmailRepository}
48 |
49 | elif reqtype == 'local':
50 | name = account.getconf('localrepository')
51 | typemap = {'IMAP': MappedIMAPRepository,
52 | 'Maildir': MaildirRepository,
53 | 'GmailMaildir': GmailMaildirRepository}
54 |
55 | elif reqtype == 'status':
56 | # create and return a LocalStatusRepository.
57 | name = account.getconf('localrepository')
58 | return LocalStatusRepository(name, account)
59 |
60 | else:
61 | errstr = "Repository type %s not supported" % reqtype
62 | raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO)
63 |
64 | # Get repository type.
65 | config = account.getconfig()
66 | try:
67 | repostype = config.get('Repository ' + name, 'type').strip()
68 | except NoSectionError as e:
69 | errstr = ("Could not find section '%s' in configuration. Required "
70 | "for account '%s'." % ('Repository %s' % name, account))
71 | six.reraise(OfflineImapError,
72 | OfflineImapError(errstr, OfflineImapError.ERROR.REPO),
73 | exc_info()[2])
74 |
75 | try:
76 | repo = typemap[repostype]
77 | except KeyError:
78 | errstr = "'%s' repository not supported for '%s' repositories."% \
79 | (repostype, reqtype)
80 | six.reraise(OfflineImapError,
81 | OfflineImapError(errstr, OfflineImapError.ERROR.REPO),
82 | exc_info()[2])
83 |
84 | return repo(name, account)
85 |
86 | def __init__(self, account, reqtype):
87 | """Load the correct Repository type and return that. The
88 | __init__ of the corresponding Repository class will be
89 | executed instead of this stub
90 |
91 | :param account: :class:`Account`
92 | :param regtype: 'remote', 'local', or 'status'
93 | """
94 | pass
95 |
--------------------------------------------------------------------------------
/tests/create_conf_file.py:
--------------------------------------------------------------------------------
1 | #!/bin/env python
2 | # Copyright 2018 Espace LLC/espacenetworks.com. Written by @chris001.
3 | # This must be run from the main directory of the offlineimap project.
4 | # Typically this script will be run by Travis to create the config files needed for running the automated tests.
5 | # python ./tests/create_conf_file.py
6 | # Input: Seven shell environment variables.
7 | # Output: it writes the config settings to "filename" (./oli-travis.conf) and "additionalfilename" (./test/credentials.conf).
8 | # "filename" is used by normal run of ./offlineimap -c ./oli-travis.conf , "additionalfilename" is used by "pytest".
9 | # They are the same conf file, copie to two different locations for convenience.
10 |
11 | import os
12 | import shutil
13 | try:
14 | import ConfigParser
15 | Config = ConfigParser.ConfigParser()
16 | except ImportError:
17 | import configparser
18 | Config = configparser.ConfigParser()
19 |
20 | filename = "./oli-travis.conf"
21 | additionalfilename = "./test/credentials.conf" # for the 'pytest' which automatically finds and runs the unittests.
22 |
23 | #TODO: detect OS we running on now, and set sslcacertfile location accordingly.
24 | sslcacertfile = "/etc/pki/tls/cert.pem" # CentOS 7
25 | sslcacertfile = "" # TODO: https://gist.github.com/1stvamp/2158128 Current Mac OSX now must download the cacertfile.
26 | sslcacertfile = "/etc/ssl/certs/ca-certificates.crt" # Ubuntu Trusty 14.04 (Travis linux test container 2018.)
27 | if os.environ["TRAVIS_OS_NAME"] == "osx":
28 | sslcacertfile = os.environ["OSX_BREW_SSLCACERTFILE"]
29 |
30 | # lets create that config file.
31 | cfgfile = open(filename,'w')
32 |
33 | # add the settings to the structure of the file, and lets write it out.
34 | sect = 'general'
35 | Config.add_section(sect)
36 | Config.set(sect,'accounts','Test')
37 | Config.set(sect,'maxsyncaccounts', '1')
38 |
39 | sect = 'Account Test'
40 | Config.add_section(sect)
41 | Config.set(sect,'localrepository','IMAP') # Outlook.
42 | Config.set(sect,'remoterepository', 'Gmail')
43 |
44 | ### "Repository IMAP" is hardcoded in test/OLItest/TestRunner.py it should dynamically get the Repository names but it doesn't.
45 | sect = 'Repository IMAP' # Outlook.
46 | Config.add_section(sect)
47 | Config.set(sect,'type','IMAP')
48 | Config.set(sect,'remotehost', 'imap-mail.outlook.com')
49 | Config.set(sect,'remoteport', '993')
50 | Config.set(sect,'auth_mechanisms', os.environ["OUTLOOK_AUTH"])
51 | Config.set(sect,'ssl', 'True')
52 | #Config.set(sect,'tls_level', 'tls_compat') #Default is 'tls_compat'.
53 | #Config.set(sect,'ssl_version', 'tls1_2') # Leave this unset. Will auto select between tls1_1 and tls1_2 for tls_secure.
54 | Config.set(sect,'sslcacertfile', sslcacertfile)
55 | Config.set(sect,'remoteuser', os.environ["secure_outlook_email_address"])
56 | Config.set(sect,'remotepass', os.environ["secure_outlook_email_pw"])
57 | Config.set(sect,'createfolders', 'True')
58 | Config.set(sect,'folderfilter', 'lambda f: f not in ["Inbox", "[Gmail]/All Mail"]') #Capitalization of Inbox INBOX was causing runtime failure.
59 | #Config.set(sect,'folderfilter', 'lambda f: f not in ["[Gmail]/All Mail"]')
60 |
61 |
62 | ### "Repository Gmail" is also hardcoded into test/OLItest/TestRunner.py
63 | sect = 'Repository Gmail'
64 | Config.add_section(sect)
65 | Config.set(sect,'type', 'Gmail')
66 | Config.set(sect,'remoteport', '993')
67 | Config.set(sect,'auth_mechanisms', os.environ["GMAIL_AUTH"])
68 | Config.set(sect,'oauth2_client_id', os.environ["secure_gmail_oauth2_client_id"])
69 | Config.set(sect,'oauth2_client_secret', os.environ["secure_gmail_oauth2_client_secret"])
70 | Config.set(sect,'oauth2_refresh_token', os.environ["secure_gmail_oauth2_refresh_token"])
71 | Config.set(sect,'remoteuser', os.environ["secure_gmail_email_address"])
72 | Config.set(sect,'ssl', 'True')
73 | #Config.set(sect,'tls_level', 'tls_compat')
74 | #Config.set(sect,'ssl_version', 'tls1_2')
75 | Config.set(sect,'sslcacertfile', sslcacertfile)
76 | Config.set(sect,'createfolders', 'True')
77 | Config.set(sect,'folderfilter', 'lambda f: f not in ["INBOX", "[Gmail]/All Mail"]')
78 |
79 | Config.write(cfgfile)
80 | cfgfile.close()
81 |
82 | shutil.copy(filename, additionalfilename)
83 |
--------------------------------------------------------------------------------
/test/tests/test_00_imaputil.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2012- Sebastian Spaeth & contributors
2 | #
3 | # This program is free software; you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation; either version 2 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program; if not, write to the Free Software
15 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16 | import unittest
17 | import logging
18 |
19 | from offlineimap import imaputil
20 | from offlineimap.ui import UI_LIST, setglobalui
21 | from offlineimap.CustomConfig import CustomConfigParser
22 |
23 | from test.OLItest import OLITestLib
24 |
25 | # Things need to be setup first, usually setup.py initializes everything.
26 | # but if e.g. called from command line, we take care of default values here:
27 | if not OLITestLib.cred_file:
28 | OLITestLib(cred_file='./test/credentials.conf', cmd='./offlineimap.py')
29 |
30 | def setUpModule():
31 | logging.info("Set Up test module %s" % __name__)
32 | tdir = OLITestLib.create_test_dir(suffix=__name__)
33 |
34 | def tearDownModule():
35 | logging.info("Tear Down test module")
36 | # comment out next line to keep testdir after test runs. TODO: make nicer
37 | OLITestLib.delete_test_dir()
38 |
39 | #Stuff that can be used
40 | #self.assertEqual(self.seq, range(10))
41 | # should raise an exception for an immutable sequence
42 | #self.assertRaises(TypeError, random.shuffle, (1,2,3))
43 | #self.assertTrue(element in self.seq)
44 | #self.assertFalse(element in self.seq)
45 |
46 | class TestInternalFunctions(unittest.TestCase):
47 | """While the other test files test OfflineImap as a program, these
48 | tests directly invoke internal helper functions to guarantee that
49 | they deliver results as expected"""
50 |
51 | @classmethod
52 | def setUpClass(cls):
53 | #This is run before all tests in this class
54 | config= OLITestLib.get_default_config()
55 | setglobalui(UI_LIST['quiet'](config))
56 |
57 | def test_01_imapsplit(self):
58 | """Test imaputil.imapsplit()"""
59 | res = imaputil.imapsplit(b'(\\HasNoChildren) "." "INBOX.Sent"')
60 | self.assertEqual(res, [b'(\\HasNoChildren)', b'"."', b'"INBOX.Sent"'])
61 |
62 | res = imaputil.imapsplit(b'"mo\\" o" sdfsdf')
63 | self.assertEqual(res, [b'"mo\\" o"', b'sdfsdf'])
64 |
65 | def test_02_flagsplit(self):
66 | """Test imaputil.flagsplit()"""
67 | res = imaputil.flagsplit(b'(\\Draft \\Deleted)')
68 | self.assertEqual(res, [b'\\Draft', b'\\Deleted'])
69 |
70 | res = imaputil.flagsplit(b'(FLAGS (\\Seen Old) UID 4807)')
71 | self.assertEqual(res, [b'FLAGS', b'(\\Seen Old)', b'UID', b'4807'])
72 |
73 | def test_04_flags2hash(self):
74 | """Test imaputil.flags2hash()"""
75 | res = imaputil.flags2hash(b'(FLAGS (\\Seen Old) UID 4807)')
76 | self.assertEqual(res, {b'FLAGS': b'(\\Seen Old)', b'UID': b'4807'})
77 |
78 | def test_05_flagsimap2maildir(self):
79 | """Test imaputil.flagsimap2maildir()"""
80 | res = imaputil.flagsimap2maildir(b'(\\Draft \\Deleted)')
81 | self.assertEqual(res, set(b'DT'))
82 |
83 | def test_06_flagsmaildir2imap(self):
84 | """Test imaputil.flagsmaildir2imap()"""
85 | res = imaputil.flagsmaildir2imap(set(b'DR'))
86 | self.assertEqual(res, b'(\\Answered \\Draft)')
87 | # test all possible flags
88 | res = imaputil.flagsmaildir2imap(set(b'SRFTD'))
89 | self.assertEqual(res, b'(\\Answered \\Deleted \\Draft \\Flagged \\Seen)')
90 |
91 | def test_07_uid_sequence(self):
92 | """Test imaputil.uid_sequence()"""
93 | res = imaputil.uid_sequence([1,2,3,4,5,10,12,13])
94 | self.assertEqual(res, b'1:5,10,12:13')
95 |
--------------------------------------------------------------------------------
/offlineimap/repository/Gmail.py:
--------------------------------------------------------------------------------
1 | # Gmail IMAP repository support
2 | # Copyright (C) 2008-2016 Riccardo Murri &
3 | # contributors
4 | #
5 | # This program is free software; you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation; either version 2 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program; if not, write to the Free Software
17 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 |
19 | from offlineimap.repository.IMAP import IMAPRepository
20 | from offlineimap import folder, OfflineImapError
21 |
22 |
23 | class GmailRepository(IMAPRepository):
24 | """Gmail IMAP repository.
25 |
26 | This class just has default settings for GMail's IMAP service. So
27 | you can do 'type = Gmail' instead of 'type = IMAP' and skip
28 | specifying the hostname, port etc. See
29 | http://mail.google.com/support/bin/answer.py?answer=78799&topic=12814
30 | for the values we use."""
31 | def __init__(self, reposname, account):
32 | """Initialize a GmailRepository object."""
33 | IMAPRepository.__init__(self, reposname, account)
34 |
35 | def gethost(self):
36 | """Return the server name to connect to.
37 |
38 | We first check the usual IMAP settings, and then fall back to
39 | imap.gmail.com if nothing is specified."""
40 | try:
41 | return super(GmailRepository, self).gethost()
42 | except OfflineImapError:
43 | # Nothing was configured, cache and return hardcoded
44 | # one. See the parent class (IMAPRepository) for how this
45 | # cache is used.
46 | self._host = "imap.gmail.com"
47 | return self._host
48 |
49 | def getoauth2_request_url(self):
50 | """Return the OAuth URL to request tokens from.
51 |
52 | We first check the usual OAuth settings, and then fall back to
53 | https://accounts.google.com/o/oauth2/token if nothing is
54 | specified."""
55 |
56 | url = super(GmailRepository, self).getoauth2_request_url()
57 | if url is None:
58 | # Nothing was configured, cache and return hardcoded one.
59 | self.setoauth2_request_url("https://accounts.google.com/o/oauth2/token")
60 | else:
61 | self.setoauth2_request_url(url)
62 | return self.oauth2_request_url
63 |
64 | def getport(self):
65 | """Return the port number to connect to.
66 |
67 | This Gmail implementation first checks for the usual IMAP settings
68 | and falls back to 993 if nothing is specified."""
69 |
70 | port = super(GmailRepository, self).getport()
71 |
72 | if port is None:
73 | return 993
74 | else:
75 | return port
76 |
77 | def getssl(self):
78 | ssl = self.getconfboolean('ssl', None)
79 |
80 | if ssl is None:
81 | # Nothing was configured, return our default setting for
82 | # GMail. Maybe this should look more similar to gethost &
83 | # we could just rely on the global "ssl = yes" default.
84 | return True
85 | else:
86 | return ssl
87 |
88 | def getpreauthtunnel(self):
89 | return None
90 |
91 | def getfolder(self, foldername, decode=True):
92 | return self.getfoldertype()(self.imapserver, foldername,
93 | self, decode)
94 |
95 | def getfoldertype(self):
96 | return folder.Gmail.GmailFolder
97 |
98 | def gettrashfolder(self, foldername):
99 | # Where deleted mail should be moved
100 | return self.getconf('trashfolder', '[Gmail]/Trash')
101 |
102 | def getspamfolder(self):
103 | # Depending on the IMAP settings (Settings -> Forwarding and
104 | # POP/IMAP -> IMAP Access -> "When I mark a message in IMAP as
105 | # deleted") GMail might also deletes messages upon EXPUNGE in
106 | # the Spam folder.
107 | return self.getconf('spamfolder', '[Gmail]/Spam')
108 |
--------------------------------------------------------------------------------
/docs/rfcs/rfc2088.IMAP4_non_synchronizing_literals.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Network Working Group J. Myers
8 | Request for Comments: 2088 Carnegie Mellon
9 | Cateogry: Standards Track January 1997
10 |
11 |
12 | IMAP4 non-synchronizing literals
13 |
14 | Status of this Memo
15 |
16 | This document specifies an Internet standards track protocol for the
17 | Internet community, and requests discussion and suggestions for
18 | improvements. Please refer to the current edition of the "Internet
19 | Official Protocol Standards" (STD 1) for the standardization state
20 | and status of this protocol. Distribution of this memo is unlimited.
21 |
22 | 1. Abstract
23 |
24 | The Internet Message Access Protocol [IMAP4] contains the "literal"
25 | syntactic construct for communicating strings. When sending a
26 | literal from client to server, IMAP4 requires the client to wait for
27 | the server to send a command continuation request between sending the
28 | octet count and the string data. This document specifies an
29 | alternate form of literal which does not require this network round
30 | trip.
31 |
32 | 2. Conventions Used in this Document
33 |
34 | In examples, "C:" and "S:" indicate lines sent by the client and
35 | server respectively.
36 |
37 | 3. Specification
38 |
39 | The non-synchronizing literal is added an alternate form of literal,
40 | and may appear in communication from client to server instead of the
41 | IMAP4 form of literal. The IMAP4 form of literal, used in
42 | communication from client to server, is referred to as a
43 | synchronizing literal.
44 |
45 | Non-synchronizing literals may be used with any IMAP4 server
46 | implementation which returns "LITERAL+" as one of the supported
47 | capabilities to the CAPABILITY command. If the server does not
48 | advertise the LITERAL+ capability, the client must use synchronizing
49 | literals instead.
50 |
51 | The non-synchronizing literal is distinguished from the original
52 | synchronizing literal by having a plus ('+') between the octet count
53 | and the closing brace ('}'). The server does not generate a command
54 | continuation request in response to a non-synchronizing literal, and
55 |
56 |
57 |
58 | Myers Standards Track [Page 1]
59 |
60 | RFC 2088 LITERAL January 1997
61 |
62 |
63 | clients are not required to wait before sending the octets of a non-
64 | synchronizing literal.
65 |
66 | The protocol receiver of an IMAP4 server must check the end of every
67 | received line for an open brace ('{') followed by an octet count, a
68 | plus ('+'), and a close brace ('}') immediately preceeding the CRLF.
69 | If it finds this sequence, it is the octet count of a non-
70 | synchronizing literal and the server MUST treat the specified number
71 | of following octets and the following line as part of the same
72 | command. A server MAY still process commands and reject errors on a
73 | line-by-line basis, as long as it checks for non-synchronizing
74 | literals at the end of each line.
75 |
76 | Example: C: A001 LOGIN {11+}
77 | C: FRED FOOBAR {7+}
78 | C: fat man
79 | S: A001 OK LOGIN completed
80 |
81 | 4. Formal Syntax
82 |
83 | The following syntax specification uses the augmented Backus-Naur
84 | Form (BNF) notation as specified in [RFC-822] as modified by [IMAP4].
85 | Non-terminals referenced but not defined below are as defined by
86 | [IMAP4].
87 |
88 | literal ::= "{" number ["+"] "}" CRLF *CHAR8
89 | ;; Number represents the number of CHAR8 octets
90 |
91 | 6. References
92 |
93 | [IMAP4] Crispin, M., "Internet Message Access Protocol - Version 4",
94 | draft-crispin-imap-base-XX.txt, University of Washington, April 1996.
95 |
96 | [RFC-822] Crocker, D., "Standard for the Format of ARPA Internet Text
97 | Messages", STD 11, RFC 822.
98 |
99 | 7. Security Considerations
100 |
101 | There are no known security issues with this extension.
102 |
103 | 8. Author's Address
104 |
105 | John G. Myers
106 | Carnegie-Mellon University
107 | 5000 Forbes Ave.
108 | Pittsburgh PA, 15213-3890
109 |
110 | Email: jgm+@cmu.edu
111 |
112 |
113 |
114 | Myers Standards Track [Page 2]
115 |
116 |
--------------------------------------------------------------------------------
/offlineimap/ui/TTY.py:
--------------------------------------------------------------------------------
1 | # TTY UI
2 | # Copyright (C) 2002-2018 John Goerzen & contributors
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 | import logging
19 | import sys
20 | import time
21 | from getpass import getpass
22 |
23 | from offlineimap import banner
24 | from offlineimap.ui.UIBase import UIBase
25 |
26 |
27 | class TTYFormatter(logging.Formatter):
28 | """Specific Formatter that adds thread information to the log output."""
29 |
30 | def __init__(self, *args, **kwargs):
31 | #super() doesn't work in py2.6 as 'logging' uses old-style class
32 | logging.Formatter.__init__(self, *args, **kwargs)
33 | self._last_log_thread = None
34 |
35 | def format(self, record):
36 | """Override format to add thread information."""
37 |
38 | #super() doesn't work in py2.6 as 'logging' uses old-style class
39 | log_str = logging.Formatter.format(self, record)
40 | # If msg comes from a different thread than our last, prepend
41 | # thread info. Most look like 'Account sync foo' or 'Folder
42 | # sync foo'.
43 | t_name = record.threadName
44 | if t_name == 'MainThread':
45 | return log_str # main thread doesn't get things prepended
46 | if t_name != self._last_log_thread:
47 | self._last_log_thread = t_name
48 | log_str = "%s:\n %s" % (t_name, log_str)
49 | else:
50 | log_str = " %s"% log_str
51 | return log_str
52 |
53 |
54 | class TTYUI(UIBase):
55 | def setup_consolehandler(self):
56 | """Backend specific console handler
57 |
58 | Sets up things and adds them to self.logger.
59 | :returns: The logging.Handler() for console output"""
60 |
61 | # create console handler with a higher log level
62 | ch = logging.StreamHandler()
63 | #ch.setLevel(logging.DEBUG)
64 | # create formatter and add it to the handlers
65 | self.formatter = TTYFormatter("%(message)s")
66 | ch.setFormatter(self.formatter)
67 | # add the handlers to the logger
68 | self.logger.addHandler(ch)
69 | self.logger.info(banner)
70 | # init lock for console output
71 | ch.createLock()
72 | return ch
73 |
74 | def isusable(self):
75 | """TTYUI is reported as usable when invoked on a terminal."""
76 |
77 | return sys.stdout.isatty() and sys.stdin.isatty()
78 |
79 | def getpass(self, username, config, errmsg=None):
80 | """TTYUI backend is capable of querying the password."""
81 |
82 | if errmsg:
83 | self.warn("%s: %s"% (username, errmsg))
84 | self._log_con_handler.acquire() # lock the console output
85 | try:
86 | return getpass("Enter password for user '%s': " % username)
87 | finally:
88 | self._log_con_handler.release()
89 |
90 | def mainException(self):
91 | if isinstance(sys.exc_info()[1], KeyboardInterrupt):
92 | self.logger.warn("Timer interrupted at user request; program "
93 | "terminating.\n")
94 | self.terminate()
95 | else:
96 | UIBase.mainException(self)
97 |
98 | def sleeping(self, sleepsecs, remainingsecs):
99 | """Sleep for sleepsecs, display remainingsecs to go.
100 |
101 | Does nothing if sleepsecs <= 0.
102 | Display a message on the screen if we pass a full minute.
103 |
104 | This implementation in UIBase does not support this, but some
105 | implementations return 0 for successful sleep and 1 for an
106 | 'abort', ie a request to sync immediately."""
107 |
108 | if sleepsecs > 0:
109 | if remainingsecs//60 != (remainingsecs-sleepsecs)//60:
110 | self.logger.info("Next refresh in %.1f minutes" % (
111 | remainingsecs/60.0))
112 | time.sleep(sleepsecs)
113 | return 0
114 |
--------------------------------------------------------------------------------
/contrib/tested-by.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | """
4 |
5 | Put into Public Domain, by Nicolas Sebrecht.
6 |
7 | Manage the feedbacks of the testers for the release notes.
8 |
9 | """
10 |
11 | from os import system
12 | import argparse
13 |
14 | from helpers import CACHEDIR, EDITOR, Testers, User, Git
15 |
16 |
17 | class App(object):
18 | def __init__(self):
19 | self.args = None
20 | self.testers = Testers()
21 | self.feedbacks = None
22 |
23 |
24 | def _getTestersByFeedback(self):
25 | if self.feedbacks is not None:
26 | return self.feedbacks
27 |
28 | feedbackOk = []
29 | feedbackNo = []
30 |
31 | for tester in self.testers.get():
32 | if tester.getFeedback() is True:
33 | feedbackOk.append(tester)
34 | else:
35 | feedbackNo.append(tester)
36 |
37 | for array in [feedbackOk, feedbackNo]:
38 | array.sort(key=lambda t: t.getName())
39 |
40 | self.feedbacks = feedbackOk + feedbackNo
41 |
42 | def parseArgs(self):
43 | parser = argparse.ArgumentParser(description='Manage the feedbacks.')
44 |
45 | parser.add_argument('--add', '-a', dest='add_tester',
46 | help='Add tester')
47 | parser.add_argument('--delete', '-d', dest='delete_tester',
48 | type=int,
49 | help='Delete tester NUMBER')
50 | parser.add_argument('--list', '-l', dest='list_all_testers',
51 | action='store_true',
52 | help='List the testers')
53 | parser.add_argument('--switchFeedback', '-s', dest='switch_feedback',
54 | action='store_true',
55 | help='Switch the feedback of a tester')
56 |
57 | self.args = parser.parse_args()
58 |
59 | def run(self):
60 | if self.args.list_all_testers is True:
61 | self.listTesters()
62 | if self.args.switch_feedback is True:
63 | self.switchFeedback()
64 | elif self.args.add_tester:
65 | self.addTester(self.args.add_tester)
66 | elif type(self.args.delete_tester) == int:
67 | self.deleteTester(self.args.delete_tester)
68 |
69 | def addTester(self, strTester):
70 | try:
71 | splitted = strTester.split('<')
72 | name = splitted[0].strip()
73 | email = "<{}".format(splitted[1]).strip()
74 | except Exception as e:
75 | print(e)
76 | print("expected format is: 'Firstname Lastname '")
77 | exit(2)
78 | self.testers.add(name, email)
79 | self.testers.write()
80 |
81 | def deleteTester(self, number):
82 | self.listTesters()
83 | removed = self.feedbacks.pop(number)
84 | self.testers.remove(removed)
85 |
86 | print("New list:")
87 | self.feedbacks = None
88 | self.listTesters()
89 | print("Removed: {}".format(removed))
90 | ans = User.request("Save on disk? (s/Q)").lower()
91 | if ans in ['s']:
92 | self.testers.write()
93 |
94 |
95 | def listTesters(self):
96 | self._getTestersByFeedback()
97 |
98 | count = 0
99 | for tester in self.feedbacks:
100 | feedback = "ok"
101 | if tester.getFeedback() is not True:
102 | feedback = "no"
103 | print("{:02d} - {} {}: {}".format(
104 | count, tester.getName(), tester.getEmail(), feedback
105 | )
106 | )
107 | count += 1
108 |
109 | def switchFeedback(self):
110 | self._getTestersByFeedback()
111 | msg = "Switch tester: [/s/q]"
112 |
113 | self.listTesters()
114 | number = User.request(msg)
115 | while number.lower() not in ['s', 'save', 'q', 'quit']:
116 | if number == '':
117 | continue
118 | try:
119 | number = int(number)
120 | self.feedbacks[number].switchFeedback()
121 | except (ValueError, IndexError) as e:
122 | print(e)
123 | exit(1)
124 | finally:
125 | self.listTesters()
126 | number = User.request(msg)
127 | if number in ['s', 'save']:
128 | self.testers.write()
129 | self.listTesters()
130 |
131 | def reset(self):
132 | self.testers.reset()
133 | self.testers.write()
134 |
135 | #def updateMailaliases(self):
136 |
137 | if __name__ == '__main__':
138 | Git.chdirToRepositoryTopLevel()
139 |
140 | app = App()
141 | app.parseArgs()
142 | app.run()
143 |
--------------------------------------------------------------------------------
/TODO.rst:
--------------------------------------------------------------------------------
1 | .. vim: spelllang=en ts=2 expandtab :
2 |
3 | .. _coding style: https://github.com/OfflineIMAP/offlineimap/blob/next/docs/CodingGuidelines.rst
4 |
5 | ============================
6 | TODO list by relevance order
7 | ============================
8 |
9 | Should be the starting point to improve the `coding style`_.
10 |
11 | Write your WIP directly in this file.
12 |
13 | TODO list
14 | ---------
15 |
16 | * Better names for variables, objects, etc.
17 |
18 |
19 | * Improve comments.
20 |
21 | Most of the current comments assume a very good
22 | knowledge of the internals. That sucks because I guess nobody is
23 | anymore aware of ALL of them. Time when this was a one guy made
24 | project has long passed.
25 |
26 |
27 | * Better policy on objects.
28 |
29 | - Turn ALL attributes private and use accessors. This is not
30 | "pythonic" but such pythonic thing turn the code into intricated
31 | code.
32 |
33 | - Turn ALL methods not intended to be used outside, private.
34 |
35 |
36 | * Revamp the factorization.
37 |
38 | It's not unusual to find "factorized" code
39 | for bad reasons: because it made the code /look/ nicer, but the
40 | factorized function/methods is actually called from ONE place. While it
41 | might locally help, such practice globally defeat the purpose because
42 | we lose the view of what is true factorized code and what is not.
43 |
44 |
45 | * Namespace the factorized code.
46 |
47 | If a method require a local function, DON'T USE yet another method. Use a
48 | local namespaced function.::
49 |
50 | class BLah(object):
51 | def _internal_method(self, arg):
52 | def local_factorized(local_arg):
53 | # local_factorized's code
54 | # _internal_method's code.
55 |
56 | Python allows local namespaced functions for good reasons.
57 |
58 |
59 | * Better inheritance policy.
60 |
61 | Take the sample of the folder/LocalStatus(SQlite) and folder/Base stuffs. It's
62 | *nearly IMPOSSIBLE* to know and understand what parent method is used by what
63 | child, for what purpose, etc. So, instead of (re)defining methods in the wild,
64 | keep the well common NON-redefined stuff into the parent and define the
65 | required methods in the childs. We really don't want anything like::
66 |
67 | def method(self):
68 | raise NotImplemented
69 |
70 | While this is common practice in Python, think about that again: how a
71 | parent object should know all the expected methods/accessors of all the
72 | possible kind of childs?
73 |
74 | Inheritance is about factorizing, certainly **NOT** about **defining the
75 | interface** of the childs.
76 |
77 |
78 | * Introduce as many as intermediate inherited objects as required.
79 |
80 | Keeping linear inheritance is good because Python sucks at playing
81 | with multiple parents and it keeps things simple. But a parent should
82 | have ALL its methods used in ALL the childs. If not, it's a good
83 | sign that a new intermediate object should be introduced in the
84 | inheritance line.
85 |
86 | * Don't blindly inherit from library objects.
87 |
88 | We do want **well defined interfaces**. For example, we do too much things
89 | like imapobj.methodcall() while the imapobj is far inherited from imaplib2.
90 |
91 | We have NO clue about what we currently use from the library.
92 | Having a dump wrappper for each call should be made mandatory for
93 | objects inherited from a library. Using composed objects should be
94 | seriously considered in this case, instead of using inheritance.
95 |
96 | * Use factories.
97 |
98 | Current objects do too much initialization stuff varying with the context it
99 | is used. Move things like that into factories and keep the objects definitions
100 | clean.
101 |
102 |
103 | * Make it clear when we expect a composite object and what we expect
104 | exactly.
105 |
106 | Even the more obvious composed objects are badly defined. For example,
107 | the ``conf`` instances are spread across a lot of objects. Did you know
108 | that such composed objects are sometimes restricted to the section the
109 | object works on, and most of the time it's not restricted at all?
110 | How many time it requires to find and understand on what we are
111 | currently working?
112 |
113 |
114 | * Seriously improve our debugging/hacking sessions (AGAIN).
115 |
116 | Until now, we have limited the improvements to allow better/full stack traces.
117 | While this was actually required, we now hit some limitations of the whole
118 | exception-based paradigm. For example, it's very HARD to follow an instance
119 | during its life time. I have a good overview of what we could do in this area,
120 | so don't matter much about that if you don't get the point or what could be
121 | done.
122 |
123 | * Support Unicode.
124 |
--------------------------------------------------------------------------------
/docs/offlineimapui.txt:
--------------------------------------------------------------------------------
1 |
2 | offlineimapui(7)
3 | ================
4 |
5 | NAME
6 | ----
7 | offlineimapui - The User Interfaces
8 |
9 | DESCRIPTION
10 | -----------
11 |
12 | OfflineIMAP comes with different UIs, each aiming its own purpose.
13 |
14 |
15 | TTYUI
16 | ------
17 |
18 | TTYUI interface is for people running in terminals. It prints out basic
19 | status messages and is generally friendly to use on a console or xterm.
20 |
21 |
22 | Basic
23 | ------
24 |
25 | Basic is designed for situations in which OfflineIMAP will be run non-attended
26 | and the status of its execution will be logged.
27 |
28 | This user interface is not capable of reading a password from the keyboard;
29 | account passwords must be specified using one of the configuration file
30 | options. For example, it will not print periodic sleep announcements and tends
31 | to be a tad less verbose, in general.
32 |
33 |
34 | Blinkenlights
35 | -------------
36 |
37 | Blinkenlights is an interface designed to be sleek, fun to watch, and
38 | informative of the overall picture of what OfflineIMAP is doing.
39 |
40 | Blinkenlights contains a row of "LEDs" with command buttons and a log. The
41 | log shows more detail about what is happening and is color-coded to match the
42 | color of the lights.
43 |
44 | Each light in the Blinkenlights interface represents a thread of execution --
45 | that is, a particular task that OfflineIMAP is performing right now. The
46 | colors indicate what task the particular thread is performing, and are as
47 | follows:
48 |
49 | * Black
50 |
51 | indicates that this light's thread has terminated; it will light up again
52 | later when new threads start up. So, black indicates no activity.
53 |
54 | * Red (Meaning 1)
55 |
56 | is the color of the main program's thread, which basically does nothing but
57 | monitor the others. It might remind you of HAL 9000 in 2001.
58 |
59 | * Gray
60 |
61 | indicates that the thread is establishing a new connection to the IMAP
62 | server.
63 |
64 | * Purple
65 |
66 | is the color of an account synchronization thread that is monitoring the
67 | progress of the folders in that account (not generating any I/O).
68 |
69 | * Cyan
70 |
71 | indicates that the thread is syncing a folder.
72 |
73 | * Green
74 |
75 | means that a folder's message list is being loaded.
76 |
77 | * Blue
78 |
79 | is the color of a message synchronization controller thread.
80 |
81 | * Orange
82 |
83 | indicates that an actual message is being copied. (We use fuchsia for fake
84 | messages.)
85 |
86 | * Red (meaning 2)
87 |
88 | indicates that a message is being deleted.
89 |
90 | * Yellow / bright orange
91 |
92 | indicates that message flags are being added.
93 |
94 | * Pink / bright red
95 |
96 | indicates that message flags are being removed.
97 |
98 | * Red / Black Flashing
99 |
100 | corresponds to the countdown timer that runs between synchronizations.
101 |
102 |
103 | The name of this interfaces derives from a bit of computer history. Eric
104 | Raymond's Jargon File defines blinkenlights, in part, as:
105 |
106 | Front-panel diagnostic lights on a computer, esp. a dinosaur. Now that
107 | dinosaurs are rare, this term usually refers to status lights on a modem,
108 | network hub, or the like.
109 |
110 | This term derives from the last word of the famous blackletter-Gothic sign in
111 | mangled pseudo-German that once graced about half the computer rooms in the
112 | English-speaking world. One version ran in its entirety as follows:
113 |
114 | ACHTUNG! ALLES LOOKENSPEEPERS!
115 |
116 | Das computermachine ist nicht fuer gefingerpoken und mittengrabben.
117 | Ist easy schnappen der springenwerk, blowenfusen und poppencorken
118 | mit spitzensparken. Ist nicht fuer gewerken bei das dumpkopfen.
119 | Das rubbernecken sichtseeren keepen das cotten-pickenen hans in das
120 | pockets muss; relaxen und watchen das blinkenlichten.
121 |
122 |
123 | Quiet
124 | -----
125 |
126 | It will output nothing except errors and serious warnings. Like Basic, this
127 | user interface is not capable of reading a password from the keyboard; account
128 | passwords must be specified using one of the configuration file options.
129 |
130 |
131 | Syslog
132 | ------
133 |
134 | Syslog is designed for situations where OfflineIMAP is run as a daemon (e.g.,
135 | as a systemd --user service), but errors should be forwarded to the system log.
136 | Like Basic, this user interface is not capable of reading a password from the
137 | keyboard; account passwords must be specified using one of the configuration
138 | file options.
139 |
140 |
141 | MachineUI
142 | ---------
143 |
144 | MachineUI generates output in a machine-parsable format. It is designed
145 | for other programs that will interface to OfflineIMAP.
146 |
147 |
148 | See Also
149 | --------
150 |
151 | offlineimap(1)
152 |
--------------------------------------------------------------------------------
/docs/website-doc.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # vim: expandtab ts=2 :
4 |
5 | ARGS=$*
6 |
7 | SPHINXBUILD=sphinx-build
8 | TMPDIR='/tmp/offlineimap-sphinx-doctrees'
9 | WEBSITE='./website'
10 | DOCBASE="${WEBSITE}/_doc"
11 | DESTBASE="${DOCBASE}/versions"
12 | VERSIONS_YML="${WEBSITE}/_data/versions.yml"
13 | ANNOUNCES_YML="${WEBSITE}/_data/announces.yml"
14 | ANNOUNCES_YML_LIMIT=31
15 | ANNOUNCES_YML_TMP="${ANNOUNCES_YML}.tmp"
16 | CONTRIB_YML="${WEBSITE}/_data/contribs.yml"
17 | CONTRIB="${DOCBASE}/contrib"
18 | HEADER="# DO NOT EDIT MANUALLY: it is generated by a script (website-doc.sh)."
19 |
20 |
21 | function fix_pwd () {
22 | cd "$(git rev-parse --show-toplevel)" || \
23 | exit 2 "cannot determine the root of the repository"
24 | test -d "$DESTBASE" || exit 1
25 | }
26 |
27 | fix_pwd
28 | version="v$(./offlineimap.py --version)"
29 |
30 |
31 |
32 | #
33 | # Add the doc for the contrib files.
34 | #
35 | function contrib () {
36 | echo $HEADER > "$CONTRIB_YML"
37 | # systemd
38 | cp -afv "./contrib/systemd/README.md" "${CONTRIB}/systemd.md"
39 | echo "- {filename: 'systemd', linkname: 'Integrate with systemd'}" >> "$CONTRIB_YML"
40 | }
41 |
42 |
43 |
44 | #
45 | # Build the sphinx documentation.
46 | #
47 | function api () {
48 | # Build the doc with sphinx.
49 | dest="${DESTBASE}/${version}"
50 | echo "Cleaning target directory: $dest"
51 | rm -rf "$dest"
52 | $SPHINXBUILD -b html -d "$TMPDIR" ./docs/doc-src "$dest"
53 |
54 | # Build the JSON definitions for Jekyll.
55 | # This let know the website about the available APIs documentations.
56 | echo "Building Jekyll data: $VERSIONS_YML"
57 | # Erase previous content.
58 | echo > "$VERSIONS_YML" <> "$VERSIONS_YML"
69 | }
70 |
71 |
72 |
73 | #
74 | # Return title from release entry.
75 | # $1: full release title
76 | #
77 | function parse_releases_get_link () {
78 | echo $1 | sed -r -e 's,^### (OfflineIMAP.*)\),\1,' \
79 | | tr '[:upper:]' '[:lower:]' \
80 | | sed -r -e 's,[\.("],,g' \
81 | | sed -r -e 's, ,-,g'
82 | }
83 |
84 | #
85 | # Return version from release entry.
86 | # $1: full release title
87 | #
88 | function parse_releases_get_version () {
89 | echo $1 | sed -r -e 's,^### [a-Z]+ (v[^ ]+).*,\1,'
90 | }
91 |
92 | #
93 | # Return date from release entry.
94 | # $1: full release title
95 | #
96 | function parse_releases_get_date () {
97 | echo $1 | sed -r -e 's,.*\(([0-9]+-[0-9]+-[0-9]+).*,\1,'
98 | }
99 |
100 | #
101 | # Make Changelog public and save links to them as JSON.
102 | #
103 | function releases () {
104 | # Copy the Changelogs.
105 | for foo in ./Changelog.md ./Changelog.maint.md
106 | do
107 | cp -afv "$foo" "$DOCBASE"
108 | done
109 |
110 | # Build the announces JSON list. Format is JSON:
111 | # - {version: '', link: ''}
112 | # - ...
113 | echo "$HEADER" > "$ANNOUNCES_YML"
114 | # Announces for the mainline.
115 | grep -E '^### OfflineIMAP' ./Changelog.md | while read title
116 | do
117 | link="$(parse_releases_get_link "$title")"
118 | v="$(parse_releases_get_version "$title")"
119 | d="$(parse_releases_get_date "$title")"
120 | echo "- {date: '${d}', version: '${v}', link: 'Changelog.html#${link}'}"
121 | done | tee -a "$ANNOUNCES_YML_TMP"
122 | # Announces for the maintenance releases.
123 | grep -E '^### OfflineIMAP' ./Changelog.maint.md | while read title
124 | do
125 | link="$(parse_releases_get_link "$title")"
126 | v="$(parse_releases_get_version "$title")"
127 | d="$(parse_releases_get_date "$title")"
128 | echo "- {date: '${d}', version: '${v}', link: 'Changelog.maint.html#${link}'}"
129 | done | tee -a "$ANNOUNCES_YML_TMP"
130 | sort -nr "$ANNOUNCES_YML_TMP" | head -n $ANNOUNCES_YML_LIMIT >> "$ANNOUNCES_YML"
131 | rm -f "$ANNOUNCES_YML_TMP"
132 | }
133 |
134 | function manhtml () {
135 | set -e
136 |
137 | cd ./docs
138 | make manhtml
139 | cd ..
140 | cp -afv ./docs/manhtml/* "$DOCBASE"
141 | }
142 |
143 |
144 | exit_code=0
145 | test "n$ARGS" = 'n' && ARGS='usage' # no option passed
146 | for arg in $ARGS
147 | do
148 | # PWD was fixed at the very beginning.
149 | case "n$arg" in
150 | "nreleases")
151 | releases
152 | ;;
153 | "napi")
154 | api
155 | ;;
156 | "nhtml")
157 | manhtml
158 | ;;
159 | "ncontrib")
160 | contrib
161 | ;;
162 | "nusage")
163 | echo "Usage: website-doc.sh "
164 | ;;
165 | *)
166 | echo "unkown option $arg"
167 | exit_code=$(( $exit_code + 1 ))
168 | ;;
169 | esac
170 | done
171 |
172 | exit $exit_code
173 |
--------------------------------------------------------------------------------
/CONTRIBUTING.rst:
--------------------------------------------------------------------------------
1 | .. -*- coding: utf-8 -*-
2 | .. vim: spelllang=en ts=2 expandtab:
3 |
4 | .. _OfflineIMAP: https://github.com/OfflineIMAP/offlineimap
5 | .. _Github: https://github.com/OfflineIMAP/offlineimap
6 | .. _repository: git://github.com/OfflineIMAP/offlineimap.git
7 | .. _maintainers: https://github.com/OfflineIMAP/offlineimap/blob/next/MAINTAINERS.rst
8 | .. _mailing list: http://lists.alioth.debian.org/mailman/listinfo/offlineimap-project
9 | .. _Developer's Certificate of Origin: https://github.com/OfflineIMAP/offlineimap/blob/next/docs/doc-src/dco.rst
10 | .. _Community's website: http://www.offlineimap.org
11 | .. _APIs in OfflineIMAP: http://www.offlineimap.org/documentation.html#available-apis
12 | .. _documentation: http://www.offlineimap.org/documentation.html
13 | .. _Coding Guidelines: http://www.offlineimap.org/doc/CodingGuidelines.html
14 | .. _Know the status of your patches: http://www.offlineimap.org/doc/GitAdvanced.html#know-the-status-of-your-patch-after-submission
15 | .. _How to fix a bug in open source software: https://opensource.com/life/16/8/how-get-bugs-fixed-open-source-software
16 |
17 |
18 | =================
19 | HOW TO CONTRIBUTE
20 | =================
21 |
22 | You'll find here the **basics** to contribute to OfflineIMAP_, addressed to
23 | users as well as learning or experienced developers to quickly provide
24 | contributions.
25 |
26 | **For more detailed documentation, see the** `Community's website`_.
27 |
28 | .. contents:: :depth: 3
29 |
30 |
31 | Submit issues
32 | =============
33 |
34 | Issues are welcome to both Github_ and the `mailing list`_, at your own
35 | convenience. Provide the following information:
36 | - system/distribution (with version)
37 | - offlineimap version (`offlineimap -V`)
38 | - Python version
39 | - server name or domain
40 | - CLI options
41 | - Configuration file (offlineimaprc)
42 | - pythonfile (if any)
43 | - Logs, error
44 | - Steps to reproduce the error
45 |
46 | Worth the read: `How to fix a bug in open source software`_.
47 |
48 | You might help closing some issues, too. :-)
49 |
50 |
51 | For the imaptients
52 | ==================
53 |
54 | - `Coding Guidelines`_
55 | - `APIs in OfflineIMAP`_
56 | - `Know the status of your patches`_ after submission
57 | - All the `documentation`_
58 |
59 |
60 | Community
61 | =========
62 |
63 | All contributors to OfflineIMAP_ are benevolent volunteers. This makes hacking
64 | to OfflineIMAP_ **fun and open**.
65 |
66 | Thanks to Python, almost every developer can quickly become productive. Students
67 | and novices are welcome. Third-parties patches are essential and proved to be a
68 | wonderful source of changes for both fixes and new features.
69 |
70 | OfflineIMAP_ is entirely written in Python, works on IMAP and source code is
71 | tracked with Git.
72 |
73 | *It is expected that most contributors don't have skills to all of these areas.*
74 | That's why the best thing you could do for you, is to ask us about any
75 | difficulty or question raising in your mind. We actually do our best to help new
76 | comers. **We've all started like this.**
77 |
78 | - The official repository_ is maintained by the core team maintainers_.
79 |
80 | - The `mailing list`_ is where all the exciting things happen.
81 |
82 |
83 | Getting started
84 | ===============
85 |
86 | Occasional contributors
87 | -----------------------
88 |
89 | * Clone the official repository_.
90 |
91 | Regular contributors
92 | --------------------
93 |
94 | * Create an account and login to Github.
95 | * Fork the official repository_.
96 | * Clone your own fork to your local workspace.
97 | * Add a reference to your fork (once)::
98 |
99 | $ git remote add myfork https://github.com//offlineimap.git
100 |
101 | * Regularly fetch the changes applied by the maintainers::
102 |
103 | $ git fetch origin
104 | $ git checkout master
105 | $ git merge offlineimap/master
106 | $ git checkout next
107 | $ git merge offlineimap/next
108 |
109 |
110 | Making changes (all contributors)
111 | ---------------------------------
112 |
113 | 1. Create your own topic branch off of ``next`` (recently updated) via::
114 |
115 | $ git checkout -b my_topic next
116 |
117 | 2. Check for unnecessary whitespaces with ``git diff --check`` before committing.
118 | 3. Commit your changes into logical/atomic commits. **Sign-off your work** to
119 | confirm you agree with the `Developer's Certificate of Origin`_.
120 | 4. Write a good *commit message* about **WHY** this patch (take samples from
121 | the ``git log``).
122 |
123 |
124 | Learn more
125 | ==========
126 |
127 | There is already a lot of documentation. Here's where you might want to look
128 | first:
129 |
130 | - The directory ``offlineimap/docs`` has all kind of additional documentation
131 | (man pages, RFCs).
132 |
133 | - The file ``offlineimap.conf`` allows to know all the supported features.
134 |
135 | - The file ``TODO.rst`` express code changes we'd like and current *Work In
136 | Progress* (WIP).
137 |
138 |
--------------------------------------------------------------------------------
/offlineimap/repository/LocalStatus.py:
--------------------------------------------------------------------------------
1 | # Local status cache repository support
2 | # Copyright (C) 2002-2017 John Goerzen & contributors
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 | import os
19 |
20 | from offlineimap.folder.LocalStatus import LocalStatusFolder
21 | from offlineimap.folder.LocalStatusSQLite import LocalStatusSQLiteFolder
22 | from offlineimap.repository.Base import BaseRepository
23 | from offlineimap.error import OfflineImapError
24 |
25 |
26 | class LocalStatusRepository(BaseRepository):
27 | def __init__(self, reposname, account):
28 | BaseRepository.__init__(self, reposname, account)
29 |
30 | # class and root for all backends.
31 | self.backends = {}
32 | self.backends['sqlite'] = {
33 | 'class': LocalStatusSQLiteFolder,
34 | 'root': os.path.join(account.getaccountmeta(), 'LocalStatus-sqlite')
35 | }
36 | self.backends['plain'] = {
37 | 'class': LocalStatusFolder,
38 | 'root': os.path.join(account.getaccountmeta(), 'LocalStatus')
39 | }
40 |
41 | if self.account.getconf('status_backend', None) is not None:
42 | raise OfflineImapError(
43 | "the 'status_backend' configuration option is not supported"
44 | " anymore; please, remove this configuration option.",
45 | OfflineImapError.ERROR.REPO
46 | )
47 | # Set class and root for sqlite.
48 | self.setup_backend('sqlite')
49 |
50 | if not os.path.exists(self.root):
51 | os.mkdir(self.root, 0o700)
52 |
53 | # self._folders is a dict of name:LocalStatusFolders().
54 | self._folders = {}
55 |
56 | def _instanciatefolder(self, foldername):
57 | return self.LocalStatusFolderClass(foldername, self) # Instanciate.
58 |
59 | def setup_backend(self, backend):
60 | if backend in self.backends.keys():
61 | self._backend = backend
62 | self.root = self.backends[backend]['root']
63 | self.LocalStatusFolderClass = self.backends[backend]['class']
64 |
65 | def import_other_backend(self, folder):
66 | for bk, dic in self.backends.items():
67 | # Skip folder's own type.
68 | if dic['class'] == type(folder):
69 | continue
70 |
71 | repobk = LocalStatusRepository(self.name, self.account)
72 | repobk.setup_backend(bk) # Fake the backend.
73 | folderbk = dic['class'](folder.name, repobk)
74 |
75 | # If backend contains data, import it to folder.
76 | if not folderbk.isnewfolder():
77 | self.ui._msg("Migrating LocalStatus cache from %s to %s "
78 | "status folder for %s:%s"%
79 | (bk, self._backend, self.name, folder.name))
80 |
81 | folderbk.cachemessagelist()
82 | folder.messagelist = folderbk.messagelist
83 | folder.saveall()
84 | break
85 |
86 | def getsep(self):
87 | return '.'
88 |
89 | def makefolder(self, foldername):
90 | """Create a LocalStatus Folder."""
91 |
92 | if self.account.dryrun:
93 | return # Bail out in dry-run mode.
94 |
95 | # Create an empty StatusFolder.
96 | folder = self._instanciatefolder(foldername)
97 | # First delete any existing data to make sure we won't consider obsolete
98 | # data. This might happen if the user removed the folder (maildir) and
99 | # it is re-created afterwards.
100 | folder.purge()
101 | folder.openfiles()
102 | folder.save()
103 | folder.closefiles()
104 |
105 | # Invalidate the cache.
106 | self.forgetfolders()
107 |
108 | def getfolder(self, foldername):
109 | """Return the Folder() object for a foldername.
110 |
111 | Caller must call closefiles() on the folder when done."""
112 |
113 | if foldername in self._folders:
114 | return self._folders[foldername]
115 |
116 | folder = self._instanciatefolder(foldername)
117 |
118 | # If folder is empty, try to import data from an other backend.
119 | if folder.isnewfolder():
120 | self.import_other_backend(folder)
121 |
122 | self._folders[foldername] = folder
123 | return folder
124 |
125 | def getfolders(self):
126 | """Returns a list of all cached folders.
127 |
128 | Does nothing for this backend. We mangle the folder file names
129 | (see getfolderfilename) so we can not derive folder names from
130 | the file names that we have available. TODO: need to store a
131 | list of folder names somehow?"""
132 |
133 | pass
134 |
135 | def forgetfolders(self):
136 | """Forgets the cached list of folders, if any. Useful to run
137 | after a sync run."""
138 |
139 | self._folders = {}
140 |
--------------------------------------------------------------------------------
/docs/rfcs/rfc2061.compatibility_IMAP4-IMAP2bis.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Network Working Group M. Crispin
8 | Request for Comments: 2061 University of Washington
9 | Category: Informational December 1996
10 |
11 |
12 | IMAP4 COMPATIBILITY WITH IMAP2BIS
13 |
14 | Status of this Memo
15 |
16 | This memo provides information for the Internet community. This memo
17 | does not specify an Internet standard of any kind. Distribution of
18 | this memo is unlimited.
19 |
20 | Introduction
21 |
22 | The Internet Message Access Protocol (IMAP) has been through several
23 | revisions and variants in its 10-year history. Many of these are
24 | either extinct or extremely rare; in particular, several undocumented
25 | variants and the variants described in RFC 1064, RFC 1176, and RFC
26 | 1203 fall into this category.
27 |
28 | One variant, IMAP2bis, is at the time of this writing very common and
29 | has been widely distributed with the Pine mailer. Unfortunately,
30 | there is no definite document describing IMAP2bis. This document is
31 | intended to be read along with RFC 1176 and the most recent IMAP4
32 | specification (RFC 2060) to assist implementors in creating an IMAP4
33 | implementation to interoperate with implementations that conform to
34 | earlier specifications. Nothing in this document is required by the
35 | IMAP4 specification; implementors must decide for themselves whether
36 | they want their implementation to fail if it encounters old software.
37 |
38 | At the time of this writing, IMAP4 has been updated from the version
39 | described in RFC 1730. An implementor who wishes to interoperate
40 | with both RFC 1730 and RFC 2060 should refer to both documents.
41 |
42 | This information is not complete; it reflects current knowledge of
43 | server and client implementations as well as "folklore" acquired in
44 | the evolution of the protocol. It is NOT a description of how to
45 | interoperate with all variants of IMAP, but rather with the old
46 | variant that is most likely to be encountered. For detailed
47 | information on interoperating with other old variants, refer to RFC
48 | 1732.
49 |
50 | IMAP4 client interoperability with IMAP2bis servers
51 |
52 | A quick way to check whether a server implementation supports the
53 | IMAP4 specification is to try the CAPABILITY command. An OK response
54 | will indicate which variant(s) of IMAP4 are supported by the server.
55 |
56 |
57 |
58 | Crispin Informational [Page 1]
59 |
60 | RFC 2061 IMAP4 Compatibility December 1996
61 |
62 |
63 | If the client does not find any of its known variant in the response,
64 | it should treat the server as IMAP2bis. A BAD response indicates an
65 | IMAP2bis or older server.
66 |
67 | Most IMAP4 facilities are in IMAP2bis. The following exceptions
68 | exist:
69 |
70 | CAPABILITY command
71 | The absense of this command indicates IMAP2bis (or older).
72 |
73 | AUTHENTICATE command.
74 | Use the LOGIN command.
75 |
76 | LSUB, SUBSCRIBE, and UNSUBSCRIBE commands
77 | No direct functional equivalent. IMAP2bis had a concept
78 | called "bboards" which is not in IMAP4. RFC 1176 supported
79 | these with the BBOARD and FIND BBOARDS commands. IMAP2bis
80 | augmented these with the FIND ALL.BBOARDS, SUBSCRIBE BBOARD,
81 | and UNSUBSCRIBE BBOARD commands. It is recommended that
82 | none of these commands be implemented in new software,
83 | including servers that support old clients.
84 |
85 | LIST command
86 | Use the command FIND ALL.MAILBOXES, which has a similar syn-
87 | tax and response to the FIND MAILBOXES command described in
88 | RFC 1176. The FIND MAILBOXES command is unlikely to produce
89 | useful information.
90 |
91 | * in a sequence
92 | Use the number of messages in the mailbox from the EXISTS
93 | unsolicited response.
94 |
95 | SEARCH extensions (character set, additional criteria)
96 | Reformulate the search request using only the RFC 1176 syn-
97 | tax. This may entail doing multiple searches to achieve the
98 | desired results.
99 |
100 | BODYSTRUCTURE fetch data item
101 | Use the non-extensible BODY data item.
102 |
103 | body sections HEADER, TEXT, MIME, HEADER.FIELDS, HEADER.FIELDS.NOT
104 | Use body section numbers only.
105 |
106 | BODY.PEEK[section]
107 | Use BODY[section] and manually clear the \Seen flag as
108 | necessary.
109 |
110 |
111 |
112 |
113 |
114 | Crispin Informational [Page 2]
115 |
116 | RFC 2061 IMAP4 Compatibility December 1996
117 |
118 |
119 | FLAGS.SILENT, +FLAGS.SILENT, and -FLAGS.SILENT store data items
120 | Use the corresponding non-SILENT versions and ignore the
121 | untagged FETCH responses which come back.
122 |
123 | UID fetch data item and the UID commands
124 | No functional equivalent.
125 |
126 | CLOSE command
127 | No functional equivalent.
128 |
129 |
130 | In IMAP2bis, the TRYCREATE special information token is sent as a
131 | separate unsolicited OK response instead of inside the NO response.
132 |
133 | IMAP2bis is ambiguous about whether or not flags or internal dates
134 | are preserved on COPY. It is impossible to know what behavior is
135 | supported by the server.
136 |
137 | IMAP4 server interoperability with IMAP2bis clients
138 |
139 | The only interoperability problem between an IMAP4 server and a
140 | well-written IMAP2bis client is an incompatibility with the use of
141 | "\" in quoted strings. This is best avoided by using literals
142 | instead of quoted strings if "\" or <"> is embedded in the string.
143 |
144 | Security Considerations
145 |
146 | Security issues are not discussed in this memo.
147 |
148 | Author's Address
149 |
150 | Mark R. Crispin
151 | Networks and Distributed Computing
152 | University of Washington
153 | 4545 15th Aveneue NE
154 | Seattle, WA 98105-4527
155 |
156 | Phone: (206) 543-5762
157 | EMail: MRC@CAC.Washington.EDU
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | Crispin Informational [Page 3]
171 |
172 |
--------------------------------------------------------------------------------
/docs/rfcs/rfc1733.models_in_IMAP4.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Network Working Group M. Crispin
8 | Request for Comments: 1733 University of Washington
9 | Category: Informational December 1994
10 |
11 |
12 | DISTRIBUTED ELECTRONIC MAIL MODELS IN IMAP4
13 |
14 |
15 | Status of this Memo
16 |
17 | This memo provides information for the Internet community. This memo
18 | does not specify an Internet standard of any kind. Distribution of
19 | this memo is unlimited.
20 |
21 |
22 | Distributed Electronic Mail Models
23 |
24 | There are three fundamental models of client/server email: offline,
25 | online, and disconnected use. IMAP4 can be used in any one of these
26 | three models.
27 |
28 | The offline model is the most familiar form of client/server email
29 | today, and is used by protocols such as POP-3 (RFC 1225) and UUCP.
30 | In this model, a client application periodically connects to a
31 | server. It downloads all the pending messages to the client machine
32 | and deletes these from the server. Thereafter, all mail processing
33 | is local to the client. This model is store-and-forward; it moves
34 | mail on demand from an intermediate server (maildrop) to a single
35 | destination machine.
36 |
37 | The online model is most commonly used with remote filesystem
38 | protocols such as NFS. In this model, a client application
39 | manipulates mailbox data on a server machine. A connection to the
40 | server is maintained throughout the session. No mailbox data are
41 | kept on the client; the client retrieves data from the server as is
42 | needed. IMAP4 introduces a form of the online model that requires
43 | considerably less network bandwidth than a remote filesystem
44 | protocol, and provides the opportunity for using the server for CPU
45 | or I/O intensive functions such as parsing and searching.
46 |
47 | The disconnected use model is a hybrid of the offline and online
48 | models, and is used by protocols such as PCMAIL (RFC 1056). In this
49 | model, a client user downloads some set of messages from the server,
50 | manipulates them offline, then at some later time uploads the
51 | changes. The server remains the authoritative repository of the
52 | messages. The problems of synchronization (particularly when
53 | multiple clients are involved) are handled through the means of
54 | unique identifiers for each message.
55 |
56 |
57 |
58 | Crispin [Page 1]
59 |
60 | RFC 1733 IMAP4 - Model December 1994
61 |
62 |
63 | Each of these models have their own strengths and weaknesses:
64 |
65 | Feature Offline Online Disc
66 | ------- ------- ------ ----
67 | Can use multiple clients NO YES YES
68 | Minimum use of server connect time YES NO YES
69 | Minimum use of server resources YES NO NO
70 | Minimum use of client disk resources NO YES NO
71 | Multiple remote mailboxes NO YES YES
72 | Fast startup NO YES NO
73 | Mail processing when not online YES NO YES
74 |
75 | Although IMAP4 has its origins as a protocol designed to accommodate
76 | the online model, it can support the other two models as well. This
77 | makes possible the creation of clients that can be used in any of the
78 | three models. For example, a user may wish to switch between the
79 | online and disconnected models on a regular basis (e.g. owing to
80 | travel).
81 |
82 | IMAP4 is designed to transmit message data on demand, and to provide
83 | the facilities necessary for a client to decide what data it needs at
84 | any particular time. There is generally no need to do a wholesale
85 | transfer of an entire mailbox or even of the complete text of a
86 | message. This makes a difference in situations where the mailbox is
87 | large, or when the link to the server is slow.
88 |
89 | More specifically, IMAP4 supports server-based RFC 822 and MIME
90 | processing. With this information, it is possible for a client to
91 | determine in advance whether it wishes to retrieve a particular
92 | message or part of a message. For example, a user connected to an
93 | IMAP4 server via a dialup link can determine that a message has a
94 | 2000 byte text segment and a 40 megabyte video segment, and elect to
95 | fetch only the text segment.
96 |
97 | In IMAP4, the client/server relationship lasts only for the duration
98 | of the TCP connection. There is no registration of clients. Except
99 | for any unique identifiers used in disconnected use operation, the
100 | client initially has no knowledge of mailbox state and learns it from
101 | the IMAP4 server when a mailbox is selected. This initial transfer
102 | is minimal; the client requests additional state data as it needs.
103 |
104 | As noted above, the choice for the location of mailbox data depends
105 | upon the model chosen. The location of message state (e.g. whether
106 | or not a message has been read or answered) is also determined by the
107 | model, and is not necessarily the same as the location of the mailbox
108 | data. For example, in the online model message state can be co-
109 | located with mailbox data; it can also be located elsewhere (on the
110 | client or on a third agent) using unique identifiers to achieve
111 |
112 |
113 |
114 | Crispin [Page 2]
115 |
116 | RFC 1733 IMAP4 - Model December 1994
117 |
118 |
119 | common reference across sessions. The latter is particularly useful
120 | with a server that exports public data such as netnews and does not
121 | maintain per-user state.
122 |
123 | The IMAP4 protocol provides the generality to implement these
124 | different models. This is done by means of server and (especially)
125 | client configuration, and not by requiring changes to the protocol or
126 | the implementation of the protocol.
127 |
128 |
129 | Security Considerations
130 |
131 | Security issues are not discussed in this memo.
132 |
133 |
134 | Author's Address:
135 |
136 | Mark R. Crispin
137 | Networks and Distributed Computing, JE-30
138 | University of Washington
139 | Seattle, WA 98195
140 |
141 | Phone: (206) 543-5762
142 |
143 | EMail: MRC@CAC.Washington.EDU
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | Crispin [Page 3]
171 |
172 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Financial contributors: [](https://opencollective.com/offlineimap-organization)
3 |
4 | [offlineimap]: http://github.com/OfflineIMAP/offlineimap
5 | [offlineimap3]: http://github.com/OfflineIMAP/offlineimap3
6 | [website]: http://www.offlineimap.org
7 | [wiki]: http://github.com/OfflineIMAP/offlineimap/wiki
8 | [blog]: http://www.offlineimap.org/posts.html
9 |
10 | Links:
11 | * Official github code repository: [offlineimap]
12 | * Website: [website]
13 | * Wiki: [wiki]
14 | * Blog: [blog]
15 |
16 | # OfflineIMAP
17 |
18 | ***"Get the emails where you need them."***
19 |
20 | > IMPORTANT NOTE: This repository is for python2 only. The support for offlineimap3
21 | > is happening in [Official offlineimap for python3][offlineimap3].
22 | >
23 | > I'll still lazily maintain this legacy offlineimap but users should definitely go with
24 | > offlineimap3.
25 |
26 | - [Official offlineimap for python3][offlineimap3].
27 | - [Official offlineimap for python2][offlineimap].
28 |
29 |
30 | ## Description
31 |
32 | OfflineIMAP is software that downloads your email mailbox(es) as **local
33 | Maildirs**. OfflineIMAP will synchronize both sides via *IMAP*.
34 |
35 | ## Why should I use OfflineIMAP?
36 |
37 | IMAP's main downside is that you have to **trust** your email provider to
38 | not lose your email. While certainly unlikely, it's not impossible.
39 | With OfflineIMAP, you can download your Mailboxes and make you own backups of
40 | your [Maildir](https://en.wikipedia.org/wiki/Maildir).
41 |
42 | This allows reading your email offline without the need for your mail
43 | reader (MUA) to support IMAP operations. Need an attachment from a
44 | message without internet connection? No problem, the message is still there.
45 |
46 |
47 | ## Project status and future
48 |
49 | The [offlineimap][offlineimap] project was forked to
50 | [offlineimap3][offlineimap3] to support python3. Contributions are welcome to
51 | this project.
52 |
53 |
54 | ## Contributors
55 |
56 | ### Code Contributors
57 |
58 | This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
59 |
60 |
61 | ### Financial Contributors
62 |
63 | Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/offlineimap-organization/contribute)]
64 |
65 | #### Individuals
66 |
67 |
68 |
69 | #### Organizations
70 |
71 | Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/offlineimap-organization/contribute)]
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | ## License
85 |
86 | GNU General Public License v2.
87 |
88 |
89 | ## Downloads
90 |
91 | You should first check if your distribution already packages OfflineIMAP for you.
92 | Downloads releases as [tarball or zipball](https://github.com/OfflineIMAP/offlineimap/tags).
93 |
94 | If you are running Linux Os, you can install offlineimap with:
95 |
96 | - openSUSE `zypper in offlineimap`
97 | - Arch Linux `pacman -S offlineimap`
98 | - fedora `dnf install offlineimap`
99 |
100 | ## Feedbacks and contributions
101 |
102 | **The user discussions, development, announcements and all the exciting stuff take
103 | place on the mailing list.** While not mandatory to send emails, you can
104 | [subscribe here](http://lists.alioth.debian.org/mailman/listinfo/offlineimap-project).
105 |
106 | Bugs, issues and contributions can be requested to both the mailing list or the
107 | [official Github project][offlineimap]. Provide the following information:
108 | - system/distribution (with version)
109 | - offlineimap version (`offlineimap -V`)
110 | - Python version
111 | - server name or domain
112 | - CLI options
113 | - Configuration file (offlineimaprc)
114 | - pythonfile (if any)
115 | - Logs, error
116 | - Steps to reproduce the error
117 |
118 |
119 | ## The community
120 |
121 | * OfflineIMAP's main site is the [project page at Github][offlineimap].
122 | * There is the [OfflineIMAP community's website][website].
123 | * And finally, [the wiki][wiki].
124 |
125 |
126 | ## Requirements & dependencies
127 |
128 | * Python v2.7.x
129 | * six (required)
130 | * rfc6555 (required)
131 | * imaplib2 >= 2.57 (optional)
132 | * gssapi (optional), for Kerberos authentication
133 | * portalocker (optional), if you need to run offlineimap in Cygwin for Windows
134 |
135 | * Python v3: See the [offlineimap3][offlineimap3] fork of
136 | [offlineimap][offlineimap].
137 |
138 | ## Documentation
139 |
140 | All current and updated documentation is on the [community's website][website].
141 |
142 |
143 | ### Read documentation locally
144 |
145 | You might want to read the documentation locally. Get the sources of the website.
146 | For the other documentation, run the appropriate make target:
147 |
148 | ```sh
149 | $ ./scripts/get-repository.sh website
150 | $ cd docs
151 | $ make html # Requires rst2html
152 | $ make man # Requires a2x (http://asciidoc.org)
153 | $ make api # Requires sphinx
154 | ```
155 |
--------------------------------------------------------------------------------
/docs/offlineimap.known_issues.txt:
--------------------------------------------------------------------------------
1 |
2 | * Deletions.
3 | +
4 | While in usual run the deletions are propagated. To prevent from data loss,
5 | removing a folder makes offlineimap re-sync the folder. However, propagating the
6 | removal of the whole content of a folder can happen in the two following cases:
7 |
8 | - The whole content of a folder is deleted but the folder directory still
9 | exists.
10 |
11 | - The parent directory of the folder was deleted.
12 |
13 | * SSL3 write pending.
14 | +
15 | Users enabling SSL may hit a bug about "SSL3 write pending". If so, the
16 | account(s) will stay unsynchronised from the time the bug appeared. Running
17 | OfflineIMAP again can help. We are still working on this bug. Patches or
18 | detailed bug reports would be appreciated. Please check you're running the
19 | last stable version and send us a report to the mailing list including the
20 | full log.
21 |
22 | * IDLE support is incomplete and experimental. Bugs may be encountered.
23 |
24 | - No hook exists for "run after an IDLE response".
25 | +
26 | Email will show up, but may not be processed until the next refresh cycle.
27 |
28 | - nametrans may not be supported correctly.
29 |
30 | - IMAP IDLE <-> IMAP IDLE doesn't work yet.
31 |
32 | - IDLE might stop syncing on a system suspend/resume.
33 |
34 | - IDLE may only work "once" per refresh.
35 | +
36 | If you encounter this bug, please send a report to the list!
37 |
38 | * Maildir support in Windows drive.
39 | +
40 | Maildir uses colon character (:) in message file names. Colon is however
41 | forbidden character in windows drives. There are several workarounds for that
42 | situation:
43 |
44 | . Enable file name character translation in windows registry (not tested).
45 | -
46 |
47 | . Use cygwin managed mount (not tested).
48 | - not available anymore since cygwin 1.7
49 |
50 | . Use "maildir-windows-compatible = yes" account OfflineIMAP configuration.
51 | - That makes OfflineIMAP to use exclamation mark (!) instead of colon for
52 | storing messages. Such files can be written to windows partitions. But
53 | you will probably loose compatibility with other programs trying to
54 | read the same Maildir.
55 | +
56 | - Exclamation mark was chosen because of the note in
57 | http://docs.python.org/library/mailbox.html
58 | +
59 | - If you have some messages already stored without this option, you will
60 | have to re-sync them again
61 |
62 | * OfflineIMAP confused after system suspend.
63 | +
64 | When resuming a suspended session, OfflineIMAP does not cleanly handles the
65 | broken socket(s) if socktimeout option is not set.
66 | You should enable this option with a value like 10.
67 |
68 | * OfflineIMAP confused when mails change while in a sync.
69 | +
70 | When OfflineIMAP is syncing, some events happening since the invocation on
71 | remote or local side are badly handled. OfflineIMAP won't track for changes
72 | during the sync.
73 |
74 |
75 | * Sharing a maildir with multiple IMAP servers.
76 | +
77 | Generally a word of caution mixing IMAP repositories on the same Maildir root.
78 | You have to be careful that you *never* use the same maildir folder for 2 IMAP
79 | servers. In the best case, the folder MD5 will be different, and you will get
80 | a loop where it will upload your mails to both servers in turn (infinitely!)
81 | as it thinks you have placed new mails in the local Maildir. In the worst
82 | case, the MD5 is the same (likely) and mail UIDs overlap (likely too!) and it
83 | will fail to sync some mails as it thinks they are already existent.
84 | +
85 | I would create a new local Maildir Repository for the Personal Gmail and
86 | use a different root to be on the safe side here. You could e.g. use
87 |
88 | `~/mail/Pro' as Maildir root for the ProGmail and
89 | `~/mail/Personal' as root for the personal one.
90 | +
91 | If you then point your local mutt, or whatever MUA you use to `~/mail/'
92 | as root, it should still recognize all folders.
93 |
94 |
95 | * Edge cases with maxage causing too many messages to be synced.
96 | +
97 | All messages from at most maxage days ago (+/- a few hours, depending on
98 | timezones) are synced, but there are cases in which older messages can also be
99 | synced. This happens when a message's UID is significantly higher than those of
100 | other messages with similar dates, e.g. when messages are added to the local
101 | folder behind offlineimap's back, causing them to get assigned a new UID, or
102 | when offlineimap first syncs a pre-existing Maildir. In the latter case, it
103 | could appear as if a noticeable and random subset of old messages are synced.
104 |
105 | * Offlineimap hangs.
106 | +
107 | When having unexpected hangs it's advised to set `singlethreadperfolder' to
108 | 'yes', especially when in IMAP/IMAP mode (no maildir).
109 |
110 | * Passwords in netrc.
111 | +
112 | Offlineimap doesn't know how to retrieve passwords when more than one account is
113 | stored in the netrc file. See
114 | .
115 |
116 | * XOAUTH2
117 | +
118 | XOAUTH2 might be a bit tricky to set up. Make sure you've followed the step to
119 | step guide in 'offlineimap.conf'. The known bugs about Gmail are tracked at
120 | .
121 | +
122 | Sometimes, you might hit one of the following error:
123 |
124 | - [imap]: xoauth2handler: response "{u'error': u'invalid_grant'}"
125 | - oauth2handler got: {u'error': u'invalid_grant'}
126 |
127 | +
128 | In such case, we had reports that generating a new refresh token from the same
129 | client ID and secret can help.
130 | +
131 | .Google documentation on "invalid_grant"
132 | ----
133 | When you try to use a refresh token, the following returns you an
134 | invalid_grant error:
135 |
136 | - Your server's clock is not in sync with network time protocol - NTP.
137 | - The refresh token limit has been exceeded.
138 | ----
139 | +
140 | .Token expiration
141 | ----
142 | It is possible that a granted token might no longer work. A token might stop
143 | working for one of these reasons:
144 |
145 | - The user has revoked access.
146 | - The token has not been used for six months.
147 | - The user changed passwords and the token contains Gmail scopes.
148 | - The user account has exceeded a certain number of token requests.
149 |
150 | There is currently a limit of 50 refresh tokens per user account per client. If
151 | the limit is reached, creating a new token automatically invalidates the oldest
152 | token without warning. This limit does not apply to service accounts.
153 | ----
154 | +
155 | See
156 | and
157 | to know more.
158 |
159 | * "does not have message with UID" with Microsoft servers
160 | +
161 | `ERROR: IMAP server 'Server ### Remote' does not have a message with UID 'xxx'`
162 | +
163 | Microsoft IMAP servers are not compliant with the RFC. It is currently required
164 | to folderfilter some faulting folders. See
165 | http://www.offlineimap.org/doc/FAQ.html#exchange-and-office365 for a detailed
166 | list.
167 |
168 |
--------------------------------------------------------------------------------
/docs/doc-src/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # pyDNS documentation build configuration file, created by
4 | # sphinx-quickstart on Tue Feb 2 10:00:47 2010.
5 | #
6 | # This file is execfile()d with the current directory set to its containing dir.
7 | #
8 | # Note that not all possible configuration values are present in this
9 | # autogenerated file.
10 | #
11 | # All configuration values have a default; values that are commented out
12 | # serve to show the default.
13 |
14 | import sys, os
15 |
16 | # If extensions (or modules to document with autodoc) are in another directory,
17 | # add these directories to sys.path here. If the directory is relative to the
18 | # documentation root, use os.path.abspath to make it absolute, like shown here.
19 | sys.path.insert(0, os.path.abspath('../..'))
20 |
21 | from offlineimap import __version__, __author__, __copyright__
22 | # -- General configuration -----------------------------------------------------
23 |
24 | # Add any Sphinx extension module names here, as strings. They can be extensions
25 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
26 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest',
27 | 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.viewcode']
28 | autoclass_content = "both"
29 |
30 | # Add any paths that contain templates here, relative to this directory.
31 | templates_path = ['_templates']
32 |
33 | # The suffix of source filenames.
34 | source_suffix = '.rst'
35 |
36 | # The encoding of source files.
37 | #source_encoding = 'utf-8'
38 |
39 | # The master toctree document.
40 | master_doc = 'index'
41 |
42 | # General information about the project.
43 | project = u'OfflineIMAP'
44 | copyright = __copyright__
45 |
46 | # The version info for the project you're documenting, acts as replacement for
47 | # |version| and |release|, also used in various other places throughout the
48 | # built documents.
49 | #
50 | # The short X.Y version.
51 | version = __version__
52 | # The full version, including alpha/beta/rc tags.
53 | release = __version__
54 |
55 | # The language for content autogenerated by Sphinx. Refer to documentation
56 | # for a list of supported languages.
57 | #language = None
58 |
59 | # There are two options for replacing |today|: either, you set today to some
60 | # non-false value, then it is used:
61 | #today = ''
62 | # Else, today_fmt is used as the format for a strftime call.
63 | #today_fmt = '%B %d, %Y'
64 |
65 | # List of documents that shouldn't be included in the build.
66 | #unused_docs = []
67 |
68 | # List of directories, relative to source directory, that shouldn't be searched
69 | # for source files.
70 | exclude_trees = []
71 |
72 | # The reST default role (used for this markup: `text`) to use for all documents.
73 | #default_role = None
74 |
75 | # If true, '()' will be appended to :func: etc. cross-reference text.
76 | #add_function_parentheses = True
77 |
78 | # If true, the current module name will be prepended to all description
79 | # unit titles (such as .. function::).
80 | add_module_names = False
81 |
82 | # If true, sectionauthor and moduleauthor directives will be shown in the
83 | # output. They are ignored by default.
84 | #show_authors = False
85 |
86 | # The name of the Pygments (syntax highlighting) style to use.
87 | pygments_style = 'sphinx'
88 |
89 | # A list of ignored prefixes for module index sorting.
90 | #modindex_common_prefix = []
91 |
92 |
93 | # -- Options for HTML output ---------------------------------------------------
94 |
95 | # The theme to use for HTML and HTML Help pages. Major themes that come with
96 | # Sphinx are currently 'default' and 'sphinxdoc'.
97 | html_theme = 'default'
98 | #html_style = ''
99 | # Theme options are theme-specific and customize the look and feel of a theme
100 | # further. For a list of options available for each theme, see the
101 | # documentation.
102 | #html_theme_options = {}
103 |
104 | # Add any paths that contain custom themes here, relative to this directory.
105 | #html_theme_path = []
106 |
107 | # The name for this set of Sphinx documents. If None, it defaults to
108 | # " v documentation".
109 | #html_title = None
110 |
111 | # A shorter title for the navigation bar. Default is the same as html_title.
112 | #html_short_title = None
113 |
114 | # The name of an image file (relative to this directory) to place at the top
115 | # of the sidebar.
116 | #html_logo = None
117 |
118 | # The name of an image file (within the static path) to use as favicon of the
119 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
120 | # pixels large.
121 | #html_favicon = None
122 |
123 | # Add any paths that contain custom static files (such as style sheets) here,
124 | # relative to this directory. They are copied after the builtin static files,
125 | # so a file named "default.css" will overwrite the builtin "default.css".
126 | #html_static_path = ['html']
127 |
128 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
129 | # using the given strftime format.
130 | #html_last_updated_fmt = '%b %d, %Y'
131 |
132 | # If true, SmartyPants will be used to convert quotes and dashes to
133 | # typographically correct entities.
134 | #html_use_smartypants = True
135 |
136 | # Custom sidebar templates, maps document names to template names.
137 | #html_sidebars = {}
138 |
139 | # Additional templates that should be rendered to pages, maps page names to
140 | # template names.
141 | #html_additional_pages = {}
142 |
143 | # If false, no module index is generated.
144 | html_use_modindex = False
145 |
146 | # If false, no index is generated.
147 | #html_use_index = True
148 |
149 | # If true, the index is split into individual pages for each letter.
150 | #html_split_index = False
151 |
152 | # If true, links to the reST sources are added to the pages.
153 | #html_show_sourcelink = True
154 |
155 | # If true, an OpenSearch description file will be output, and all pages will
156 | # contain a tag referring to it. The value of this option must be the
157 | # base URL from which the finished HTML is served.
158 | #html_use_opensearch = ''
159 |
160 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
161 | #html_file_suffix = ''
162 |
163 | # Output file base name for HTML help builder.
164 | htmlhelp_basename = 'dev-doc'
165 |
166 |
167 | # -- Options for LaTeX output --------------------------------------------------
168 |
169 | # The paper size ('letter' or 'a4').
170 | #latex_paper_size = 'letter'
171 |
172 | # The font size ('10pt', '11pt' or '12pt').
173 | #latex_font_size = '10pt'
174 |
175 | # Grouping the document tree into LaTeX files. List of tuples
176 | # (source start file, target name, title, author, documentclass [howto/manual]).
177 | latex_documents = [
178 | ('index', 'offlineimap.tex', u'OfflineIMAP Documentation',
179 | u'OfflineIMAP contributors', 'manual'),
180 | ]
181 |
182 | # The name of an image file (relative to this directory) to place at the top of
183 | # the title page.
184 | #latex_logo = None
185 |
186 | # For "manual" documents, if this is true, then toplevel headings are parts,
187 | # not chapters.
188 | #latex_use_parts = False
189 |
190 | # Additional stuff for the LaTeX preamble.
191 | #latex_preamble = ''
192 |
193 | # Documents to append as an appendix to all manuals.
194 | #latex_appendices = []
195 |
196 | # If false, no module index is generated.
197 | #latex_use_modindex = True
198 |
199 |
200 | # Example configuration for intersphinx: refer to the Python standard library.
201 | intersphinx_mapping = {'http://docs.python.org/': None}
202 |
--------------------------------------------------------------------------------
/test/tests/test_01_basic.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2012- Sebastian Spaeth & contributors
2 | #
3 | # This program is free software; you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation; either version 2 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program; if not, write to the Free Software
15 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16 | import random
17 | import unittest
18 | import logging
19 | import os, sys
20 | from test.OLItest import OLITestLib
21 |
22 | # Things need to be setup first, usually setup.py initializes everything.
23 | # but if e.g. called from command line, we take care of default values here:
24 | if not OLITestLib.cred_file:
25 | OLITestLib(cred_file='./test/credentials.conf', cmd='./offlineimap.py')
26 |
27 |
28 | def setUpModule():
29 | logging.info("Set Up test module %s" % __name__)
30 | tdir = OLITestLib.create_test_dir(suffix=__name__)
31 |
32 | def tearDownModule():
33 | logging.info("Tear Down test module")
34 | OLITestLib.delete_test_dir()
35 |
36 | #Stuff that can be used
37 | #self.assertEqual(self.seq, range(10))
38 | # should raise an exception for an immutable sequence
39 | #self.assertRaises(TypeError, random.shuffle, (1,2,3))
40 | #self.assertTrue(element in self.seq)
41 | #self.assertFalse(element in self.seq)
42 |
43 | class TestBasicFunctions(unittest.TestCase):
44 | def setUp(self):
45 | OLITestLib.delete_remote_testfolders()
46 |
47 | def tearDown(self):
48 | OLITestLib.delete_remote_testfolders()
49 |
50 | def test_01_olistartup(self):
51 | """Tests if OLI can be invoked without exceptions
52 |
53 | Cleans existing remote test folders. Then syncs all "OLItest*
54 | (specified in the default config) to our local Maildir. The
55 | result should be 0 folders and 0 mails."""
56 | code, res = OLITestLib.run_OLI()
57 | self.assertEqual(res, "")
58 | boxes, mails = OLITestLib.count_maildir_mails('')
59 | self.assertTrue((boxes, mails)==(0,0), msg="Expected 0 folders and 0 "
60 | "mails, but sync led to {0} folders and {1} mails".format(
61 | boxes, mails))
62 |
63 | def test_02_createdir(self):
64 | """Create local 'OLItest 1', sync"""
65 | OLITestLib.delete_maildir('') #Delete all local maildir folders
66 | OLITestLib.create_maildir('INBOX.OLItest 1')
67 | code, res = OLITestLib.run_OLI()
68 | self.assertEqual(res, "")
69 | boxes, mails = OLITestLib.count_maildir_mails('')
70 | self.assertTrue((boxes, mails)==(1,0), msg="Expected 1 folders and 0 "
71 | "mails, but sync led to {0} folders and {1} mails".format(
72 | boxes, mails))
73 |
74 | def test_03_createdir_quote(self):
75 | """Create local 'OLItest "1"' maildir, sync
76 |
77 | Folder names with quotes used to fail and have been fixed, so
78 | one is included here as a small challenge."""
79 | OLITestLib.delete_maildir('') #Delete all local maildir folders
80 | OLITestLib.create_maildir('INBOX.OLItest "1"')
81 | code, res = OLITestLib.run_OLI()
82 | if 'unallowed folder' in res:
83 | raise unittest.SkipTest("remote server doesn't handle quote")
84 | self.assertEqual(res, "")
85 | boxes, mails = OLITestLib.count_maildir_mails('')
86 | self.assertTrue((boxes, mails)==(1,0), msg="Expected 1 folders and 0 "
87 | "mails, but sync led to {0} folders and {1} mails".format(
88 | boxes, mails))
89 |
90 | def test_04_nametransmismatch(self):
91 | """Create mismatching remote and local nametrans rules
92 |
93 | This should raise an error."""
94 | config = OLITestLib.get_default_config()
95 | config.set('Repository IMAP', 'nametrans',
96 | 'lambda f: f' )
97 | config.set('Repository Maildir', 'nametrans',
98 | 'lambda f: f + "moo"' )
99 | OLITestLib.write_config_file(config)
100 | code, res = OLITestLib.run_OLI()
101 | #logging.warn("%s %s "% (code, res))
102 | # We expect an INFINITE FOLDER CREATION WARNING HERE....
103 | mismatch = "ERROR: INFINITE FOLDER CREATION DETECTED!" in res
104 | self.assertEqual(mismatch, True, msg="Mismatching nametrans rules did "
105 | "NOT trigger an 'infinite folder generation' error. Output was:\n"
106 | "{0}".format(res))
107 | # Write out default config file again
108 | OLITestLib.write_config_file()
109 |
110 |
111 | def test_05_createmail(self):
112 | """Create mail in OLItest 1, sync, wipe folder sync
113 |
114 | Currently, this will mean the folder will be recreated
115 | locally. At some point when remote folder deletion is
116 | implemented, this behavior will change."""
117 | OLITestLib.delete_remote_testfolders()
118 | OLITestLib.delete_maildir('') #Delete all local maildir folders
119 | OLITestLib.create_maildir('INBOX.OLItest')
120 | OLITestLib.create_mail('INBOX.OLItest')
121 | code, res = OLITestLib.run_OLI()
122 | #logging.warn("%s %s "% (code, res))
123 | self.assertEqual(res, "")
124 | boxes, mails = OLITestLib.count_maildir_mails('')
125 | self.assertTrue((boxes, mails)==(1,1), msg="Expected 1 folders and 1 "
126 | "mails, but sync led to {0} folders and {1} mails".format(
127 | boxes, mails))
128 | # The local Mail should have been assigned a proper UID now, check!
129 | uids = OLITestLib.get_maildir_uids('INBOX.OLItest')
130 | self.assertFalse (None in uids, msg = "All mails should have been "+ \
131 | "assigned the IMAP's UID number, but {0} messages had no valid ID "\
132 | .format(len([None for x in uids if x==None])))
133 |
134 | def test_06_createfolders(self):
135 | """Test if createfolders works as expected
136 |
137 | Create a local Maildir, then sync with remote "createfolders"
138 | disabled. Delete local Maildir and sync. We should have no new
139 | local maildir then. TODO: Rewrite this test to directly test
140 | and count the remote folders when the helper functions have
141 | been written"""
142 | config = OLITestLib.get_default_config()
143 | config.set('Repository IMAP', 'createfolders',
144 | 'False' )
145 | OLITestLib.write_config_file(config)
146 |
147 | # delete all remote and local testfolders
148 | OLITestLib.delete_remote_testfolders()
149 | OLITestLib.delete_maildir('')
150 | OLITestLib.create_maildir('INBOX.OLItest')
151 | code, res = OLITestLib.run_OLI()
152 | #logging.warn("%s %s "% (code, res))
153 | self.assertEqual(res, "")
154 | OLITestLib.delete_maildir('INBOX.OLItest')
155 | code, res = OLITestLib.run_OLI()
156 | self.assertEqual(res, "")
157 | boxes, mails = OLITestLib.count_maildir_mails('')
158 | self.assertTrue((boxes, mails)==(0,0), msg="Expected 0 folders and 0 "
159 | "mails, but sync led to {} folders and {} mails".format(
160 | boxes, mails))
161 |
--------------------------------------------------------------------------------
/docs/rfcs/rfc2177.IMAP4_IDLE_command.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Network Working Group B. Leiba
8 | Request for Comments: 2177 IBM T.J. Watson Research Center
9 | Category: Standards Track June 1997
10 |
11 |
12 | IMAP4 IDLE command
13 |
14 | Status of this Memo
15 |
16 | This document specifies an Internet standards track protocol for the
17 | Internet community, and requests discussion and suggestions for
18 | improvements. Please refer to the current edition of the "Internet
19 | Official Protocol Standards" (STD 1) for the standardization state
20 | and status of this protocol. Distribution of this memo is unlimited.
21 |
22 | 1. Abstract
23 |
24 | The Internet Message Access Protocol [IMAP4] requires a client to
25 | poll the server for changes to the selected mailbox (new mail,
26 | deletions). It's often more desirable to have the server transmit
27 | updates to the client in real time. This allows a user to see new
28 | mail immediately. It also helps some real-time applications based on
29 | IMAP, which might otherwise need to poll extremely often (such as
30 | every few seconds). (While the spec actually does allow a server to
31 | push EXISTS responses aysynchronously, a client can't expect this
32 | behaviour and must poll.)
33 |
34 | This document specifies the syntax of an IDLE command, which will
35 | allow a client to tell the server that it's ready to accept such
36 | real-time updates.
37 |
38 | 2. Conventions Used in this Document
39 |
40 | In examples, "C:" and "S:" indicate lines sent by the client and
41 | server respectively.
42 |
43 | The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", and "MAY"
44 | in this document are to be interpreted as described in RFC 2060
45 | [IMAP4].
46 |
47 | 3. Specification
48 |
49 | IDLE Command
50 |
51 | Arguments: none
52 |
53 | Responses: continuation data will be requested; the client sends
54 | the continuation data "DONE" to end the command
55 |
56 |
57 |
58 | Leiba Standards Track [Page 1]
59 |
60 | RFC 2177 IMAP4 IDLE command June 1997
61 |
62 |
63 |
64 | Result: OK - IDLE completed after client sent "DONE"
65 | NO - failure: the server will not allow the IDLE
66 | command at this time
67 | BAD - command unknown or arguments invalid
68 |
69 | The IDLE command may be used with any IMAP4 server implementation
70 | that returns "IDLE" as one of the supported capabilities to the
71 | CAPABILITY command. If the server does not advertise the IDLE
72 | capability, the client MUST NOT use the IDLE command and must poll
73 | for mailbox updates. In particular, the client MUST continue to be
74 | able to accept unsolicited untagged responses to ANY command, as
75 | specified in the base IMAP specification.
76 |
77 | The IDLE command is sent from the client to the server when the
78 | client is ready to accept unsolicited mailbox update messages. The
79 | server requests a response to the IDLE command using the continuation
80 | ("+") response. The IDLE command remains active until the client
81 | responds to the continuation, and as long as an IDLE command is
82 | active, the server is now free to send untagged EXISTS, EXPUNGE, and
83 | other messages at any time.
84 |
85 | The IDLE command is terminated by the receipt of a "DONE"
86 | continuation from the client; such response satisfies the server's
87 | continuation request. At that point, the server MAY send any
88 | remaining queued untagged responses and then MUST immediately send
89 | the tagged response to the IDLE command and prepare to process other
90 | commands. As in the base specification, the processing of any new
91 | command may cause the sending of unsolicited untagged responses,
92 | subject to the ambiguity limitations. The client MUST NOT send a
93 | command while the server is waiting for the DONE, since the server
94 | will not be able to distinguish a command from a continuation.
95 |
96 | The server MAY consider a client inactive if it has an IDLE command
97 | running, and if such a server has an inactivity timeout it MAY log
98 | the client off implicitly at the end of its timeout period. Because
99 | of that, clients using IDLE are advised to terminate the IDLE and
100 | re-issue it at least every 29 minutes to avoid being logged off.
101 | This still allows a client to receive immediate mailbox updates even
102 | though it need only "poll" at half hour intervals.
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | Leiba Standards Track [Page 2]
115 |
116 | RFC 2177 IMAP4 IDLE command June 1997
117 |
118 |
119 | Example: C: A001 SELECT INBOX
120 | S: * FLAGS (Deleted Seen)
121 | S: * 3 EXISTS
122 | S: * 0 RECENT
123 | S: * OK [UIDVALIDITY 1]
124 | S: A001 OK SELECT completed
125 | C: A002 IDLE
126 | S: + idling
127 | ...time passes; new mail arrives...
128 | S: * 4 EXISTS
129 | C: DONE
130 | S: A002 OK IDLE terminated
131 | ...another client expunges message 2 now...
132 | C: A003 FETCH 4 ALL
133 | S: * 4 FETCH (...)
134 | S: A003 OK FETCH completed
135 | C: A004 IDLE
136 | S: * 2 EXPUNGE
137 | S: * 3 EXISTS
138 | S: + idling
139 | ...time passes; another client expunges message 3...
140 | S: * 3 EXPUNGE
141 | S: * 2 EXISTS
142 | ...time passes; new mail arrives...
143 | S: * 3 EXISTS
144 | C: DONE
145 | S: A004 OK IDLE terminated
146 | C: A005 FETCH 3 ALL
147 | S: * 3 FETCH (...)
148 | S: A005 OK FETCH completed
149 | C: A006 IDLE
150 |
151 | 4. Formal Syntax
152 |
153 | The following syntax specification uses the augmented Backus-Naur
154 | Form (BNF) notation as specified in [RFC-822] as modified by [IMAP4].
155 | Non-terminals referenced but not defined below are as defined by
156 | [IMAP4].
157 |
158 | command_auth ::= append / create / delete / examine / list / lsub /
159 | rename / select / status / subscribe / unsubscribe
160 | / idle
161 | ;; Valid only in Authenticated or Selected state
162 |
163 | idle ::= "IDLE" CRLF "DONE"
164 |
165 |
166 |
167 |
168 |
169 |
170 | Leiba Standards Track [Page 3]
171 |
172 | RFC 2177 IMAP4 IDLE command June 1997
173 |
174 |
175 | 5. References
176 |
177 | [IMAP4] Crispin, M., "Internet Message Access Protocol - Version
178 | 4rev1", RFC 2060, December 1996.
179 |
180 | 6. Security Considerations
181 |
182 | There are no known security issues with this extension.
183 |
184 | 7. Author's Address
185 |
186 | Barry Leiba
187 | IBM T.J. Watson Research Center
188 | 30 Saw Mill River Road
189 | Hawthorne, NY 10532
190 |
191 | Email: leiba@watson.ibm.com
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 | Leiba Standards Track [Page 4]
227 |
228 |
--------------------------------------------------------------------------------
/offlineimap/ui/Machine.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2007-2018 John Goerzen & contributors.
2 | #
3 | # This program is free software; you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation; either version 2 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program; if not, write to the Free Software
15 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16 | try:
17 | from urllib import urlencode
18 | except ImportError: # python3
19 | from urllib.parse import urlencode
20 | import sys
21 | import time
22 | import logging
23 | from threading import currentThread
24 |
25 | import offlineimap
26 | from offlineimap.ui.UIBase import UIBase
27 |
28 | protocol = '7.2.0'
29 |
30 | class MachineLogFormatter(logging.Formatter):
31 | """urlencodes any outputted line, to avoid multi-line output"""
32 |
33 | def format(s, record):
34 | # Mapping of log levels to historic tag names
35 | severity_map = {
36 | 'info': 'msg',
37 | 'warning': 'warn',
38 | }
39 | line = super(MachineLogFormatter, s).format(record)
40 | severity = record.levelname.lower()
41 | if severity in severity_map:
42 | severity = severity_map[severity]
43 | if hasattr(record, "machineui"):
44 | command = record.machineui["command"]
45 | whoami = record.machineui["id"]
46 | else:
47 | command = ""
48 | whoami = currentThread().getName()
49 |
50 | prefix = "%s:%s"% (command, urlencode([('', whoami)])[1:])
51 | return "%s:%s:%s"% (severity, prefix, urlencode([('', line)])[1:])
52 |
53 |
54 | class MachineUI(UIBase):
55 | def __init__(s, config, loglevel=logging.INFO):
56 | super(MachineUI, s).__init__(config, loglevel)
57 | s._log_con_handler.createLock()
58 | """lock needed to block on password input"""
59 | # Set up the formatter that urlencodes the strings...
60 | s._log_con_handler.setFormatter(MachineLogFormatter())
61 |
62 | # Arguments:
63 | # - handler: must be method from s.logger that reflects
64 | # the severity of the passed message
65 | # - command: command that produced this message
66 | # - msg: the message itself
67 | def _printData(s, handler, command, msg):
68 | handler(msg,
69 | extra = {
70 | 'machineui': {
71 | 'command': command,
72 | 'id': currentThread().getName(),
73 | }
74 | })
75 |
76 | def _msg(s, msg):
77 | s._printData(s.logger.info, '_display', msg)
78 |
79 | def warn(s, msg, minor=0):
80 | # TODO, remove and cleanup the unused minor stuff
81 | s._printData(s.logger.warning, '', msg)
82 |
83 | def registerthread(s, account):
84 | super(MachineUI, s).registerthread(account)
85 | s._printData(s.logger.info, 'registerthread', account)
86 |
87 | def unregisterthread(s, thread):
88 | UIBase.unregisterthread(s, thread)
89 | s._printData(s.logger.info, 'unregisterthread', thread.getName())
90 |
91 | def debugging(s, debugtype):
92 | s._printData(s.logger.debug, 'debugging', debugtype)
93 |
94 | def acct(s, accountname):
95 | s._printData(s.logger.info, 'acct', accountname)
96 |
97 | def acctdone(s, accountname):
98 | s._printData(s.logger.info, 'acctdone', accountname)
99 |
100 | def validityproblem(s, folder):
101 | s._printData(s.logger.warning, 'validityproblem', "%s\n%s\n%s\n%s"%
102 | (folder.getname(), folder.getrepository().getname(),
103 | folder.get_saveduidvalidity(), folder.get_uidvalidity()))
104 |
105 | def connecting(s, reposname, hostname, port):
106 | s._printData(s.logger.info, 'connecting', "%s\n%s\n%s"% (hostname,
107 | str(port), reposname))
108 |
109 | def syncfolders(s, srcrepos, destrepos):
110 | s._printData(s.logger.info, 'syncfolders', "%s\n%s"% (s.getnicename(srcrepos),
111 | s.getnicename(destrepos)))
112 |
113 | def syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder):
114 | s._printData(s.logger.info, 'syncingfolder', "%s\n%s\n%s\n%s\n"%
115 | (s.getnicename(srcrepos), srcfolder.getname(),
116 | s.getnicename(destrepos), destfolder.getname()))
117 |
118 | def loadmessagelist(s, repos, folder):
119 | s._printData(s.logger.info, 'loadmessagelist', "%s\n%s"% (s.getnicename(repos),
120 | folder.getvisiblename()))
121 |
122 | def messagelistloaded(s, repos, folder, count):
123 | s._printData(s.logger.info, 'messagelistloaded', "%s\n%s\n%d"%
124 | (s.getnicename(repos), folder.getname(), count))
125 |
126 | def syncingmessages(s, sr, sf, dr, df):
127 | s._printData(s.logger.info, 'syncingmessages', "%s\n%s\n%s\n%s\n"%
128 | (s.getnicename(sr), sf.getname(), s.getnicename(dr),
129 | df.getname()))
130 |
131 | def ignorecopyingmessage(s, uid, srcfolder, destfolder):
132 | s._printData(s.logger.info, 'ignorecopyingmessage', "%d\n%s\n%s\n%s[%s]"%
133 | (uid, s.getnicename(srcfolder), srcfolder.getname(),
134 | s.getnicename(destfolder), destfolder))
135 |
136 | def copyingmessage(s, uid, num, num_to_copy, srcfolder, destfolder):
137 | s._printData(s.logger.info, 'copyingmessage', "%d\n%s\n%s\n%s[%s]"%
138 | (uid, s.getnicename(srcfolder), srcfolder.getname(),
139 | s.getnicename(destfolder), destfolder))
140 |
141 | def folderlist(s, list):
142 | return ("\f".join(["%s\t%s"% (s.getnicename(x), x.getname()) for x in list]))
143 |
144 | def uidlist(s, list):
145 | return ("\f".join([str(u) for u in list]))
146 |
147 | def deletingmessages(s, uidlist, destlist):
148 | ds = s.folderlist(destlist)
149 | s._printData(s.logger.info, 'deletingmessages', "%s\n%s"% (s.uidlist(uidlist), ds))
150 |
151 | def addingflags(s, uidlist, flags, dest):
152 | s._printData(s.logger.info, "addingflags", "%s\n%s\n%s"% (s.uidlist(uidlist),
153 | "\f".join(flags),
154 | dest))
155 |
156 | def deletingflags(s, uidlist, flags, dest):
157 | s._printData(s.logger.info, 'deletingflags', "%s\n%s\n%s"% (s.uidlist(uidlist),
158 | "\f".join(flags),
159 | dest))
160 |
161 | def threadException(s, thread):
162 | s._printData(s.logger.warning, 'threadException', "%s\n%s"%
163 | (thread.getName(), s.getThreadExceptionString(thread)))
164 | s.delThreadDebugLog(thread)
165 | s.terminate(100)
166 |
167 | def terminate(s, exitstatus=0, errortitle='', errormsg=''):
168 | s._printData(s.logger.info, 'terminate', "%d\n%s\n%s"% (exitstatus, errortitle, errormsg))
169 | sys.exit(exitstatus)
170 |
171 | def mainException(s):
172 | s._printData(s.logger.warning, 'mainException', s.getMainExceptionString())
173 |
174 | def threadExited(s, thread):
175 | s._printData(s.logger.info, 'threadExited', thread.getName())
176 | UIBase.threadExited(s, thread)
177 |
178 | def sleeping(s, sleepsecs, remainingsecs):
179 | s._printData(s.logger.info, 'sleeping', "%d\n%d"% (sleepsecs, remainingsecs))
180 | if sleepsecs > 0:
181 | time.sleep(sleepsecs)
182 | return 0
183 |
184 |
185 | def getpass(s, username, config, errmsg=None):
186 | if errmsg:
187 | s._printData(s.logger.warning,
188 | 'getpasserror', "%s\n%s"% (username, errmsg),
189 | False)
190 |
191 | s._log_con_handler.acquire() # lock the console output
192 | try:
193 | s._printData(s.logger.info, 'getpass', username)
194 | return (sys.stdin.readline()[:-1])
195 | finally:
196 | s._log_con_handler.release()
197 |
198 | def init_banner(s):
199 | s._printData(s.logger.info, 'protocol', protocol)
200 | s._printData(s.logger.info, 'initbanner', offlineimap.banner)
201 |
202 | def callhook(s, msg):
203 | s._printData(s.logger.info, 'callhook', msg)
204 |
--------------------------------------------------------------------------------
/offlineimap/threadutil.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2002-2016 John Goerzen & contributors
2 | # Thread support module
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 | from threading import Lock, Thread, BoundedSemaphore
19 | try:
20 | from Queue import Queue, Empty
21 | except ImportError: # python3
22 | from queue import Queue, Empty
23 | import traceback
24 | from offlineimap.ui import getglobalui
25 |
26 |
27 | STOP_MONITOR = 'STOP_MONITOR'
28 |
29 | ######################################################################
30 | # General utilities
31 | ######################################################################
32 |
33 | def semaphorereset(semaphore, originalstate):
34 | """Block until `semaphore` gets back to its original state, ie all acquired
35 | resources have been released."""
36 |
37 | for i in range(originalstate):
38 | semaphore.acquire()
39 | # Now release these.
40 | for i in range(originalstate):
41 | semaphore.release()
42 |
43 | class accountThreads(object):
44 | """Store the list of all threads in the software so it can be used to find out
45 | what's running and what's not."""
46 |
47 | def __init__(self):
48 | self.lock = Lock()
49 | self.list = []
50 |
51 | def add(self, thread):
52 | with self.lock:
53 | self.list.append(thread)
54 |
55 | def remove(self, thread):
56 | with self.lock:
57 | self.list.remove(thread)
58 |
59 | def pop(self):
60 | with self.lock:
61 | if len(self.list) < 1:
62 | return None
63 | return self.list.pop()
64 |
65 | def wait(self):
66 | while True:
67 | thread = self.pop()
68 | if thread is None:
69 | break
70 | thread.join()
71 |
72 |
73 | ######################################################################
74 | # Exit-notify threads
75 | ######################################################################
76 |
77 | exitedThreads = Queue()
78 |
79 | def monitor():
80 | """An infinite "monitoring" loop watching for finished ExitNotifyThread's.
81 |
82 | This one is supposed to run in the main thread.
83 | :param callback: the function to call when a thread terminated. That
84 | function is called with a single argument -- the
85 | ExitNotifyThread that has terminated. The monitor will
86 | not continue to monitor for other threads until
87 | 'callback' returns, so if it intends to perform long
88 | calculations, it should start a new thread itself -- but
89 | NOT an ExitNotifyThread, or else an infinite loop
90 | may result.
91 | Furthermore, the monitor will hold the lock all the
92 | while the other thread is waiting.
93 | :type callback: a callable function
94 | """
95 |
96 | global exitedThreads
97 | ui = getglobalui()
98 |
99 | while True:
100 | # Loop forever and call 'callback' for each thread that exited
101 | try:
102 | # We need a timeout in the get() call, so that ctrl-c can throw a
103 | # SIGINT (http://bugs.python.org/issue1360). A timeout with empty
104 | # Queue will raise `Empty`.
105 | #
106 | # ExitNotifyThread add themselves to the exitedThreads queue once
107 | # they are done (normally or with exception).
108 | thread = exitedThreads.get(True, 60)
109 | # Request to abort when callback returns True.
110 |
111 | if thread.exit_exception is not None:
112 | if isinstance(thread.exit_exception, SystemExit):
113 | # Bring a SystemExit into the main thread.
114 | # Do not send it back to UI layer right now.
115 | # Maybe later send it to ui.terminate?
116 | raise SystemExit
117 | ui.threadException(thread) # Expected to terminate the program.
118 | # Should never hit this line.
119 | raise AssertionError("thread has 'exit_exception' set to"
120 | " '%s' [%s] but this value is unexpected"
121 | " and the ui did not stop the program."%
122 | (repr(thread.exit_exception), type(thread.exit_exception)))
123 |
124 | # Only the monitor thread has this exit message set.
125 | elif thread.exit_message == STOP_MONITOR:
126 | break # Exit the loop here.
127 | else:
128 | ui.threadExited(thread)
129 | except Empty:
130 | pass
131 |
132 |
133 | class ExitNotifyThread(Thread):
134 | """This class is designed to alert a "monitor" to the fact that a
135 | thread has exited and to provide for the ability for it to find out
136 | why. All instances are made daemon threads (setDaemon(True), so we
137 | bail out when the mainloop dies.
138 |
139 | The thread can set instance variables self.exit_message for a human
140 | readable reason of the thread exit.
141 |
142 | There is one instance of this class at runtime. The main thread waits for
143 | the monitor to end."""
144 |
145 | def __init__(self, *args, **kwargs):
146 | super(ExitNotifyThread, self).__init__(*args, **kwargs)
147 | # These are all child threads that are supposed to go away when
148 | # the main thread is killed.
149 | self.setDaemon(True)
150 | self.exit_message = None
151 | self._exit_exc = None
152 | self._exit_stacktrace = None
153 |
154 | def run(self):
155 | """Allow profiling of a run and store exceptions."""
156 |
157 | global exitedThreads
158 | try:
159 | Thread.run(self)
160 | except Exception as e:
161 | # Thread exited with Exception, store it
162 | tb = traceback.format_exc()
163 | self.set_exit_exception(e, tb)
164 |
165 | exitedThreads.put(self, True)
166 |
167 | def set_exit_exception(self, exc, st=None):
168 | """Sets Exception and stacktrace of a thread, so that other
169 | threads can query its exit status"""
170 |
171 | self._exit_exc = exc
172 | self._exit_stacktrace = st
173 |
174 | @property
175 | def exit_exception(self):
176 | """Returns the cause of the exit, one of:
177 | Exception() -- the thread aborted with this exception
178 | None -- normal termination."""
179 |
180 | return self._exit_exc
181 |
182 | @property
183 | def exit_stacktrace(self):
184 | """Returns a string representing the stack trace if set"""
185 |
186 | return self._exit_stacktrace
187 |
188 |
189 | ######################################################################
190 | # Instance-limited threads
191 | ######################################################################
192 |
193 | limitedNamespaces = {}
194 |
195 | def initInstanceLimit(limitNamespace, instancemax):
196 | """Initialize the instance-limited thread implementation.
197 |
198 | Run up to intancemax threads for the given limitNamespace. This allows to
199 | honor maxsyncaccounts and maxconnections."""
200 |
201 | global limitedNamespaces
202 |
203 | if not limitNamespace in limitedNamespaces:
204 | limitedNamespaces[limitNamespace] = BoundedSemaphore(instancemax)
205 |
206 |
207 | class InstanceLimitedThread(ExitNotifyThread):
208 | def __init__(self, limitNamespace, *args, **kwargs):
209 | self.limitNamespace = limitNamespace
210 | super(InstanceLimitedThread, self).__init__(*args, **kwargs)
211 |
212 | def start(self):
213 | global limitedNamespaces
214 |
215 | # Will block until the semaphore has free slots.
216 | limitedNamespaces[self.limitNamespace].acquire()
217 | ExitNotifyThread.start(self)
218 |
219 | def run(self):
220 | global limitedNamespaces
221 |
222 | try:
223 | ExitNotifyThread.run(self)
224 | finally:
225 | if limitedNamespaces and limitedNamespaces[self.limitNamespace]:
226 | limitedNamespaces[self.limitNamespace].release()
227 |
--------------------------------------------------------------------------------
/docs/rfcs/rfc3691.IMAP_UNSELECT_command.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Network Working Group A. Melnikov
8 | Request for Comments: 3691 Isode Ltd.
9 | Category: Standards Track February 2004
10 |
11 |
12 | Internet Message Access Protocol (IMAP) UNSELECT command
13 |
14 | Status of this Memo
15 |
16 | This document specifies an Internet standards track protocol for the
17 | Internet community, and requests discussion and suggestions for
18 | improvements. Please refer to the current edition of the "Internet
19 | Official Protocol Standards" (STD 1) for the standardization state
20 | and status of this protocol. Distribution of this memo is unlimited.
21 |
22 | Copyright Notice
23 |
24 | Copyright (C) The Internet Society (2004). All Rights Reserved.
25 |
26 | Abstract
27 |
28 | This document defines an UNSELECT command that can be used to close
29 | the current mailbox in an Internet Message Access Protocol - version
30 | 4 (IMAP4) session without expunging it. Certain types of IMAP
31 | clients need to release resources associated with the selected
32 | mailbox without selecting a different mailbox. While IMAP4 provides
33 | this functionality (via a SELECT command with a nonexistent mailbox
34 | name or reselecting the same mailbox with EXAMINE command), a more
35 | clean solution is desirable.
36 |
37 | Table of Contents
38 |
39 | 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 2
40 | 2. UNSELECT command . . . . . . . . . . . . . . . . . . . . . . . 2
41 | 3. Security Considerations. . . . . . . . . . . . . . . . . . . . 3
42 | 4. Formal Syntax. . . . . . . . . . . . . . . . . . . . . . . . . 3
43 | 5. IANA Considerations. . . . . . . . . . . . . . . . . . . . . . 3
44 | 6. Acknowledgments. . . . . . . . . . . . . . . . . . . . . . . . 3
45 | 7. Normative References . . . . . . . . . . . . . . . . . . . . . 4
46 | 8. Author's Address . . . . . . . . . . . . . . . . . . . . . . . 4
47 | 9. Full Copyright Statement . . . . . . . . . . . . . . . . . . . 5
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | Melnikov Standards Track [Page 1]
59 |
60 | RFC 3691 IMAP UNSELECT command February 2004
61 |
62 |
63 | 1. Introduction
64 |
65 | Certain types of IMAP clients need to release resources associated
66 | with the selected mailbox without selecting a different mailbox.
67 | While [IMAP4] provides this functionality (via a SELECT command with
68 | a nonexistent mailbox name or reselecting the same mailbox with
69 | EXAMINE command), a more clean solution is desirable.
70 |
71 | [IMAP4] defines the CLOSE command that closes the selected mailbox as
72 | well as permanently removes all messages with the \Deleted flag set.
73 |
74 | However [IMAP4] lacks a command that simply closes the mailbox
75 | without expunging it. This document defines the UNSELECT command for
76 | this purpose.
77 |
78 | A server which supports this extension indicates this with a
79 | capability name of "UNSELECT".
80 |
81 | "C:" and "S:" in examples show lines sent by the client and server
82 | respectively.
83 |
84 | The keywords "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", and "MAY" in
85 | this document when typed in uppercase are to be interpreted as
86 | defined in "Key words for use in RFCs to Indicate Requirement Levels"
87 | [KEYWORDS].
88 |
89 | 2. UNSELECT Command
90 |
91 | Arguments: none
92 |
93 | Responses: no specific responses for this command
94 |
95 | Result: OK - unselect completed, now in authenticated state
96 | BAD - no mailbox selected, or argument supplied but
97 | none permitted
98 |
99 | The UNSELECT command frees server's resources associated with the
100 | selected mailbox and returns the server to the authenticated
101 | state. This command performs the same actions as CLOSE, except
102 | that no messages are permanently removed from the currently
103 | selected mailbox.
104 |
105 | Example: C: A341 UNSELECT
106 | S: A341 OK Unselect completed
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | Melnikov Standards Track [Page 2]
115 |
116 | RFC 3691 IMAP UNSELECT command February 2004
117 |
118 |
119 | 3. Security Considerations
120 |
121 | It is believed that this extension doesn't raise any additional
122 | security concerns not already discussed in [IMAP4].
123 |
124 | 4. Formal Syntax
125 |
126 | The following syntax specification uses the Augmented Backus-Naur
127 | Form (ABNF) notation as specified in [ABNF]. Non-terminals
128 | referenced but not defined below are as defined by [IMAP4].
129 |
130 | Except as noted otherwise, all alphabetic characters are case-
131 | insensitive. The use of upper or lower case characters to define
132 | token strings is for editorial clarity only. Implementations MUST
133 | accept these strings in a case-insensitive fashion.
134 |
135 | command-select /= "UNSELECT"
136 |
137 | 5. IANA Considerations
138 |
139 | IMAP4 capabilities are registered by publishing a standards track or
140 | IESG approved experimental RFC. The registry is currently located
141 | at:
142 |
143 | http://www.iana.org/assignments/imap4-capabilities
144 |
145 | This document defines the UNSELECT IMAP capabilities. IANA has added
146 | this capability to the registry.
147 |
148 | 6. Acknowledgments
149 |
150 | UNSELECT command was originally implemented by Tim Showalter in Cyrus
151 | IMAP server.
152 |
153 | Also, the author of the document would like to thank Vladimir Butenko
154 | and Mark Crispin for reminding that UNSELECT has to be documented.
155 | Also thanks to Simon Josefsson for pointing out that there are
156 | multiple ways to implement UNSELECT.
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | Melnikov Standards Track [Page 3]
171 |
172 | RFC 3691 IMAP UNSELECT command February 2004
173 |
174 |
175 | 7. Normative References
176 |
177 | [KEYWORDS] Bradner, S., "Key words for use in RFCs to Indicate
178 | Requirement Levels", BCP 14, RFC 2119, March 1997.
179 |
180 | [IMAP4] Crispin, M., "Internet Message Access Protocol - Version
181 | 4rev1", RFC 3501, March 2003.
182 |
183 | [ABNF] Crocker, D., Ed. and P. Overell, "Augmented BNF for Syntax
184 | Specifications: ABNF", RFC 2234, November 1997.
185 |
186 | 8. Author's Address
187 |
188 | Alexey Melnikov
189 | Isode Limited
190 | 5 Castle Business Village
191 | Hampton, Middlesex TW12 2BX
192 |
193 | EMail: Alexey.Melnikov@isode.com
194 | URI: http://www.melnikov.ca/
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 | Melnikov Standards Track [Page 4]
227 |
228 | RFC 3691 IMAP UNSELECT command February 2004
229 |
230 |
231 | 9. Full Copyright Statement
232 |
233 | Copyright (C) The Internet Society (2004). This document is subject
234 | to the rights, licenses and restrictions contained in BCP 78 and
235 | except as set forth therein, the authors retain all their rights.
236 |
237 | This document and the information contained herein are provided on an
238 | "AS IS" basis and THE CONTRIBUTOR, THE ORGANIZATION HE/SHE
239 | REPRESENTS OR IS SPONSORED BY (IF ANY), THE INTERNET SOCIETY AND THE
240 | INTERNET ENGINEERING TASK FORCE DISCLAIM ALL WARRANTIES, EXPRESS OR
241 | IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF
242 | THE INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED
243 | WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
244 |
245 | Intellectual Property
246 |
247 | The IETF takes no position regarding the validity or scope of any
248 | Intellectual Property Rights or other rights that might be claimed
249 | to pertain to the implementation or use of the technology
250 | described in this document or the extent to which any license
251 | under such rights might or might not be available; nor does it
252 | represent that it has made any independent effort to identify any
253 | such rights. Information on the procedures with respect to
254 | rights in RFC documents can be found in BCP 78 and BCP 79.
255 |
256 | Copies of IPR disclosures made to the IETF Secretariat and any
257 | assurances of licenses to be made available, or the result of an
258 | attempt made to obtain a general license or permission for the use
259 | of such proprietary rights by implementers or users of this
260 | specification can be obtained from the IETF on-line IPR repository
261 | at http://www.ietf.org/ipr.
262 |
263 | The IETF invites any interested party to bring to its attention
264 | any copyrights, patents or patent applications, or other
265 | proprietary rights that may cover technology that may be required
266 | to implement this standard. Please address the information to the
267 | IETF at ietf-ipr@ietf.org.
268 |
269 | Acknowledgement
270 |
271 | Funding for the RFC Editor function is currently provided by the
272 | Internet Society.
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 | Melnikov Standards Track [Page 5]
283 |
284 |
--------------------------------------------------------------------------------
/docs/rfcs/rfc2087.IMAP4_QUOTA_extension.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Network Working Group J. Myers
8 | Request for Comments: 2087 Carnegie Mellon
9 | Category: Standards Track January 1997
10 |
11 |
12 | IMAP4 QUOTA extension
13 |
14 | Status of this Memo
15 |
16 | This document specifies an Internet standards track protocol for the
17 | Internet community, and requests discussion and suggestions for
18 | improvements. Please refer to the current edition of the "Internet
19 | Official Protocol Standards" (STD 1) for the standardization state
20 | and status of this protocol. Distribution of this memo is unlimited.
21 |
22 | 1. Abstract
23 |
24 | The QUOTA extension of the Internet Message Access Protocol [IMAP4]
25 | permits administrative limits on resource usage (quotas) to be
26 | manipulated through the IMAP protocol.
27 |
28 | Table of Contents
29 |
30 | 1. Abstract........................................... 1
31 | 2. Conventions Used in this Document.................. 1
32 | 3. Introduction and Overview.......................... 2
33 | 4. Commands........................................... 2
34 | 4.1. SETQUOTA Command................................... 2
35 | 4.2. GETQUOTA Command................................... 2
36 | 4.3. GETQUOTAROOT Command............................... 3
37 | 5. Responses.......................................... 3
38 | 5.1. QUOTA Response..................................... 3
39 | 5.2. QUOTAROOT Response................................. 4
40 | 6. Formal syntax...................................... 4
41 | 7. References......................................... 5
42 | 8. Security Considerations............................ 5
43 | 9. Author's Address................................... 5
44 |
45 |
46 | 2. Conventions Used in this Document
47 |
48 | In examples, "C:" and "S:" indicate lines sent by the client and
49 | server respectively.
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | Myers Standards Track [Page 1]
59 |
60 | RFC 2087 QUOTA January 1997
61 |
62 |
63 | 3. Introduction and Overview
64 |
65 | The QUOTA extension is present in any IMAP4 implementation which
66 | returns "QUOTA" as one of the supported capabilities to the
67 | CAPABILITY command.
68 |
69 | An IMAP4 server which supports the QUOTA capability may support
70 | limits on any number of resources. Each resource has an atom name
71 | and an implementation-defined interpretation which evaluates to an
72 | integer. Examples of such resources are:
73 |
74 | Name Interpretation
75 |
76 | STORAGE Sum of messages' RFC822.SIZE, in units of 1024 octets
77 | MESSAGE Number of messages
78 |
79 |
80 | Each mailbox has zero or more implementation-defined named "quota
81 | roots". Each quota root has zero or more resource limits. All
82 | mailboxes that share the same named quota root share the resource
83 | limits of the quota root.
84 |
85 | Quota root names do not necessarily have to match the names of
86 | existing mailboxes.
87 |
88 | 4. Commands
89 |
90 | 4.1. SETQUOTA Command
91 |
92 | Arguments: quota root
93 | list of resource limits
94 |
95 | Data: untagged responses: QUOTA
96 |
97 | Result: OK - setquota completed
98 | NO - setquota error: can't set that data
99 | BAD - command unknown or arguments invalid
100 |
101 | The SETQUOTA command takes the name of a mailbox quota root and a
102 | list of resource limits. The resource limits for the named quota root
103 | are changed to be the specified limits. Any previous resource limits
104 | for the named quota root are discarded.
105 |
106 | If the named quota root did not previously exist, an implementation
107 | may optionally create it and change the quota roots for any number of
108 | existing mailboxes in an implementation-defined manner.
109 |
110 |
111 |
112 |
113 |
114 | Myers Standards Track [Page 2]
115 |
116 | RFC 2087 QUOTA January 1997
117 |
118 |
119 | Example: C: A001 SETQUOTA "" (STORAGE 512)
120 | S: * QUOTA "" (STORAGE 10 512)
121 | S: A001 OK Setquota completed
122 |
123 | 4.2. GETQUOTA Command
124 |
125 | Arguments: quota root
126 |
127 | Data: untagged responses: QUOTA
128 |
129 | Result: OK - getquota completed
130 | NO - getquota error: no such quota root, permission
131 | denied
132 | BAD - command unknown or arguments invalid
133 |
134 | The GETQUOTA command takes the name of a quota root and returns the
135 | quota root's resource usage and limits in an untagged QUOTA response.
136 |
137 | Example: C: A003 GETQUOTA ""
138 | S: * QUOTA "" (STORAGE 10 512)
139 | S: A003 OK Getquota completed
140 |
141 | 4.3. GETQUOTAROOT Command
142 |
143 | Arguments: mailbox name
144 |
145 | Data: untagged responses: QUOTAROOT, QUOTA
146 |
147 | Result: OK - getquota completed
148 | NO - getquota error: no such mailbox, permission denied
149 | BAD - command unknown or arguments invalid
150 |
151 | The GETQUOTAROOT command takes the name of a mailbox and returns the
152 | list of quota roots for the mailbox in an untagged QUOTAROOT
153 | response. For each listed quota root, it also returns the quota
154 | root's resource usage and limits in an untagged QUOTA response.
155 |
156 | Example: C: A003 GETQUOTAROOT INBOX
157 | S: * QUOTAROOT INBOX ""
158 | S: * QUOTA "" (STORAGE 10 512)
159 | S: A003 OK Getquota completed
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | Myers Standards Track [Page 3]
171 |
172 | RFC 2087 QUOTA January 1997
173 |
174 |
175 | 5. Responses
176 |
177 | 5.1. QUOTA Response
178 |
179 | Data: quota root name
180 | list of resource names, usages, and limits
181 |
182 | This response occurs as a result of a GETQUOTA or GETQUOTAROOT
183 | command. The first string is the name of the quota root for which
184 | this quota applies.
185 |
186 | The name is followed by a S-expression format list of the resource
187 | usage and limits of the quota root. The list contains zero or
188 | more triplets. Each triplet conatins a resource name, the current
189 | usage of the resource, and the resource limit.
190 |
191 | Resources not named in the list are not limited in the quota root.
192 | Thus, an empty list means there are no administrative resource
193 | limits in the quota root.
194 |
195 | Example: S: * QUOTA "" (STORAGE 10 512)
196 |
197 | 5.2. QUOTAROOT Response
198 |
199 | Data: mailbox name
200 | zero or more quota root names
201 |
202 | This response occurs as a result of a GETQUOTAROOT command. The
203 | first string is the mailbox and the remaining strings are the
204 | names of the quota roots for the mailbox.
205 |
206 | Example: S: * QUOTAROOT INBOX ""
207 | S: * QUOTAROOT comp.mail.mime
208 |
209 | 6. Formal syntax
210 |
211 | The following syntax specification uses the augmented Backus-Naur
212 | Form (BNF) notation as specified in RFC 822 with one exception; the
213 | delimiter used with the "#" construct is a single space (SP) and not
214 | one or more commas.
215 |
216 | Except as noted otherwise, all alphabetic characters are case-
217 | insensitive. The use of upper or lower case characters to define
218 | token strings is for editorial clarity only. Implementations MUST
219 | accept these strings in a case-insensitive fashion.
220 |
221 |
222 |
223 |
224 |
225 |
226 | Myers Standards Track [Page 4]
227 |
228 | RFC 2087 QUOTA January 1997
229 |
230 |
231 | getquota ::= "GETQUOTA" SP astring
232 |
233 | getquotaroot ::= "GETQUOTAROOT" SP astring
234 |
235 | quota_list ::= "(" #quota_resource ")"
236 |
237 | quota_resource ::= atom SP number SP number
238 |
239 | quota_response ::= "QUOTA" SP astring SP quota_list
240 |
241 | quotaroot_response
242 | ::= "QUOTAROOT" SP astring *(SP astring)
243 |
244 | setquota ::= "SETQUOTA" SP astring SP setquota_list
245 |
246 | setquota_list ::= "(" 0#setquota_resource ")"
247 |
248 | setquota_resource ::= atom SP number
249 |
250 | 7. References
251 |
252 | [IMAP4] Crispin, M., "Internet Message Access Protocol - Version 4",
253 | RFC 1730, University of Washington, December 1994.
254 |
255 | [RFC-822] Crocker, D., "Standard for the Format of ARPA Internet
256 | Text Messages", STD 11, RFC 822.
257 |
258 | 8. Security Considerations
259 |
260 | Implementors should be careful to make sure the implementation of
261 | these commands does not violate the site's security policy. The
262 | resource usage of other users is likely to be considered confidential
263 | information and should not be divulged to unauthorized persons.
264 |
265 | 9. Author's Address
266 |
267 | John G. Myers
268 | Carnegie-Mellon University
269 | 5000 Forbes Ave.
270 | Pittsburgh PA, 15213-3890
271 |
272 | EMail: jgm+@cmu.edu
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 | Myers Standards Track [Page 5]
283 |
284 |
--------------------------------------------------------------------------------