├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.md ├── MAINTAINERS.md ├── README.md ├── doc └── man │ └── imapfw.txt ├── imapfw.py ├── imapfw ├── __init__.py ├── actions │ ├── __init__.py │ ├── devel.py │ ├── examine.py │ ├── interface.py │ ├── noop.py │ ├── shell.py │ ├── syncaccounts.py │ ├── testrascal.py │ └── unittests.py ├── annotation.py ├── api │ ├── actions │ │ └── __init__.py │ ├── concurrency │ │ └── __init__.py │ ├── controllers │ │ └── __init__.py │ ├── drivers │ │ └── __init__.py │ ├── engines │ │ └── __init__.py │ ├── shells │ │ └── __init__.py │ └── types │ │ └── __init__.py ├── architects │ ├── __init__.py │ ├── account.py │ ├── architect.py │ ├── debug.py │ ├── driver.py │ ├── engine.py │ └── folder.py ├── concurrency │ ├── __init__.py │ └── concurrency.py ├── conf │ ├── __init__.py │ ├── clioptions.py │ └── conf.py ├── constants.py ├── controllers │ ├── __init__.py │ ├── controller.py │ ├── duplicate.py │ ├── examine.py │ ├── fake.py │ ├── filter.py │ ├── nametrans.py │ └── transcoder.py ├── drivers │ ├── __init__.py │ ├── driver.py │ ├── imap.py │ └── maildir.py ├── edmp.py ├── engines │ ├── __init__.py │ ├── account.py │ ├── engine.py │ └── folder.py ├── error.py ├── imap │ ├── __init__.py │ ├── imap.py │ ├── imapc │ │ └── interface.py │ └── imaplib3 │ │ └── imaplib2.py ├── init.py ├── interface.py ├── mmp │ ├── __init__.py │ ├── account.py │ ├── driver.py │ ├── folder.py │ ├── manager.py │ └── serializer.py ├── rascal.py ├── runners │ ├── __init__.py │ ├── driver.py │ └── toprunner.py ├── runtime.py ├── shells │ ├── __init__.py │ └── shell.py ├── testing │ ├── __init__.py │ ├── architect.py │ ├── concurrency.py │ ├── edmp.py │ ├── folder.py │ ├── libcore.py │ ├── maildir.py │ ├── maildirs │ │ ├── .keep │ │ ├── recursive_A │ │ │ ├── .keep │ │ │ ├── cur │ │ │ │ ├── .keep │ │ │ │ └── 1444285591_0.15004.vidovic.ultras.lan,U=207316,FMD5=75097416fde6f7c2e8da35793159ef76:2,S │ │ │ ├── new │ │ │ │ └── .keep │ │ │ └── tmp │ │ │ │ └── .keep │ │ └── recursive_B │ │ │ ├── .keep │ │ │ ├── cur │ │ │ └── .keep │ │ │ ├── ignore │ │ │ └── .keep │ │ │ ├── new │ │ │ └── .keep │ │ │ ├── subfolder_A │ │ │ ├── .keep │ │ │ ├── cur │ │ │ │ └── .keep │ │ │ ├── new │ │ │ │ └── .keep │ │ │ └── tmp │ │ │ │ └── .keep │ │ │ ├── subfolder_B │ │ │ ├── .keep │ │ │ ├── cur │ │ │ │ └── .keep │ │ │ ├── new │ │ │ │ └── .keep │ │ │ ├── subsubfolder_X │ │ │ │ ├── cur │ │ │ │ │ └── .keep │ │ │ │ ├── new │ │ │ │ │ └── .keep │ │ │ │ └── tmp │ │ │ │ │ └── .keep │ │ │ └── tmp │ │ │ │ └── .keep │ │ │ └── tmp │ │ │ └── .keep │ ├── message.py │ ├── nullui.py │ ├── rascal.py │ ├── rascals │ │ ├── basic.rascal │ │ └── empty.rascal │ ├── testrascal.py │ └── types.py ├── toolkit.py ├── types │ ├── __init__.py │ ├── account.py │ ├── folder.py │ ├── imap.py │ ├── maildir.py │ ├── message.py │ └── repository.py └── ui │ ├── __init__.py │ └── tty.py ├── internals ├── Makefile └── source │ ├── _static │ └── .keep │ ├── _templates │ └── .keep │ ├── conf.py │ ├── imapfw.actions.rst │ ├── imapfw.architects.rst │ ├── imapfw.concurrency.rst │ ├── imapfw.conf.rst │ ├── imapfw.controllers.rst │ ├── imapfw.drivers.rst │ ├── imapfw.engines.rst │ ├── imapfw.imap.rst │ ├── imapfw.mmp.rst │ ├── imapfw.rst │ ├── imapfw.runners.rst │ ├── imapfw.shells.rst │ ├── imapfw.testing.rst │ ├── imapfw.types.rst │ ├── imapfw.ui.rst │ ├── index.rst │ └── modules.rst ├── logo.png ├── rascals ├── controllers.rascal ├── demo.rascal ├── dev.rascal ├── local.driver.rascal ├── one.rascal.example └── simple │ └── starter.rascal ├── requirements.txt ├── tests ├── syncaccounts.1.rascal └── syncaccounts.1 │ └── MaildirA │ └── .keep └── travis-tests.sh /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | > This v0.1 template stands in `.github/`. 2 | 3 | ### Peers review 4 | 5 | Trick to [fetch the pull request](https://help.github.com/articles/checking-out-pull-requests-locally): there is a (read-only) `refs/pull/` namespace. 6 | 7 | ``` bash 8 | git fetch origin pull/PULL_ID/head:LOCAL_BRANCH_NAME 9 | ``` 10 | 11 | ### This PR 12 | 13 | > Add character 'x': `[x]`. 14 | 15 | - [] I've read the [DCO](http://www.offlineimap.org/doc/dco.html). 16 | - [] The relevant informations about the changes stands in the commit message, not here in the message of the pull request. 17 | - [] Code changes follow the style of the files they change. 18 | - [] Code is tested (provide details). 19 | 20 | ### References 21 | 22 | - Issue #no_space 23 | 24 | ### Additional information 25 | 26 | 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | .*.swo 3 | *~ 4 | *.pyc 5 | __pycache__/ 6 | /wiki/ 7 | /website/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: python 3 | python: 4 | - "3.3" 5 | - "3.4" 6 | - "3.5" 7 | - "3.5-dev" # 3.5 development branch 8 | - "nightly" # currently points to 3.6-dev 9 | #- "pypy3" # won't work since it implements Python 3.2.5. 10 | 11 | branches: 12 | only: 13 | - master 14 | - coverage 15 | - next 16 | - travis # Let testing travis configuration. 17 | 18 | before_install: 19 | - pip install --upgrade pip 20 | - pip install coverage codecov coveralls 21 | 22 | # command to install dependencies 23 | install: "pip install -r requirements.txt" 24 | 25 | #before_script: 26 | #- cp -a ./tests "$TRAVIS_BUILD_DIR" 27 | 28 | notifications: 29 | webhooks: 30 | urls: 31 | - https://webhooks.gitter.im/e/0d4fe7da7dd072da59ea 32 | on_success: always # options: [always|never|change] default: always 33 | on_failure: always # options: [always|never|change] default: always 34 | on_start: never 35 | 36 | # command to run tests 37 | script: 38 | - coverage run --source=imapfw,imapfw.py -p -m imapfw.edmp 39 | - coverage run --source=imapfw,imapfw.py -p -m imapfw.mmp.manager 40 | - coverage run --source=imapfw,imapfw.py -p ./imapfw.py -h 41 | - coverage run --source=imapfw,imapfw.py -p ./imapfw.py noop 42 | - coverage run --source=imapfw,imapfw.py -p ./imapfw.py -c multiprocessing unitTests 43 | - coverage run --source=imapfw,imapfw.py -p ./imapfw.py -c threading unitTests 44 | - coverage run --source=imapfw,imapfw.py -p ./imapfw.py -r ./tests/syncaccounts.1.rascal -d all noop 45 | - coverage run --source=imapfw,imapfw.py -p ./imapfw.py -r ./tests/syncaccounts.1.rascal -d all testRascal 46 | - coverage run --source=imapfw,imapfw.py -p ./imapfw.py -r ./tests/syncaccounts.1.rascal -d all examine 47 | - coverage run --source=imapfw,imapfw.py -p ./imapfw.py -r ./tests/syncaccounts.1.rascal -d all -c multiprocessing syncAccounts -a AccountA 48 | - coverage run --source=imapfw,imapfw.py -p ./imapfw.py -r ./tests/syncaccounts.1.rascal -d all -c threading syncAccounts -a AccountA 49 | - coverage run --source=imapfw,imapfw.py -p ./imapfw.py -r ./tests/syncaccounts.1.rascal -d all -c threading syncAccounts -a AccountA -a AccountA -a AccountA 50 | - coverage combine .coverage* 51 | 52 | after_success: 53 | - coveralls 54 | - codecov 55 | # vim: expandtab ts=2 : 56 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Ideas, feedbacks and discussions 4 | 5 | You can contact us to talk about you ideas or feedbacks in [our chat 6 | room][gitter] or [mailing list][subscribe]. 7 | 8 | The [TODO list][wiki] is open and writable to everybody. 9 | 10 | 11 | # Being involved 12 | 13 | There are lot of ideas and WIP you might like to contribute to. **We welcome 14 | newcomers and make our best to help so that everyone become quickly effective 15 | on the project.** 16 | 17 | 18 | [gitter]: https://gitter.im/OfflineIMAP/imapfw 19 | [subscribe]: http://lists.alioth.debian.org/mailman/listinfo/offlineimap-project 20 | [wiki]: https://github.com/OfflineIMAP/imapfw/wiki 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2015, Nicolas Sebrecht & contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Official maintainers 4 | ==================== 5 | 6 | Nicolas Sebrecht 7 | - email: nicolas.s-dev at laposte.net 8 | - github: nicolas33 https://github.com/nicolas33 9 | 10 | 13 | -------------------------------------------------------------------------------- /doc/man/imapfw.txt: -------------------------------------------------------------------------------- 1 | 2 | imapfw(1) 3 | ========= 4 | 5 | NAME 6 | ---- 7 | impfw - mailboxes framework 8 | 9 | SYNOPSIS 10 | -------- 11 | [verse] 12 | 'imapfw' (options) 13 | 14 | DESCRIPTION 15 | ----------- 16 | 17 | Framework to work with your mailboxes. Supports IMAP. 18 | 19 | 20 | OPTIONS 21 | ------- 22 | 23 | -h:: 24 | --help:: 25 | 26 | Display summary of options. 27 | 28 | --version:: 29 | 30 | Output version. 31 | 32 | 33 | Exit codes 34 | ---------- 35 | 36 | 1:: An error occured while initiating the program. 37 | 38 | 2:: Cannot load the Rascal or it's missing from CLI. 39 | 40 | 3:: 41 | An exception occured while running the requested ACTION, requiring to stop 42 | the run. 43 | 44 | 4:: The timeout was reached while running a hook. 45 | 46 | 10:: At least one concurrent task did not end successfully. 47 | 48 | 99:: Internal logic error. 49 | 50 | >99:: Reserved for the user. 51 | 52 | 53 | Main authors 54 | ------------ 55 | 56 | Nicolas Sebrecht. 57 | 58 | 59 | See Also 60 | -------- 61 | 62 | openssl(1) 63 | 64 | -------------------------------------------------------------------------------- /imapfw.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from imapfw import Imapfw 4 | 5 | # sys.exit(0) at this point would be MUCH bug free. 6 | 7 | Imapfw().run() 8 | -------------------------------------------------------------------------------- /imapfw/__init__.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (C) 2015-2016, Nicolas Sebrecht and contributors. 3 | 4 | 5 | __productname__ = 'Imapfw' 6 | __version__ = "0.026" 7 | __copyright__ = "Copyright 2015-2016 Nicolas Sebrecht & contributors" 8 | __author__ = "Nicolas Sebrecht" 9 | __author_email__= "nicolas.s-dev@laposte.net" 10 | __description__ = "Framework for working with IMAP and emails" 11 | __license__ = "The MIT License (MIT)" 12 | __homepage__ = "http://github.com/OfflineIMAP/imapfw" 13 | 14 | 15 | from imapfw.init import Imapfw # Avoid circular dependencies. 16 | from imapfw import runtime # Import this module ASAP. 17 | -------------------------------------------------------------------------------- /imapfw/actions/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Order of imports is order of actions in command line help message. 3 | from .unittests import UnitTests 4 | from .noop import Noop 5 | from .testrascal import TestRascal 6 | from .examine import Examine 7 | from .shell import ShellAction 8 | from .devel import Devel 9 | from .syncaccounts import SyncAccounts 10 | -------------------------------------------------------------------------------- /imapfw/actions/devel.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | from imapfw import runtime 5 | from imapfw.interface import implements, checkInterfaces 6 | from imapfw.conf import Parser 7 | 8 | from .interface import ActionInterface 9 | 10 | # Annotations. 11 | from imapfw.annotation import ExceptionClass 12 | 13 | 14 | @checkInterfaces() 15 | @implements(ActionInterface) 16 | class Devel(object): 17 | """For development purpose only.""" 18 | 19 | honorHooks = False 20 | requireRascal = True 21 | 22 | def __init__(self): 23 | self._exitCode = 0 24 | 25 | def exception(self, e: ExceptionClass) -> None: 26 | self._exitCode = 3 27 | 28 | def getExitCode(self) -> int: 29 | return self._exitCode 30 | 31 | def init(self, parser: Parser) -> None: 32 | pass 33 | 34 | def run(self) -> None: 35 | runtime.ui.infoL(1, 'running devel action') 36 | 37 | from ..imap.imap import Imap 38 | 39 | imap = Imap('imaplib2-2.50') 40 | imap.connect('127.0.0.1', 10143) 41 | imap.logout() 42 | 43 | Parser.addAction('devel', Devel, help="development") 44 | -------------------------------------------------------------------------------- /imapfw/actions/examine.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | from imapfw import runtime 5 | from imapfw.types.account import Account, loadAccount 6 | from imapfw.controllers.examine import ExamineController 7 | from imapfw.drivers.driver import DriverInterface 8 | from imapfw.interface import implements, checkInterfaces 9 | from imapfw.conf import Parser 10 | 11 | from .interface import ActionInterface 12 | 13 | # Annotations. 14 | from imapfw.annotation import ExceptionClass, Dict 15 | 16 | 17 | @checkInterfaces() 18 | @implements(ActionInterface) 19 | class Examine(object): 20 | """Examine repositories (all run sequentially).""" 21 | 22 | honorHooks = False 23 | requireRascal = True 24 | 25 | def __init__(self): 26 | self._exitCode = 0 27 | self.ui = runtime.ui 28 | 29 | self._architects = [] 30 | 31 | def exception(self, e: ExceptionClass) -> None: 32 | self._exitCode = 3 33 | 34 | def getExitCode(self) -> int: 35 | return self._exitCode 36 | 37 | def init(self, parser: Parser) -> None: 38 | pass 39 | 40 | def run(self) -> None: 41 | class Report(object): 42 | def __init__(self): 43 | self._number = 0 44 | self.content = {} 45 | 46 | def _getNumber(self): 47 | self._number += 1 48 | return self._number 49 | 50 | def line(self, line: str=''): 51 | self.content[self._getNumber()] = ('line', (line,)) 52 | 53 | def list(self, elements: list=[]): 54 | self.content[self._getNumber()] = ('list', (elements,)) 55 | 56 | def title(self, title: str, level: int=1): 57 | self.content[self._getNumber()] = ('title', (title, level)) 58 | 59 | def markdown(self): 60 | for lineDef in self.content.values(): 61 | kind, args = lineDef 62 | 63 | if kind == 'title': 64 | title, level = args 65 | prefix = '#' * level 66 | print("\n%s %s\n"% (prefix, title)) 67 | 68 | if kind == 'list': 69 | for elem in args[0]: 70 | print("* %s"% elem) 71 | 72 | if kind == 'line': 73 | print(args[0]) 74 | 75 | 76 | cls_accounts = runtime.rascal.getAll([Account]) 77 | 78 | repositories = [] 79 | for cls_account in cls_accounts: 80 | account = loadAccount(cls_account) 81 | repositories.append(account.fw_getLeft()) 82 | repositories.append(account.fw_getRight()) 83 | 84 | report = Report() 85 | for repository in repositories: 86 | if isinstance(repository, DriverInterface): 87 | continue 88 | try: 89 | repository.fw_insertController(ExamineController, 90 | {'report': report}) 91 | driver = repository.fw_getDriver() 92 | 93 | report.title("Repository %s (driver %s)"% 94 | (repository.getClassName(), driver.getDriverClassName())) 95 | report.line("controllers: %s"% 96 | [x.__name__ for x in repository.controllers]) 97 | 98 | driver.connect() 99 | driver.getFolders() 100 | 101 | report = driver.fw_getReport() 102 | except Exception as e: 103 | raise 104 | self.ui.warn("got %s %s"% (repr(e), str(e))) 105 | report.markdown() 106 | 107 | 108 | Parser.addAction('examine', Examine, help="examine repositories") 109 | -------------------------------------------------------------------------------- /imapfw/actions/interface.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | from imapfw.interface import Interface 5 | from imapfw.conf import Parser 6 | 7 | # Annotations. 8 | from imapfw.annotation import ExceptionClass 9 | 10 | 11 | class ActionInterface(Interface): 12 | 13 | scope = Interface.INTERNAL 14 | 15 | honorHooks = True 16 | requireRascal = True 17 | 18 | def exception(self, e: ExceptionClass) -> None: 19 | """Called on unexpected errors.""" 20 | 21 | def getExitCode(self) -> int: 22 | """Return exit code.""" 23 | 24 | def init(self, parser: Parser) -> None: 25 | """Initialize action.""" 26 | 27 | def run(self) -> None: 28 | """Run action.""" 29 | -------------------------------------------------------------------------------- /imapfw/actions/noop.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | from imapfw.interface import implements, checkInterfaces 5 | from imapfw.conf import Parser 6 | 7 | from .interface import ActionInterface 8 | 9 | # Annotations. 10 | from imapfw.annotation import ExceptionClass 11 | 12 | 13 | @checkInterfaces() 14 | @implements(ActionInterface) 15 | class Noop(object): 16 | """The noop action allows testing the loading of the rascal.""" 17 | 18 | honorHooks = False 19 | requireRascal = False 20 | 21 | def exception(self, e: ExceptionClass) -> None: 22 | raise NotImplementedError 23 | 24 | def getExitCode(self) -> int: 25 | return 0 26 | 27 | def init(self, parser: Parser) -> None: 28 | pass 29 | 30 | def run(self) -> None: 31 | pass 32 | 33 | Parser.addAction('noop', Noop, help="test if the rascal can be loaded") 34 | -------------------------------------------------------------------------------- /imapfw/actions/shell.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | from imapfw import runtime 5 | from imapfw.shells import Shell 6 | from imapfw.interface import implements, checkInterfaces 7 | from imapfw.conf import Parser 8 | 9 | from .interface import ActionInterface 10 | 11 | # Annotations. 12 | from imapfw.annotation import ExceptionClass 13 | 14 | 15 | @checkInterfaces() 16 | @implements(ActionInterface) 17 | class ShellAction(object): 18 | """Run in (interactive) shell mode.""" 19 | 20 | honorHooks = False 21 | requireRascal = True 22 | 23 | def __init__(self): 24 | self._shellName = None 25 | self._repositoryName = None 26 | self._exitCode = -1 27 | 28 | def _setExitCode(self, exitCode): 29 | if exitCode > self._exitCode: 30 | self._exitCode = exitCode 31 | 32 | def exception(self, e: ExceptionClass) -> None: 33 | self.exitCode = 3 34 | raise NotImplementedError #TODO 35 | 36 | def getExitCode(self) -> int: 37 | return self._exitCode 38 | 39 | def init(self, parser: Parser) -> None: 40 | self._shellName = parser.get('shell_name') 41 | 42 | def run(self) -> None: 43 | cls_shell = runtime.rascal.get(self._shellName, [Shell]) 44 | shell = cls_shell() 45 | shell.beforeSession() 46 | shell.configureCompletion() 47 | shell.session() 48 | exitCode = shell.afterSession() 49 | self._setExitCode(exitCode) 50 | 51 | actionParser = Parser.addAction('shell', ShellAction, help="run in shell mode") 52 | 53 | actionParser.add_argument(dest="shell_name", 54 | default=None, 55 | metavar="SHELL_NAME", 56 | help="the shell from the rascal to run") 57 | -------------------------------------------------------------------------------- /imapfw/actions/syncaccounts.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | from imapfw import runtime 5 | from imapfw.interface import implements, checkInterfaces 6 | from imapfw.conf import Parser 7 | from imapfw.architects.account import SyncAccountsArchitect 8 | 9 | from .interface import ActionInterface 10 | 11 | # Annotations. 12 | from imapfw.annotation import ExceptionClass 13 | 14 | 15 | @checkInterfaces() 16 | @implements(ActionInterface) 17 | class SyncAccounts(object): 18 | """Sync the requested accounts as defined in the rascal, in async mode.""" 19 | 20 | honorHooks = True 21 | requireRascal = True 22 | 23 | def __init__(self): 24 | self.accountList = None 25 | self.engineName = None 26 | self.exitCode = -1 27 | 28 | def exception(self, e: ExceptionClass) -> None: 29 | self.exitCode = 3 30 | raise NotImplementedError #TODO 31 | 32 | def getExitCode(self) -> int: 33 | return self.exitCode 34 | 35 | def init(self, parser: Parser) -> None: 36 | self.accountList = parser.get('accounts') 37 | self.engineName = parser.get('engine') 38 | 39 | def run(self) -> None: 40 | """Enable the syncing of the accounts in an async fashion. 41 | 42 | Code here is about setting up the environment, start the jobs and 43 | monitor.""" 44 | 45 | 46 | maxConcurrentAccounts = min( 47 | runtime.rascal.getMaxSyncAccounts(), 48 | len(self.accountList)) 49 | 50 | accountsArchitect = SyncAccountsArchitect(self.accountList) 51 | accountsArchitect.start(maxConcurrentAccounts) 52 | self.exitCode = accountsArchitect.run() 53 | 54 | 55 | # syncAccounts CLI options. 56 | actionParser = Parser.addAction('syncAccounts', 57 | SyncAccounts, help="sync on or more accounts") 58 | 59 | actionParser.add_argument("-a", "--account", dest="accounts", 60 | default=[], 61 | action='append', 62 | metavar='ACCOUNT', 63 | required=True, 64 | help="one or more accounts to sync") 65 | 66 | actionParser.add_argument("-e", "--engine", dest="engine", 67 | default="SyncAccount", 68 | help="the sync engine") 69 | -------------------------------------------------------------------------------- /imapfw/actions/testrascal.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | 5 | from imapfw import runtime 6 | from imapfw.interface import implements, checkInterfaces 7 | from imapfw.conf import Parser 8 | 9 | from .interface import ActionInterface 10 | 11 | # Annotations. 12 | from imapfw.annotation import ExceptionClass 13 | 14 | 15 | @checkInterfaces() 16 | @implements(ActionInterface) 17 | class TestRascal(object): 18 | """Test the rascal.""" 19 | 20 | honorHooks = False 21 | requireRascal = True 22 | 23 | def __init__(self): 24 | self._suite = None 25 | self._exitCode = -1 26 | 27 | def _setExitCode(self, exitCode): 28 | if exitCode > self._exitCode: 29 | self._exitCode = exitCode 30 | 31 | def exception(self, e: ExceptionClass) -> None: 32 | # This should not happen since all exceptions are handled at lower level. 33 | raise NotImplementedError 34 | 35 | def getExitCode(self) -> int: 36 | return self._exitCode 37 | 38 | def init(self, parser: Parser) -> None: 39 | pass 40 | 41 | def run(self) -> None: 42 | import unittest 43 | 44 | from imapfw.types.account import Account 45 | from imapfw.testing.testrascal import TestRascalAccount 46 | 47 | suite = unittest.TestSuite() 48 | 49 | for def_account in runtime.rascal.getAll([Account]): 50 | newTest = type(def_account.__name__, (TestRascalAccount,), {}) 51 | newTest.DEF_ACCOUNT = def_account 52 | suite.addTest(unittest.makeSuite(newTest)) 53 | 54 | runner = unittest.TextTestRunner(verbosity=2, failfast=True) 55 | testResult = runner.run(suite) 56 | self._setExitCode(len(testResult.failures)) 57 | 58 | if testResult.wasSuccessful(): 59 | print("TODO: run tests for the repositories and drivers.") 60 | 61 | 62 | Parser.addAction('testRascal', TestRascal, help="test your rascal") 63 | -------------------------------------------------------------------------------- /imapfw/actions/unittests.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | from imapfw.interface import implements, checkInterfaces 5 | from imapfw.conf import Parser 6 | 7 | from .interface import ActionInterface 8 | 9 | # Annotations. 10 | from imapfw.annotation import ExceptionClass 11 | 12 | 13 | @checkInterfaces() 14 | @implements(ActionInterface) 15 | class UnitTests(object): 16 | """Run all the unit tests.""" 17 | 18 | honorHooks = False 19 | requireRascal = False 20 | 21 | def __init__(self): 22 | self._suite = None 23 | self._exitCode = 1 24 | 25 | def exception(self, e: ExceptionClass) -> None: 26 | raise 27 | 28 | def getExitCode(self) -> int: 29 | return self._exitCode 30 | 31 | def init(self, parser: Parser) -> None: 32 | import unittest 33 | 34 | self._suite = unittest.TestSuite() 35 | 36 | # Load all available unit tests. 37 | from imapfw.testing.concurrency import TestConcurrency 38 | from imapfw.testing.rascal import TestRascal 39 | from imapfw.testing.folder import TestFolder 40 | from imapfw.testing.message import TestMessage, TestMessages 41 | from imapfw.testing.maildir import TestMaildirDriver 42 | from imapfw.testing.edmp import TestEDMP 43 | from imapfw.testing.types import TestTypeAccount, TestTypeRepository 44 | from imapfw.testing.architect import TestArchitect, TestDriverArchitect 45 | from imapfw.testing.architect import TestDriversArchitect 46 | from imapfw.testing.architect import TestEngineArchitect 47 | 48 | self._suite.addTest(unittest.makeSuite(TestConcurrency)) 49 | self._suite.addTest(unittest.makeSuite(TestRascal)) 50 | self._suite.addTest(unittest.makeSuite(TestFolder)) 51 | self._suite.addTest(unittest.makeSuite(TestMessage)) 52 | self._suite.addTest(unittest.makeSuite(TestMessages)) 53 | self._suite.addTest(unittest.makeSuite(TestMaildirDriver)) 54 | self._suite.addTest(unittest.makeSuite(TestEDMP)) 55 | self._suite.addTest(unittest.makeSuite(TestTypeAccount)) 56 | self._suite.addTest(unittest.makeSuite(TestTypeRepository)) 57 | self._suite.addTest(unittest.makeSuite(TestArchitect)) 58 | self._suite.addTest(unittest.makeSuite(TestDriverArchitect)) 59 | self._suite.addTest(unittest.makeSuite(TestDriversArchitect)) 60 | self._suite.addTest(unittest.makeSuite(TestEngineArchitect)) 61 | 62 | def run(self) -> None: 63 | import unittest 64 | 65 | runner = unittest.TextTestRunner(verbosity=2) 66 | testResult = runner.run(self._suite) 67 | if testResult.wasSuccessful(): 68 | self._exitCode = len(testResult.failures) 69 | 70 | 71 | Parser.addAction('unitTests', UnitTests, help="run the integrated unit tests") 72 | -------------------------------------------------------------------------------- /imapfw/annotation.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | """ 5 | 6 | Where Python3 annotations must be defined. 7 | 8 | """ 9 | 10 | from typing import Any, Dict, Iterable, List, Tuple, TypeVar, Union 11 | 12 | # Global. 13 | Function = TypeVar('Function') 14 | 15 | # actions.clioptions, 16 | ActionClass = TypeVar('ActionClass') 17 | 18 | # edmp, 19 | ExceptionClass = TypeVar('Exception class') 20 | 21 | # interface, 22 | Requirement = Any 23 | InterfaceClass = TypeVar('Interface class') 24 | InterfaceDefinitions = Dict[InterfaceClass, Tuple['arguments']] 25 | 26 | # drivers.driver, 27 | DriverClass = TypeVar('Driver based class') 28 | 29 | # actions.interface, 30 | ExceptionClass = TypeVar('Exception class') 31 | -------------------------------------------------------------------------------- /imapfw/api/actions/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | The public API. 4 | 5 | Import the objects made public from the real objects defined in their 6 | uncorrelated path. This allows more fine-grained control of what is made public 7 | and how to structure the underlying code. 8 | 9 | """ 10 | 11 | __all__ = [ 12 | 'SyncAccounts', 13 | ] 14 | 15 | from imapfw.actions.syncaccounts import SyncAccounts 16 | -------------------------------------------------------------------------------- /imapfw/api/concurrency/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | The public API. 4 | 5 | Import the objects made public from the real objects defined in their 6 | uncorrelated path. This allows more fine-grained control of what is made public 7 | and how to structure the underlying code. 8 | 9 | """ 10 | 11 | __all__ = [ 12 | 'SimpleLock', 13 | 'WorkerSafe', 14 | ] 15 | 16 | from imapfw.concurrency.concurrency import SimpleLock, WorkerSafe 17 | -------------------------------------------------------------------------------- /imapfw/api/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | The public API. 4 | 5 | Import the objects made public from the real objects defined in their 6 | uncorrelated path. This allows more fine-grained control of what is made public 7 | and how to structure the underlying code. 8 | 9 | """ 10 | 11 | __all__ = [ 12 | 'Controller', 13 | 'Duplicate', 14 | 'Examine', 15 | 'FakeDriver', 16 | 'Filter', 17 | 'NameTrans', 18 | 'Transcoder', 19 | ] 20 | 21 | from imapfw.controllers.controller import Controller 22 | from imapfw.controllers.duplicate import Duplicate 23 | from imapfw.controllers.examine import ExamineController as Examine 24 | from imapfw.controllers.fake import FakeDriver 25 | from imapfw.controllers.filter import Filter 26 | from imapfw.controllers.nametrans import NameTrans 27 | from imapfw.controllers.transcoder import Transcoder 28 | -------------------------------------------------------------------------------- /imapfw/api/drivers/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | The public API. 4 | 5 | Import the objects made public from the real objects defined in their 6 | uncorrelated path. This allows more fine-grained control of what is made public 7 | and how to structure the underlying code. 8 | 9 | """ 10 | 11 | __all__ = [ 12 | 'FetchAttributes', 13 | 'Imap', 14 | 'Maildir', 15 | 'SearchConditions', 16 | ] 17 | 18 | from imapfw.drivers.imap import Imap 19 | from imapfw.drivers.maildir import Maildir 20 | 21 | from imapfw.imap import FetchAttributes, SearchConditions 22 | -------------------------------------------------------------------------------- /imapfw/api/engines/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | The public API. 4 | 5 | Import the objects made public from the real objects defined in their 6 | uncorrelated path. This allows more fine-grained control of what is made public 7 | and how to structure the underlying code. 8 | 9 | """ 10 | 11 | __all__ = [ 12 | 'SyncAccounts', 13 | 'SyncFolders', 14 | ] 15 | 16 | from imapfw.engines import SyncAccounts, SyncFolders 17 | -------------------------------------------------------------------------------- /imapfw/api/shells/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | The public API. 4 | 5 | Import the objects made public from the real objects defined in their 6 | uncorrelated path. This allows more fine-grained control of what is made public 7 | and how to structure the underlying code. 8 | 9 | """ 10 | 11 | __all__ = [ 12 | 'Shell', 13 | 'DriveDriver', 14 | ] 15 | 16 | from imapfw.shells import Shell, DriveDriver 17 | -------------------------------------------------------------------------------- /imapfw/api/types/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | The public API. 4 | 5 | Import the objects made public from the real objects defined in their 6 | uncorrelated path. This allows more fine-grained control of what is made public 7 | and how to structure the underlying code. 8 | 9 | """ 10 | 11 | __all__ = [ 12 | 'folder', 13 | 'message', 14 | 'Account', 15 | 'Imap', 16 | 'Maildir', 17 | 'Repository', 18 | ] 19 | 20 | from imapfw.types import folder 21 | from imapfw.types import message 22 | from imapfw.types.account import Account 23 | from imapfw.types.imap import Imap 24 | from imapfw.types.maildir import Maildir 25 | from imapfw.types.repository import Repository 26 | -------------------------------------------------------------------------------- /imapfw/architects/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .architect import Architect 3 | from .engine import EngineArchitect 4 | from .account import SyncArchitect, SyncAccountsArchitect 5 | from .folder import SyncFolderArchitect, SyncFoldersArchitect 6 | from .driver import DriverArchitect, DriversArchitect 7 | -------------------------------------------------------------------------------- /imapfw/architects/architect.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015-2016, Nicolas Sebrecht & contributors. 3 | 4 | """ 5 | 6 | The achitects are high level objects to support actions with dynamic process 7 | handling. 8 | 9 | They are helpers for the actions/softwares. They handles workers and whatever 10 | required to enable other components. 11 | 12 | This is the wrong place for anything about business logic. Architects are not 13 | problem solving for the purpose of the actions/softwares. 14 | 15 | """ 16 | 17 | from imapfw import runtime 18 | 19 | from .debug import debugArchitect 20 | 21 | # Interfaces. 22 | from imapfw.interface import Interface, implements 23 | 24 | # Annotations. 25 | from imapfw.annotation import Function 26 | 27 | 28 | class ArchitectInterface(Interface): 29 | 30 | scope = Interface.INTERNAL 31 | 32 | def kill(self) -> None: 33 | """Kill worker.""" 34 | 35 | def start(self, runner: Function, runnerArgs: tuple) -> None: 36 | """Start worker.""" 37 | 38 | def stop(self) -> None: 39 | """Stop worker.""" 40 | 41 | @debugArchitect 42 | @implements(ArchitectInterface) 43 | class Architect(object): 44 | def __init__(self, workerName: str): 45 | self.workerName = workerName 46 | 47 | self.name = self.__class__.__name__ 48 | self.worker = None 49 | 50 | def kill(self) -> None: 51 | self.worker.kill() 52 | 53 | def start(self, runner: Function, runnerArgs: tuple) -> None: 54 | self.worker = runtime.concurrency.createWorker( 55 | self.workerName, runner, runnerArgs) 56 | self.worker.start() 57 | 58 | def stop(self) -> None: 59 | self.worker.join() 60 | 61 | -------------------------------------------------------------------------------- /imapfw/architects/debug.py: -------------------------------------------------------------------------------- 1 | 2 | from imapfw import runtime 3 | from imapfw.constants import ARC 4 | 5 | def debugArchitect(cls): 6 | """Decorate methods of an Architect for better debugging experience.""" 7 | 8 | import inspect 9 | import functools 10 | 11 | def debugWrapper(func): 12 | @functools.wraps(func) 13 | def debugMethod(*args, **kwargs): 14 | runtime.ui.debugC(ARC, "D: %s.%s: %s %s"% 15 | (cls.__name__, func.__name__, repr(args[1:]), repr(kwargs))) 16 | result = func(*args, **kwargs) 17 | return result 18 | 19 | return debugMethod 20 | 21 | for name, method in inspect.getmembers(cls, inspect.isfunction): 22 | setattr(cls, name, debugWrapper(method)) 23 | return cls 24 | 25 | -------------------------------------------------------------------------------- /imapfw/architects/driver.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | from imapfw import runtime 5 | from imapfw.constants import ARC 6 | from imapfw.edmp import newEmitterReceiver 7 | from imapfw.runners import DriverRunner, topRunner 8 | 9 | from .debug import debugArchitect 10 | 11 | # Interfaces. 12 | from imapfw.interface import implements, Interface, checkInterfaces 13 | # Annotations. 14 | from imapfw.edmp import Emitter 15 | 16 | 17 | class DriverArchitectInterface(object): 18 | def _debug(self, msg) -> None: 19 | """Debug.""" 20 | 21 | def getEmitter(self) -> Emitter: 22 | """Return the emitter for the driver.""" 23 | 24 | def init(self) -> None: 25 | """Initialize object.""" 26 | 27 | def kill(self) -> None: 28 | """Kill the driver.""" 29 | 30 | def start(self) -> None: 31 | """Start the driver.""" 32 | 33 | def stop(self) -> None: 34 | """Stop the driver.""" 35 | 36 | @debugArchitect 37 | @checkInterfaces() 38 | @implements(DriverArchitectInterface) 39 | class DriverArchitect(object): 40 | """Architect to manage a driver worker.""" 41 | 42 | def __init__(self, workerName: str): 43 | self.workerName = workerName 44 | 45 | self.emitter = None 46 | self.worker = None 47 | self.name = self.__class__.__name__ 48 | 49 | self._debug("__init__(%s)"% workerName) 50 | 51 | def _debug(self, msg) -> None: 52 | runtime.ui.debugC(ARC, "%s %s"% (self.workerName, msg)) 53 | 54 | def getEmitter(self) -> Emitter: 55 | self._debug("getEmitter()") 56 | assert self.emitter is not None 57 | return self.emitter 58 | 59 | def init(self) -> None: 60 | receiver, self.emitter = newEmitterReceiver(self.workerName) 61 | driverRunner = DriverRunner(self.workerName, receiver) 62 | 63 | self.worker = runtime.concurrency.createWorker(self.workerName, 64 | topRunner, 65 | (self.workerName, driverRunner.run) 66 | ) 67 | 68 | def kill(self) -> None: 69 | self._debug("kill()") 70 | self.emitter.stopServing() 71 | self.worker.kill() 72 | 73 | def start(self) -> None: 74 | self._debug("start()") 75 | self.worker.start() 76 | 77 | def stop(self) -> None: 78 | self._debug("stop()") 79 | self.emitter.stopServing() 80 | self.worker.join() 81 | 82 | 83 | @checkInterfaces() 84 | @implements(DriverArchitectInterface) 85 | class ReuseDriverArchitect(DriverArchitect): 86 | """Architect to manage a driver worker with en emitter already defined.""" 87 | 88 | def __init__(self, emitter: Emitter): 89 | self.emitter = emitter 90 | 91 | def _debug(self, msg) -> None: 92 | super(ReuseDriverArchitect, self)._debug(msg) 93 | 94 | def getEmitter(self) -> Emitter: 95 | return self.emitter 96 | 97 | def init(self) -> None: 98 | pass 99 | 100 | def kill(self) -> None: 101 | self.emitter.stopServing() 102 | 103 | def start(self) -> None: 104 | pass 105 | 106 | def stop(self) -> None: 107 | self.emitter.stopServing() 108 | 109 | 110 | class DriversArchitectInterface(Interface): 111 | """Manage driver architects.""" 112 | 113 | scope = Interface.INTERNAL 114 | 115 | def getEmitter(self, number: int) -> Emitter: 116 | """Return the emitter for given number.""" 117 | 118 | def init(self) -> None: 119 | """Setup and start end-drivers.""" 120 | 121 | def kill(self) -> None: 122 | """Kill the workers.""" 123 | 124 | def start(self) -> None: 125 | """Start the workers.""" 126 | 127 | def stop(self) -> None: 128 | """Stop the workers.""" 129 | 130 | @checkInterfaces() 131 | @implements(DriversArchitectInterface) 132 | class DriversArchitect(object): 133 | """Handles a collection of DriverArchitect.""" 134 | 135 | def __init__(self, workerName: str, number: int): 136 | self.workerName = workerName 137 | self.number = number 138 | 139 | self.driverArchitects = {} 140 | 141 | def getEmitter(self, number: int) -> Emitter: 142 | return self.driverArchitects[number].getEmitter() 143 | 144 | def init(self) -> None: 145 | for i in range(self.number): 146 | workerName = "%s.Driver.%i"% (self.workerName, i) 147 | driver = DriverArchitect(workerName) 148 | driver.init() 149 | self.driverArchitects[i] = driver 150 | 151 | def kill(self) -> None: 152 | for architect in self.driverArchitects.values(): 153 | architect.kill() 154 | 155 | def start(self) -> None: 156 | for architect in self.driverArchitects.values(): 157 | architect.start() 158 | 159 | def stop(self) -> None: 160 | for architect in self.driverArchitects.values(): 161 | architect.stop() 162 | -------------------------------------------------------------------------------- /imapfw/architects/engine.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2016, Nicolas Sebrecht & contributors. 3 | 4 | """ 5 | 6 | Architect for basic engine. 7 | 8 | """ 9 | 10 | from .architect import Architect 11 | from .driver import DriversArchitect 12 | 13 | from .debug import debugArchitect 14 | 15 | # Interfaces. 16 | from imapfw.interface import Interface, implements, checkInterfaces 17 | 18 | # Annotations. 19 | from imapfw.edmp import Emitter 20 | from imapfw.annotation import Function 21 | 22 | 23 | class EngineArchitectInterface(Interface): 24 | """Architect for running an engine with both side drivers. 25 | 26 | Aggregate the engine architect and the drivers.""" 27 | 28 | scope = Interface.INTERNAL 29 | 30 | def getLeftEmitter(self) -> Emitter: 31 | """Return the emitter of the left-side driver.""" 32 | 33 | def getRightEmitter(self) -> Emitter: 34 | """Return the emitter of the right-side driver.""" 35 | 36 | def init(self) -> None: 37 | """Initialize the architect. Helps to compose components easily.""" 38 | 39 | def kill(self) -> None: 40 | """Kill workers.""" 41 | 42 | def start(self, runner: Function, runnerArgs: tuple) -> None: 43 | """Start the workers.""" 44 | 45 | def stop(self) -> None: 46 | """Stop workers.""" 47 | 48 | @debugArchitect 49 | @checkInterfaces() 50 | @implements(EngineArchitectInterface) 51 | class EngineArchitect(object): 52 | def __init__(self, workerName: str): 53 | self.workerName = workerName 54 | 55 | self.architect = None 56 | self.drivers = None 57 | 58 | def getLeftEmitter(self) -> Emitter: 59 | return self.drivers.getEmitter(0) 60 | 61 | def getRightEmitter(self) -> Emitter: 62 | return self.drivers.getEmitter(1) 63 | 64 | def init(self) -> None: 65 | self.architect = Architect(self.workerName) 66 | self.drivers = DriversArchitect(self.workerName, 2) 67 | self.drivers.init() 68 | 69 | def kill(self) -> None: 70 | self.drivers.kill() 71 | self.architect.kill() 72 | 73 | def start(self, runner: Function, runnerArgs: tuple) -> None: 74 | self.drivers.start() 75 | self.architect.start(runner, runnerArgs) 76 | 77 | def stop(self) -> None: 78 | self.drivers.stop() 79 | self.architect.stop() 80 | 81 | -------------------------------------------------------------------------------- /imapfw/concurrency/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .concurrency import Concurrency 3 | 4 | # Usefull for annotations. 5 | from .concurrency import QueueInterface as Queue 6 | -------------------------------------------------------------------------------- /imapfw/conf/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .conf import ImapfwConfig 3 | from .clioptions import Parser 4 | -------------------------------------------------------------------------------- /imapfw/conf/clioptions.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | """ 5 | 6 | Parse the command line options. 7 | 8 | """ 9 | 10 | from argparse import ArgumentParser 11 | 12 | from imapfw import __version__, __copyright__, __license__ 13 | from imapfw.constants import DEBUG_CATEGORIES 14 | 15 | # Annotations. 16 | from imapfw.annotation import ActionClass 17 | 18 | 19 | class _CLIOptions(object): 20 | """Command line options parser. 21 | 22 | Makes CLI parsing agnostic from the underlying parser. 23 | TODO: to be fully agnostic, it must wrap the action sub-parser.""" 24 | 25 | def __init__(self): 26 | self.options = None 27 | self.parser = None 28 | self.actions = None 29 | self.actionClasses = {} 30 | 31 | def addAction(self, name: str, actionClass: ActionClass, help: str): 32 | self.actionClasses[name] = actionClass 33 | return self.actions.add_parser(name, help=help) 34 | 35 | def get(self, name): 36 | return self.options.get(name) 37 | 38 | def getAction(self): 39 | actionName = self.options.get('action') 40 | return actionName, self.actionClasses[actionName] 41 | 42 | def init(self): 43 | self.parser = ArgumentParser( 44 | prog='imapfw', 45 | description="%s.\n\n%s."% (__copyright__, __license__)) 46 | 47 | self.parser.add_argument("--log-level", dest="info", 48 | type=int, 49 | default=3, 50 | choices=[0, 1, 2, 3], 51 | help="define the logging level for the output (default is 3)") 52 | 53 | self.parser.add_argument("-c", dest="concurrency", 54 | default='multiprocessing', 55 | choices=['multiprocessing', 'threading'], 56 | help="the concurrency backend to use (default is multiprocessing)") 57 | 58 | self.parser.add_argument("-r", dest="rascalfile", 59 | metavar="RASCAL", 60 | default=None, 61 | help="the rascal file to use") 62 | 63 | self.parser.add_argument("-d", "--debug", dest="debug", 64 | default=[], 65 | action='append', 66 | choices=list(DEBUG_CATEGORIES.keys()), 67 | help="enable debugging for the requested partial(s)") 68 | 69 | self.parser.add_argument("-v", action='version', 70 | version=__version__) 71 | 72 | # Actions. 73 | self.actions = self.parser.add_subparsers( 74 | title='Available actions', 75 | prog='action', 76 | description="Each action might allow its own options. Run" 77 | " 'imapfw ACTION -h' to know more.", 78 | metavar='ACTION', 79 | dest='action', 80 | ) 81 | 82 | def parse(self): 83 | # Let modules in actions add parsers. 84 | from imapfw import actions 85 | 86 | self.options = vars(self.parser.parse_args()) 87 | 88 | Parser = _CLIOptions() 89 | Parser.init() 90 | -------------------------------------------------------------------------------- /imapfw/conf/conf.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | import logging 5 | 6 | from imapfw.runtime import set_module 7 | from imapfw.concurrency import Concurrency 8 | from imapfw.rascal import Rascal 9 | from imapfw.ui.tty import TTY 10 | 11 | from .clioptions import Parser 12 | 13 | 14 | class ImapfwConfig(object): 15 | def __init__(self): 16 | self.parser = Parser 17 | 18 | self.concurrency = None 19 | self.rascal = None 20 | self.ui = None 21 | 22 | def getAction(self): 23 | return self.parser.getAction() 24 | 25 | def getLogger(self): 26 | return logging 27 | 28 | def setupConcurrency(self): 29 | self.concurrency = Concurrency(self.parser.get('concurrency')) 30 | set_module('concurrency', self.concurrency) # Export concurrency module. 31 | 32 | def loadRascal(self): 33 | rascalFile = self.parser.get('rascalfile') 34 | if rascalFile is not None: 35 | self.rascal = Rascal() 36 | self.rascal.load(rascalFile) 37 | set_module('rascal', self.rascal) # Export the rascal. 38 | 39 | def parseCLI(self): 40 | self.parser.parse() 41 | 42 | def setupUI(self): 43 | ui = TTY(self.concurrency.createLock()) 44 | ui.configure() 45 | 46 | # Let ui prefix log lines with the worker name. 47 | ui.setCurrentWorkerNameFunction(self.concurrency.getCurrentWorkerNameFunction()) 48 | # Apply CLI options. 49 | ui.enableDebugCategories(self.parser.get('debug')) 50 | ui.setInfoLevel(self.parser.get('info')) 51 | 52 | self.ui = ui 53 | set_module('ui', ui) # Export ui module. 54 | -------------------------------------------------------------------------------- /imapfw/constants.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Available debug categories. 4 | DEBUG_CATEGORIES = { 5 | 'architects': False, 6 | 'callbacks': False, 7 | 'controllers': False, 8 | 'drivers': False, 9 | 'emitters': False, 10 | 'imap': False, 11 | 'managers': False, 12 | 'workers': False, 13 | 'all': False, 14 | } 15 | 16 | # Default categories for the 'all' keyword. 17 | DEBUG_ALL_CATEGORIES = [ 18 | 'callbacks', 19 | 'controllers', 20 | 'drivers', 21 | 'emitters', 22 | 'imap', 23 | 'managers', 24 | 'workers', 25 | ] 26 | 27 | ARC = 'architects' 28 | CLB = 'callbacks' 29 | CTL = 'controllers' 30 | DRV = 'drivers' 31 | EMT = 'emitters' 32 | MGR = 'managers' 33 | WRK = 'workers' 34 | 35 | IMAP = 'imap' 36 | 37 | 38 | # Time to sleep for a response of another worker. This value is used by the edmp 39 | # module where appropriate. This allows not eating too much CPU. 40 | #TODO: expose to the rascal. 41 | SLEEP = 0.02 42 | -------------------------------------------------------------------------------- /imapfw/controllers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/controllers/__init__.py -------------------------------------------------------------------------------- /imapfw/controllers/controller.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | """ 5 | 6 | A controller is defined on a repository to control its end-driver. 7 | 8 | Controllers have the same public interface as the drivers. Each responds like a 9 | driver, does its job and relays the requests (and results) to (or from) the 10 | underlying driver. 11 | 12 | Controllers can be chained to each others so that the flows pass through each. 13 | 14 | They can either be passive or active. Passive controllers follow-up the requests 15 | as-is and returns the unchanged results. Active controllers changes the flow to 16 | achieve their tasks. IOW, each controller can only view what the underlying 17 | controller accepts to show. Hence, the order in the chain is important. 18 | 19 | The controller base "Controller" is a passive controller (see code below). 20 | 21 | The chain of controller is defined in the rascal. 22 | 23 | 24 | SCHEMATIC OVERVIEW EXAMPLE (right side) 25 | --------------------------------------- 26 | 27 | (filter) (tracker) (encoder) 28 | +----------+ +----------+ +----------+ +----------+ +----------+ 29 | | |-->| |-->| |-->| |-->| | 30 | | engine | |controller+ |controller| |controller| | driver | 31 | | |<--| |<--| |<--| |<--| | 32 | +----------+ +----------+ +----------+ +----------+ +----------+ 33 | [active] [passive] [active] 34 | notifications, UTF-7/UTF-8 35 | debugging 36 | 37 | 38 | """ 39 | 40 | from imapfw import runtime 41 | from imapfw.constants import CTL 42 | 43 | # Annotations. 44 | from typing import Union, TypeVar 45 | 46 | 47 | ControllerClass = TypeVar('Controller class and derivates') 48 | 49 | 50 | class ControllerInternalInterface(object): 51 | def fw_drive(self): raise NotImplementedError 52 | 53 | 54 | class Controller(ControllerInternalInterface): 55 | 56 | conf = {} 57 | 58 | def __init__(self, repositoryName: str, repositoryConf: dict, conf: dict): 59 | self.repositoryName = repositoryName 60 | # Merge the repository configuration with the controller configuration. 61 | self.conf = repositoryConf.copy() 62 | self.conf.update(conf.copy()) 63 | 64 | self.driver = None 65 | 66 | def __getattr__(self, name): 67 | return getattr(self.driver, name) 68 | 69 | def fw_drive(self, driver): 70 | runtime.ui.debugC(CTL, "chaining driver '%s' with controller '%s'"% 71 | (driver.getClassName(), self.getClassName())) 72 | self.driver = driver 73 | 74 | def getClassName(self): 75 | return self.__class__.__name__ 76 | 77 | def init(self): 78 | """Override this method to make initialization in the rascal.""" 79 | 80 | pass 81 | 82 | 83 | def loadController(obj: Union[ControllerClass, dict], 84 | repositoryName: str, repositoryConf: dict) -> Controller: 85 | 86 | if isinstance(obj, dict): 87 | cls_controller = obj.get('type') # Must be the controller class. 88 | controllerConf = obj.get('conf') 89 | else: 90 | cls_controller = obj 91 | controllerConf = obj.conf 92 | 93 | if not issubclass(cls_controller, Controller): 94 | raise TypeError("controller %s of %s does not derivates from" 95 | " types.controllers.Controller"% 96 | (cls_controller.__name__, repositoryName)) 97 | 98 | controller = cls_controller(repositoryName, repositoryConf, controllerConf) 99 | controller.init() 100 | 101 | return controller 102 | -------------------------------------------------------------------------------- /imapfw/controllers/duplicate.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | from imapfw import runtime 24 | 25 | from .controller import Controller 26 | 27 | #TODO 28 | class Duplicate(Controller): 29 | """Controller to duplicate writes to another driver.""" 30 | 31 | conf = None 32 | 33 | def fw_initController(self): 34 | self.duplicateDriver = None #TODO: setup driver... 35 | self.ui = runtime.ui 36 | 37 | self.mode = self.conf.get('exception').lower() # fatal, warn or pass. 38 | 39 | def __getattr__(self, name): 40 | raise AttributeError("method '%s' missing in Duplicate controller"% name) 41 | 42 | def _call(self, name, *args, **kwargs): 43 | try: 44 | values = getattr(self.duplicateDriver, name)(*args, **kwargs) 45 | except Exception as e: 46 | if self.mode == 'pass': 47 | pass 48 | elif self.mode == 'warn': 49 | self.ui.warn('TODO: warning not implemented') 50 | else: 51 | raise 52 | finally: 53 | return getattr(self.driver, name)(*args, **kwargs) 54 | 55 | def connect(self): 56 | values = self._call('connect') 57 | 58 | #TODO: implement DriverInterface. 59 | 60 | -------------------------------------------------------------------------------- /imapfw/controllers/examine.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | from .controller import Controller 24 | 25 | #TODO 26 | class ExamineController(Controller): 27 | """Controller to examine a repository.""" 28 | 29 | def __init__(self, *args, **kwargs): 30 | super(ExamineController, self).__init__(*args, **kwargs) 31 | self._report = self.conf.get('report') 32 | 33 | def fw_getReport(self): 34 | return self._report 35 | 36 | def connect(self, *args, **kwargs): 37 | self._report.title("Configuration", 2) 38 | elements = [] 39 | for k, v in self.driver.conf.items(): 40 | if k == 'password' and isinstance(v, (str, bytes)): 41 | v = '' 42 | elements.append("%s: %s"% (k, v)) 43 | self._report.list(elements) 44 | return self.driver.connect(*args, **kwargs) 45 | 46 | def getFolders(self): 47 | folders = self.driver.getFolders() 48 | self._report.title("Infos", 2) 49 | self._report.line("Found %i folders: %s"%(len(folders), folders)) 50 | self._report.line() 51 | return folders 52 | -------------------------------------------------------------------------------- /imapfw/controllers/fake.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | from imapfw.types.folder import Folders, Folder 5 | from imapfw.types.message import Messages, Message 6 | 7 | from .controller import Controller 8 | 9 | 10 | class FakeDriver(Controller): 11 | 12 | conf = None 13 | 14 | ImapConf = { 15 | 'folders': [b'INBOX', b'INBOX/spam', b'INBOX/outbox', 16 | b'INBOX/sp&AOk-cial', 17 | ] 18 | } 19 | 20 | MaildirConf = { 21 | 'folders': [b'INBOX', b'INBOX/maidir_archives'] 22 | } 23 | 24 | def __getattr__(self, name): 25 | if name.startswith('fw_'): 26 | return getattr(self.driver, name) 27 | message = ("FakeDriver %s did not handle call to '%s'"% 28 | (self.getClassName(), name)) 29 | raise AttributeError(message) 30 | 31 | def _folders(self): 32 | folders = Folders() 33 | for folderName in self.conf.get('folders'): 34 | folders.append(Folder(folderName)) 35 | return folders 36 | 37 | def connect(self): 38 | return True 39 | 40 | def getCapability(self): 41 | return ["TODO=CAPABILITY"] #TODO 42 | 43 | def getClassName(self): 44 | return self.__class__.__name__ 45 | 46 | def getDriverClassName(self): 47 | return self.driver.getClassName() 48 | 49 | def getFolders(self): 50 | return self._folders() 51 | 52 | def getNamespace(self): 53 | return "TODO" #TODO 54 | 55 | def getRepositoryName(self): 56 | return self.repositoryName 57 | 58 | def init(self): 59 | pass 60 | 61 | def isLocal(self): 62 | return self.driver.isLocal() 63 | 64 | def login(self): 65 | return True 66 | 67 | def logout(self): 68 | return True 69 | 70 | def search(self, conditions): 71 | return Messages() #TODO 72 | 73 | def select(self, folder): 74 | return True #TODO 75 | -------------------------------------------------------------------------------- /imapfw/controllers/filter.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | from .controller import Controller 24 | 25 | #TODO 26 | class Filter(Controller): 27 | """Controller to filter mails.""" 28 | pass 29 | -------------------------------------------------------------------------------- /imapfw/controllers/nametrans.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | from .controller import Controller 24 | 25 | #TODO 26 | class NameTrans(Controller): 27 | """Controller to change the folder names.""" 28 | 29 | conf = None 30 | 31 | def fw_initController(self): 32 | self._toDriverTrans = self.conf.get('toDriverTrans') 33 | self._fromDriverTrans = self.conf.get('fromDriverTrans') 34 | self._encoding = self.conf.get('encoding') 35 | 36 | def getFolders(self): 37 | folders = self.driver.getFolders() 38 | for folder in folders: 39 | name = folder.getName(self._encoding) 40 | transName = self._fromDriverTrans(name) 41 | if name != transName: 42 | folder.setName(transName, self._encoding) 43 | return folders 44 | 45 | def select(self, folder): 46 | folder.setName(folder.getName(self._ancoding), self._encoding) 47 | return self.driver.select(folder) 48 | -------------------------------------------------------------------------------- /imapfw/controllers/transcoder.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | 24 | from .controller import Controller 25 | 26 | #TODO 27 | class Transcoder(Controller): 28 | """Controller to encode/decode folder names.""" 29 | pass 30 | -------------------------------------------------------------------------------- /imapfw/drivers/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .maildir import Maildir 3 | from .imap import Imap 4 | from .driver import Driver, DriverClass, DriverInterface, loadDriver 5 | -------------------------------------------------------------------------------- /imapfw/drivers/driver.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | from imapfw.interface import Interface, implements, checkInterfaces 5 | 6 | # Annotations. 7 | from imapfw.annotation import DriverClass 8 | 9 | 10 | class DriverInterface(Interface): 11 | """The Driver base class. 12 | 13 | This is the middleware for the drivers: 14 | - this is the base class to all drivers (e.g. Maildir, Driver, etc). 15 | - does not enable controllers machinery at this point. 16 | 17 | This interface is the API to anyone working with a driver (engines, shells, 18 | etc). 19 | """ 20 | 21 | # While updating this interface think about updating the fake controller, 22 | # too. 23 | 24 | scope = Interface.PUBLIC 25 | 26 | conf = {} # The configuration of the type has to be there. 27 | local = None 28 | 29 | def getClassName(self) -> str: 30 | """Return the class name, as defined by the rascal.""" 31 | 32 | #TODO: why? 33 | def getDriverClassName(self) -> str: 34 | """Return the class name, as defined by the rascal.""" 35 | 36 | def getRepositoryName(self) -> str: 37 | """Return the repository name of this driver.""" 38 | 39 | def init(self) -> None: 40 | """Override this method to make initialization in the rascal.""" 41 | 42 | def isLocal(self) -> bool: 43 | """Return True of False whether drived data is local.""" 44 | 45 | 46 | @checkInterfaces() 47 | @implements(DriverInterface) 48 | class Driver(object): 49 | def __init__(self, repositoryName: str, conf: dict): 50 | self.repositoryName = repositoryName 51 | self.conf = conf 52 | 53 | def getClassName(self) -> str: 54 | return self.__class__.__name__ 55 | 56 | def getDriverClassName(self) -> str: 57 | return self.getClassName() 58 | 59 | def getRepositoryName(self) -> str: 60 | return self.repositoryName 61 | 62 | def init(self) -> None: 63 | pass 64 | 65 | def isLocal(self) -> bool: 66 | return self.local 67 | 68 | 69 | def loadDriver(cls_driver: DriverClass, repositoryName: str, 70 | repositoryConf: dict) -> Driver: 71 | 72 | # Build the final end-driver. 73 | if not issubclass(cls_driver, Driver): 74 | raise TypeError("driver %s of %s does not satisfy" 75 | " DriverInterface"% (cls_driver.__name__, repositoryName)) 76 | 77 | driver = cls_driver(repositoryName, repositoryConf) 78 | driver.init() 79 | 80 | return driver 81 | -------------------------------------------------------------------------------- /imapfw/drivers/imap.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | from imapfw.imap import Imap as ImapBackend 5 | from imapfw.interface import adapts, checkInterfaces 6 | 7 | from .driver import Driver, DriverInterface 8 | 9 | # Annotations. 10 | from imapfw.imap import SearchConditions, FetchAttributes 11 | from imapfw.types.folder import Folders, Folder 12 | from imapfw.types.message import Messages 13 | 14 | 15 | #TODO: remove "reverse" later: the DriverInterface must define all the 16 | # interfaces of this object. 17 | @checkInterfaces(reverse=False) 18 | @adapts(DriverInterface) 19 | class Imap(Driver): 20 | """The Imap driver, possibly redefined by the rascal.""" 21 | 22 | local = False 23 | 24 | def __init__(self, *args): 25 | super(Imap, self).__init__(*args) 26 | self.imap = ImapBackend(self.conf.get('backend')) 27 | 28 | def connect(self): 29 | host = self.conf.get('host') 30 | port = int(self.conf.get('port')) 31 | return self.imap.connect(host, port) 32 | 33 | def getCapability(self): 34 | return self.imap.getCapability() 35 | 36 | def getFolders(self) -> Folders: 37 | return self.imap.getFolders() 38 | 39 | def getMessages(self, messages: Messages, 40 | attributes: FetchAttributes) -> Messages: 41 | 42 | return self.imap.getMessages(messages, attributes) 43 | 44 | def getNamespace(self): 45 | return self.imap.getNamespace() 46 | 47 | def login(self) -> None: 48 | user = self.conf.get('username') 49 | password = self.conf.get('password') 50 | return self.imap.login(user, password) 51 | 52 | def logout(self) -> None: 53 | self.imap.logout() 54 | 55 | def searchUID(self, conditions: SearchConditions=SearchConditions()): 56 | return self.imap.searchUID(conditions) 57 | 58 | def select(self, folder: Folder) -> None: 59 | return self.imap.select(folder) 60 | 61 | #def append(self, server, mail): 62 | #response = server.append(mail) 63 | #return response 64 | 65 | #def update(self, server, mail): 66 | #response = server.update(mail) 67 | #return response 68 | 69 | #def fetch(self, server, uids): 70 | #response = server.fetch(uids) 71 | #return response 72 | -------------------------------------------------------------------------------- /imapfw/drivers/maildir.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | import os 24 | 25 | from imapfw import runtime 26 | from imapfw.toolkit import expandPath 27 | from imapfw.error import DriverFatalError 28 | from imapfw.constants import DRV 29 | from imapfw.types.folder import Folders, Folder 30 | from imapfw.interface import adapts, checkInterfaces 31 | 32 | from .driver import Driver, DriverInterface 33 | 34 | 35 | 36 | #TODO: remove this later: the DriverInterface must define the interfaces of 37 | # this object. 38 | @checkInterfaces(reverse=False) 39 | @adapts(DriverInterface) 40 | class Maildir(Driver): 41 | """Exposed Maildir driver, possibly redefined by the rascal.""" 42 | 43 | local = True 44 | 45 | def __init__(self, *args): 46 | super(Maildir, self).__init__(*args) 47 | self._folders = None 48 | 49 | def _debug(self, msg): 50 | runtime.ui.debugC(DRV, "driver of %s %s"% (self.getRepositoryName(), msg)) 51 | 52 | def _recursiveScanMaildir(self, relativePath=None): 53 | """Scan the Maildir recusively. 54 | 55 | Updates self._folders with what is found in the configured maildir path. 56 | TODO: fix encoding. 57 | 58 | :args: 59 | - maildirPath: path to the Maildir (as defined in the conf). 60 | """ 61 | 62 | def isFolder(path): 63 | return (os.path.isdir(os.path.join(path, 'cur')) and 64 | os.path.isdir(os.path.join(path, 'new')) and 65 | os.path.isdir(os.path.join(path, 'tmp')) 66 | ) 67 | 68 | def scanChildren(path, relativePath): 69 | for directory in os.listdir(path): 70 | if directory in ['cur', 'new', 'tmp']: 71 | continue # Ignore special directories ASAP. 72 | 73 | folderPath = os.path.join(path, directory) 74 | if not os.path.isdir(folderPath): 75 | continue 76 | 77 | if relativePath is None: 78 | newRelativePath = directory 79 | else: 80 | newRelativePath = os.path.join(relativePath, directory) 81 | 82 | self._recursiveScanMaildir(newRelativePath) # Recurse! 83 | 84 | # Fix local variables to their default values if needed. 85 | maildirPath = self.conf.get('path') 86 | sep = self.conf.get('sep') 87 | 88 | # Set the fullPath. 89 | if relativePath is None: 90 | fullPath = maildirPath 91 | else: 92 | fullPath = os.path.join(maildirPath, relativePath) 93 | 94 | if isFolder(fullPath): 95 | #TODO: get encoding from conf. 96 | if relativePath is None: 97 | # We are the root of the maildir. Fix the name to '/'. 98 | folder = Folder('/', encoding='UTF-8') 99 | else: 100 | # Fix separator to '/' ASAP. ,-) 101 | folder = Folder('/'.join(relativePath.split(sep)), encoding='UTF-8') 102 | self._folders.append(folder) 103 | 104 | if sep == '/': # Recurse if nested folders are allowed. 105 | scanChildren(fullPath, relativePath) 106 | else: 107 | # The maildirPath as given by the user might not be a real maildir 108 | # but a base path of maildirs. Scan this path. 109 | if relativePath is None: 110 | scanChildren(fullPath, relativePath) 111 | 112 | def connect(self): 113 | path = expandPath(self.conf.get('path')) 114 | try: 115 | os.mkdir(path) 116 | except FileExistsError: 117 | pass 118 | except FileNotFoundError: 119 | raise DriverFatalError( 120 | "parent directory of '%s' does not exists"% path) 121 | if not os.path.isdir(path): 122 | raise DriverFatalError("path is not a directory: %s"% path) 123 | self.conf['path'] = path # Record expanted path. 124 | return True 125 | 126 | def getFolders(self): 127 | self._folders = Folders() # Erase whatever we had. 128 | self._debug('scanning folders') 129 | self._recursiveScanMaildir() # Put result into self._folders. 130 | return self._folders 131 | 132 | def select(self, mailbox): 133 | #TODO 134 | return True 135 | 136 | def logout(self): 137 | self._debug('logging out') 138 | -------------------------------------------------------------------------------- /imapfw/engines/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .account import SyncAccounts 3 | from .folder import SyncFolders 4 | -------------------------------------------------------------------------------- /imapfw/engines/account.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015-2016, Nicolas Sebrecht & contributors. 3 | 4 | """ 5 | 6 | Engines to work with accounts. 7 | 8 | """ 9 | 10 | from imapfw import runtime 11 | from imapfw.edmp import Channel 12 | from imapfw.types.folder import Folders 13 | from imapfw.types.account import loadAccount 14 | 15 | from .engine import SyncEngine, EngineInterface 16 | 17 | # Interfaces. 18 | from imapfw.interface import implements, checkInterfaces 19 | 20 | # Annotations. 21 | from imapfw.edmp import Emitter 22 | from imapfw.concurrency import Queue 23 | from imapfw.types.account import Account 24 | 25 | 26 | @checkInterfaces() 27 | @implements(EngineInterface) 28 | class SyncAccounts(SyncEngine): 29 | """The sync account engine.""" 30 | 31 | def __init__(self, workerName: str, referent: Emitter, 32 | left: Emitter, right: Emitter): 33 | super(SyncAccounts, self).__init__(workerName) 34 | 35 | self.referent = referent 36 | self.left = left 37 | self.rght = right 38 | 39 | # Outlined. 40 | def _syncAccount(self, account: Account): 41 | """Sync one account.""" 42 | 43 | accountName = account.getClassName() 44 | runtime.ui.infoL(3, "merging folders for %s"% accountName) 45 | 46 | # Get the repository instances from the rascal. 47 | leftRepository = account.fw_getLeft() 48 | rghtRepository = account.fw_getRight() 49 | 50 | self.left.buildDriver(accountName, 'left') 51 | self.rght.buildDriver(accountName, 'right') 52 | 53 | # Connect the drivers. 54 | self.left.connect() 55 | self.rght.connect() 56 | 57 | self.left.getFolders() 58 | self.rght.getFolders() 59 | 60 | # Get the folders from both sides so we can feed the folder tasks. 61 | leftFolders = self.left.getFolders_sync() 62 | rghtFolders = self.rght.getFolders_sync() 63 | 64 | # Merge the folder lists. 65 | mergedFolders = Folders() 66 | for sideFolders in [leftFolders, rghtFolders]: 67 | for folder in sideFolders: 68 | if folder not in mergedFolders: 69 | mergedFolders.append(folder) 70 | 71 | runtime.ui.infoL(3, "%s merged folders %s"% 72 | (accountName, mergedFolders)) 73 | 74 | # Pass the list to the rascal. 75 | rascalFolders = account.syncFolders(mergedFolders) 76 | 77 | # The rascal might request for non-existing folders! 78 | syncFolders = Folders() 79 | ignoredFolders = Folders() 80 | for folder in rascalFolders: 81 | if folder in mergedFolders: 82 | syncFolders.append(folder) 83 | else: 84 | ignoredFolders.append(folder) 85 | 86 | if len(ignoredFolders) > 0: 87 | runtime.ui.warn("rascal, you asked to sync non-existing folders" 88 | " for '%s': %s", accountName, ignoredFolders) 89 | 90 | if len(syncFolders) < 1: 91 | runtime.ui.infoL(3, "%s: no folder to sync"% accountName) 92 | return # Nothing more to do. 93 | 94 | #TODO: make max_connections mandatory in rascal. 95 | maxFolderWorkers = min( 96 | len(syncFolders), 97 | rghtRepository.conf.get('max_connections'), 98 | leftRepository.conf.get('max_connections')) 99 | 100 | runtime.ui.infoL(3, "%s syncing folders %s"% (accountName, syncFolders)) 101 | 102 | # Syncing folders is not the job of this engine. Use sync mode to ensure 103 | # the referent starts syncing of folders before this engine stops. 104 | self.referent.syncFolders_sync( 105 | accountName, maxFolderWorkers, syncFolders) 106 | 107 | # Wait for all the folders to be synced before processing the next 108 | # account. 109 | while self.referent.areSyncFoldersDone_sync() is not True: 110 | pass 111 | 112 | def run(self, taskQueue: Queue) -> None: 113 | """Sequentially process the accounts.""" 114 | 115 | # 116 | # Loop over the available account names. 117 | # 118 | for accountName in Channel(taskQueue): 119 | # The syncer let explode errors it can't recover from. 120 | try: 121 | self.processing(accountName) 122 | # Get the account instance from the rascal. 123 | account = loadAccount(accountName) 124 | self._syncAccount(account) # Wait until folders are done. 125 | self.setExitCode(0) 126 | #TODO: Here, we only keep max exit code. Would worth using the 127 | # rascal at the end of the process for each account. 128 | 129 | except Exception as e: 130 | runtime.ui.error("could not sync account %s"% accountName) 131 | runtime.ui.exception(e) 132 | #TODO: honor rascal! 133 | self.setExitCode(10) # See manual. 134 | 135 | self.checkExitCode() # Sanity check. 136 | self.referent.accountEngineDone(self.getExitCode()) 137 | -------------------------------------------------------------------------------- /imapfw/engines/engine.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015-2016, Nicolas Sebrecht & contributors. 3 | 4 | """ 5 | 6 | Engines are users of the the drivers. The logic to apply to the data is put in 7 | the engines, e.g. a 3-way merge engine. They are likely put in their own worker 8 | and should mostly send events to work "with the outside" (the other workers). 9 | 10 | Engines might be rather complex. Hence, they aren't meant at being exposed in 11 | the rascal and best practices must be applied to the code. 12 | 13 | Engines follows the components design pattern because factoring engine code is 14 | not easy. 15 | 16 | IOW, engines here are meant to be parent objects. Each is a pure data container 17 | and provide some default processing code for the behaviour. It is the component. 18 | 19 | Good components modeling seperate each related data into one simple and 20 | dedicated component which is easy to re-use. Combine compenents into object 21 | (called entities) to use them in the application. 22 | 23 | """ 24 | 25 | from imapfw import runtime 26 | 27 | #TODO: engines debug logs. 28 | from imapfw.constants import WRK 29 | 30 | # Interfaces. 31 | from imapfw.interface import Interface, implements, checkInterfaces 32 | 33 | # Annotations. 34 | from imapfw.edmp import Emitter 35 | from imapfw.concurrency import Queue 36 | 37 | 38 | class EngineInterface(Interface): 39 | 40 | scope = Interface.INTERNAL 41 | 42 | def run(self, taskQueue: Queue) -> None: 43 | """Run the engine.""" 44 | 45 | 46 | class SyncEngineInterface(Interface): 47 | 48 | scope = Interface.INTERNAL 49 | 50 | def checkExitCode(self) -> None: 51 | """Check exit code.""" 52 | 53 | def debug(self, msg: str) -> None: 54 | """Debug logging.""" 55 | 56 | def getExitCode(self) -> int: 57 | """Get exit code.""" 58 | 59 | def processing(self, task: str) -> None: 60 | """Log what is processed by the engine.""" 61 | 62 | def setExitCode(self, exitCode: int) -> None: 63 | """Set exit code.""" 64 | 65 | 66 | @checkInterfaces() 67 | @implements(SyncEngineInterface) 68 | class SyncEngine(object): 69 | def __init__(self, workerName: str): 70 | self._exitCode = -1 # Force the run to set a valid exit code. 71 | self._gotTask = False 72 | self.workerName = workerName 73 | 74 | def checkExitCode(self) -> None: 75 | if self._gotTask is False: 76 | self.setExitCode(0) 77 | else: 78 | if self._exitCode < 0: 79 | runtime.ui.critical("%s exit code was not set correctly"% 80 | self.workerName) 81 | self.setExitCode(99) 82 | 83 | def debug(self, msg: str) -> None: 84 | runtime.ui.debugC(WRK, "%s: %s"% (self.workerName, msg)) 85 | 86 | def getExitCode(self) -> int: 87 | return self._exitCode 88 | 89 | def processing(self, task: str) -> None: 90 | runtime.ui.infoL(2, "%s processing: %s"% (self.workerName, task)) 91 | self._gotTask = True 92 | 93 | def setExitCode(self, exitCode: int) -> None: 94 | if exitCode > self._exitCode: 95 | self._exitCode = exitCode 96 | -------------------------------------------------------------------------------- /imapfw/engines/folder.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015-2016, Nicolas Sebrecht & contributors. 3 | 4 | """ 5 | 6 | Engines to work with folders/maiboxes. 7 | 8 | """ 9 | 10 | from imapfw import runtime 11 | from imapfw.edmp import Channel 12 | from imapfw.types.account import loadAccount 13 | 14 | from .engine import SyncEngine, EngineInterface, SyncEngineInterface 15 | 16 | # Interfaces. 17 | from imapfw.interface import implements, adapts, checkInterfaces 18 | 19 | # Annotations. 20 | from imapfw.edmp import Emitter 21 | from imapfw.concurrency import Queue 22 | from imapfw.types.folder import Folder 23 | 24 | 25 | @checkInterfaces() 26 | @adapts(SyncEngine) 27 | @implements(EngineInterface) 28 | class SyncFolders(SyncEngine): 29 | """The engine to sync a folder in a worker.""" 30 | 31 | def __init__(self, workerName: str, referent: Emitter, 32 | left: Emitter, right: Emitter, accountName: str): 33 | 34 | super(SyncFolders, self).__init__(workerName) 35 | self.referent = referent 36 | self.left = left 37 | self.rght = right 38 | self.accountName = accountName 39 | 40 | def _infoL(self, level, msg): 41 | runtime.ui.infoL(level, "%s %s"% (self.workerName, msg)) 42 | 43 | # Outlined. 44 | def _syncFolder(self, folder: Folder) -> int: 45 | """Sync one folder.""" 46 | 47 | # account = loadAccount(self.accountName) 48 | # leftRepository = account.fw_getLeft() 49 | # rightRepository = account.fw_getRight() 50 | 51 | if self.left.isDriverBuilt_sync() is False: 52 | self.left.buildDriver(self.accountName, 'left') 53 | if self.rght.isDriverBuilt_sync() is False: 54 | self.rght.buildDriver(self.accountName, 'right') 55 | 56 | self.left.connect() 57 | self.rght.connect() 58 | 59 | self.left.select_sync(folder) 60 | self.rght.select_sync(folder) 61 | 62 | return 0 63 | 64 | def run(self, taskQueue: Queue) -> None: 65 | """Runner for the sync folder engine. 66 | 67 | Sequentially process the folders.""" 68 | 69 | # 70 | # Loop over the available folder names. 71 | # 72 | for folder in Channel(taskQueue): 73 | self.processing(folder) 74 | 75 | # The engine will let explode errors it can't recover from. 76 | try: 77 | exitCode = self._syncFolder(folder) 78 | self.setExitCode(exitCode) 79 | 80 | except Exception as e: 81 | runtime.ui.error("could not sync folder %s"% folder) 82 | runtime.ui.exception(e) 83 | #TODO: honor hook! 84 | self.setExitCode(10) # See manual. 85 | 86 | self.checkExitCode() 87 | self.referent.stop(self.getExitCode()) 88 | -------------------------------------------------------------------------------- /imapfw/error.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | 24 | class DriverFatalError(Exception): pass # For drivers. 25 | -------------------------------------------------------------------------------- /imapfw/imap/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .imap import Imap, SearchConditions, FetchAttributes 3 | -------------------------------------------------------------------------------- /imapfw/imap/imapc/interface.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | class IMAPcInterface(object): 24 | pass #TODO 25 | 26 | -------------------------------------------------------------------------------- /imapfw/init.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | import sys 5 | import traceback 6 | 7 | from imapfw import runtime 8 | from imapfw.conf import ImapfwConfig, Parser 9 | from imapfw.toolkit import runHook 10 | 11 | 12 | class Imapfw(object): 13 | def run(self): 14 | config = ImapfwConfig() 15 | try: 16 | config.parseCLI() # Parse CLI options. 17 | config.setupConcurrency() # Exports concurrency to runtime module. 18 | config.setupUI() # Exports ui to the runtime module. 19 | except Exception as e: 20 | traceback.print_exc(file=sys.stderr) 21 | sys.exit(1) 22 | 23 | try: 24 | config.loadRascal() # Exports the rascal to the runtime module. 25 | except FileNotFoundError as e: 26 | runtime.ui.critical(e) 27 | except Exception: 28 | raise #FIXME 29 | sys.exit(2) 30 | 31 | rascal = runtime.rascal 32 | # The rascal must use the thread-safe ui, too! 33 | if rascal is not None: 34 | rascalConfigure = rascal.getFunction('configure') 35 | rascalConfigure(runtime.ui) 36 | 37 | # "Action", you said? Do you really want action? 38 | # Fine... 39 | try: 40 | actionName, cls_action = config.getAction() 41 | except KeyError as e: 42 | runtime.ui.critical("unkown action: %s"% e) 43 | sys.exit(1) 44 | action = cls_action() 45 | 46 | if action.requireRascal is True and rascal is None: 47 | runtime.ui.critical( 48 | "a rascal is required but is not defined, use '-r'.") 49 | sys.exit(2) 50 | 51 | try: 52 | # PreHook. 53 | if action.honorHooks is True: 54 | timedout = runHook(rascal.getPreHook(), actionName, Parser) 55 | if timedout: 56 | runtime.ui.critical("preHook reached timeout") 57 | sys.exit(4) 58 | 59 | # Doing the job. 60 | action.init(Parser) 61 | action.run() 62 | 63 | # PostHook. 64 | if action.honorHooks is True: 65 | timedout = runHook(rascal.getPostHook()) 66 | if timedout: 67 | runtime.ui.error('postHook reached timed out') 68 | except Exception as e: 69 | def outputException(error, message): 70 | runtime.ui.critical(message) 71 | import traceback, sys 72 | runtime.ui.exception(error) 73 | traceback.print_exc(file=sys.stdout) 74 | 75 | # ExceptionHook. 76 | try: 77 | if action.honorHooks is True: 78 | timedout = runHook(rascal.getExceptionHook(), e) 79 | if timedout: 80 | runtime.ui.error('exceptionHook reached timeout') 81 | except Exception as hookError: 82 | outputException(hookError, "exception occured while running" 83 | " exceptionHook: %s"% str(hookError)) 84 | 85 | # Let the Action instance know the exception. 86 | try: 87 | # This way, exceptions can be handled in a per-action basis. 88 | action.exception(e) 89 | except Exception as actionError: 90 | outputException(actionError, "exception occured while running" 91 | " internal 'action.exception()': %s"% str(actionError)) 92 | raise #TODO: raise only unkown errors. 93 | 94 | sys.exit(action.getExitCode()) 95 | -------------------------------------------------------------------------------- /imapfw/mmp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/mmp/__init__.py -------------------------------------------------------------------------------- /imapfw/mmp/account.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | 24 | from .manager import Manager 25 | 26 | from ..engines.account import SyncAccount 27 | from ..constants import MGR 28 | 29 | 30 | class AccountManager(Manager): 31 | """The account manager to implement the receiver and define the emitter API. 32 | 33 | This does NOT defines what the worker actually does since this is the purpose 34 | of the engines. 35 | 36 | The only available emitter API is 'action'. 37 | 38 | The code of this object is run by the receiver into the account worker. 39 | All the returned values are sent from the receiver to the emitter.""" 40 | 41 | 42 | def __init__(self, workerName, accountTasks, leftEmitter, rightEmitter): 43 | super(AccountManager, self).__init__() 44 | 45 | self._workerName = workerName 46 | self._accountTasks = accountTasks 47 | self._leftEmitter = leftEmitter 48 | self._rightEmitter = rightEmitter 49 | 50 | self._engine = None 51 | 52 | def _debug(self, msg): 53 | self.ui.debugC(MGR, "%s: %s"% (self._name, msg)) 54 | 55 | def ex_action_getNextAccountName(self, engineName): 56 | self._accountName = self._accountTasks.get_nowait() 57 | 58 | if self._accountName is None: 59 | return False # Flag that there is no more task. 60 | 61 | # Build the engine. 62 | if engineName is 'SyncAccount': 63 | # Build the syncAccount engine which consumes the accountTasks. 64 | self._engine = SyncAccount( 65 | self._workerName, 66 | self._leftEmitter, 67 | self._rightEmitter, 68 | ) 69 | 70 | return True # Receiver is ready for a run. 71 | 72 | def ex_action_run(self): 73 | self.ui.debug('would run the engine') 74 | return None, None, None 75 | self._engine.run(self._accountName) 76 | return self._engine.getResults() 77 | 78 | def ex_action_stopServing(self): 79 | self.stopServing() 80 | -------------------------------------------------------------------------------- /imapfw/mmp/driver.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | from imapfw import runtime 24 | 25 | from .manager import Manager 26 | 27 | from ..constants import DRV 28 | from ..types.repository import RepositoryInterface 29 | 30 | 31 | #TODO: catch exceptions? 32 | class DriverManager(Manager): 33 | """The driver manager. 34 | 35 | The receiver builds and runs the low-level driver (from rascal api.drivers). 36 | 37 | All the api.drivers types use the same DriverInterface interface. This 38 | interface is low-level interface which is not directly mapped as the public 39 | DriverManager interface. 40 | 41 | The user of the emitter controls both the worker and the driver. The user of 42 | the emitter might over time. 43 | 44 | All the code here runs inside the worker.""" 45 | 46 | def __init__(self, workerName): 47 | super(DriverManager, self).__init__() 48 | 49 | self._workerName = workerName 50 | 51 | self.ui = runtime.ui 52 | self.rascal = runtime.rascal 53 | 54 | self._driver = None # Might change over time. 55 | self._folders = None 56 | 57 | self.ui.debugC(DRV, "%s manager created"% workerName) 58 | 59 | def connect(self, repositoryName): 60 | """Connect the driver for this repository (name).""" 61 | 62 | repository = self.rascal.get(repositoryName, [RepositoryInterface]) 63 | repository.fw_init() 64 | 65 | # Build the driver. 66 | driver = repository.fw_chainControllers() 67 | driver.fw_init(repository.conf, repositoryName) # Initialize. 68 | driver.fw_sanityChecks(driver) # Catch common errors early. 69 | 70 | self.ui.debugC(DRV, "built driver '{}' for '{}'", 71 | driver.getName(), repositoryName) 72 | self.ui.debugC(DRV, "'{}' has conf {}", repositoryName, driver.conf) 73 | 74 | # Ready, connect. 75 | if driver.isLocal: 76 | self.ui.debugC(DRV, '{} working in {}', driver.getOwnerName(), 77 | driver.conf.get('path')) 78 | else: 79 | self.ui.debugC(DRV, '{} connecting to {}:{}', 80 | driver.getOwnerName(), driver.conf.get('host'), 81 | driver.conf.get('port')) 82 | 83 | connected = driver.connect() 84 | self.ui.debugC(DRV, "driver of {} connected", driver.getOwnerName()) 85 | if connected: 86 | self._driver = driver 87 | else: 88 | raise Exception("%s: driver could not connect"% self._workerName) 89 | 90 | def ex_account_connect(self): 91 | self.connect() 92 | 93 | def ex_account_fetchFolders(self): 94 | self.ui.debugC(DRV, "driver of {} starts fetching of folders", 95 | self._driver.getOwnerName()) 96 | self._folders = self._driver.getFolders() 97 | 98 | def ex_account_getFolders(self): 99 | self.ui.debugC(DRV, "driver of {} got folders: {}", 100 | self._driver.getOwnerName(), self._folders) 101 | return self._folders 102 | 103 | def ex_account_logout(self): 104 | self._driver.logout() 105 | self.ui.debugC(DRV, "driver of {} logged out", self._driver.getOwnerName()) 106 | self._driver = None 107 | 108 | def ex_architect_stopServing(self): 109 | self.stopServing() 110 | -------------------------------------------------------------------------------- /imapfw/mmp/folder.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | from imapfw import runtime 24 | 25 | from .manager import Manager 26 | 27 | 28 | class FolderManager(Manager): 29 | def __init__(self): 30 | super(FolderManager, self).__init__() 31 | 32 | self.rascal = runtime.rascal 33 | -------------------------------------------------------------------------------- /imapfw/mmp/serializer.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | 24 | class SerializerInterface(object): 25 | """Allow sending serialized objects between workers.""" 26 | 27 | def dump(self): raise NotImplementedError 28 | def load(self, serialized): raise NotImplementedError 29 | -------------------------------------------------------------------------------- /imapfw/rascal.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | import imp #TODO: use library importlib instead of deprecated imp. 5 | 6 | from imapfw.api import types 7 | 8 | # Annotations. 9 | from typing import List, TypeVar 10 | 11 | 12 | Function = TypeVar('Function') 13 | Class = TypeVar('Class') 14 | 15 | 16 | class Rascal(object): 17 | """The Rascal. 18 | 19 | Turn the rascal (the user Python file given at CLI) into a more concrete 20 | thing (a Python module). 21 | 22 | This is where the Inversion of Control happen: we give to the rascal the 23 | illusion he's living a real life while we keep full control of him.""" 24 | 25 | def __init__(self): 26 | self._rascal = {} # The module. 27 | # Cached literals. 28 | self._mainConf = None 29 | 30 | def _isDict(self, obj: object) -> bool: 31 | try: 32 | return type(obj) == dict 33 | except: 34 | raise TypeError("'%s' must be a dictionnary, got '%s'"% 35 | (obj.__name__, type(obj))) 36 | 37 | def _getHook(self, name: str) -> Function: 38 | try: 39 | return self.getFunction(name) 40 | except: 41 | return lambda hook, *args: hook.ended() 42 | 43 | def _getLiteral(self, name: str) -> type: 44 | return getattr(self._rascal, name) 45 | 46 | def get(self, name: str, expectedTypes: List[Class]): 47 | cls = self._getLiteral(name) 48 | 49 | for expectedType in expectedTypes: 50 | if issubclass(cls, expectedType): 51 | return cls 52 | 53 | raise TypeError("class '%s' is not a sub-class of '%s'"% 54 | (name, expectedTypes)) 55 | 56 | def getAll(self, targetTypes: List[Class]) -> List[Class]: 57 | classes = [] 58 | for literal in dir(self._rascal): 59 | if literal.startswith('_'): 60 | continue 61 | try: 62 | classes.append(self.get(literal, targetTypes)) 63 | except TypeError: 64 | pass 65 | return classes 66 | 67 | def getExceptionHook(self) -> Function: 68 | return self._getHook('exceptionHook') 69 | 70 | def getFunction(self, name: str) -> Function: 71 | func = self._getLiteral(name) 72 | if not callable(func): 73 | raise TypeError("function expected for '%s'"% name) 74 | return func 75 | 76 | def getMaxConnections(self, accountName: str) -> int: 77 | def getValue(repository): 78 | try: 79 | return int(repository.conf.get('max_connections')) 80 | except AttributeError: 81 | return 999 82 | 83 | account = self.get(accountName, [types.Account]) 84 | max_sync = min(getValue(account.left), 85 | getValue(account.right)) 86 | return max_sync 87 | 88 | def getMaxSyncAccounts(self) -> int: 89 | return int(self._mainConf.get('max_sync_accounts')) 90 | 91 | def getPostHook(self) -> Function: 92 | return self._getHook('postHook') 93 | 94 | def getPreHook(self) -> Function: 95 | return self._getHook('preHook') 96 | 97 | def getSettings(self, name: str) -> dict: 98 | literal = getattr(self._rascal, name) 99 | if not isinstance(literal, dict): 100 | raise TypeError("expected dict for '%s', got '%s'"% 101 | (name, type(literal))) 102 | return literal 103 | 104 | def load(self, path: str) -> None: 105 | # Create empty module. 106 | rascal_mod = imp.new_module('rascal') 107 | rascal_mod.__file__ = path 108 | 109 | with open(path) as rascal_file: 110 | exec(compile( 111 | rascal_file.read(), path, 'exec'), rascal_mod.__dict__) 112 | self._rascal = rascal_mod 113 | 114 | self._mainConf = self.getSettings('MainConf') 115 | 116 | # Turn accounts definitions from MainConf into global of rascal 117 | # literals. 118 | if 'accounts' in self._mainConf: 119 | for accountDict in self._mainConf.get('accounts'): 120 | setattr(self._rascal, accountDict.get('name'), accountDict) 121 | -------------------------------------------------------------------------------- /imapfw/runners/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .driver import DriverRunner 3 | from .toprunner import topRunner 4 | -------------------------------------------------------------------------------- /imapfw/runners/driver.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015-2016, Nicolas Sebrecht & contributors. 3 | 4 | import inspect 5 | 6 | from imapfw import runtime 7 | from imapfw.constants import DRV 8 | from imapfw.types.account import loadAccount 9 | from imapfw.types.repository import Repository, loadRepository 10 | 11 | # Annotations. 12 | from imapfw.edmp import Receiver 13 | 14 | 15 | #TODO: catch exceptions? 16 | class DriverRunner(object): 17 | """The Driver to make use of any driver (with the controllers). 18 | 19 | Runs a complete low-level driver in a worker. 20 | 21 | The low-level drivers and controllers use the same low-level interface which 22 | is directly exposed to the engine. 23 | 24 | Also, this runner allows to re-use any running worker with different 25 | repositories during its lifetime. This feature is a required by design. 26 | """ 27 | 28 | #FIXME: unused workerName 29 | def __init__(self, workerName: str, receiver: Receiver): 30 | self.receiver = receiver 31 | 32 | self.repositoryName = None 33 | self.driver = None # Might change over time. 34 | self.repositoryName = 'UNKOWN_REPOSITORY' 35 | self.driver = None 36 | 37 | def _debug(self, msg: str) -> None: 38 | runtime.ui.debugC(DRV, "%s %s"% (self.repositoryName, msg)) 39 | 40 | def _debugBuild(self): 41 | runtime.ui.debugC(DRV, "built driver '{}' for '{}'", 42 | self.driver.getClassName(), self.driver.getRepositoryName()) 43 | runtime.ui.debugC(DRV, "'{}' has conf {}", 44 | self.repositoryName, self.driver.conf) 45 | 46 | def _driverAccept(self) -> None: 47 | for name, method in inspect.getmembers(self.driver, inspect.ismethod): 48 | if name.startswith('_') or name.startswith('fw_'): 49 | continue 50 | 51 | #FIXME: we should clear previous accepted events. 52 | self.receiver.accept(name, method) 53 | 54 | def _info(self, msg: str) -> None: 55 | runtime.ui.info("%s %s"% (self.repositoryName, msg)) 56 | 57 | def _buildDriver(self, repository: Repository) -> None: 58 | self.repositoryName = repository.getClassName() 59 | self.driver = repository.fw_getDriver() 60 | self._driverAccept() 61 | self._debugBuild() 62 | self._info("driver ready!") 63 | 64 | def buildDriver(self, accountName: str, side: str, 65 | reuse: bool=False) -> None: 66 | """Build the driver object in the worker from this account side.""" 67 | 68 | if reuse is True and self.driver is not None: 69 | return None 70 | 71 | account = loadAccount(accountName) 72 | repository = account.fw_getSide(side) 73 | self._buildDriver(repository) 74 | 75 | def buildDriverFromRepositoryName(self, repositoryName: str, 76 | reuse: bool=False) -> None: 77 | """Build the driver object in the worker from this repository name. 78 | 79 | The repository must be globally defined in the rascal.""" 80 | 81 | if reuse is True and self.driver is not None: 82 | return None 83 | 84 | cls_repository = runtime.rascal.get(repositoryName, [Repository]) 85 | repository = loadRepository(cls_repository) 86 | self._buildDriver(repository) 87 | 88 | def isDriverBuilt(self) -> bool: 89 | return self.driver is not None 90 | 91 | def logout(self) -> None: 92 | """Logout from server. Allows to be called more than once.""" 93 | 94 | if self.driver is not None: 95 | self.driver.logout_sync() 96 | self._debug("logged out") 97 | self.driver = None 98 | return True 99 | 100 | def run(self) -> None: 101 | runtime.ui.debugC(DRV, "manager running") 102 | 103 | # Bind all public methods to events. 104 | for name in ['buildDriver', 'buildDriverFromRepositoryName', 105 | 'isDriverBuilt', 'logout']: 106 | self.receiver.accept(name, getattr(self, name)) 107 | 108 | while self.receiver.react(): 109 | pass 110 | -------------------------------------------------------------------------------- /imapfw/runners/toprunner.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | from imapfw import runtime 24 | from imapfw.constants import WRK 25 | 26 | 27 | def topRunner(workerName, runner, *args): 28 | ui = runtime.ui 29 | ui.debugC(WRK, "[runner] %s starts"% workerName) 30 | try: 31 | runner(*args) 32 | except Exception as e: 33 | ui.error("[runner] %s interrupted: %s"% (workerName, e)) 34 | ui.exception(e) 35 | ui.debugC(WRK, "[runner] %s stopped"% workerName) 36 | -------------------------------------------------------------------------------- /imapfw/runtime.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | """ 5 | 6 | The module to handle modules configured at runtime. 7 | 8 | Once modules are set, can be used like this: 9 | 10 | import runtime 11 | 12 | def whatever(): 13 | ui = runtime.ui 14 | 15 | Since the import is done at import time, this won't work: 16 | 17 | import runtime 18 | ui = runtime.ui # Gets None. 19 | 20 | """ 21 | 22 | import sys 23 | 24 | 25 | class CacheUI(object): 26 | def __init__(self): 27 | self.number = 0 28 | self.cached = {} 29 | self.lastName = None 30 | 31 | def __getattr__(self, name): 32 | self.lastName = name 33 | return self.cache 34 | 35 | def _getNumber(self): 36 | self.number += 1 37 | return self.number 38 | 39 | def cache(self, *args, **kwargs): 40 | self.cached[self._getNumber()] = (self.lastName, args, kwargs) 41 | 42 | def unCache(self, ui): 43 | for cached in self.cached.values(): 44 | name, args, kwargs = cached 45 | getattr(ui, name)(*args, **kwargs) 46 | 47 | 48 | # Put this runtime module into _this variable so we use setattr. 49 | _this = sys.modules.get(__name__) 50 | 51 | ui = CacheUI() # Cache logs until true UI is set. 52 | concurrency = None 53 | rascal = None 54 | 55 | def set_module(name, mod): 56 | if name == 'ui': 57 | previousUI = getattr(_this, 'ui') 58 | try: 59 | previousUI.unCache(mod) 60 | except AttributeError: 61 | pass 62 | setattr(_this, name, mod) 63 | -------------------------------------------------------------------------------- /imapfw/shells/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .shell import Shell, DriveDriver 3 | -------------------------------------------------------------------------------- /imapfw/shells/shell.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | 5 | from imapfw import runtime 6 | from imapfw.interface import implements, Interface, checkInterfaces 7 | 8 | 9 | class ShellInterface(Interface): 10 | 11 | scope = Interface.PUBLIC 12 | 13 | def afterSession(self) -> int: 14 | """What to do on exit. Return the exit code.""" 15 | 16 | def beforeSession(self) -> None: 17 | """Method to set up the environment.""" 18 | 19 | def configureCompletion(self) -> None: 20 | """Configure the complement for theinteractive session.""" 21 | 22 | def interactive(self) -> None: 23 | """Start the interactive session when called.""" 24 | 25 | def register(self, name: str, alias: str=None) -> None: 26 | """Add a variable to the interactive environment. 27 | 28 | Attribute name to pass to the interpreter. The name MUST be an 29 | attribute of this object.""" 30 | 31 | def session(self) -> None: 32 | """Build driver and start interactive mode.""" 33 | 34 | def setBanner(self, banner: str) -> None: 35 | """Erase the default banner.""" 36 | 37 | 38 | @checkInterfaces() 39 | @implements(ShellInterface) 40 | class Shell(object): 41 | 42 | conf = None 43 | 44 | def __init__(self): 45 | self._env = {} 46 | self.banner = "Welcome" 47 | 48 | def afterSession(self) -> int: 49 | return 0 50 | 51 | def configureCompletion(self) -> None: 52 | try: 53 | from jedi.utils import setup_readline 54 | setup_readline() 55 | except ImportError: 56 | # Fallback to the stdlib readline completer if it is installed. 57 | # Taken from http://docs.python.org/2/library/rlcompleter.html 58 | runtime.ui.info("jedi is not installed, falling back to readline" 59 | " for completion") 60 | try: 61 | import readline 62 | import rlcompleter 63 | readline.parse_and_bind("tab: complete") 64 | except ImportError: 65 | runtime.ui.info("readline is not installed either." 66 | " No tab completion is enabled.") 67 | 68 | def beforeSession(self) -> None: 69 | pass 70 | 71 | def interactive(self) -> None: 72 | import code 73 | try: 74 | code.interact(banner=self.banner, local=self._env) 75 | except: 76 | pass 77 | 78 | def register(self, name: str, alias: str=None) -> None: 79 | if alias is None: 80 | alias = name 81 | self._env[alias] = getattr(self, name) 82 | 83 | def session(self) -> None: 84 | self.interactive() 85 | 86 | def setBanner(self, banner: str) -> None: 87 | self.banner = banner 88 | 89 | 90 | class DriveDriverInterface(Interface): 91 | def buildDriver(self) -> None: 92 | """Build the driver for the repository in conf.""" 93 | 94 | @checkInterfaces() 95 | @implements(ShellInterface, DriveDriverInterface) 96 | class DriveDriver(Shell): 97 | """Shell to play with a repository. Actually drive the driver yourself. 98 | 99 | The conf must define the repository to use (str). Start it to learn more. 100 | 101 | ``` 102 | conf = { 'repository': RepositoryClass } 103 | ``` 104 | """ 105 | 106 | conf = {'repository': None} 107 | 108 | def __init__(self): 109 | super(DriveDriver, self).__init__() 110 | 111 | self.driverArchitect = None 112 | self.repository = None 113 | self.dict_events = None 114 | self.driver = None 115 | self.d = None 116 | 117 | def _events(self) -> None: 118 | print("\n".join(self.dict_events)) 119 | 120 | def afterSession(self) -> int: 121 | self.driverArchitect.stop() 122 | return 0 123 | 124 | def buildDriver(self) -> None: 125 | self.d.buildDriverFromRepositoryName(self.repository.getClassName()) 126 | 127 | def beforeSession(self) -> None: 128 | import inspect 129 | 130 | from imapfw.runners.driver import DriverRunner 131 | from imapfw.types.repository import loadRepository 132 | from imapfw.architects import DriverArchitect 133 | from imapfw.edmp import SyncEmitter 134 | 135 | self.repository = loadRepository(self.conf.get('repository')) 136 | repositoryName = self.repository.getClassName() 137 | self.driverArchitect = DriverArchitect("%s.Driver"% repositoryName) 138 | self.driverArchitect.init() 139 | self.driverArchitect.start() 140 | self.driverArch = self.driverArchitect 141 | 142 | self.driver = self.driverArchitect.getEmitter() 143 | self.d = SyncEmitter(self.driver) 144 | self.buildDriver() 145 | 146 | self.register('repository') 147 | self.register('driverArch') 148 | self.register('driver') 149 | self.register('d') 150 | 151 | # Setup banner. 152 | events = [] 153 | for name, method in inspect.getmembers(DriverRunner, 154 | inspect.isfunction): 155 | if name.startswith('_') or name == 'run': 156 | continue 157 | events.append("- d.%s%s\n%s\n"% 158 | (name, inspect.signature(method), method.__doc__)) 159 | self.dict_events = events 160 | self.events = self._events 161 | self.register('events') 162 | 163 | banner = """ 164 | Welcome to the shell! The driver is running in a worker. Take control of it with 165 | the pre-configured emitter. It is available from both the "driver" and 166 | "d" variables. "d" will send any event in sync mode. Ctrl+D: quit 167 | 168 | Available commands: 169 | - events(): print available events for the driver. 170 | 171 | Example: 172 | >>> d.help() 173 | 174 | The driver was already built in the default beforeSession() method of this shell. 175 | """ 176 | self.setBanner(banner) 177 | -------------------------------------------------------------------------------- /imapfw/testing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/__init__.py -------------------------------------------------------------------------------- /imapfw/testing/architect.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015-2006, Nicolas Sebrecht & contributors. 3 | 4 | import unittest 5 | 6 | from imapfw import runtime 7 | from imapfw.architects.architect import Architect 8 | from imapfw.architects.engine import EngineArchitect 9 | from imapfw.architects.driver import DriverArchitect, DriversArchitect 10 | from imapfw.edmp import Emitter 11 | 12 | 13 | class TestArchitect(unittest.TestCase): 14 | def setUp(self): 15 | self.arc = Architect('testWorker') 16 | 17 | def test_00_start_stop(self): 18 | def runner(): 19 | pass 20 | 21 | self.arc.start(runner, ()) 22 | self.arc.stop() 23 | 24 | def test_01_start_kill(self): 25 | def runner(): 26 | while True: 27 | pass 28 | 29 | self.arc.start(runner, ()) 30 | self.arc.kill() 31 | 32 | 33 | class TestDriverArchitect(unittest.TestCase): 34 | def setUp(self): 35 | self.arc = DriverArchitect('Driver') 36 | 37 | def test_00_start_stop(self): 38 | self.arc.init() 39 | self.arc.start() 40 | self.arc.stop() 41 | 42 | def test_01_start_kill(self): 43 | self.arc.init() 44 | self.arc.start() 45 | self.arc.kill() 46 | 47 | def test_02_getEmitter(self): 48 | self.assertRaises(AssertionError, self.arc.getEmitter) 49 | self.arc.init() 50 | emitter = self.arc.getEmitter() 51 | self.assertIsInstance(emitter, Emitter) 52 | 53 | 54 | class TestDriversArchitect(unittest.TestCase): 55 | def setUp(self): 56 | self.arc = DriversArchitect('Drivers', 3) 57 | 58 | def test_00_start_stop(self): 59 | self.arc.init() 60 | self.arc.start() 61 | self.arc.stop() 62 | 63 | def test_01_start_kill(self): 64 | self.arc.init() 65 | self.arc.start() 66 | self.arc.kill() 67 | 68 | def test_02_getEmitter(self): 69 | self.assertRaises(KeyError, self.arc.getEmitter, 0) 70 | self.arc.init() 71 | emitter = self.arc.getEmitter(0) 72 | self.assertIsInstance(emitter, Emitter) 73 | emitter = self.arc.getEmitter(1) 74 | self.assertIsInstance(emitter, Emitter) 75 | emitter = self.arc.getEmitter(2) 76 | self.assertIsInstance(emitter, Emitter) 77 | self.assertRaises(KeyError, self.arc.getEmitter, 3) 78 | 79 | class TestEngineArchitect(unittest.TestCase): 80 | def setUp(self): 81 | def runner(): 82 | pass 83 | 84 | self.runner = runner 85 | self.arc = EngineArchitect('Engine') 86 | 87 | def test_00_start_stop(self): 88 | self.arc.init() 89 | self.arc.start(self.runner, ()) 90 | self.arc.stop() 91 | 92 | def test_01_start_kill(self): 93 | self.arc.init() 94 | self.arc.start(self.runner, ()) 95 | self.arc.kill() 96 | 97 | def test_02_getEmitter(self): 98 | self.arc.init() 99 | emitter = self.arc.getLeftEmitter() 100 | self.assertIsInstance(emitter, Emitter) 101 | emitter = self.arc.getRightEmitter() 102 | self.assertIsInstance(emitter, Emitter) 103 | 104 | 105 | if __name__ == '__main__': 106 | from imapfw.concurrency import Concurrency 107 | from imapfw.ui.tty import TTY 108 | 109 | runtime.set_module('concurrency', Concurrency('multiprocessing')) 110 | ui = TTY(runtime.concurrency.createLock()) 111 | ui.configure() 112 | # ui.enableDebugCategories(['architects']) 113 | runtime.set_module('ui', ui) 114 | unittest.main(verbosity=2) 115 | -------------------------------------------------------------------------------- /imapfw/testing/concurrency.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | import unittest 24 | 25 | from imapfw import runtime 26 | from imapfw.concurrency.concurrency import * 27 | 28 | 29 | class TestConcurrency(unittest.TestCase): 30 | def setUp(self): 31 | def noop(): 32 | pass 33 | 34 | def blocking(): 35 | while True: 36 | pass 37 | 38 | self.noop = noop 39 | self.blocking = blocking 40 | 41 | def test_00_concurrency_interface(self): 42 | self.assertIsInstance(runtime.concurrency, ConcurrencyInterface) 43 | 44 | def test_01_queue_interface(self): 45 | self.assertIsInstance(runtime.concurrency.createQueue(), QueueInterface) 46 | 47 | def test_02_lock_interface(self): 48 | self.assertIsInstance(runtime.concurrency.createLock(), LockBase) 49 | 50 | def test_03_worker_interface(self): 51 | self.assertIsInstance( 52 | runtime.concurrency.createWorker('noop', self.noop, ()), 53 | WorkerInterface) 54 | 55 | def test_04_worker_start_join(self): 56 | worker = runtime.concurrency.createWorker('noop', self.noop, ()) 57 | 58 | worker.start() 59 | self.assertEqual(worker.getName(), 'noop') 60 | 61 | worker.join() 62 | 63 | def test_05_worker_start_kill(self): 64 | worker = runtime.concurrency.createWorker('blocking', self.blocking, ()) 65 | 66 | worker.start() 67 | self.assertEqual(worker.getName(), 'blocking') 68 | 69 | worker.kill() 70 | 71 | 72 | if __name__ == '__main__': 73 | unittest.main(verbosity=2) 74 | -------------------------------------------------------------------------------- /imapfw/testing/edmp.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | import unittest 24 | import time 25 | 26 | from imapfw import runtime 27 | from imapfw.edmp import * 28 | 29 | from .nullui import NullUI 30 | 31 | 32 | class TestEDMP(unittest.TestCase): 33 | def setUp(self): 34 | self.c = runtime.concurrency 35 | ui = NullUI() 36 | runtime.set_module('ui', ui) 37 | 38 | def test_channel(self): 39 | queue = self.c.createQueue() 40 | for i in range(3): 41 | queue.put(i) 42 | chan = Channel(queue) 43 | 44 | # The internal channel queue.get_nowait() might be racy with 45 | # multiprocessing. Sleeping for 0.1 secs should be enough but this is 46 | # still RACY. 47 | time.sleep(0.1) 48 | self.assertEqual([x for x in chan], [0, 1, 2]) 49 | 50 | def test_newEmitterReceiver(self): 51 | r, e = newEmitterReceiver('test') 52 | self.assertIsInstance(r, Receiver) 53 | self.assertIsInstance(e, Emitter) 54 | 55 | def test_event(self): 56 | def onEvent(true): 57 | self.assertEqual(true, True) 58 | 59 | r, e = newEmitterReceiver('test') 60 | r.accept('event', onEvent) 61 | e.event(True) 62 | e.stopServing() 63 | while r.react(): 64 | pass 65 | 66 | def test_event_multiple_args(self): 67 | def onEvent(true, false, none=False): 68 | self.assertEqual(true, True) 69 | self.assertEqual(false, False) 70 | self.assertEqual(none, None) 71 | 72 | r, e = newEmitterReceiver('test') 73 | r.accept('event', onEvent) 74 | e.event(True, False, none=None) 75 | e.stopServing() 76 | while r.react(): 77 | pass 78 | 79 | def test_event_receiver_arg(self): 80 | def onEvent(true): 81 | self.assertEqual(true, True) 82 | 83 | r, e = newEmitterReceiver('test') 84 | r.accept('event', onEvent, True) 85 | e.event() 86 | e.stopServing() 87 | while r.react(): 88 | pass 89 | 90 | def test_event_receiver_args(self): 91 | def onEvent(true, false): 92 | self.assertEqual(true, True) 93 | self.assertEqual(false, False) 94 | 95 | r, e = newEmitterReceiver('test') 96 | r.accept('event', onEvent, True, False) 97 | e.event() 98 | e.stopServing() 99 | while r.react(): 100 | pass 101 | 102 | def test_event_emitter_receiver_args(self): 103 | def onEvent(eight, nine, one, two, three): 104 | self.assertEqual(one, 1) 105 | self.assertEqual(two, 2) 106 | self.assertEqual(three, 3) 107 | self.assertEqual(eight, 8) 108 | self.assertEqual(nine, 9) 109 | 110 | r, e = newEmitterReceiver('test') 111 | r.accept('event', onEvent, 8, 9) 112 | e.event(1, 2, 3) 113 | e.stopServing() 114 | while r.react(): 115 | pass 116 | 117 | def test_event_errors(self): 118 | def onEvent(): 119 | raise RuntimeError('error') 120 | 121 | r, e = newEmitterReceiver('test') 122 | r.accept('event', onEvent, True, False) 123 | e.event() 124 | e.stopServing() 125 | 126 | # Must not raise anything. 127 | while r.react(): 128 | pass 129 | 130 | def test_event_errors_sync(self): 131 | def onEvent(): 132 | raise RuntimeError('error') 133 | 134 | def runner(r): 135 | while r.react(): 136 | pass 137 | 138 | r, e = newEmitterReceiver('test') 139 | r.accept('event', onEvent) 140 | w = self.c.createWorker('runner', runner, (r,)) 141 | w.start() 142 | 143 | # Must raise error. 144 | self.assertRaises(RuntimeError, e.event_sync) 145 | e.stopServing() 146 | w.join() 147 | 148 | 149 | if __name__ == '__main__': 150 | unittest.main(verbosity=2) 151 | -------------------------------------------------------------------------------- /imapfw/testing/folder.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | import unittest 24 | 25 | from imapfw.types.folder import Folders, Folder 26 | 27 | 28 | class TestFolder(unittest.TestCase): 29 | def setUp(self): 30 | self.folderA = Folder(b'A') 31 | self.folderB = Folder(b'A') 32 | 33 | self.folderI = Folder(b'A/B') 34 | self.folderJ = Folder(b'A/B') 35 | 36 | self.foldersX = Folders(self.folderA, self.folderI) 37 | self.foldersY = Folders(self.folderB, self.folderJ) 38 | 39 | def test_folder_equal_one_level(self): 40 | self.assertEqual(self.folderA, self.folderB) 41 | 42 | def test_folder_equal_two_levels(self): 43 | self.assertEqual(self.folderI, self.folderJ) 44 | 45 | def test_folders_equal(self): 46 | self.assertEqual(self.foldersX, self.foldersY) 47 | 48 | 49 | if __name__ == '__main__': 50 | unittest.main(verbosity=2) 51 | -------------------------------------------------------------------------------- /imapfw/testing/libcore.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | import os 24 | import sys 25 | 26 | 27 | def testingPath(): 28 | return os.path.join( 29 | os.path.abspath(sys.modules['imapfw'].__path__[0]), 30 | 'testing') 31 | -------------------------------------------------------------------------------- /imapfw/testing/maildir.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | import unittest 24 | import os 25 | 26 | from imapfw import runtime 27 | from imapfw.api import drivers 28 | from imapfw.drivers.driver import loadDriver 29 | from imapfw.testing import libcore 30 | from imapfw.types.folder import Folders, Folder 31 | 32 | 33 | class TestMaildirDriver(unittest.TestCase): 34 | def setUp(self): 35 | confBase = { 'sep': '/' } 36 | confA, confB = confBase.copy(), confBase.copy() 37 | 38 | confA['path'] = os.path.join(libcore.testingPath(), 'maildirs', 'recursive_A') 39 | confB['path'] = os.path.join(libcore.testingPath(), 'maildirs', 'recursive_B') 40 | 41 | self.driverA = loadDriver(drivers.Maildir, 'MaildirA', confA) 42 | self.driverB = loadDriver(drivers.Maildir, 'MaildirB', confB) 43 | 44 | def test_getFolders_of_recursive_A(self): 45 | folders = self.driverA.getFolders() 46 | expected = Folders(Folder(b'/')) 47 | self.assertEqual(folders, expected) 48 | 49 | def test_getFolders_of_recursive_B(self): 50 | folders = self.driverB.getFolders() 51 | expected = Folders( 52 | Folder(b'/'), 53 | Folder(b'subfolder_A'), 54 | Folder(b'subfolder_B'), 55 | Folder(b'subfolder_B/subsubfolder_X'), 56 | ) 57 | self.assertEqual(folders, expected) 58 | 59 | 60 | if __name__ == '__main__': 61 | unittest.main(verbosity=2) 62 | -------------------------------------------------------------------------------- /imapfw/testing/maildirs/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_A/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_A/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_A/cur/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_A/cur/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_A/cur/1444285591_0.15004.vidovic.ultras.lan,U=207316,FMD5=75097416fde6f7c2e8da35793159ef76:2,S: -------------------------------------------------------------------------------- 1 | Return-Path: nicolas.s-dev-return@laposte.net 2 | Received: from 10.128.63.20 (LHLO lpn-prd-vrin019) (10.128.63.20) by 3 | lpn-prd-mstr083 with LMTP; Thu, 8 Oct 2015 08:01:50 +0200 (CEST) 4 | Received: from lpn-prd-vrin019 (localhost [127.0.0.1]) 5 | by lpn-prd-vrin019 (Postfix) with ESMTP id 21E781029B78 6 | for ; Thu, 8 Oct 2015 08:01:50 +0200 (CEST) 7 | Received: from mail-wi0-f180.google.com (mail-wi0-f180.google.com [209.85.212.180]) 8 | (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) 9 | (No client certificate requested) 10 | by lpn-prd-vrin019 (Postfix) with ESMTPS id 097041029271 11 | for ; Thu, 8 Oct 2015 08:01:50 +0200 (CEST) 12 | Received: by mail-wi0-f180.google.com with SMTP id gb1so9672299wic.1 13 | for ; Wed, 07 Oct 2015 23:01:50 -0700 (PDT) 14 | DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; 15 | d=gmail.com; s=20120113; 16 | h=sender:date:from:to:cc:subject:message-id:mime-version:content-type 17 | :content-disposition:user-agent; 18 | bh=ragiKPcYGhn92IBlYkq3542CcZ8IrlYJ8QWZx96ph9I=; 19 | b=YWBcYTH7kXVhp3Rz+uKICqs7634KikSFP3MlJfMnNFXQPDN//npTLanKZ91/b0gMm9 20 | +JtSdwYkU50L+rJOwfAfDA3iBedTmjfUshiAsTKcn5BFDdiFLqnqttxLhbDDI1S+Raqi 21 | J4vUcKeGM1y0cV3IGTMQoHiurZspPVyG0g4KCKvNG+RDKv/of2lvuPr6Npuh9GNrJ0if 22 | N+CE7XciEoBgxOhA8dbkwYJpUTtDBspqXoFDP/vn97hSB9Xd2Rcmfxoej2s6DIcGcvOt 23 | mdUl5exN0V59bWOYIYvwp2XgOYDx4KCnSYFsIRtbCX/N+vT+JxLqJaohKePop1bm130X 24 | MlGQ== 25 | X-Received: by 10.194.242.65 with SMTP id wo1mr6226967wjc.15.1444284109902; 26 | Wed, 07 Oct 2015 23:01:49 -0700 (PDT) 27 | Received: from vidovic.ultras.lan (212-198-74-192.rev.numericable.fr. [212.198.74.192]) 28 | by smtp.gmail.com with ESMTPSA id an4sm5380633wjc.36.2015.10.07.23.01.48 29 | (version=TLSv1.2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); 30 | Wed, 07 Oct 2015 23:01:48 -0700 (PDT) 31 | Sender: Nicolas Sebrecht 32 | Date: Thu, 8 Oct 2015 08:01:46 +0200 33 | From: Nicolas Sebrecht 34 | To: offlineimap-project@lists.alioth.debian.org 35 | Cc: nicolas.s-dev-cc@laposte.net 36 | Bcc: nicolas.s-dev-bcc@laposte.net 37 | Subject: [ANNOUNCE] the new IMAP software "imapfw" is made public 38 | Message-ID: <20151008060146.GA2542@vidovic.ultras.lan> 39 | MIME-Version: 1.0 40 | Content-Type: text/plain; charset=us-ascii 41 | Content-Disposition: inline 42 | User-Agent: Mutt/1.5.23 (2014-03-12) 43 | X-VR-SrcIP: 209.85.212.180 44 | X-VR-FullState: 0 45 | X-VR-Score: 0 46 | X-VR-Cause-1: gggruggvucftvghtrhhoucdtuddrfeekgedrjeeggddutdelucetufdoteggodetrfcurfhrohhfihhl 47 | X-VR-Cause-2: vgemucfntefrqffuvffgnecuuegrihhlohhuthemucehtddtnecunecujfgurhepshffhffvuffkgggt 48 | X-VR-Cause-3: uggfsehttdertddtredvnecuhfhrohhmpefpihgtohhlrghsucfuvggsrhgvtghhthcuoehnihgtohhl 49 | X-VR-Cause-4: rghsrdhsqdguvghvsehlrghpohhsthgvrdhnvghtqeenucffohhmrghinhepohhffhhlihhnvghimhgr 50 | X-VR-Cause-5: phdrohhrghenucfrrghrrghmpehhvghlohepmhgrihhlqdifihdtqdhfudektddrghhoohhglhgvrdgt 51 | X-VR-Cause-6: ohhmpdhinhgvthepvddtledrkeehrddvuddvrddukedtpdhmrghilhhfrhhomhepnhhirdhsrdhnihhs 52 | X-VR-Cause-7: feefsehgmhgrihhlrdgtohhmpdhrtghpthhtohepnhhitgholhgrshdrshdquggvvheslhgrphhoshht 53 | X-VR-Cause-8: vgdrnhgvth 54 | X-VR-AvState: No 55 | X-VR-State: 0 56 | Content-Length: 381 57 | 58 | I'm happy to announce I'm publishing imapfw today. I aim imapfw to 59 | replace OfflineIMAP in the long run and I think it's the good time to 60 | share it. 61 | 62 | This is Python like OfflineIMAP but it comes under the MIT license. 63 | 64 | I wrote a blog post about this new tool on the website: 65 | http://offlineimap.org/posts.html 66 | 67 | Feel free to share your opinions. 68 | 69 | Have fun! ,-) 70 | 71 | -- 72 | Nicolas Sebrecht 73 | -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_A/new/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_A/new/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_A/tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_A/tmp/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_B/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_B/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_B/cur/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_B/cur/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_B/ignore/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_B/ignore/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_B/new/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_B/new/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_B/subfolder_A/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_B/subfolder_A/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_B/subfolder_A/cur/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_B/subfolder_A/cur/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_B/subfolder_A/new/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_B/subfolder_A/new/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_B/subfolder_A/tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_B/subfolder_A/tmp/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_B/subfolder_B/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_B/subfolder_B/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_B/subfolder_B/cur/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_B/subfolder_B/cur/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_B/subfolder_B/new/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_B/subfolder_B/new/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_B/subfolder_B/subsubfolder_X/cur/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_B/subfolder_B/subsubfolder_X/cur/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_B/subfolder_B/subsubfolder_X/new/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_B/subfolder_B/subsubfolder_X/new/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_B/subfolder_B/subsubfolder_X/tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_B/subfolder_B/subsubfolder_X/tmp/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_B/subfolder_B/tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_B/subfolder_B/tmp/.keep -------------------------------------------------------------------------------- /imapfw/testing/maildirs/recursive_B/tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/testing/maildirs/recursive_B/tmp/.keep -------------------------------------------------------------------------------- /imapfw/testing/message.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | 5 | import unittest 6 | 7 | from imapfw.types.message import Messages, Message 8 | 9 | 10 | class TestMessage(unittest.TestCase): 11 | def setUp(self): 12 | self.messageA = Message(2) 13 | self.messageB = Message(2) 14 | self.message3 = Message(3) 15 | 16 | def test_00_message_equal(self): 17 | self.assertEqual(self.messageA, self.messageB) 18 | 19 | def test_01_message_greater(self): 20 | self.assertGreater(self.message3, self.messageA) 21 | 22 | 23 | class TestMessages(unittest.TestCase): 24 | def setUp(self): 25 | self.message1 = Message(1) 26 | self.message2 = Message(2) 27 | self.message3 = Message(3) 28 | self.messagesI = Messages(self.message2, self.message1) 29 | 30 | def test_00_in(self): 31 | self.assertIn(self.message2, self.messagesI) 32 | 33 | def test_01_add(self): 34 | self.messagesI.add(self.message1) 35 | self.assertIn(self.message1, self.messagesI) 36 | 37 | def test_02_remove(self): 38 | self.messagesI.remove(self.message2) 39 | self.assertNotIn(self.message2, self.messagesI) 40 | 41 | 42 | if __name__ == '__main__': 43 | unittest.main(verbosity=2) 44 | -------------------------------------------------------------------------------- /imapfw/testing/nullui.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | #TODO: use a "null" logging instead. 24 | class NullUI(object): 25 | def critical(self, *args): pass 26 | def debug(self, *args): pass 27 | def debugC(self, category, *args): pass 28 | def error(self, *args): pass 29 | def exception(self, *args): pass 30 | def format(self, *args): pass 31 | def info(self, *args): pass 32 | def infoL(self, level, *args): pass 33 | def setInfoLevel(self, level): pass 34 | def warn(self, *args): pass 35 | -------------------------------------------------------------------------------- /imapfw/testing/rascal.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | import unittest 5 | import os 6 | 7 | from imapfw.rascal import Rascal 8 | from imapfw.api import types 9 | from imapfw.testing import libcore 10 | 11 | 12 | class TestRascal(unittest.TestCase): 13 | def setUp(self): 14 | def loadRascal(path): 15 | rascal = Rascal() 16 | rascal.load(path) 17 | return rascal 18 | 19 | rascalsDir = os.path.join( 20 | libcore.testingPath(), 21 | 'rascals', 22 | ) 23 | 24 | self.rascal = loadRascal(os.path.join(rascalsDir, 'basic.rascal')) 25 | self.empty = loadRascal(os.path.join(rascalsDir, 'empty.rascal')) 26 | 27 | def test_getAccountA(self): 28 | cls_account = self.rascal.get('AccountA', [types.Account]) 29 | self.assertIsInstance(cls_account(), types.Account) 30 | 31 | def test_getExceptionHook(self): 32 | self.assertEqual(type(self.rascal.getExceptionHook()), type(lambda x:x)) 33 | 34 | def test_getExceptionHookEmpty(self): 35 | self.assertTrue(callable(self.empty.getExceptionHook())) 36 | 37 | def test_getMaxConnections(self): 38 | self.assertEqual(self.rascal.getMaxConnections('AccountA'), 9) 39 | 40 | def test_getMaxSyncAccounts(self): 41 | self.assertEqual(self.rascal.getMaxSyncAccounts(), 7) 42 | 43 | def test_getPostHook(self): 44 | self.assertEqual(type(self.rascal.getPostHook()), type(lambda x:x)) 45 | 46 | def test_getPostHookEmpty(self): 47 | self.assertEqual(type(self.empty.getPostHook()), type(lambda x:x)) 48 | 49 | def test_runPreHook(self): 50 | from imapfw.toolkit import runHook 51 | 52 | stop = runHook(self.rascal.getPreHook(), 'actionName', {'action': 'actionName'}) 53 | self.assertFalse(stop) 54 | 55 | def test_runPreHookEmpty(self): 56 | from imapfw.toolkit import runHook 57 | 58 | stop = runHook(self.rascal.getPreHook(), 'actionName', {'action': 'actionName'}) 59 | self.assertFalse(stop) 60 | 61 | 62 | if __name__ == '__main__': 63 | unittest.main(verbosity=2) 64 | -------------------------------------------------------------------------------- /imapfw/testing/rascals/basic.rascal: -------------------------------------------------------------------------------- 1 | 2 | MainConf = { 3 | 'concurrency_backend': 'multiprocessing', 4 | 'max_sync_accounts': 7, 5 | } 6 | 7 | UI = None 8 | 9 | def configure(ui): 10 | global UI 11 | UI = ui 12 | 13 | def preHook(hook, actionName, actionOptions): 14 | hook.ended() 15 | 16 | def postHook(hook): 17 | hook.ended() 18 | 19 | def exceptionHook(hook, error): 20 | hook.ended() 21 | 22 | from imapfw.api import controllers, types, drivers 23 | 24 | 25 | MaildirConfA = { 26 | 'path': '~/Maildir', 27 | 'max_connections': 9, 28 | } 29 | 30 | ImapConfA = { 31 | 'dns': 'imap.gmail.com', 32 | 'port': '143', 33 | 'username': 'myname', 34 | 'max_connections': 11, 35 | } 36 | 37 | class MaildirA(types.Maildir): 38 | conf = MaildirConfA 39 | driver = drivers.Maildir 40 | 41 | class ImapA(types.Imap): 42 | conf = ImapConfA 43 | driver = drivers.Imap 44 | 45 | class AccountA(types.Account): 46 | left = MaildirA 47 | right = ImapA 48 | 49 | # vim: syntax=python ts=4 expandtab : 50 | -------------------------------------------------------------------------------- /imapfw/testing/rascals/empty.rascal: -------------------------------------------------------------------------------- 1 | 2 | MainConf = {} 3 | -------------------------------------------------------------------------------- /imapfw/testing/testrascal.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | """ 24 | 25 | Test the rascal. 26 | 27 | This intend to check all the content of the rascal. The API of accounts, 28 | repositories, controllers and drivers are tested using mocks. 29 | 30 | """ 31 | 32 | import unittest 33 | 34 | from imapfw import runtime 35 | from imapfw.constants import ARC 36 | 37 | from imapfw.types.account import loadAccount, Account 38 | from imapfw.types.repository import Repository 39 | 40 | 41 | class TestRascalAccount(unittest.TestCase): 42 | 43 | DEF_ACCOUNT = None 44 | LOG = None 45 | 46 | account = None 47 | repositories = [] 48 | 49 | def setUp(self): 50 | print() 51 | 52 | def test_00_loadAccount(self): 53 | account = loadAccount(self.DEF_ACCOUNT) 54 | runtime.ui.debugC(ARC, repr(account)) 55 | self.assertIsInstance(account, Account) 56 | self.__class__.account = account 57 | 58 | def test_01_getRight(self): 59 | repository = self.__class__.account.fw_getRight() 60 | runtime.ui.debugC(ARC, repr(repository)) 61 | self.assertIsInstance(repository, Repository) 62 | self.__class__.repositories.append(repository) 63 | 64 | def test_02_getLeft(self): 65 | repository = self.__class__.account.fw_getLeft() 66 | runtime.ui.debugC(ARC, repr(repository)) 67 | self.assertIsInstance(repository, Repository) 68 | self.__class__.repositories.append(repository) 69 | -------------------------------------------------------------------------------- /imapfw/testing/types.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | import unittest 24 | import inspect 25 | 26 | from imapfw import runtime 27 | from imapfw.types.account import * 28 | from imapfw.types.repository import * 29 | 30 | 31 | class TestTypeAccount(unittest.TestCase): 32 | def test_account_interface(self): 33 | interface = AccountInterface() 34 | account = Account() 35 | 36 | for name, method in inspect.getmembers(interface): 37 | self.assertEqual(hasattr(account, name), True) 38 | 39 | for name in ['left', 'right']: 40 | self.assertEqual(hasattr(account, name), True) 41 | 42 | 43 | class TestTypeRepository(unittest.TestCase): 44 | pass 45 | 46 | 47 | if __name__ == '__main__': 48 | unittest.main(verbosity=2) 49 | -------------------------------------------------------------------------------- /imapfw/toolkit.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | import os 5 | from threading import Thread 6 | 7 | 8 | def runHook(hookFunc, *args): 9 | class Hook(object): 10 | def __init__(self): 11 | self._stop = True 12 | 13 | def ended(self): 14 | self._stop = False 15 | 16 | def stop(self): 17 | return self._stop 18 | 19 | 20 | hookName = hookFunc.__name__ 21 | 22 | # Don't run hooks for action unitTests. 23 | if hookName == 'preHook': 24 | if args[0] == 'unitTests': 25 | return False 26 | 27 | hook = Hook() 28 | args = (hook,) + args 29 | 30 | thread = Thread(name=hookName, target=hookFunc, args=args, daemon=True) 31 | thread.start() 32 | thread.join(10) # TODO: get timeout from rascal. 33 | 34 | return hook.stop() 35 | 36 | 37 | def xTrans(thing, transforms): 38 | """Applies set of transformations to a thing. 39 | 40 | :args: 41 | - thing: string; if None, then no processing will take place. 42 | - transforms: iterable that returns transformation function 43 | on each turn. 44 | 45 | Returns transformed thing.""" 46 | 47 | if thing == None: 48 | return None 49 | for f in transforms: 50 | thing = f(thing) 51 | return thing 52 | 53 | def expandPath(path): 54 | xtrans = [os.path.expanduser, os.path.expandvars, os.path.abspath] 55 | return xTrans(path, xtrans) 56 | 57 | 58 | def dictValueFromPath(dictionnary, path): 59 | def getItem(tmpDict, lst_path): 60 | if len(lst_path) > 0: 61 | if isinstance(tmpDict, dict): 62 | newDict = tmpDict.get(lst_path.pop(0)) 63 | return getItem(newDict, lst_path) 64 | else: 65 | raise KeyError('invalid path') 66 | return tmpDict 67 | 68 | lst_path = path.split('.') 69 | return getItem(dictionnary, lst_path) 70 | -------------------------------------------------------------------------------- /imapfw/types/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/types/__init__.py -------------------------------------------------------------------------------- /imapfw/types/account.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | from typing import TypeVar, Union 5 | 6 | from imapfw import runtime 7 | from imapfw.types.repository import loadRepository 8 | 9 | from .folder import Folders 10 | 11 | # Annotations. 12 | from imapfw.types.repository import Repository 13 | 14 | 15 | AccountClass = TypeVar('Account based class') 16 | 17 | 18 | class AccountInternalInterface(object): 19 | def fw_getLeft(self): raise NotImplementedError 20 | def fw_getRight(self): raise NotImplementedError 21 | 22 | 23 | class AccountInterface(object): 24 | def getClassName(self): raise NotImplementedError 25 | def syncFolders(self): raise NotImplementedError 26 | 27 | 28 | class Account(AccountInterface, AccountInternalInterface): 29 | """The Account base class. 30 | 31 | The namespace `fw_` is reserved for the framework internals.""" 32 | 33 | left = None 34 | right = None 35 | 36 | def fw_getSide(self, side: str) -> Repository: 37 | if side == 'left': 38 | return self.fw_getLeft() 39 | if side == 'right': 40 | return self.fw_getRight() 41 | assert side in ['left', 'right'] 42 | 43 | def fw_getLeft(self) -> Repository: 44 | return loadRepository(self.left) 45 | 46 | def fw_getRight(self) -> Repository: 47 | return loadRepository(self.right) 48 | 49 | def getClassName(self) -> str: 50 | return self.__class__.__name__ 51 | 52 | def init(self) -> None: 53 | """Override this method to make initialization in the rascal.""" 54 | 55 | pass 56 | 57 | def syncFolders(self, folders: Folders) -> Folders: 58 | return folders 59 | 60 | 61 | def loadAccount(obj: Union[AccountClass, str]) -> Account: 62 | 63 | if isinstance(obj, str): 64 | obj = runtime.rascal.get(obj, [Account, dict]) 65 | 66 | try: 67 | if issubclass(obj, Account): 68 | cls_account = obj 69 | else: 70 | raise TypeError() 71 | 72 | except TypeError: 73 | try: 74 | if not issubclass(obj, dict): 75 | raise TypeError() 76 | 77 | else: 78 | cls_account = type(obj.get('name'), obj.get('type'), {}) 79 | 80 | # Attach attributes. 81 | for name in ['left', 'right', 'conf']: 82 | setattr(cls_account, name, obj.get(name)) 83 | except TypeError: 84 | raise TypeError("'%s' for a account is not supported"% repr(obj)) 85 | 86 | account = cls_account() 87 | account.init() 88 | return account 89 | -------------------------------------------------------------------------------- /imapfw/types/folder.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | from collections import UserList 24 | from functools import total_ordering 25 | 26 | from imapfw.interface import implements, Interface, checkInterfaces 27 | 28 | # Annotations. 29 | from imapfw.annotation import Union 30 | 31 | 32 | ENCODING = 'UTF-8' 33 | 34 | 35 | class FolderInterface(Interface): 36 | """Internal model representative of a folder. 37 | 38 | Used by any driver, controller or engine. Might be passed to the user via the 39 | rascal. 40 | 41 | Internal name is the folder name with the levels of hierarchy, type bytes. 42 | 43 | Each driver must use the same representation so that a folder from a driver 44 | can be compared to a folder from another driver. 45 | """ 46 | 47 | scope = Interface.PUBLIC 48 | 49 | def hasChildren(self) -> bool: 50 | """Return True of False whether this folder has children.""" 51 | 52 | def getName(self, encoding: str=ENCODING) -> str: 53 | """Return folder base name.""" 54 | 55 | def getRoot(self, encoding: str=ENCODING) -> str: 56 | """Return the path to the folder.""" 57 | 58 | def setName(self, name: Union[str, bytes], encoding: str=None) -> None: 59 | """Set the folder base name.""" 60 | 61 | def setHasChildren(self, hasChildren: bool) -> None: 62 | """Set if folder has children.""" 63 | 64 | def setRoot(self, root: str, encoding: str=ENCODING) -> None: 65 | """Set the path to the folder.""" 66 | 67 | 68 | @total_ordering 69 | @checkInterfaces() 70 | @implements(FolderInterface) 71 | class Folder(object): 72 | def __init__(self, name, encoding=None): 73 | self._name = None # Must be bytes. 74 | self.setName(name, encoding) 75 | 76 | self._hasChildren = None 77 | self._root = None 78 | 79 | def __bytes__(self): 80 | return self._name 81 | 82 | def __eq__(self, other): 83 | return self.getName() == other.getName() 84 | 85 | def __lt__(self, other): 86 | return self.getName() < other.getName() 87 | 88 | def __repr__(self): 89 | return repr(self._name.decode(ENCODING)) 90 | 91 | def __str__(self): 92 | return self.getName() 93 | 94 | def getName(self, encoding: str=ENCODING) -> str: 95 | return self._name.decode(encoding) 96 | 97 | def getRoot(self, encoding: str=ENCODING) -> str: 98 | return self._root.decode(encoding) 99 | 100 | def hasChildren(self) -> bool: 101 | return self._hasChildren 102 | 103 | def setName(self, name: Union[str, bytes], encoding: str=None) -> None: 104 | """Set the name of the folder. 105 | 106 | :name: folder name with hierarchy seperated by '/' (e.g. 107 | 'a/folder'). 108 | :encoding: encoding of the name. Expects bytes if not set. 109 | """ 110 | 111 | if type(name) == bytes: 112 | self._name = name 113 | else: 114 | self._name = name.encode(encoding) 115 | 116 | def setHasChildren(self, hasChildren: bool) -> None: 117 | self._hasChildren = hasChildren 118 | 119 | def setRoot(self, root: str, encoding: str=ENCODING) -> None: 120 | if type(root) == bytes: 121 | self._root = root 122 | else: 123 | self._root = root.encode(encoding) 124 | 125 | 126 | class Folders(UserList): 127 | """A list of Folder instances.""" 128 | 129 | def __init__(self, *args): 130 | super(Folders, self).__init__(list(args)) 131 | -------------------------------------------------------------------------------- /imapfw/types/imap.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | """ 24 | 25 | Imap types and derivatives. 26 | 27 | """ 28 | 29 | from .repository import Repository 30 | 31 | 32 | class Imap(Repository): 33 | 34 | isLocal = False 35 | -------------------------------------------------------------------------------- /imapfw/types/maildir.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015, Nicolas Sebrecht & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | """ 24 | 25 | Maildir types and derivatives. 26 | 27 | """ 28 | 29 | from imapfw import drivers 30 | 31 | from .repository import Repository 32 | 33 | 34 | class Maildir(Repository): 35 | 36 | isLocal = True 37 | driver = drivers.Maildir 38 | -------------------------------------------------------------------------------- /imapfw/types/message.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | from functools import total_ordering 5 | from collections import UserDict 6 | 7 | from imapfw.interface import implements, Interface, checkInterfaces 8 | 9 | # Annotations. 10 | from imapfw.annotation import List 11 | 12 | 13 | #TODO: interface. 14 | class MessageAttributes(object): 15 | def __init__(self): 16 | self.flags = [] 17 | self.internaldate = None 18 | 19 | def getFlags(self) -> List[str]: 20 | return self.flags 21 | 22 | def getInternaldate(self): 23 | return self.internaldate 24 | 25 | def setFlags(self, flags: List[str]) -> None: 26 | self.flags = flags 27 | 28 | def setInternaldate(self, internaldate: str): 29 | #TODO: make python date 30 | self.internaldate = internaldate 31 | 32 | 33 | #TODO: interface. 34 | @total_ordering 35 | class Message(object): 36 | def __init__(self, uid: int): 37 | self.uid = uid 38 | 39 | self.fd = None 40 | self.attributes = MessageAttributes() 41 | 42 | # def __bytes__(self): 43 | # return self._name 44 | 45 | def __eq__(self, other): 46 | return self.uid == other 47 | 48 | def __hash__(self): 49 | return hash(self.uid) 50 | 51 | def __lt__(self, other): 52 | return self.uid < other 53 | 54 | def __repr__(self): 55 | return ""% self.uid 56 | 57 | def __str__(self): 58 | return str(self.uid) 59 | 60 | def getAttributes(self) -> MessageAttributes: 61 | return self.attributes 62 | 63 | def getFd(self): 64 | return self.fd 65 | 66 | def getUID(self) -> int: 67 | return self.uid 68 | 69 | def setFd(self, fd) -> None: 70 | self.fd = fd 71 | 72 | def setAttributes(self, attributes: MessageAttributes) -> None: 73 | self.attributes = attributes 74 | 75 | 76 | #TODO: interface. 77 | class Messages(UserDict): 78 | """A collection of messages, by UID.""" 79 | 80 | def __init__(self, *args): 81 | self.data = {} 82 | for message in args: 83 | self.data[message.getUID()] = message 84 | 85 | def add(self, message: Message) -> None: 86 | self.update({message.getUID(): message}) 87 | 88 | def coalesceUIDs(self) -> str: 89 | """Return a string of coalesced continous ranges and UIDs. 90 | 91 | E.g.: '1,3:7,9' 92 | """ 93 | 94 | uids = [] # List of UIDs and coalesced sub-sequences ['1', '3:7', '9']. 95 | 96 | def coalesce(start, end): 97 | if start == end: 98 | return str(start) # Non-coalesced UID: '1'. 99 | return "%s:%s"% (start, end) # Coalesced sub-sequence: '3:7'. 100 | 101 | start = None 102 | end = None 103 | for uid in self.keys(): 104 | if start is None: 105 | # First item. 106 | start, end = uid, uid 107 | continue 108 | 109 | if uid == end + 1: 110 | end = uid 111 | continue 112 | 113 | uids.append(coalesce(start, end)) 114 | start, end = uid, uid # Current uid is the next item to coalesce. 115 | uids.append(coalesce(start, end)) 116 | 117 | return ','.join(uids) # '1,3:7,9' 118 | 119 | def getAttributes(self, uid: int) -> MessageAttributes: 120 | return self.data[uid].getAttributes() 121 | 122 | def remove(self, message: Message) -> None: 123 | self.pop(message.getUID()) 124 | 125 | def setAttributes(self, uid: int, attributes: MessageAttributes) -> None: 126 | self.data[uid].setAttributes(attributes) 127 | -------------------------------------------------------------------------------- /imapfw/types/repository.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015, Nicolas Sebrecht & contributors. 3 | 4 | from typing import TypeVar, Union 5 | 6 | from imapfw.controllers.controller import loadController, ControllerClass 7 | from imapfw.drivers.driver import loadDriver, Driver 8 | 9 | 10 | RepositoryClass = TypeVar('Repository based class') 11 | 12 | 13 | class RepositoryInterface(object): 14 | 15 | conf = None 16 | driver = None 17 | isLocal = None 18 | 19 | def getClassName(self): raise NotImplementedError 20 | def init(self): raise NotImplementedError 21 | 22 | 23 | class RepositoryIntenalInterface(object): 24 | def fw_addController(self): raise NotImplementedError 25 | def fw_getDriver(self): raise NotImplementedError 26 | def fw_insertController(self): raise NotImplementedError 27 | 28 | 29 | class Repository(RepositoryInterface, RepositoryIntenalInterface): 30 | """The repository base class. 31 | 32 | The `fw_` namespace is reserved to the framework internals. Any method of 33 | this namespace must be overriden.""" 34 | 35 | conf = None 36 | driver = None 37 | controllers = [] 38 | 39 | def __init__(self): 40 | # Turn the class attributes into instance attributes. 41 | self.conf = self.conf.copy() 42 | self.driver = self.driver 43 | self.controllers = self.controllers.copy() 44 | 45 | def fw_appendController(self, cls_controller: ControllerClass, 46 | conf: dict=None) -> None: 47 | 48 | self.fw_insertController(cls_controller, conf, -1) 49 | 50 | def fw_getDriver(self) -> Driver: 51 | """Get the "high-level" driver with the controllers 52 | 53 | Chain the controllers on top of the driver. Controllers are run in the 54 | driver worker.""" 55 | 56 | driver = loadDriver(self.driver, self.getClassName(), self.conf) 57 | 58 | # Chain the controllers. 59 | # Keep the original attribute as-is. 60 | controllers = self.controllers.copy() 61 | # Nearest to end-driver is the last in this list. 62 | controllers.reverse() 63 | for obj in controllers: 64 | controller = loadController(obj, self.getClassName(), self.conf) 65 | 66 | controller.fw_drive(driver) # Chain here. 67 | driver = controller # The next controller will drive this. 68 | 69 | return driver 70 | 71 | def fw_insertController(self, cls_controller: ControllerClass, 72 | conf: dict=None, position: int=0): 73 | 74 | setattr(cls_controller, 'conf', conf) 75 | if position < 0: 76 | position = len(self.controllers) 77 | 78 | self.controllers.insert(position, cls_controller) 79 | 80 | def getClassName(self): 81 | return self.__class__.__name__ 82 | 83 | def init(self): 84 | """Override this method to make initialization in the rascal.""" 85 | 86 | pass 87 | 88 | 89 | def loadRepository(obj: Union[RepositoryClass, dict]) -> Repository: 90 | 91 | try: 92 | if issubclass(obj, Repository): 93 | cls_repository = obj 94 | else: 95 | raise TypeError("got unsupported %s"% repr(obj)) 96 | 97 | except TypeError: 98 | try: 99 | if not issubclass(obj, dict): 100 | raise TypeError() 101 | 102 | # The repository is defined in the dictionnary form in the rascal. 103 | # Build the class. 104 | cls_repository = type(obj.get('name'), obj.get('type'), {}) 105 | 106 | # Attached attributes. 107 | for name, mandatory in { 108 | 'conf': True, 109 | 'driver': True, 110 | 'controllers': [], 111 | }: 112 | try: 113 | setattr(cls_repository, name, obj.get(name)) 114 | except KeyError: 115 | if mandatory is True: 116 | raise Exception("mandatory key '%s' is not defined for" 117 | " %s"% (name, cls_repository.__name__)) 118 | setattr(cls_repository, name, mandatory) 119 | 120 | except TypeError: 121 | raise TypeError("'%s' for a repository is not supported"% repr(obj)) 122 | 123 | repository = cls_repository() 124 | repository.init() 125 | 126 | return repository 127 | -------------------------------------------------------------------------------- /imapfw/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/imapfw/ui/__init__.py -------------------------------------------------------------------------------- /imapfw/ui/tty.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # Copyright (c) 2015-2016, Nicolas Sebrecht & contributors. 3 | 4 | import logging 5 | import logging.config 6 | 7 | from imapfw.constants import DEBUG_CATEGORIES, DEBUG_ALL_CATEGORIES 8 | 9 | # Annotations. 10 | from imapfw.annotation import List, Function 11 | 12 | logging_config = { 13 | 'version': 1, 14 | 'formatters': { 15 | 'brief': { 16 | 'class': 'logging.Formatter', 17 | 'format': '%(message)s', 18 | }, 19 | 'default': { 20 | 'class': 'logging.Formatter', 21 | 'format': '%(levelname)-8s: %(message)s', 22 | }, 23 | 'verbose': { 24 | 'class': 'logging.Formatter', 25 | 'format': '%(levelname)-8s %(module)s: %(message)s', 26 | }, 27 | }, 28 | 'handlers': { 29 | 'console': { 30 | 'class': 'logging.StreamHandler', 31 | 'formatter': 'default', 32 | }, 33 | }, 34 | 'loggers': { 35 | 'debug': { 36 | 'level': 'DEBUG', 37 | 'handlers': ['console'], 38 | }, 39 | }, 40 | } 41 | 42 | 43 | 44 | 45 | class UIinterface(object): 46 | def critical(self): raise NotImplementedError 47 | def debug(self): raise NotImplementedError 48 | def debugC(self): raise NotImplementedError 49 | def error(self): raise NotImplementedError 50 | def exception(self): raise NotImplementedError 51 | def format(self): raise NotImplementedError 52 | def info(self): raise NotImplementedError 53 | def infoL(self): raise NotImplementedError 54 | def setInfoLevel(self): raise NotImplementedError 55 | def warn(self): raise NotImplementedError 56 | 57 | 58 | class UIbackendInterface(object): 59 | def configure(self): raise NotImplementedError 60 | def enableDebugCategories(self): raise NotImplementedError 61 | def setCurrentWorkerNameFunction(self): raise NotImplementedError 62 | 63 | 64 | class TTY(UIinterface, UIbackendInterface): 65 | def __init__(self, lock): 66 | self._lock = lock 67 | 68 | self._logger = None 69 | self._backend = logging 70 | self._currentWorkerName = lambda *args: '' 71 | self._debugCategories = DEBUG_CATEGORIES 72 | self._infoLevel = None 73 | 74 | def _safeLog(self, name: str, *args) -> None: 75 | self._lock.acquire() 76 | getattr(self._logger, name)(*args) 77 | self._lock.release() 78 | 79 | def configure(self, config: dict=logging_config) -> None: 80 | self._backend.config.dictConfig(config) 81 | self._logger = self._backend.getLogger('debug') 82 | 83 | def critical(self, *args) -> None: 84 | self._safeLog('critical', *args) 85 | 86 | def debug(self, *args) -> None: 87 | self._safeLog('debug', *args) 88 | 89 | def debugC(self, category: str, *args) -> None: 90 | if self._debugCategories.get(category) is True: 91 | self._safeLog('debug', "%s %s [%s]", 92 | self._currentWorkerName(), 93 | self.format(*args), 94 | category, 95 | ) 96 | 97 | def enableDebugCategories(self, categories: List[str]) -> None: 98 | if 'all' in categories: 99 | categories = DEBUG_ALL_CATEGORIES 100 | for category in categories: 101 | self._debugCategories[category] = True 102 | 103 | def error(self, *args) -> None: 104 | self._safeLog('error', *args) 105 | 106 | def exception(self, *args) -> None: 107 | self._safeLog('exception', *args) 108 | 109 | def format(self, *args): 110 | format_args = args[1:] 111 | try: 112 | return args[0].format(*format_args) 113 | except (IndexError, KeyError): 114 | return args[0] % args[1:] 115 | 116 | 117 | def info(self, *args) -> None: 118 | self._safeLog('info', *args) 119 | 120 | def infoL(self, level, *args) -> None: 121 | if level <= self._infoLevel: 122 | self.info(*args) 123 | 124 | def setCurrentWorkerNameFunction(self, func: Function) -> None: 125 | self._currentWorkerName = func 126 | 127 | def setInfoLevel(self, level: int) -> None: 128 | self._infoLevel = level 129 | 130 | def warn(self, *args) -> None: 131 | self._safeLog('warn', *args) 132 | -------------------------------------------------------------------------------- /internals/source/_static/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/internals/source/_static/.keep -------------------------------------------------------------------------------- /internals/source/_templates/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/internals/source/_templates/.keep -------------------------------------------------------------------------------- /internals/source/imapfw.actions.rst: -------------------------------------------------------------------------------- 1 | imapfw.actions package 2 | ====================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | imapfw.actions.devel module 8 | --------------------------- 9 | 10 | .. automodule:: imapfw.actions.devel 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | imapfw.actions.examine module 16 | ----------------------------- 17 | 18 | .. automodule:: imapfw.actions.examine 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | imapfw.actions.interface module 24 | ------------------------------- 25 | 26 | .. automodule:: imapfw.actions.interface 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | imapfw.actions.noop module 32 | -------------------------- 33 | 34 | .. automodule:: imapfw.actions.noop 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | imapfw.actions.shell module 40 | --------------------------- 41 | 42 | .. automodule:: imapfw.actions.shell 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | imapfw.actions.syncaccounts module 48 | ---------------------------------- 49 | 50 | .. automodule:: imapfw.actions.syncaccounts 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | imapfw.actions.testrascal module 56 | -------------------------------- 57 | 58 | .. automodule:: imapfw.actions.testrascal 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | imapfw.actions.unittests module 64 | ------------------------------- 65 | 66 | .. automodule:: imapfw.actions.unittests 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | 72 | Module contents 73 | --------------- 74 | 75 | .. automodule:: imapfw.actions 76 | :members: 77 | :undoc-members: 78 | :show-inheritance: 79 | -------------------------------------------------------------------------------- /internals/source/imapfw.architects.rst: -------------------------------------------------------------------------------- 1 | imapfw.architects package 2 | ========================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | imapfw.architects.account module 8 | -------------------------------- 9 | 10 | .. automodule:: imapfw.architects.account 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | imapfw.architects.architect module 16 | ---------------------------------- 17 | 18 | .. automodule:: imapfw.architects.architect 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | imapfw.architects.debug module 24 | ------------------------------ 25 | 26 | .. automodule:: imapfw.architects.debug 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | imapfw.architects.driver module 32 | ------------------------------- 33 | 34 | .. automodule:: imapfw.architects.driver 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | imapfw.architects.folder module 40 | ------------------------------- 41 | 42 | .. automodule:: imapfw.architects.folder 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | 48 | Module contents 49 | --------------- 50 | 51 | .. automodule:: imapfw.architects 52 | :members: 53 | :undoc-members: 54 | :show-inheritance: 55 | -------------------------------------------------------------------------------- /internals/source/imapfw.concurrency.rst: -------------------------------------------------------------------------------- 1 | imapfw.concurrency package 2 | ========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | imapfw.concurrency.concurrency module 8 | ------------------------------------- 9 | 10 | .. automodule:: imapfw.concurrency.concurrency 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: imapfw.concurrency 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /internals/source/imapfw.conf.rst: -------------------------------------------------------------------------------- 1 | imapfw.conf package 2 | =================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | imapfw.conf.clioptions module 8 | ----------------------------- 9 | 10 | .. automodule:: imapfw.conf.clioptions 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | imapfw.conf.conf module 16 | ----------------------- 17 | 18 | .. automodule:: imapfw.conf.conf 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: imapfw.conf 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /internals/source/imapfw.controllers.rst: -------------------------------------------------------------------------------- 1 | imapfw.controllers package 2 | ========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | imapfw.controllers.controller module 8 | ------------------------------------ 9 | 10 | .. automodule:: imapfw.controllers.controller 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | imapfw.controllers.duplicate module 16 | ----------------------------------- 17 | 18 | .. automodule:: imapfw.controllers.duplicate 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | imapfw.controllers.examine module 24 | --------------------------------- 25 | 26 | .. automodule:: imapfw.controllers.examine 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | imapfw.controllers.fake module 32 | ------------------------------ 33 | 34 | .. automodule:: imapfw.controllers.fake 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | imapfw.controllers.filter module 40 | -------------------------------- 41 | 42 | .. automodule:: imapfw.controllers.filter 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | imapfw.controllers.nametrans module 48 | ----------------------------------- 49 | 50 | .. automodule:: imapfw.controllers.nametrans 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | imapfw.controllers.transcoder module 56 | ------------------------------------ 57 | 58 | .. automodule:: imapfw.controllers.transcoder 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | 64 | Module contents 65 | --------------- 66 | 67 | .. automodule:: imapfw.controllers 68 | :members: 69 | :undoc-members: 70 | :show-inheritance: 71 | -------------------------------------------------------------------------------- /internals/source/imapfw.drivers.rst: -------------------------------------------------------------------------------- 1 | imapfw.drivers package 2 | ====================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | imapfw.drivers.driver module 8 | ---------------------------- 9 | 10 | .. automodule:: imapfw.drivers.driver 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | imapfw.drivers.imap module 16 | -------------------------- 17 | 18 | .. automodule:: imapfw.drivers.imap 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | imapfw.drivers.maildir module 24 | ----------------------------- 25 | 26 | .. automodule:: imapfw.drivers.maildir 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: imapfw.drivers 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /internals/source/imapfw.engines.rst: -------------------------------------------------------------------------------- 1 | imapfw.engines package 2 | ====================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | imapfw.engines.account module 8 | ----------------------------- 9 | 10 | .. automodule:: imapfw.engines.account 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | imapfw.engines.engine module 16 | ---------------------------- 17 | 18 | .. automodule:: imapfw.engines.engine 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | imapfw.engines.folder module 24 | ---------------------------- 25 | 26 | .. automodule:: imapfw.engines.folder 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: imapfw.engines 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /internals/source/imapfw.imap.rst: -------------------------------------------------------------------------------- 1 | imapfw.imap package 2 | =================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | imapfw.imap.imap module 8 | ----------------------- 9 | 10 | .. automodule:: imapfw.imap.imap 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: imapfw.imap 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /internals/source/imapfw.mmp.rst: -------------------------------------------------------------------------------- 1 | imapfw.mmp package 2 | ================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | imapfw.mmp.account module 8 | ------------------------- 9 | 10 | .. automodule:: imapfw.mmp.account 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | imapfw.mmp.driver module 16 | ------------------------ 17 | 18 | .. automodule:: imapfw.mmp.driver 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | imapfw.mmp.folder module 24 | ------------------------ 25 | 26 | .. automodule:: imapfw.mmp.folder 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | imapfw.mmp.manager module 32 | ------------------------- 33 | 34 | .. automodule:: imapfw.mmp.manager 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | imapfw.mmp.serializer module 40 | ---------------------------- 41 | 42 | .. automodule:: imapfw.mmp.serializer 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | 48 | Module contents 49 | --------------- 50 | 51 | .. automodule:: imapfw.mmp 52 | :members: 53 | :undoc-members: 54 | :show-inheritance: 55 | -------------------------------------------------------------------------------- /internals/source/imapfw.rst: -------------------------------------------------------------------------------- 1 | imapfw package 2 | ============== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | imapfw.actions 10 | imapfw.architects 11 | imapfw.concurrency 12 | imapfw.conf 13 | imapfw.controllers 14 | imapfw.drivers 15 | imapfw.engines 16 | imapfw.imap 17 | imapfw.mmp 18 | imapfw.runners 19 | imapfw.shells 20 | imapfw.testing 21 | imapfw.types 22 | imapfw.ui 23 | 24 | Submodules 25 | ---------- 26 | 27 | imapfw.annotation module 28 | ------------------------ 29 | 30 | .. automodule:: imapfw.annotation 31 | :members: 32 | :undoc-members: 33 | :show-inheritance: 34 | 35 | imapfw.constants module 36 | ----------------------- 37 | 38 | .. automodule:: imapfw.constants 39 | :members: 40 | :undoc-members: 41 | :show-inheritance: 42 | 43 | imapfw.edmp module 44 | ------------------ 45 | 46 | .. automodule:: imapfw.edmp 47 | :members: 48 | :undoc-members: 49 | :show-inheritance: 50 | 51 | imapfw.error module 52 | ------------------- 53 | 54 | .. automodule:: imapfw.error 55 | :members: 56 | :undoc-members: 57 | :show-inheritance: 58 | 59 | imapfw.init module 60 | ------------------ 61 | 62 | .. automodule:: imapfw.init 63 | :members: 64 | :undoc-members: 65 | :show-inheritance: 66 | 67 | imapfw.interface module 68 | ----------------------- 69 | 70 | .. automodule:: imapfw.interface 71 | :members: 72 | :undoc-members: 73 | :show-inheritance: 74 | 75 | imapfw.rascal module 76 | -------------------- 77 | 78 | .. automodule:: imapfw.rascal 79 | :members: 80 | :undoc-members: 81 | :show-inheritance: 82 | 83 | imapfw.runtime module 84 | --------------------- 85 | 86 | .. automodule:: imapfw.runtime 87 | :members: 88 | :undoc-members: 89 | :show-inheritance: 90 | 91 | imapfw.toolkit module 92 | --------------------- 93 | 94 | .. automodule:: imapfw.toolkit 95 | :members: 96 | :undoc-members: 97 | :show-inheritance: 98 | 99 | 100 | Module contents 101 | --------------- 102 | 103 | .. automodule:: imapfw 104 | :members: 105 | :undoc-members: 106 | :show-inheritance: 107 | -------------------------------------------------------------------------------- /internals/source/imapfw.runners.rst: -------------------------------------------------------------------------------- 1 | imapfw.runners package 2 | ====================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | imapfw.runners.driver module 8 | ---------------------------- 9 | 10 | .. automodule:: imapfw.runners.driver 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | imapfw.runners.toprunner module 16 | ------------------------------- 17 | 18 | .. automodule:: imapfw.runners.toprunner 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: imapfw.runners 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /internals/source/imapfw.shells.rst: -------------------------------------------------------------------------------- 1 | imapfw.shells package 2 | ===================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | imapfw.shells.shell module 8 | -------------------------- 9 | 10 | .. automodule:: imapfw.shells.shell 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: imapfw.shells 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /internals/source/imapfw.testing.rst: -------------------------------------------------------------------------------- 1 | imapfw.testing package 2 | ====================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | imapfw.testing.architect module 8 | ------------------------------- 9 | 10 | .. automodule:: imapfw.testing.architect 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | imapfw.testing.concurrency module 16 | --------------------------------- 17 | 18 | .. automodule:: imapfw.testing.concurrency 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | imapfw.testing.edmp module 24 | -------------------------- 25 | 26 | .. automodule:: imapfw.testing.edmp 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | imapfw.testing.folder module 32 | ---------------------------- 33 | 34 | .. automodule:: imapfw.testing.folder 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | imapfw.testing.libcore module 40 | ----------------------------- 41 | 42 | .. automodule:: imapfw.testing.libcore 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | imapfw.testing.maildir module 48 | ----------------------------- 49 | 50 | .. automodule:: imapfw.testing.maildir 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | imapfw.testing.message module 56 | ----------------------------- 57 | 58 | .. automodule:: imapfw.testing.message 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | imapfw.testing.nullui module 64 | ---------------------------- 65 | 66 | .. automodule:: imapfw.testing.nullui 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | imapfw.testing.rascal module 72 | ---------------------------- 73 | 74 | .. automodule:: imapfw.testing.rascal 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | imapfw.testing.testrascal module 80 | -------------------------------- 81 | 82 | .. automodule:: imapfw.testing.testrascal 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | 87 | imapfw.testing.types module 88 | --------------------------- 89 | 90 | .. automodule:: imapfw.testing.types 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | 95 | 96 | Module contents 97 | --------------- 98 | 99 | .. automodule:: imapfw.testing 100 | :members: 101 | :undoc-members: 102 | :show-inheritance: 103 | -------------------------------------------------------------------------------- /internals/source/imapfw.types.rst: -------------------------------------------------------------------------------- 1 | imapfw.types package 2 | ==================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | imapfw.types.account module 8 | --------------------------- 9 | 10 | .. automodule:: imapfw.types.account 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | imapfw.types.folder module 16 | -------------------------- 17 | 18 | .. automodule:: imapfw.types.folder 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | imapfw.types.imap module 24 | ------------------------ 25 | 26 | .. automodule:: imapfw.types.imap 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | imapfw.types.maildir module 32 | --------------------------- 33 | 34 | .. automodule:: imapfw.types.maildir 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | imapfw.types.message module 40 | --------------------------- 41 | 42 | .. automodule:: imapfw.types.message 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | imapfw.types.repository module 48 | ------------------------------ 49 | 50 | .. automodule:: imapfw.types.repository 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | 56 | Module contents 57 | --------------- 58 | 59 | .. automodule:: imapfw.types 60 | :members: 61 | :undoc-members: 62 | :show-inheritance: 63 | -------------------------------------------------------------------------------- /internals/source/imapfw.ui.rst: -------------------------------------------------------------------------------- 1 | imapfw.ui package 2 | ================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | imapfw.ui.tty module 8 | -------------------- 9 | 10 | .. automodule:: imapfw.ui.tty 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: imapfw.ui 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /internals/source/index.rst: -------------------------------------------------------------------------------- 1 | .. imapfw documentation master file, created by 2 | sphinx-quickstart on Sat Feb 27 08:00:02 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to imapfw's documentation! 7 | ================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | 23 | -------------------------------------------------------------------------------- /internals/source/modules.rst: -------------------------------------------------------------------------------- 1 | imapfw 2 | ====== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | imapfw 8 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/logo.png -------------------------------------------------------------------------------- /rascals/controllers.rascal: -------------------------------------------------------------------------------- 1 | # 2 | # Used by me to development purposes. 3 | # 4 | 5 | MainConf = { 6 | # The number of concurrent workers for the accounts. Default is the number 7 | # of accounts to sync. 8 | 'max_sync_accounts': 7, 9 | } 10 | 11 | 12 | def configure(ui): 13 | pass 14 | 15 | 16 | from imapfw.api import engines, controllers, types, drivers 17 | 18 | maildirsPath = "~/.imapfw/Mail" 19 | 20 | 21 | MaildirConfA = { 22 | 'path': maildirsPath + '/MaildirA', 23 | 'max_connections': 9, 24 | } 25 | 26 | MaildirConfB = { 27 | 'path': maildirsPath + '/MaildirB', 28 | 'max_connections': 9, 29 | } 30 | 31 | 32 | ImapConfA = { 33 | 'backend': 'imaplib3', 34 | 'host': '127.0.0.1', 35 | 'port': '10143', 36 | 'username': 'nicolas', 37 | 'password': 'sebrecht', 38 | 'max_connections': 11, 39 | 'controllers': [ 40 | { 'controller': controllers.Encoder, 41 | 'conf': { 42 | 'encoding': 'UTF-8' 43 | } 44 | }, 45 | { 'controller': controllers.Duplicate, 'conf': {} }, 46 | ] 47 | } 48 | 49 | class MaildirA(types.Maildir): 50 | conf = MaildirConfA 51 | driver = drivers.Maildir # Default: drivers.Maildir. 52 | controllers = [] # Default: TODO 53 | 54 | class MaildirB(types.Maildir): 55 | conf = MaildirConfB 56 | driver = drivers.Maildir # Default: drivers.Maildir. 57 | 58 | class ImapA(types.Imap): 59 | conf = ImapConfA 60 | driver = drivers.Imap # Default: drivers.Imap. 61 | 62 | class AccountA(types.Account): 63 | engine = engines.SyncAccount # Default: engine.SyncAccount. (TODO) 64 | left = MaildirA 65 | right = ImapA 66 | 67 | 68 | # vim: syntax=python ts=4 expandtab : 69 | -------------------------------------------------------------------------------- /rascals/demo.rascal: -------------------------------------------------------------------------------- 1 | # 2 | # Used by me to development purposes. 3 | # 4 | 5 | MainConf = { 6 | # The number of concurrent workers for the accounts. Default is the number 7 | # of accounts to sync. 8 | 'max_sync_accounts': 7, 9 | } 10 | 11 | 12 | def configure(ui): 13 | pass 14 | 15 | 16 | from imapfw.api import controllers, types, drivers, shells 17 | 18 | # 19 | # Global variables. 20 | # 21 | maildirsPath = "~/.imapfw/Mail" 22 | 23 | # 24 | # Controllers. 25 | # 26 | class FakeMaildir(controllers.FakeDriver): 27 | conf = controllers.FakeDriver.MaildirConf 28 | 29 | import time, random 30 | class FakeImap(controllers.FakeDriver): 31 | conf = controllers.FakeDriver.ImapConf 32 | 33 | def select(self, *args, **kwargs): 34 | time.sleep(random.randint(1, 30) / 5) 35 | return super(FakeImap, self).select(*args, **kwargs) 36 | 37 | # 38 | # Repositories. 39 | # 40 | MaildirConfA = { 41 | 'path': maildirsPath + '/MaildirA', 42 | 'max_connections': 9, 43 | } 44 | class MaildirA(types.Maildir): 45 | conf = MaildirConfA 46 | driver = drivers.Maildir # Default: drivers.Maildir. 47 | controllers = [FakeMaildir] 48 | 49 | MaildirConfB = { 50 | 'path': maildirsPath + '/MaildirB', 51 | 'max_connections': 2, 52 | } 53 | class MaildirB(types.Maildir): 54 | conf = MaildirConfB 55 | driver = drivers.Maildir # Default: drivers.Maildir. 56 | controllers = [FakeMaildir] 57 | 58 | ImapConfA = { 59 | 'backend': 'imaplib3', 60 | 'host': '127.0.0.1', 61 | 'port': '10143', 62 | 'username': 'nicolas', 63 | 'password': 'sebrecht', 64 | 'max_connections': 3, 65 | } 66 | class ImapA(types.Imap): 67 | conf = ImapConfA 68 | driver = drivers.Imap # Default: drivers.Imap. 69 | controllers = [FakeImap] 70 | 71 | # 72 | # Accounts. 73 | # 74 | class Home(types.Account): 75 | left = MaildirA 76 | right = ImapA 77 | 78 | class Foundation(types.Account): 79 | left = MaildirB 80 | right = ImapA 81 | 82 | # vim: syntax=python ts=4 expandtab : 83 | -------------------------------------------------------------------------------- /rascals/dev.rascal: -------------------------------------------------------------------------------- 1 | # 2 | # Used by me to development purposes. 3 | # 4 | 5 | MainConf = { 6 | # The number of concurrent workers for the accounts. Default is the number 7 | # of accounts to sync. 8 | 'max_sync_accounts': 2, 9 | } 10 | 11 | UI = None 12 | 13 | def configure(ui): 14 | global UI 15 | UI = ui 16 | 17 | def preHook(hook, actionName, actionOptions): 18 | hook.ended() 19 | 20 | def postHook(hook): 21 | hook.ended() 22 | 23 | def exceptionHook(hook, error): 24 | hook.ended() 25 | 26 | from imapfw.api import controllers, types, drivers, shells 27 | 28 | maildirsPath = "~/.imapfw/Mail" 29 | 30 | 31 | MaildirConfA = { 32 | 'path': maildirsPath + '/MaildirA', 33 | 'max_connections': 9, 34 | } 35 | 36 | MaildirConfB = { 37 | 'path': maildirsPath + '/MaildirB', 38 | 'max_connections': 2, 39 | } 40 | 41 | ImapConfA = { 42 | 'backend': 'imaplib3', 43 | 'host': '127.0.0.1', 44 | 'port': '10143', 45 | 'username': 'nicolas', 46 | 'password': 'sebrecht', 47 | 'max_connections': 3, 48 | } 49 | 50 | class FakeMaildirA(controllers.FakeDriver): 51 | conf = controllers.FakeDriver.MaildirConf 52 | 53 | class MaildirA(types.Maildir): 54 | conf = MaildirConfA 55 | driver = drivers.Maildir # Default: drivers.Maildir. 56 | controllers = [FakeMaildirA] 57 | 58 | 59 | class FakeMaildirB(controllers.FakeDriver): 60 | conf = controllers.FakeDriver.MaildirConf 61 | 62 | class MaildirB(types.Maildir): 63 | conf = MaildirConfB 64 | driver = drivers.Maildir # Default: drivers.Maildir. 65 | controllers = [FakeMaildirB] 66 | 67 | 68 | class FakeImapA(controllers.FakeDriver): 69 | conf = controllers.FakeDriver.ImapConf 70 | 71 | class ImapA(types.Imap): 72 | conf = ImapConfA 73 | driver = drivers.Imap # Default: drivers.Imap. 74 | controllers = [FakeImapA] 75 | 76 | 77 | class Home(types.Account): 78 | left = MaildirA 79 | right = ImapA 80 | 81 | class Foundation(types.Account): 82 | left = MaildirB 83 | right = ImapA 84 | 85 | 86 | # To work on IMAP driver. 87 | class DriveDriver(shells.DriveDriver): 88 | def session(self): 89 | d = self.d 90 | 91 | d.connect() 92 | d.login() 93 | self.interactive() 94 | d.logout() 95 | 96 | class DriveMaildirA(DriveDriver): 97 | conf = {'repository': MaildirA} 98 | 99 | class DriveImapA(DriveDriver): 100 | conf = {'repository': ImapA} 101 | 102 | 103 | # To work on messages. 104 | class MShell(shells.Shell): 105 | def beforeSession(self): 106 | self.m = types.message.Message(2) 107 | self.n = types.message.Message(3) 108 | self.o = types.message.Message(4) 109 | self.a = types.message.Messages(self.m, self.n) 110 | 111 | self.register('m') 112 | self.register('n') 113 | self.register('o') 114 | self.register('a') 115 | 116 | def session(self): 117 | m = self.m 118 | n = self.n 119 | o = self.o 120 | a = self.a 121 | 122 | print(m in a) 123 | print(o in a) 124 | self.interactive() 125 | 126 | 127 | 128 | # vim: syntax=python ts=4 expandtab : 129 | -------------------------------------------------------------------------------- /rascals/local.driver.rascal: -------------------------------------------------------------------------------- 1 | # 2 | # Used by me to development purposes. 3 | # 4 | 5 | MainConf = { 6 | # The number of concurrent workers for the accounts. Default is the number 7 | # of accounts to sync. 8 | 'max_sync_accounts': 7, 9 | } 10 | 11 | 12 | def configure(ui): 13 | pass 14 | 15 | 16 | from imapfw.api import controllers, types, drivers, shells 17 | 18 | ImapConfA = { 19 | 'backend': 'imaplib3', 20 | 'host': '127.0.0.1', 21 | 'port': 10143, 22 | 'username': 'nicolas', 23 | 'password': 'sebrecht', 24 | 'max_connections': 3, 25 | } 26 | 27 | 28 | class ImapA(types.Imap): 29 | conf = ImapConfA 30 | driver = drivers.Imap # Default: drivers.Imap. 31 | #controllers = [FakeImapA] 32 | 33 | 34 | class MShell(shells.Shell): 35 | def session(self): 36 | from imapfw.types.message import Messages, Message as M 37 | 38 | self.M = M 39 | self.register('M') 40 | 41 | self.Messages = Messages 42 | self.register('Messages') 43 | 44 | self.m = Messages() 45 | self.m.add(M(1)) 46 | self.m.add(M(2)) 47 | self.register('m') 48 | 49 | self.interactive() 50 | 51 | 52 | class ImapShell(shells.DriveDriver): 53 | conf = {'repository': ImapA} 54 | 55 | def session(self): 56 | self.buildDriver() 57 | 58 | d = self.d 59 | 60 | d.connect() 61 | d.getCapability() 62 | d.login() 63 | 64 | namespace = d.getNamespace() 65 | print(namespace) 66 | 67 | folders = d.getFolders() 68 | print(str(folders)) 69 | 70 | d.select(folders[-1]) 71 | messages = d.searchUID() 72 | print(messages) 73 | 74 | attributes = drivers.FetchAttributes() 75 | attributes.setDefaults() 76 | messages = d.getMessages(messages, attributes) 77 | for m in messages.values(): 78 | print(m.attributes) 79 | d.logout() 80 | 81 | 82 | # vim: syntax=python ts=4 expandtab : 83 | -------------------------------------------------------------------------------- /rascals/simple/starter.rascal: -------------------------------------------------------------------------------- 1 | 2 | 3 | __VERSION__ = 0.1 4 | 5 | 6 | from imapfw.api import types, drivers 7 | 8 | 9 | ########## 10 | # GLOBAL # 11 | ########## 12 | 13 | 14 | # UI allows to send messages to the UI library of the framework. Interface is 15 | # quite the same as the logging library: 16 | # 17 | # - critical(*args) 18 | # - debug(*args) 19 | # - error(*args) 20 | # - exception(*args) 21 | # - info(*args) 22 | # - warn(*args) 23 | # 24 | # This includes the following added methods: 25 | # 26 | # - infoL(, *args) # Honors from CLI. 27 | # - debugC(, *args) # Honors from CLI. 28 | # 29 | UI = None 30 | 31 | # The configure function is called once the rascal is loaded, before the action 32 | # gets executed. This allows configuring both the rascal and any other ressource 33 | # you need. 34 | def configure(ui): 35 | global UI 36 | UI = ui 37 | 38 | 39 | ######### 40 | # Hooks # 41 | ######### 42 | 43 | # Hooks are optional. 44 | 45 | # The hook started at the beginning of the process once initialization is done. 46 | def preHook(hook, actionName, actionOptions): 47 | UI.info("Running preHook: starting action %s with actionOptions %s"% 48 | (actionName, actionOptions)) 49 | hook.ended() # timeout not reached (10 seconds). 50 | 51 | 52 | # The hook started at the end of the process when no exception is raised. 53 | def postHook(hook): 54 | UI.info('Runing postHook') 55 | hook.ended() # timeout not reached (10 seconds). 56 | 57 | 58 | # This hook is started on unexpected exception. 59 | # Arguments: 60 | # - error: the exception error. 61 | # Don't call sys.exit() here or you will loose the correct exit code. 62 | def exceptionHook(hook, error): 63 | UI.critical("Running exceptionHook: an exception was catched!") 64 | #import traceback, sys 65 | #UI.exception(error) 66 | #traceback.print_exc(file=sys.stdout) 67 | hook.ended() # timeout not reached (10 seconds). 68 | 69 | 70 | 71 | ################ 72 | # REPOSITORIES # 73 | ################ 74 | 75 | ImapConfExample = { 76 | 'backend': 'imaplib3', # Optional. 77 | 'host': 'imap.gmail.com', 78 | 'port': '143', 79 | 'username': 'myname', 80 | 'password': 'password', 81 | 'max_connections': 2, 82 | } 83 | 84 | ImapRepositoryExample = { 85 | 'name': 'ImapRepositoryExample', 86 | 'type': types.Imap, 87 | 'conf': ImapConfExample, 88 | 'driver': drivers.Imap, 89 | } 90 | 91 | 92 | ############ 93 | # ACCOUNTS # 94 | ############ 95 | 96 | 97 | # 98 | # The main configuration options are set in this dict. 99 | # 100 | # Everything can be defined in this dict. However, we encourage to use variables 101 | # for clarity. 102 | # 103 | MainConf = { 104 | # The number of concurrent workers for the accounts. Default is the number 105 | # of accounts to sync. 106 | 'max_sync_accounts': 2, 107 | # The list of accounts. 108 | 'accounts': [ 109 | { 110 | 'name': 'AccountExample', 111 | 'type': types.Account, 112 | 113 | # account attributes. 114 | 'left': { 115 | 'name': 'MaildirExample', 116 | 'type': types.Maildir, 117 | 118 | # repository attributes. 119 | 'driver': drivers.Maildir, 120 | 'conf': { 121 | 'path': '~/.imapfw/Mail/MaildirA', 122 | 'max_connections': 2, 123 | }, 124 | }, 125 | 'right': ImapRepositoryExample, 126 | }, 127 | ], 128 | } 129 | 130 | # 131 | # Each type can be declared as global variables. For example, the "accounts" key 132 | # from above can be removed and the configuration can be defined like this: 133 | # 134 | #MaildirExample = { 135 | # 'name': MaildirExample, 136 | # 'type': types.Maildir, 137 | # 'driver': drivers.Maildir, 138 | # 'conf': { 139 | # 'path': '~/Maildir/MaildirA', 140 | # 'max_connections': 2, 141 | # }, 142 | #} 143 | # 144 | #AccountExample = { 145 | # 'type': types.Account, 146 | # 'conf': { 147 | # 'path': '~/Maildir/MaildirA', 148 | # 'max_connections': 2, 149 | # }, 150 | # 'left': MaildirExample, 151 | # 'right': ImapRepositoryExample, 152 | #} 153 | 154 | # 155 | # For even better clarity, types can be written as python objects. For example, 156 | # the MaildirExample can be rewritten like this: 157 | # 158 | #MaildirExample(types.Maildir): 159 | # driver = drivers.Maildir, 160 | # conf = { 161 | # 'path': '~/Maildir/MaildirA', 162 | # 'max_connections': 2, 163 | # } 164 | 165 | 166 | 167 | # vim: syntax=python ts=4 expandtab : 168 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | typing; python_version < '3.5' 2 | -------------------------------------------------------------------------------- /tests/syncaccounts.1.rascal: -------------------------------------------------------------------------------- 1 | # 2 | # Used by me to development purposes. 3 | # 4 | 5 | MainConf = { 6 | 7 | # Concurrency backend to use for the workers: 8 | # - multiprocessing; 9 | # - threading; 10 | # 11 | # Because most of the wait time is due to I/O (disk and network, there 12 | # should be no visible performance differences. 13 | # 14 | # Be aware that the rascal gets COPIED into each worker. This means that 15 | # any changed ressource (including global variable) after the call to 16 | # configure() won't be updated for all the running code. 17 | 'concurrency_backend': 'multiprocessing', 18 | 19 | # The number of concurrent workers for the accounts. Default is the number 20 | # of accounts to sync. 21 | 'max_sync_accounts': 2, 22 | } 23 | 24 | UI = None 25 | 26 | def configure(ui): 27 | global UI 28 | UI = ui 29 | 30 | def preHook(hook, actionName, actionOptions): 31 | hook.ended() 32 | 33 | def postHook(hook): 34 | hook.ended() 35 | 36 | def exceptionHook(hook, error): 37 | hook.ended() 38 | 39 | from imapfw.api import controllers, types, drivers 40 | 41 | import os 42 | maildirsPath = os.path.join( 43 | os.path.dirname(os.path.realpath(__file__)), 44 | "syncaccounts.1") 45 | 46 | 47 | MaildirConfA = { 48 | 'path': maildirsPath + '/MaildirA', 49 | 'max_connections': 9, 50 | } 51 | 52 | class MaildirA(types.Maildir): 53 | conf = MaildirConfA 54 | driver = drivers.Maildir # Default: drivers.Maildir. 55 | 56 | 57 | FakeImapConfA = { 58 | 'folders': [b'INBOX', b'INBOX/spam', b'INBOX/outbox', 59 | b'INBOX/sp&AOk-cial', 60 | ] 61 | } 62 | 63 | class FakeImapA(controllers.FakeDriver): 64 | conf = FakeImapConfA 65 | 66 | ImapConfA = { 67 | 'backend': 'imaplib3', 68 | 'host': '127.0.0.1', 69 | 'port': 10143, 70 | 'username': 'nicolas', 71 | 'password': 'sebrecht', 72 | 'max_connections': 11, 73 | } 74 | 75 | class ImapA(types.Imap): 76 | conf = ImapConfA 77 | driver = drivers.Imap 78 | controllers = [FakeImapA] 79 | 80 | class AccountA(types.Account): 81 | engine = "SyncAccount" # Default: engine.SyncAccount. (TODO) 82 | left = MaildirA 83 | right = ImapA 84 | 85 | # vim: syntax=python ts=4 expandtab : 86 | -------------------------------------------------------------------------------- /tests/syncaccounts.1/MaildirA/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/imapfw/740a4fed1a1de28e4134a115a1dd9c6e90e29ec1/tests/syncaccounts.1/MaildirA/.keep -------------------------------------------------------------------------------- /travis-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Run the travis tests locally. 4 | 5 | GET_TESTS_FILE='get_tests.awk' 6 | 7 | cat < "$GET_TESTS_FILE" 8 | #!/usr/bin/awk -f 9 | 10 | BEGIN { 11 | FS="\n" 12 | } 13 | 14 | { 15 | if (\$1 == "script:") { 16 | FS="- " 17 | } 18 | if (FS == "- " && \$1 == " ") { 19 | if (\$2 == "coverage combine .coverage*") { 20 | exit 0 21 | } 22 | sub(/coverage run [^ ]+ -p/, "python3", \$2) 23 | print \$2 24 | } 25 | } 26 | EOF 27 | 28 | chmod u+x "$GET_TESTS_FILE" 29 | 30 | 31 | errors=0 32 | total=0 33 | scripts="" 34 | 35 | ./"$GET_TESTS_FILE" .travis.yml | while read script 36 | do 37 | printf "\n======= Running $script" 38 | total=$(($total + 1)) 39 | $script 40 | if test $? -eq 0 41 | then 42 | scripts="$scripts\n- $script" 43 | else 44 | echo "======== FAILED" 45 | errors=$(($errors + 1)) 46 | scripts="$scripts\n- $script (FAILED)" 47 | fi 48 | 49 | printf "\n\nTests done:" 50 | printf "$scripts\n" 51 | printf "\nTotal: $total, errors: $errors\n" 52 | done 53 | 54 | rm -f "$GET_TESTS_FILE" > /dev/null 2>&1 55 | 56 | # vim: expandtab ts=2 : 57 | --------------------------------------------------------------------------------