├── .github ├── dependabot.yml └── workflows │ └── plugin.yml ├── .gitignore ├── .landscape.yaml ├── .tx └── config ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── conf.py ├── index.rst └── setup.rst ├── modoboa_webmail ├── __init__.py ├── apps.py ├── constants.py ├── exceptions.py ├── forms.py ├── handlers.py ├── lib │ ├── __init__.py │ ├── attachments.py │ ├── fetch_parser.py │ ├── imapemail.py │ ├── imapheader.py │ ├── imaputils.py │ ├── rfc6266.py │ ├── sendmail.py │ ├── signature.py │ └── utils.py ├── locale │ ├── cs_CZ │ │ └── LC_MESSAGES │ │ │ ├── django.po │ │ │ └── djangojs.po │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.po │ │ │ └── djangojs.po │ ├── el_GR │ │ └── LC_MESSAGES │ │ │ ├── django.po │ │ │ └── djangojs.po │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.po │ │ │ └── djangojs.po │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.po │ │ │ └── djangojs.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── django.po │ │ │ └── djangojs.po │ ├── it │ │ └── LC_MESSAGES │ │ │ ├── django.po │ │ │ └── djangojs.po │ ├── ja_JP │ │ └── LC_MESSAGES │ │ │ ├── django.po │ │ │ └── djangojs.po │ ├── nl_NL │ │ └── LC_MESSAGES │ │ │ ├── django.po │ │ │ └── djangojs.po │ ├── pt_BR │ │ └── LC_MESSAGES │ │ │ ├── django.po │ │ │ └── djangojs.po │ ├── pt_PT │ │ └── LC_MESSAGES │ │ │ ├── django.po │ │ │ └── djangojs.po │ ├── ro_RO │ │ └── LC_MESSAGES │ │ │ ├── django.po │ │ │ └── djangojs.po │ ├── ru │ │ └── LC_MESSAGES │ │ │ ├── django.po │ │ │ └── djangojs.po │ ├── sv │ │ └── LC_MESSAGES │ │ │ ├── django.po │ │ │ └── djangojs.po │ └── zh_TW │ │ └── LC_MESSAGES │ │ ├── django.po │ │ └── djangojs.po ├── modo_extension.py ├── static │ └── modoboa_webmail │ │ ├── css │ │ ├── attachments.css │ │ └── webmail.css │ │ └── js │ │ └── webmail.js ├── templates │ └── modoboa_webmail │ │ ├── ask_password.html │ │ ├── attachments.html │ │ ├── compose.html │ │ ├── compose_menubar.html │ │ ├── email_list.html │ │ ├── folder.html │ │ ├── folders.html │ │ ├── headers.html │ │ ├── index.html │ │ ├── mail_source.html │ │ ├── main_action_bar.html │ │ └── upload_done.html ├── templatetags │ ├── __init__.py │ └── webmail_tags.py ├── tests │ ├── __init__.py │ ├── data.py │ ├── test_fetch_parser.py │ └── test_views.py ├── urls.py ├── validators.py └── views.py ├── pylintrc ├── requirements.txt ├── setup.cfg ├── setup.py ├── test-requirements.txt └── test_project ├── manage.py └── test_project ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/workflows/plugin.yml: -------------------------------------------------------------------------------- 1 | name: Modoboa webmail plugin 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | release: 9 | branches: [ master ] 10 | types: [ published ] 11 | 12 | env: 13 | POSTGRES_HOST: localhost 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | services: 19 | postgres: 20 | image: postgres:12 21 | env: 22 | POSTGRES_USER: postgres 23 | POSTGRES_PASSWORD: postgres 24 | POSTGRES_DB: postgres 25 | ports: 26 | # will assign a random free host port 27 | - 5432/tcp 28 | # needed because the postgres container does not provide a healthcheck 29 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 30 | mysql: 31 | image: mysql:8.0 32 | env: 33 | MYSQL_ROOT_PASSWORD: root 34 | MYSQL_USER: modoboa 35 | MYSQL_PASSWORD: modoboa 36 | MYSQL_DATABASE: modoboa 37 | ports: 38 | - 3306/tcp 39 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 40 | redis: 41 | image: redis 42 | ports: 43 | - 6379/tcp 44 | options: >- 45 | --health-cmd "redis-cli ping" 46 | --health-interval 10s 47 | --health-timeout 5s 48 | --health-retries 5 49 | 50 | strategy: 51 | matrix: 52 | database: ['postgres', 'mysql'] 53 | python-version: [3.8, 3.9, '3.10'] 54 | fail-fast: false 55 | 56 | steps: 57 | - uses: actions/checkout@v3 58 | - name: Set up Python ${{ matrix.python-version }} 59 | uses: actions/setup-python@v4 60 | with: 61 | python-version: ${{ matrix.python-version }} 62 | - name: Install dependencies 63 | run: | 64 | sudo apt-get update -y \ 65 | && sudo apt-get update -y && sudo apt-get install -y librrd-dev rrdtool redis-server 66 | python -m pip install --upgrade pip 67 | pip install -r requirements.txt 68 | pip install -r test-requirements.txt 69 | python setup.py develop 70 | echo "Testing redis connection" 71 | redis-cli -h $REDIS_HOST -p $REDIS_PORT ping 72 | env: 73 | REDIS_HOST: localhost 74 | REDIS_PORT: ${{ job.services.redis.ports[6379] }} 75 | - name: Install postgres requirements 76 | if: ${{ matrix.database == 'postgres' }} 77 | run: | 78 | pip install coverage 79 | echo "DB=postgres" >> $GITHUB_ENV 80 | - name: Install mysql requirements 81 | if: ${{ matrix.database == 'mysql' }} 82 | run: | 83 | echo "DB=mysql" >> $GITHUB_ENV 84 | - name: Test with pytest 85 | if: ${{ matrix.python-version != '3.10' || matrix.database != 'postgres' }} 86 | run: | 87 | cd test_project 88 | python3 manage.py test modoboa_webmail 89 | env: 90 | # use localhost for the host here because we are running the job on the VM. 91 | # If we were running the job on in a container this would be postgres 92 | POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} # get randomly assigned published port 93 | MYSQL_HOST: 127.0.0.1 94 | MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} # get randomly assigned published port 95 | MYSQL_USER: root 96 | REDIS_HOST: localhost 97 | REDIS_PORT: ${{ job.services.redis.ports[6379] }} 98 | 99 | - name: Test with pytest and coverage 100 | if: ${{ matrix.python-version == '3.10' && matrix.database == 'postgres' }} 101 | run: | 102 | cd test_project 103 | coverage run --source ../modoboa_webmail manage.py test modoboa_webmail 104 | coverage xml 105 | coverage report 106 | env: 107 | # use localhost for the host here because we are running the job on the VM. 108 | # If we were running the job on in a container this would be postgres 109 | POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} # get randomly assigned published port 110 | MYSQL_HOST: 127.0.0.1 111 | MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} # get randomly assigned published port 112 | MYSQL_USER: root 113 | REDIS_HOST: localhost 114 | REDIS_PORT: ${{ job.services.redis.ports[6379] }} 115 | - name: Upload coverage result 116 | if: ${{ matrix.python-version == '3.10' && matrix.database == 'postgres' }} 117 | uses: actions/upload-artifact@v3 118 | with: 119 | name: coverage-results 120 | path: test_project/coverage.xml 121 | 122 | coverage: 123 | needs: test 124 | runs-on: ubuntu-latest 125 | steps: 126 | - uses: actions/checkout@v3 127 | - name: Download coverage results 128 | uses: actions/download-artifact@v3 129 | with: 130 | name: coverage-results 131 | - name: Upload coverage to Codecov 132 | uses: codecov/codecov-action@v3 133 | with: 134 | files: ./coverage.xml 135 | 136 | release: 137 | if: github.event_name != 'pull_request' 138 | needs: coverage 139 | runs-on: ubuntu-latest 140 | steps: 141 | - uses: actions/checkout@v3 142 | with: 143 | fetch-depth: 0 144 | - name: Set up Python 3.10 145 | uses: actions/setup-python@v4 146 | with: 147 | python-version: '3.10' 148 | - name: Build packages 149 | run: | 150 | sudo apt-get install librrd-dev rrdtool libssl-dev gettext 151 | python -m pip install --upgrade pip setuptools wheel 152 | pip install -r requirements.txt 153 | cd modoboa_webmail 154 | django-admin compilemessages 155 | cd .. 156 | python setup.py sdist bdist_wheel 157 | - name: Publish to Test PyPI 158 | if: endsWith(github.event.ref, '/master') 159 | uses: pypa/gh-action-pypi-publish@master 160 | with: 161 | user: __token__ 162 | password: ${{ secrets.test_pypi_password }} 163 | repository_url: https://test.pypi.org/legacy/ 164 | skip_existing: true 165 | - name: Publish distribution to PyPI 166 | if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release' 167 | uses: pypa/gh-action-pypi-publish@master 168 | with: 169 | user: __token__ 170 | password: ${{ secrets.pypi_password }} 171 | skip_existing: true 172 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib64/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | .eggs 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /.landscape.yaml: -------------------------------------------------------------------------------- 1 | doc-warnings: yes 2 | test-warnings: no 3 | strictness: medium 4 | max-line-length: 80 5 | uses: 6 | - django 7 | autodetect: yes 8 | requirements: 9 | - requirements.txt 10 | ignore-paths: 11 | - docs 12 | ignore-patterns: 13 | - (^|/)migrations(/|$) 14 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [modoboa.modoboa-webmail-djangopo] 5 | file_filter = modoboa_webmail/locale//LC_MESSAGES/django.po 6 | source_file = modoboa_webmail/locale/en/LC_MESSAGES/django.po 7 | source_lang = en 8 | type = PO 9 | 10 | [modoboa.modoboa-webmail-djangojspo] 11 | file_filter = modoboa_webmail/locale//LC_MESSAGES/djangojs.po 12 | source_file = modoboa_webmail/locale/en/LC_MESSAGES/djangojs.po 13 | source_lang = en 14 | type = PO 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Antoine Nguyen 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE requirements.txt 2 | recursive-include modoboa_webmail *.html *.js *.css *.png *.po *.mo 3 | recursive-include doc * 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | modoboa-webmail 2 | =============== 3 | 4 | |gha| |codecov| |rtfd| 5 | 6 | The webmail of Modoboa. 7 | 8 | Installation 9 | ------------ 10 | 11 | Install this extension system-wide or inside a virtual environment by 12 | running the following command:: 13 | 14 | $ pip install modoboa-webmail 15 | 16 | Edit the settings.py file of your modoboa instance and add 17 | ``modoboa_webmail`` inside the ``MODOBOA_APPS`` variable like this:: 18 | 19 | MODOBOA_APPS = ( 20 | 'modoboa', 21 | 'modoboa.core', 22 | 'modoboa.lib', 23 | 'modoboa.admin', 24 | 'modoboa.relaydomains', 25 | 'modoboa.limits', 26 | 'modoboa.parameters', 27 | # Extensions here 28 | # ... 29 | 'modoboa_webmail', 30 | ) 31 | 32 | Run the following commands to setup the database tables and collect static files:: 33 | 34 | $ cd 35 | $ python manage.py load_initial_data 36 | $ python manage.py collectstatic 37 | 38 | Finally, restart the python process running modoboa (uwsgi, gunicorn, 39 | apache, whatever). 40 | 41 | .. |gha| image:: https://github.com/modoboa/modoboa-webmail/actions/workflows/plugin.yml/badge.svg 42 | :target: https://github.com/modoboa/modoboa-webmail/actions/workflows/plugin.yml 43 | 44 | .. |codecov| image:: https://codecov.io/gh/modoboa/modoboa-webmail/branch/master/graph/badge.svg 45 | :target: https://codecov.io/gh/modoboa/modoboa-webmail 46 | 47 | .. |rtfd| image:: https://readthedocs.org/projects/modoboa-webmail/badge/?version=latest 48 | :target: https://readthedocs.org/projects/modoboa-webmail/?badge=latest 49 | :alt: Documentation Status 50 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/modoboa-webmail.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/modoboa-webmail.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/modoboa-webmail" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/modoboa-webmail" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # modoboa-webmail documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Apr 5 14:19:58 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.intersphinx', 33 | ] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates'] 37 | 38 | # The suffix of source filenames. 39 | source_suffix = '.rst' 40 | 41 | # The encoding of source files. 42 | #source_encoding = 'utf-8-sig' 43 | 44 | # The master toctree document. 45 | master_doc = 'index' 46 | 47 | # General information about the project. 48 | project = u'modoboa-webmail' 49 | copyright = u'2017, Antoine Nguyen' 50 | 51 | # The version info for the project you're documenting, acts as replacement for 52 | # |version| and |release|, also used in various other places throughout the 53 | # built documents. 54 | # 55 | # The short X.Y version. 56 | version = '1.2' 57 | # The full version, including alpha/beta/rc tags. 58 | release = '1.2.1' 59 | 60 | # The language for content autogenerated by Sphinx. Refer to documentation 61 | # for a list of supported languages. 62 | #language = None 63 | 64 | # There are two options for replacing |today|: either, you set today to some 65 | # non-false value, then it is used: 66 | #today = '' 67 | # Else, today_fmt is used as the format for a strftime call. 68 | #today_fmt = '%B %d, %Y' 69 | 70 | # List of patterns, relative to source directory, that match files and 71 | # directories to ignore when looking for source files. 72 | exclude_patterns = ['_build'] 73 | 74 | # The reST default role (used for this markup: `text`) to use for all 75 | # documents. 76 | #default_role = None 77 | 78 | # If true, '()' will be appended to :func: etc. cross-reference text. 79 | #add_function_parentheses = True 80 | 81 | # If true, the current module name will be prepended to all description 82 | # unit titles (such as .. function::). 83 | #add_module_names = True 84 | 85 | # If true, sectionauthor and moduleauthor directives will be shown in the 86 | # output. They are ignored by default. 87 | #show_authors = False 88 | 89 | # The name of the Pygments (syntax highlighting) style to use. 90 | pygments_style = 'sphinx' 91 | 92 | # A list of ignored prefixes for module index sorting. 93 | #modindex_common_prefix = [] 94 | 95 | # If true, keep warnings as "system message" paragraphs in the built documents. 96 | #keep_warnings = False 97 | 98 | 99 | # -- Options for HTML output ---------------------------------------------- 100 | 101 | # The theme to use for HTML and HTML Help pages. See the documentation for 102 | # a list of builtin themes. 103 | html_theme = 'default' 104 | 105 | # Theme options are theme-specific and customize the look and feel of a theme 106 | # further. For a list of options available for each theme, see the 107 | # documentation. 108 | #html_theme_options = {} 109 | 110 | # Add any paths that contain custom themes here, relative to this directory. 111 | #html_theme_path = [] 112 | 113 | # The name for this set of Sphinx documents. If None, it defaults to 114 | # " v documentation". 115 | #html_title = None 116 | 117 | # A shorter title for the navigation bar. Default is the same as html_title. 118 | #html_short_title = None 119 | 120 | # The name of an image file (relative to this directory) to place at the top 121 | # of the sidebar. 122 | #html_logo = None 123 | 124 | # The name of an image file (within the static path) to use as favicon of the 125 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 126 | # pixels large. 127 | #html_favicon = None 128 | 129 | # Add any paths that contain custom static files (such as style sheets) here, 130 | # relative to this directory. They are copied after the builtin static files, 131 | # so a file named "default.css" will overwrite the builtin "default.css". 132 | html_static_path = ['_static'] 133 | 134 | # Add any extra paths that contain custom files (such as robots.txt or 135 | # .htaccess) here, relative to this directory. These files are copied 136 | # directly to the root of the documentation. 137 | #html_extra_path = [] 138 | 139 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 140 | # using the given strftime format. 141 | #html_last_updated_fmt = '%b %d, %Y' 142 | 143 | # If true, SmartyPants will be used to convert quotes and dashes to 144 | # typographically correct entities. 145 | #html_use_smartypants = True 146 | 147 | # Custom sidebar templates, maps document names to template names. 148 | #html_sidebars = {} 149 | 150 | # Additional templates that should be rendered to pages, maps page names to 151 | # template names. 152 | #html_additional_pages = {} 153 | 154 | # If false, no module index is generated. 155 | #html_domain_indices = True 156 | 157 | # If false, no index is generated. 158 | #html_use_index = True 159 | 160 | # If true, the index is split into individual pages for each letter. 161 | #html_split_index = False 162 | 163 | # If true, links to the reST sources are added to the pages. 164 | #html_show_sourcelink = True 165 | 166 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 167 | #html_show_sphinx = True 168 | 169 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 170 | #html_show_copyright = True 171 | 172 | # If true, an OpenSearch description file will be output, and all pages will 173 | # contain a tag referring to it. The value of this option must be the 174 | # base URL from which the finished HTML is served. 175 | #html_use_opensearch = '' 176 | 177 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 178 | #html_file_suffix = None 179 | 180 | # Output file base name for HTML help builder. 181 | htmlhelp_basename = 'modoboa-webmaildoc' 182 | 183 | 184 | # -- Options for LaTeX output --------------------------------------------- 185 | 186 | latex_elements = { 187 | # The paper size ('letterpaper' or 'a4paper'). 188 | #'papersize': 'letterpaper', 189 | 190 | # The font size ('10pt', '11pt' or '12pt'). 191 | #'pointsize': '10pt', 192 | 193 | # Additional stuff for the LaTeX preamble. 194 | #'preamble': '', 195 | } 196 | 197 | # Grouping the document tree into LaTeX files. List of tuples 198 | # (source start file, target name, title, 199 | # author, documentclass [howto, manual, or own class]). 200 | latex_documents = [ 201 | ('index', 'modoboa-webmail.tex', u'modoboa-webmail Documentation', 202 | u'Antoine Nguyen', 'manual'), 203 | ] 204 | 205 | # The name of an image file (relative to this directory) to place at the top of 206 | # the title page. 207 | #latex_logo = None 208 | 209 | # For "manual" documents, if this is true, then toplevel headings are parts, 210 | # not chapters. 211 | #latex_use_parts = False 212 | 213 | # If true, show page references after internal links. 214 | #latex_show_pagerefs = False 215 | 216 | # If true, show URL addresses after external links. 217 | #latex_show_urls = False 218 | 219 | # Documents to append as an appendix to all manuals. 220 | #latex_appendices = [] 221 | 222 | # If false, no module index is generated. 223 | #latex_domain_indices = True 224 | 225 | 226 | # -- Options for manual page output --------------------------------------- 227 | 228 | # One entry per manual page. List of tuples 229 | # (source start file, name, description, authors, manual section). 230 | man_pages = [ 231 | ('index', 'modoboa-webmail', u'modoboa-webmail Documentation', 232 | [u'Antoine Nguyen'], 1) 233 | ] 234 | 235 | # If true, show URL addresses after external links. 236 | #man_show_urls = False 237 | 238 | 239 | # -- Options for Texinfo output ------------------------------------------- 240 | 241 | # Grouping the document tree into Texinfo files. List of tuples 242 | # (source start file, target name, title, author, 243 | # dir menu entry, description, category) 244 | texinfo_documents = [ 245 | ('index', 'modoboa-webmail', u'modoboa-webmail Documentation', 246 | u'Antoine Nguyen', 'modoboa-webmail', 'One line description of project.', 247 | 'Miscellaneous'), 248 | ] 249 | 250 | # Documents to append as an appendix to all manuals. 251 | #texinfo_appendices = [] 252 | 253 | # If false, no module index is generated. 254 | #texinfo_domain_indices = True 255 | 256 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 257 | #texinfo_show_urls = 'footnote' 258 | 259 | # If true, do not generate a @detailmenu in the "Top" node's menu. 260 | #texinfo_no_detailmenu = False 261 | 262 | 263 | # Example configuration for intersphinx: refer to the Python standard library. 264 | intersphinx_mapping = {'http://docs.python.org/': None} 265 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. modoboa-webmail documentation master file, created by 2 | sphinx-quickstart on Sun Apr 5 14:19:58 2015. 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 modoboa-webmail's documentation! 7 | =========================================== 8 | 9 | Modoboa provides a simple webmail: 10 | 11 | * Browse, read and compose messages, attachments are supported 12 | * HTML messages are supported 13 | * `CKeditor `_ integration 14 | * Manipulate mailboxes (create, move, remove) 15 | * Quota display 16 | 17 | Contents: 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | 22 | setup 23 | -------------------------------------------------------------------------------- /docs/setup.rst: -------------------------------------------------------------------------------- 1 | ##### 2 | Setup 3 | ##### 4 | 5 | To use it, go to the online panel and modify the following parameters 6 | to communicate with your *IMAP* server (under *IMAP settings*): 7 | 8 | +--------------------+--------------------+--------------------+ 9 | |Name |Description |Default value | 10 | +====================+====================+====================+ 11 | |Server address |Address of your IMAP|127.0.0.1 | 12 | | |server | | 13 | +--------------------+--------------------+--------------------+ 14 | |Use a secured |Use a secured |no | 15 | |connection |connection to access| | 16 | | |IMAP server | | 17 | +--------------------+--------------------+--------------------+ 18 | |Server port |Listening port of |143 | 19 | | |your IMAP server | | 20 | +--------------------+--------------------+--------------------+ 21 | 22 | Do the same to communicate with your SMTP server (under *SMTP settings*): 23 | 24 | +--------------------+--------------------+--------------------+ 25 | |Name |Description |Default value | 26 | +====================+====================+====================+ 27 | |Server address |Address of your SMTP|127.0.0.1 | 28 | | |server | | 29 | +--------------------+--------------------+--------------------+ 30 | |Secured connection |Use a secured |None | 31 | |mode |connection to access| | 32 | | |SMTP server | | 33 | +--------------------+--------------------+--------------------+ 34 | |Server port |Listening port of |25 | 35 | | |your SMTP server | | 36 | +--------------------+--------------------+--------------------+ 37 | |Authentication |Server needs |no | 38 | |required |authentication | | 39 | +--------------------+--------------------+--------------------+ 40 | 41 | .. note:: 42 | 43 | The size of each attachment sent with a message is limited. You can 44 | change the default value by modifying the **Maximum attachment 45 | size** parameter. 46 | 47 | Using CKeditor 48 | ============== 49 | 50 | Modoboa supports CKeditor to compose HTML messages. Each user has the 51 | possibility to choose between CKeditor and the raw text editor to 52 | compose their messages. (see *User > Settings > Preferences > 53 | Webmail*) 54 | -------------------------------------------------------------------------------- /modoboa_webmail/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """DMARC related tools for Modoboa.""" 4 | 5 | from __future__ import unicode_literals 6 | 7 | from pkg_resources import get_distribution, DistributionNotFound 8 | 9 | 10 | try: 11 | __version__ = get_distribution(__name__).version 12 | except DistributionNotFound: 13 | # package is not installed 14 | __version__ = '9.9.9' 15 | 16 | default_app_config = "modoboa_webmail.apps.WebmailConfig" 17 | -------------------------------------------------------------------------------- /modoboa_webmail/apps.py: -------------------------------------------------------------------------------- 1 | """AppConfig for webmail.""" 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class WebmailConfig(AppConfig): 7 | """App configuration.""" 8 | 9 | name = "modoboa_webmail" 10 | verbose_name = "Simple webmail for Modoboa" 11 | 12 | def ready(self): 13 | from . import handlers 14 | -------------------------------------------------------------------------------- /modoboa_webmail/constants.py: -------------------------------------------------------------------------------- 1 | """Webmail constants.""" 2 | 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | 6 | SORT_ORDERS = [ 7 | ("date", _("Date")), 8 | ("from", _("Sender")), 9 | ("size", _("Size")), 10 | ("subject", _("Subject")), 11 | ] 12 | -------------------------------------------------------------------------------- /modoboa_webmail/exceptions.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | :mod:`exceptions` --- Webmail custom exceptions 4 | ----------------------------------------------- 5 | 6 | """ 7 | import re 8 | 9 | from django.utils.translation import gettext as _ 10 | 11 | from modoboa.lib.exceptions import ModoboaException, InternalError 12 | 13 | 14 | class WebmailInternalError(InternalError): 15 | errorexpr = re.compile(r'\[([^\]]+)\]\s*([^\.]+)') 16 | 17 | def __init__(self, reason, ajax=False): 18 | match = WebmailInternalError.errorexpr.match(reason) 19 | if not match: 20 | self.reason = reason 21 | else: 22 | self.reason = "%s: %s" % (_("Server response"), match.group(2)) 23 | self.ajax = ajax 24 | 25 | def __str__(self): 26 | return self.reason 27 | 28 | 29 | class UnknownAction(ModoboaException): 30 | 31 | """ 32 | Use this exception when the webmail encounter an unknown action. 33 | """ 34 | http_code = 404 35 | 36 | def __init__(self): 37 | super(UnknownAction, self).__init__(_("Unknown action")) 38 | 39 | 40 | class ImapError(ModoboaException): 41 | 42 | http_code = 500 43 | 44 | def __init__(self, reason): 45 | self.reason = reason 46 | 47 | def __str__(self): 48 | return str(self.reason) 49 | -------------------------------------------------------------------------------- /modoboa_webmail/handlers.py: -------------------------------------------------------------------------------- 1 | """Webmail handlers.""" 2 | 3 | from django.urls import reverse 4 | from django.dispatch import receiver 5 | from django.utils.translation import gettext as _ 6 | 7 | from modoboa.core import signals as core_signals 8 | 9 | from . import exceptions 10 | from . import lib 11 | 12 | 13 | @receiver(core_signals.extra_user_menu_entries) 14 | def menu(sender, location, user, **kwargs): 15 | """Return extra menu entry.""" 16 | if location != "top_menu" or not hasattr(user, "mailbox"): 17 | return [] 18 | return [ 19 | {"name": "webmail", 20 | "label": _("Webmail"), 21 | "url": reverse("modoboa_webmail:index")}, 22 | ] 23 | 24 | 25 | @receiver(core_signals.user_logout) 26 | def userlogout(sender, request, **kwargs): 27 | """Close IMAP connection.""" 28 | if not hasattr(request.user, "mailbox"): 29 | return 30 | try: 31 | m = lib.IMAPconnector(user=request.user.username, 32 | password=request.session["password"]) 33 | except Exception: 34 | # TODO silent exception are bad : we should at least log it 35 | return 36 | 37 | # The following statement may fail under Python 2.6... 38 | try: 39 | m.logout() 40 | except exceptions.ImapError: 41 | pass 42 | 43 | 44 | @receiver(core_signals.extra_static_content) 45 | def extra_js(sender, caller, st_type, user, **kwargs): 46 | """Add javascript.""" 47 | if caller != "user_index" or st_type != "js": 48 | return "" 49 | return """function toggleSignatureEditor() { 50 | var editorId = 'id_modoboa_webmail-signature'; 51 | if ($(this).val() === 'html') { 52 | CKEDITOR.replace(editorId, $('#' + editorId).data('config')); 53 | } else { 54 | var instance = CKEDITOR.instances[editorId]; 55 | instance.destroy(); 56 | } 57 | } 58 | 59 | $(document).on( 60 | 'change', 'input[name=modoboa_webmail-editor]', toggleSignatureEditor); 61 | $(document).on('preferencesLoaded', function() { 62 | $('input[name=modoboa_webmail-editor]:checked').change(); 63 | }); 64 | """ 65 | -------------------------------------------------------------------------------- /modoboa_webmail/lib/__init__.py: -------------------------------------------------------------------------------- 1 | from .attachments import ( 2 | create_mail_attachment, save_attachment, clean_attachments, 3 | set_compose_session, AttachmentUploadHandler 4 | ) 5 | from .imapemail import ImapEmail, ReplyModifier, ForwardModifier 6 | from .imaputils import ( 7 | BodyStructure, IMAPconnector, get_imapconnector, separate_mailbox 8 | ) 9 | from .signature import EmailSignature 10 | from .utils import decode_payload, WebmailNavigationParameters 11 | from .sendmail import send_mail 12 | 13 | 14 | __all__ = [ 15 | 'AttachmentUploadHandler', 16 | 'BodyStructure', 17 | 'EmailSignature', 18 | 'ForwardModifier', 19 | 'IMAPconnector', 20 | 'ImapEmail', 21 | 'ReplyModifier', 22 | 'WebmailNavigationParameters', 23 | 'clean_attachments', 24 | 'create_mail_attachment', 25 | 'decode_payload', 26 | 'get_imapconnector', 27 | 'save_attachment', 28 | 'send_mail', 29 | 'separate_mailbox', 30 | 'set_compose_session', 31 | ] 32 | -------------------------------------------------------------------------------- /modoboa_webmail/lib/attachments.py: -------------------------------------------------------------------------------- 1 | from email import encoders 2 | from email.mime.base import MIMEBase 3 | import os 4 | import uuid 5 | 6 | import six 7 | 8 | from django.conf import settings 9 | from django.core.files.uploadhandler import FileUploadHandler, SkipFile 10 | from django.utils.encoding import smart_bytes 11 | 12 | from modoboa.lib.exceptions import InternalError 13 | from modoboa.lib.web_utils import size2integer 14 | from modoboa.parameters import tools as param_tools 15 | 16 | from .rfc6266 import build_header 17 | 18 | 19 | def get_storage_path(filename): 20 | """Return a path to store a file.""" 21 | storage_dir = os.path.join(settings.MEDIA_ROOT, "webmail") 22 | if not filename: 23 | return storage_dir 24 | return os.path.join(storage_dir, filename) 25 | 26 | 27 | def set_compose_session(request): 28 | """Initialize a new "compose" session. 29 | 30 | It is used to keep track of attachments defined with a new 31 | message. Each new message will be associated with a unique ID (in 32 | order to avoid conflicts between users). 33 | 34 | :param request: a Request object. 35 | :return: the new unique ID. 36 | """ 37 | randid = str(uuid.uuid4()).replace("-", "") 38 | request.session["compose_mail"] = {"id": randid, "attachments": []} 39 | return randid 40 | 41 | 42 | def save_attachment(f): 43 | """Save a new attachment to the filesystem. 44 | 45 | The attachment is not saved using its own name to the 46 | filesystem. To avoid conflicts, a random name is generated and 47 | used instead. 48 | 49 | :param f: an uploaded file object (see Django's documentation) or bytes 50 | :return: the new random name 51 | """ 52 | from tempfile import NamedTemporaryFile 53 | 54 | try: 55 | fp = NamedTemporaryFile(dir=get_storage_path(""), delete=False) 56 | except Exception as e: 57 | raise InternalError(str(e)) 58 | if isinstance(f, (six.binary_type, six.text_type)): 59 | fp.write(smart_bytes(f)) 60 | else: 61 | for chunk in f.chunks(): 62 | fp.write(chunk) 63 | fp.close() 64 | return fp.name 65 | 66 | 67 | def clean_attachments(attlist): 68 | """Remove all attachments from the filesystem 69 | 70 | :param attlist: a list of 2-uple. Each element must contain the 71 | following information : (random name, real name). 72 | """ 73 | for att in attlist: 74 | fullpath = get_storage_path(att["tmpname"]) 75 | try: 76 | os.remove(fullpath) 77 | except OSError: 78 | pass 79 | 80 | 81 | def create_mail_attachment(attdef, payload=None): 82 | """Create the MIME part corresponding to the given attachment. 83 | 84 | Mandatory keys: 'fname', 'tmpname', 'content-type' 85 | 86 | :param attdef: a dictionary containing the attachment definition 87 | :return: a MIMEBase object 88 | """ 89 | if "content-type" in attdef: 90 | maintype, subtype = attdef["content-type"].split("/") 91 | elif "Content-Type" in attdef: 92 | maintype, subtype = attdef["Content-Type"].split("/") 93 | else: 94 | return None 95 | res = MIMEBase(maintype, subtype) 96 | if payload is None: 97 | path = get_storage_path(attdef["tmpname"]) 98 | with open(path, "rb") as fp: 99 | res.set_payload(fp.read()) 100 | else: 101 | res.set_payload(payload) 102 | encoders.encode_base64(res) 103 | if isinstance(attdef["fname"], six.binary_type): 104 | attdef["fname"] = attdef["fname"].decode("utf-8") 105 | content_disposition = build_header(attdef["fname"]) 106 | if isinstance(content_disposition, six.binary_type): 107 | res["Content-Disposition"] = content_disposition.decode("utf-8") 108 | else: 109 | res["Content-Disposition"] = content_disposition 110 | return res 111 | 112 | 113 | class AttachmentUploadHandler(FileUploadHandler): 114 | 115 | """ 116 | Simple upload handler to limit the size of the attachments users 117 | can upload. 118 | """ 119 | 120 | def __init__(self, request=None): 121 | super(AttachmentUploadHandler, self).__init__(request) 122 | self.total_upload = 0 123 | self.toobig = False 124 | self.maxsize = size2integer( 125 | param_tools.get_global_parameter("max_attachment_size")) 126 | 127 | def receive_data_chunk(self, raw_data, start): 128 | self.total_upload += len(raw_data) 129 | if self.total_upload >= self.maxsize: 130 | self.toobig = True 131 | raise SkipFile() 132 | return raw_data 133 | 134 | def file_complete(self, file_size): 135 | return None 136 | -------------------------------------------------------------------------------- /modoboa_webmail/lib/fetch_parser.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """Simple parser for FETCH responses. 4 | 5 | The ``imaplib`` module doesn't parse IMAP responses, it returns raw 6 | values. Since Modoboa relies on BODYSTRUCTURE attributes to display 7 | messages (we don't want to overload the server), a parser is required. 8 | 9 | Python 2/3 compatibility note: this parser excepts bytes objects when 10 | run with Python3 and str (not unicode) ones with Python2. 11 | """ 12 | 13 | from __future__ import print_function 14 | 15 | import re 16 | 17 | import chardet 18 | import six 19 | 20 | 21 | class ParseError(Exception): 22 | """Generic parsing error""" 23 | pass 24 | 25 | 26 | class Lexer(object): 27 | """The lexical analysis part. 28 | 29 | This class provides a simple way to define tokens (with patterns) 30 | to be detected. Patterns are provided using a list of 2-uple. Each 31 | 2-uple consists of a token name and an associated pattern. 32 | 33 | Example: [("left_bracket", r'\['),] 34 | """ 35 | 36 | def __init__(self, definitions): 37 | self.definitions = definitions 38 | parts = [] 39 | for name, part in definitions: 40 | parts.append(r"(?P<%s>%s)" % (name, part)) 41 | self.regexpString = "|".join(parts) 42 | self.regexp = re.compile(self.regexpString, re.MULTILINE) 43 | self.wsregexp = re.compile(r"\s+", re.M) 44 | 45 | def curlineno(self): 46 | """Return the current line number""" 47 | return self.text[:self.pos].count("\n") + 1 48 | 49 | def scan(self, text): 50 | """Analyze some data. 51 | 52 | Analyse the passed content. Each time a token is recognized, a 53 | 2-uple containing its name and the parsed value is raised 54 | (using yield). 55 | 56 | :param text: a string containing the data to parse 57 | :raises: ParseError 58 | """ 59 | self.pos = 0 60 | self.text = text 61 | while self.pos < len(text): 62 | m = self.wsregexp.match(text, self.pos) 63 | if m is not None: 64 | self.pos = m.end() 65 | continue 66 | 67 | m = self.regexp.match(text, self.pos) 68 | if m is None: 69 | raise ParseError("unknown token {}".format(text[self.pos:])) 70 | 71 | self.pos = m.end() 72 | yield (m.lastgroup, m.group(m.lastgroup)) 73 | 74 | 75 | class FetchResponseParser(object): 76 | """A token generator. 77 | 78 | By *token*, I mean: *literal*, *quoted* or anything else until the 79 | next ' ' or ')' character (number, NIL and others should fall into 80 | this last category). 81 | """ 82 | 83 | rules = [ 84 | ("left_parenthesis", r'\('), 85 | ("right_parenthesis", r'\)'), 86 | ("string", r'"([^"\\]|\\.)*"'), 87 | ("nil", r'NIL'), 88 | ("data_item", 89 | r"(?P[A-Z][A-Z\.0-9]+)" 90 | r"(?P
\[.*\])?(?P\<\d+\>)?"), 91 | ("number", r'[0-9]+'), 92 | ("literal_marker", r'{\d+}'), 93 | ("flag", r'(\\|\$)?[a-zA-Z0-9\-_]+'), 94 | ] 95 | 96 | def __init__(self): 97 | """Constructor.""" 98 | self.lexer = Lexer(self.rules) 99 | self.__reset_parser() 100 | 101 | def __reset_parser(self): 102 | """Reset parser states.""" 103 | self.result = {} 104 | self.__current_message = {} 105 | self.__next_literal_len = 0 106 | self.__cur_data_item = None 107 | self.__args_parsing_func = None 108 | self.__expected = None 109 | self.__depth = 0 110 | self.__bs_stack = [] 111 | 112 | def set_expected(self, *args): 113 | """Indicate next expected token types.""" 114 | self.__expected = args 115 | 116 | def __default_args_parser(self, ttype, tvalue): 117 | """Default arguments parser.""" 118 | self.__current_message[self.__cur_data_item] = tvalue 119 | self.__args_parsing_func = None 120 | 121 | def __flags_args_parser(self, ttype, tvalue): 122 | """FLAGS arguments parser.""" 123 | if ttype == "left_parenthesis": 124 | self.__current_message[self.__cur_data_item] = [] 125 | self.__depth += 1 126 | elif ttype == "flag": 127 | self.__current_message[self.__cur_data_item].append(tvalue) 128 | self.set_expected("flag", "right_parenthesis") 129 | elif ttype == "right_parenthesis": 130 | self.__args_parsing_func = None 131 | self.__depth -= 1 132 | else: 133 | raise ParseError( 134 | "Unexpected token found: {}".format(ttype)) 135 | 136 | def __set_part_numbers(self, bs, prefix=""): 137 | """Set part numbers.""" 138 | cpt = 1 139 | for mp in bs: 140 | if isinstance(mp, list): 141 | self.__set_part_numbers(mp, prefix) 142 | elif isinstance(mp, dict): 143 | if isinstance(mp["struct"][0], list): 144 | nprefix = "{}{}.".format(prefix, cpt) 145 | self.__set_part_numbers(mp["struct"][0], nprefix) 146 | mp["partnum"] = "{}{}".format(prefix, cpt) 147 | cpt += 1 148 | 149 | def __bstruct_args_parser(self, ttype, tvalue): 150 | """BODYSTRUCTURE arguments parser.""" 151 | if ttype == "left_parenthesis": 152 | self.__bs_stack = [[]] + self.__bs_stack 153 | return 154 | if ttype == "right_parenthesis": 155 | if len(self.__bs_stack) > 1: 156 | part = self.__bs_stack.pop(0) 157 | # Check if we are parsing a list of mime part or a 158 | # list or arguments. 159 | condition = ( 160 | len(self.__bs_stack[0]) > 0 and 161 | not isinstance(self.__bs_stack[0][0], dict)) 162 | if condition: 163 | self.__bs_stack[0].append(part) 164 | else: 165 | self.__bs_stack[0].append({"struct": part}) 166 | else: 167 | # End of BODYSTRUCTURE 168 | if not isinstance(self.__bs_stack[0][0], list): 169 | # Special case for non multipart structures 170 | self.__bs_stack[0] = {"struct": self.__bs_stack[0]} 171 | self.__set_part_numbers(self.__bs_stack) 172 | self.__current_message[self.__cur_data_item] = self.__bs_stack 173 | self.__bs_stack = [] 174 | self.__args_parsing_func = None 175 | return 176 | if ttype == "string": 177 | tvalue = tvalue.strip('"') 178 | # Check if previous element was a mime part. If so, we are 179 | # dealing with a 'multipart' mime part... 180 | condition = ( 181 | len(self.__bs_stack[0]) and 182 | isinstance(self.__bs_stack[0][-1], dict)) 183 | if condition: 184 | self.__bs_stack[0] = [self.__bs_stack[0]] + [tvalue] 185 | return 186 | elif ttype == "number": 187 | tvalue = int(tvalue) 188 | self.__bs_stack[0].append(tvalue) 189 | 190 | def __parse_data_item(self, ttype, tvalue): 191 | """Find next data item.""" 192 | if ttype == "data_item": 193 | self.__cur_data_item = tvalue 194 | if tvalue == "BODYSTRUCTURE": 195 | self.set_expected("left_parenthesis") 196 | self.__args_parsing_func = self.__bstruct_args_parser 197 | elif tvalue == "FLAGS": 198 | self.set_expected("left_parenthesis") 199 | self.__args_parsing_func = self.__flags_args_parser 200 | else: 201 | self.__args_parsing_func = self.__default_args_parser 202 | return 203 | elif ttype == "right_parenthesis": 204 | self.__depth -= 1 205 | assert self.__depth == 0 206 | # FIXME: sometimes, FLAGS are returned outside the UID 207 | # scope (see sample 1 in tests). For now, we just ignore 208 | # them but we need a better solution! 209 | if "UID" in self.__current_message: 210 | self.result[int(self.__current_message.pop("UID"))] = ( 211 | self.__current_message) 212 | self.__current_message = {} 213 | return 214 | raise ParseError( 215 | "unexpected {} found while looking for data_item near {}" 216 | .format(ttype, tvalue)) 217 | 218 | def __convert_to_str(self, chunk): 219 | """Convert chunk to str and guess encoding.""" 220 | condition = ( 221 | six.PY2 and isinstance(chunk, six.text_type) or 222 | six.PY3 and isinstance(chunk, six.binary_type) 223 | ) 224 | if not condition: 225 | return chunk 226 | try: 227 | chunk = chunk.decode("utf-8") 228 | except UnicodeDecodeError: 229 | pass 230 | else: 231 | return chunk 232 | try: 233 | result = chardet.detect(chunk) 234 | except UnicodeDecodeError: 235 | raise RuntimeError("Can't find string encoding") 236 | return chunk.decode(result["encoding"]) 237 | 238 | def parse_chunk(self, chunk): 239 | """Parse chunk.""" 240 | if not chunk: 241 | return 242 | chunk = self.__convert_to_str(chunk) 243 | if self.__next_literal_len: 244 | literal = chunk[:self.__next_literal_len] 245 | chunk = chunk[self.__next_literal_len:] 246 | self.__next_literal_len = 0 247 | if self.__cur_data_item != "BODYSTRUCTURE": 248 | self.__current_message[self.__cur_data_item] = literal 249 | self.__args_parsing_func = None 250 | else: 251 | self.__args_parsing_func("literal", literal) 252 | for ttype, tvalue in self.lexer.scan(chunk): 253 | if self.__expected is not None: 254 | if ttype not in self.__expected: 255 | raise ParseError( 256 | "unexpected {} found while looking for {}" 257 | .format(ttype, "|".join(self.__expected))) 258 | self.__expected = None 259 | if ttype == "literal_marker": 260 | self.__next_literal_len = int(tvalue[1:-1]) 261 | continue 262 | elif self.__depth == 0: 263 | if ttype == "number": 264 | # We should start here with a message ID 265 | self.set_expected("left_parenthesis") 266 | if ttype == "left_parenthesis": 267 | self.__depth += 1 268 | continue 269 | elif self.__args_parsing_func is None: 270 | self.__parse_data_item(ttype, tvalue) 271 | continue 272 | self.__args_parsing_func(ttype, tvalue) 273 | 274 | def parse(self, data): 275 | """Parse received data.""" 276 | self.__reset_parser() 277 | for chunk in data: 278 | if isinstance(chunk, tuple): 279 | for schunk in chunk: 280 | self.parse_chunk(schunk) 281 | else: 282 | self.parse_chunk(chunk) 283 | return self.result 284 | -------------------------------------------------------------------------------- /modoboa_webmail/lib/imapheader.py: -------------------------------------------------------------------------------- 1 | """ 2 | Set of functions used to parse and transform email headers. 3 | """ 4 | 5 | from __future__ import unicode_literals 6 | 7 | import datetime 8 | import email 9 | 10 | import chardet 11 | import six 12 | 13 | from django.utils import timezone 14 | from django.utils.html import escape 15 | from django.utils.formats import date_format 16 | 17 | from modoboa.lib.email_utils import EmailAddress 18 | from modoboa.lib.signals import get_request 19 | 20 | 21 | __all__ = [ 22 | 'parse_from', 'parse_to', 'parse_message_id', 'parse_date', 23 | 'parse_reply_to', 'parse_cc', 'parse_subject' 24 | ] 25 | 26 | # date and time formats for email list 27 | # according to https://en.wikipedia.org/wiki/Date_format_by_country 28 | # and https://en.wikipedia.org/wiki/Date_and_time_representation_by_country 29 | DATETIME_FORMATS = { 30 | "cs": {'SHORT': 'l, H:i', 'LONG': 'd. N Y H:i'}, 31 | "de": {'SHORT': 'l, H:i', 'LONG': 'd. N Y H:i'}, 32 | "en": {'SHORT': 'l, P', 'LONG': 'N j, Y P'}, 33 | "es": {'SHORT': 'l, H:i', 'LONG': 'd. N Y H:i'}, 34 | "fr": {'SHORT': 'l, H:i', 'LONG': 'd. N Y H:i'}, 35 | "it": {'SHORT': 'l, H:i', 'LONG': 'd. N Y H:i'}, 36 | "ja_JP": {'SHORT': 'l, P', 'LONG': 'N j, Y P'}, 37 | "nl": {'SHORT': 'l, H:i', 'LONG': 'd. N Y H:i'}, 38 | "pl_PL": {'SHORT': 'l, H:i', 'LONG': 'd. N Y H:i'}, 39 | "pt_PT": {'SHORT': 'l, H:i', 'LONG': 'd. N Y H:i'}, 40 | "pt_BR": {'SHORT': 'l, H:i', 'LONG': 'd. N Y H:i'}, 41 | "ru": {'SHORT': 'l, H:i', 'LONG': 'd. N Y H:i'}, 42 | "sv": {'SHORT': 'l, H:i', 'LONG': 'd. N Y H:i'}, 43 | } 44 | 45 | 46 | def to_unicode(value): 47 | """Try to convert a string to unicode.""" 48 | condition = ( 49 | value is None or isinstance(value, six.text_type) 50 | ) 51 | if condition: 52 | return value 53 | try: 54 | value = value.decode("utf-8") 55 | except UnicodeDecodeError: 56 | pass 57 | else: 58 | return value 59 | try: 60 | res = chardet.detect(value) 61 | except UnicodeDecodeError: 62 | return value 63 | if res["encoding"] == "ascii": 64 | return value 65 | return value.decode(res["encoding"]) 66 | 67 | 68 | def parse_address(value, **kwargs): 69 | """Parse an email address.""" 70 | addr = EmailAddress(value) 71 | if kwargs.get("raw"): 72 | return to_unicode(addr.fulladdress) 73 | if addr.name: 74 | return u"{}".format( 75 | to_unicode(addr.address), escape(to_unicode(addr.name))) 76 | return u"{}".format(to_unicode(addr.address)) 77 | 78 | 79 | def parse_address_list(values, **kwargs): 80 | """Parse a list of email addresses.""" 81 | lst = values.split(",") 82 | result = [] 83 | for addr in lst: 84 | result.append(parse_address(addr, **kwargs)) 85 | return result 86 | 87 | 88 | def parse_from(value, **kwargs): 89 | """Parse a From: header.""" 90 | return [parse_address(value, **kwargs)] 91 | 92 | 93 | def parse_to(value, **kwargs): 94 | """Parse a To: header.""" 95 | return parse_address_list(value, **kwargs) 96 | 97 | 98 | def parse_cc(value, **kwargs): 99 | """Parse a Cc: header.""" 100 | return parse_address_list(value, **kwargs) 101 | 102 | 103 | def parse_reply_to(value, **kwargs): 104 | """Parse a Reply-To: header. 105 | """ 106 | return parse_address_list(value, **kwargs) 107 | 108 | 109 | def parse_date(value, **kwargs): 110 | """Parse a Date: header.""" 111 | tmp = email.utils.parsedate_tz(value) 112 | if not tmp: 113 | return value 114 | ndate = datetime.datetime.fromtimestamp(email.utils.mktime_tz(tmp)) 115 | if ndate.tzinfo is not None: 116 | tz = timezone.get_current_timezone() 117 | ndate = datetime.datetime.fromtimestamp(ndate).replace(tzinfo=tz) 118 | current_language = get_request().user.language 119 | if datetime.datetime.now() - ndate > datetime.timedelta(7): 120 | fmt = "LONG" 121 | else: 122 | fmt = "SHORT" 123 | return date_format( 124 | ndate, 125 | DATETIME_FORMATS.get(current_language, DATETIME_FORMATS.get("en"))[fmt] 126 | ) 127 | 128 | 129 | def parse_message_id(value, **kwargs): 130 | """Parse a Message-ID: header.""" 131 | return value.strip('\n') 132 | 133 | 134 | def parse_subject(value, **kwargs): 135 | """Parse a Subject: header.""" 136 | from modoboa.lib import u2u_decode 137 | 138 | try: 139 | subject = u2u_decode.u2u_decode(value) 140 | except UnicodeDecodeError: 141 | subject = value 142 | return to_unicode(subject) 143 | -------------------------------------------------------------------------------- /modoboa_webmail/lib/rfc6266.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copy of the original rfc6266 package, just the generation part. 3 | 4 | Orginal code is available at https://github.com/g2p/rfc6266. 5 | """ 6 | 7 | from urllib.parse import quote 8 | 9 | 10 | # RFC 2616 11 | separator_chars = "()<>@,;:\\\"/[]?={} \t" 12 | # RFC 5987 13 | attr_chars_nonalnum = '!#$&+-.^_`|~' 14 | 15 | 16 | def percent_encode(string, safe, encoding): 17 | return quote(string, safe, encoding, errors='strict') 18 | 19 | 20 | def is_token_char(ch): 21 | # Must be ascii, and neither a control char nor a separator char 22 | asciicode = ord(ch) 23 | # < 128 means ascii, exclude control chars at 0-31 and 127, 24 | # exclude separator characters. 25 | return 31 < asciicode < 127 and ch not in separator_chars 26 | 27 | 28 | def is_token(candidate): 29 | return all(is_token_char(ch) for ch in candidate) 30 | 31 | 32 | def is_ascii(text): 33 | return all(ord(ch) < 128 for ch in text) 34 | 35 | 36 | def fits_inside_codec(text, codec): 37 | try: 38 | text.encode(codec) 39 | except UnicodeEncodeError: 40 | return False 41 | else: 42 | return True 43 | 44 | 45 | def is_lws_safe(text): 46 | return normalize_ws(text) == text 47 | 48 | 49 | def normalize_ws(text): 50 | return ' '.join(text.split()) 51 | 52 | 53 | def qd_quote(text): 54 | return text.replace('\\', '\\\\').replace('"', '\\"') 55 | 56 | 57 | def build_header( 58 | filename, disposition='attachment', filename_compat=None 59 | ): 60 | """Generate a Content-Disposition header for a given filename. 61 | For legacy clients that don't understand the filename* parameter, 62 | a filename_compat value may be given. 63 | It should either be ascii-only (recommended) or iso-8859-1 only. 64 | In the later case it should be a character string 65 | (unicode in Python 2). 66 | Options for generating filename_compat (only useful for legacy clients): 67 | - ignore (will only send filename*); 68 | - strip accents using unicode's decomposing normalisations, 69 | which can be done from unicode data (stdlib), and keep only ascii; 70 | - use the ascii transliteration tables from Unidecode (PyPI); 71 | - use iso-8859-1 72 | Ignore is the safest, and can be used to trigger a fallback 73 | to the document location (which can be percent-encoded utf-8 74 | if you control the URLs). 75 | See https://tools.ietf.org/html/rfc6266#appendix-D 76 | """ 77 | 78 | # While this method exists, it could also sanitize the filename 79 | # by rejecting slashes or other weirdness that might upset a receiver. 80 | 81 | if disposition != 'attachment': 82 | assert is_token(disposition) 83 | 84 | rv = disposition 85 | 86 | if is_token(filename): 87 | rv += '; filename=%s' % (filename, ) 88 | return rv 89 | elif is_ascii(filename) and is_lws_safe(filename): 90 | qd_filename = qd_quote(filename) 91 | rv += '; filename="%s"' % (qd_filename, ) 92 | if qd_filename == filename: 93 | # RFC 6266 claims some implementations are iffy on qdtext's 94 | # backslash-escaping, we'll include filename* in that case. 95 | return rv 96 | elif filename_compat: 97 | if is_token(filename_compat): 98 | rv += '; filename=%s' % (filename_compat, ) 99 | else: 100 | assert is_lws_safe(filename_compat) 101 | rv += '; filename="%s"' % (qd_quote(filename_compat), ) 102 | 103 | # alnum are already considered always-safe, but the rest isn't. 104 | # Python encodes ~ when it shouldn't, for example. 105 | rv += "; filename*=utf-8''%s" % (percent_encode( 106 | filename, safe=attr_chars_nonalnum, encoding='utf-8'), ) 107 | 108 | # This will only encode filename_compat, if it used non-ascii iso-8859-1. 109 | return rv.encode('iso-8859-1') 110 | -------------------------------------------------------------------------------- /modoboa_webmail/lib/sendmail.py: -------------------------------------------------------------------------------- 1 | import smtplib 2 | 3 | from django.core import mail 4 | from django.template.loader import render_to_string 5 | 6 | from modoboa.lib.cryptutils import get_password 7 | from modoboa.parameters import tools as param_tools 8 | 9 | from . import get_imapconnector, clean_attachments 10 | 11 | 12 | def send_mail(request, form, posturl=None): 13 | """Email verification and sending. 14 | 15 | If the form does not present any error, a new MIME message is 16 | constructed. Then, a connection is established with the defined 17 | SMTP server and the message is finally sent. 18 | 19 | :param request: a Request object 20 | :param posturl: the url to post the message form to 21 | :return: a 2-uple (True|False, HttpResponse) 22 | """ 23 | if not form.is_valid(): 24 | editormode = request.user.parameters.get_value("editor") 25 | listing = render_to_string( 26 | "modoboa_webmail/compose.html", 27 | {"form": form, "noerrors": True, 28 | "body": form.cleaned_data.get("body", "").strip(), 29 | "posturl": posturl}, 30 | request 31 | ) 32 | return False, {"status": "ko", "listing": listing, "editor": editormode} 33 | 34 | msg = form.to_msg(request) 35 | conf = dict(param_tools.get_global_parameters("modoboa_webmail")) 36 | options = { 37 | "host": conf["smtp_server"], "port": conf["smtp_port"] 38 | } 39 | if conf["smtp_secured_mode"] == "ssl": 40 | options.update({"use_ssl": True}) 41 | elif conf["smtp_secured_mode"] == "starttls": 42 | options.update({"use_tls": True}) 43 | if conf["smtp_authentication"]: 44 | options.update({ 45 | "username": request.user.username, 46 | "password": get_password(request) 47 | }) 48 | try: 49 | with mail.get_connection(**options) as connection: 50 | msg.connection = connection 51 | msg.send() 52 | except smtplib.SMTPResponseException as inst: 53 | return False, {"status": "ko", "error": inst.smtp_error} 54 | except smtplib.SMTPRecipientsRefused as inst: 55 | error = ", ".join(["{}: {}".format(rcpt, error) 56 | for rcpt, error in inst.recipients.items()]) 57 | return False, {"status": "ko", "error": error} 58 | 59 | # Copy message to sent folder 60 | sentfolder = request.user.parameters.get_value("sent_folder") 61 | get_imapconnector(request).push_mail(sentfolder, msg.message()) 62 | clean_attachments(request.session["compose_mail"]["attachments"]) 63 | del request.session["compose_mail"] 64 | 65 | return True, {} 66 | -------------------------------------------------------------------------------- /modoboa_webmail/lib/signature.py: -------------------------------------------------------------------------------- 1 | """Tools to deal with message signatures.""" 2 | 3 | 4 | class EmailSignature(object): 5 | """User signature. 6 | 7 | :param user: User object 8 | """ 9 | 10 | def __init__(self, user): 11 | self._sig = u"" 12 | dformat = user.parameters.get_value("editor") 13 | content = user.parameters.get_value("signature") 14 | if len(content): 15 | getattr(self, "_format_sig_%s" % dformat)(content) 16 | 17 | def _format_sig_plain(self, content): 18 | self._sig = u""" 19 | --- 20 | %s""" % content 21 | 22 | def _format_sig_html(self, content): 23 | content = "---
{}".format(content) 24 | self._sig = content 25 | return 26 | 27 | def __str__(self): 28 | return self._sig 29 | -------------------------------------------------------------------------------- /modoboa_webmail/lib/utils.py: -------------------------------------------------------------------------------- 1 | """Misc. utilities.""" 2 | from functools import wraps 3 | 4 | from django.shortcuts import redirect 5 | 6 | from modoboa.lib.web_utils import NavigationParameters 7 | from modoboa.lib.cryptutils import get_password 8 | 9 | 10 | def decode_payload(encoding, payload): 11 | """Decode the payload according to the given encoding 12 | 13 | Supported encodings: base64, quoted-printable. 14 | 15 | :param encoding: the encoding's name 16 | :param payload: the value to decode 17 | :return: a string 18 | """ 19 | encoding = encoding.lower() 20 | if encoding == "base64": 21 | import base64 22 | return base64.b64decode(payload) 23 | elif encoding == "quoted-printable": 24 | import quopri 25 | return quopri.decodestring(payload) 26 | return payload 27 | 28 | 29 | class WebmailNavigationParameters(NavigationParameters): 30 | """Specific NavigationParameters subclass for the webmail.""" 31 | 32 | def __init__(self, request, defmailbox=None): 33 | super(WebmailNavigationParameters, self).__init__( 34 | request, 'webmail_navparams' 35 | ) 36 | if defmailbox is not None: 37 | self.parameters += [('mbox', defmailbox, False)] 38 | 39 | def _store_page(self): 40 | """Specific method to store the current page.""" 41 | if self.request.GET.get("reset_page", None) or "page" not in self: 42 | self["page"] = 1 43 | else: 44 | page = self.request.GET.get("page", None) 45 | if page is not None: 46 | self["page"] = int(page) 47 | 48 | 49 | def need_password(*args, **kwargs): 50 | """Check if the session holds the user password for the IMAP connection. 51 | """ 52 | def decorator(f): 53 | @wraps(f) 54 | def wrapped_f(request, *args, **kwargs): 55 | if get_password(request) is None: 56 | return redirect("modoboa_webmail:get_plain_password") 57 | return f(request, *args, **kwargs) 58 | return wrapped_f 59 | return decorator 60 | 61 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/cs_CZ/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Jarda Tesar , 2017 7 | # Miroslav Abrahám , 2013,2015 8 | msgid "" 9 | msgstr "" 10 | "Project-Id-Version: Modoboa\n" 11 | "Report-Msgid-Bugs-To: \n" 12 | "POT-Creation-Date: 2017-05-19 10:28+0200\n" 13 | "PO-Revision-Date: 2017-09-22 09:57+0000\n" 14 | "Last-Translator: Jarda Tesar \n" 15 | "Language-Team: Czech (Czech Republic) (http://www.transifex.com/tonio/modoboa/language/cs_CZ/)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Language: cs_CZ\n" 20 | "Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" 21 | 22 | #: static/modoboa_webmail/js/webmail.js:207 23 | msgid "No more message in this folder." 24 | msgstr "Žádné další zprávy v této složce" 25 | 26 | #: static/modoboa_webmail/js/webmail.js:703 27 | msgid "Remove the selected folder?" 28 | msgstr "Odebrat označenou složku?" 29 | 30 | #: static/modoboa_webmail/js/webmail.js:716 31 | msgid "Folder removed" 32 | msgstr "Složka odebrána" 33 | 34 | #: static/modoboa_webmail/js/webmail.js:1029 35 | msgid "Message sent" 36 | msgstr "Zpráva odeslána" 37 | 38 | #: static/modoboa_webmail/js/webmail.js:1362 39 | #, javascript-format 40 | msgid "Moving %s message" 41 | msgid_plural "Moving %s messages" 42 | msgstr[0] "Přesunuji %s zprávu" 43 | msgstr[1] "Přesunuji %s zprávy" 44 | msgstr[2] "Přesunuji %s zpráv" 45 | msgstr[3] "Přesunuji %s zpráv" 46 | 47 | #: static/modoboa_webmail/js/webmail.js:1434 48 | msgid "Contact added!" 49 | msgstr "Kontakt přidán!" 50 | 51 | #: static/modoboa_webmail/js/webmail.js:1443 52 | msgid "A contact with this address already exists" 53 | msgstr "Kontakt s touto adresou již existuje" 54 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/de/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Arvedui , 2014 7 | # controlc.de, 2015 8 | # Patrick Koetter , 2010 9 | msgid "" 10 | msgstr "" 11 | "Project-Id-Version: Modoboa\n" 12 | "Report-Msgid-Bugs-To: \n" 13 | "POT-Creation-Date: 2017-05-19 10:28+0200\n" 14 | "PO-Revision-Date: 2015-07-11 11:45+0000\n" 15 | "Last-Translator: controlc.de\n" 16 | "Language-Team: German (http://www.transifex.com/tonio/modoboa/language/de/)\n" 17 | "Language: de\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 22 | 23 | #: static/modoboa_webmail/js/webmail.js:207 24 | #, fuzzy 25 | #| msgid "No more message in this mailbox." 26 | msgid "No more message in this folder." 27 | msgstr "Keine weiteren Nachrichten im Postfach." 28 | 29 | #: static/modoboa_webmail/js/webmail.js:703 30 | #, fuzzy 31 | #| msgid "Remove the selected mailbox?" 32 | msgid "Remove the selected folder?" 33 | msgstr "Ausgewähltes Postfach entfernen?" 34 | 35 | #: static/modoboa_webmail/js/webmail.js:716 36 | #, fuzzy 37 | #| msgid "Mailbox removed" 38 | msgid "Folder removed" 39 | msgstr "Postfach entfernt" 40 | 41 | #: static/modoboa_webmail/js/webmail.js:1029 42 | msgid "Message sent" 43 | msgstr "Nachricht gesendet" 44 | 45 | #: static/modoboa_webmail/js/webmail.js:1362 46 | #, javascript-format 47 | msgid "Moving %s message" 48 | msgid_plural "Moving %s messages" 49 | msgstr[0] "Verschiebe %s Nachricht" 50 | msgstr[1] "Verschiebe %s Nachrichten" 51 | 52 | #: static/modoboa_webmail/js/webmail.js:1434 53 | msgid "Contact added!" 54 | msgstr "" 55 | 56 | #: static/modoboa_webmail/js/webmail.js:1443 57 | msgid "A contact with this address already exists" 58 | msgstr "" 59 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/el_GR/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Giannis Kapetanakis , 2016 7 | # Kostas Moumouris , 2017 8 | msgid "" 9 | msgstr "" 10 | "Project-Id-Version: Modoboa\n" 11 | "Report-Msgid-Bugs-To: \n" 12 | "POT-Creation-Date: 2017-05-19 10:28+0200\n" 13 | "PO-Revision-Date: 2017-10-12 13:15+0000\n" 14 | "Last-Translator: Kostas Moumouris \n" 15 | "Language-Team: Greek (Greece) (http://www.transifex.com/tonio/modoboa/language/el_GR/)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Language: el_GR\n" 20 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 21 | 22 | #: static/modoboa_webmail/js/webmail.js:207 23 | msgid "No more message in this folder." 24 | msgstr "Δεν υπάρχουν άλλα μηνύματα στο φάκελο." 25 | 26 | #: static/modoboa_webmail/js/webmail.js:703 27 | msgid "Remove the selected folder?" 28 | msgstr "Διαγραφή του επιλεγμένου φακέλου;" 29 | 30 | #: static/modoboa_webmail/js/webmail.js:716 31 | msgid "Folder removed" 32 | msgstr "Ο φάκελος διαγράφηκε" 33 | 34 | #: static/modoboa_webmail/js/webmail.js:1029 35 | msgid "Message sent" 36 | msgstr "Το μήνυμα εστάλη" 37 | 38 | #: static/modoboa_webmail/js/webmail.js:1362 39 | #, javascript-format 40 | msgid "Moving %s message" 41 | msgid_plural "Moving %s messages" 42 | msgstr[0] "Μεταφορά %s μηνύματος" 43 | msgstr[1] "Μεταφορά %s μηνυμάτων" 44 | 45 | #: static/modoboa_webmail/js/webmail.js:1434 46 | msgid "Contact added!" 47 | msgstr "Η επαφή προστέθηκε!" 48 | 49 | #: static/modoboa_webmail/js/webmail.js:1443 50 | msgid "A contact with this address already exists" 51 | msgstr "Μια επαφή με αυτή τη διεύθυνση υπάρχει ήδη" 52 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2018-08-21 09:34+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: constants.py:7 lib/imapemail.py:329 21 | msgid "Date" 22 | msgstr "" 23 | 24 | #: constants.py:8 25 | msgid "Sender" 26 | msgstr "" 27 | 28 | #: constants.py:9 29 | msgid "Size" 30 | msgstr "" 31 | 32 | #: constants.py:10 forms.py:113 lib/imapemail.py:327 33 | msgid "Subject" 34 | msgstr "" 35 | 36 | #: exceptions.py:22 37 | msgid "Server response" 38 | msgstr "" 39 | 40 | #: exceptions.py:37 41 | msgid "Unknown action" 42 | msgstr "" 43 | 44 | #: forms.py:94 45 | msgid "From" 46 | msgstr "" 47 | 48 | #: forms.py:98 templates/modoboa_webmail/compose.html:11 49 | msgid "To" 50 | msgstr "" 51 | 52 | #: forms.py:100 templates/modoboa_webmail/compose.html:28 53 | msgid "Cc" 54 | msgstr "" 55 | 56 | #: forms.py:102 forms.py:108 templates/modoboa_webmail/compose.html:15 57 | msgid "Enter one or more addresses." 58 | msgstr "" 59 | 60 | #: forms.py:106 templates/modoboa_webmail/compose.html:33 61 | msgid "Bcc" 62 | msgstr "" 63 | 64 | #: forms.py:270 65 | msgid "Select a file" 66 | msgstr "" 67 | 68 | #: forms.py:276 69 | msgid "General" 70 | msgstr "" 71 | 72 | #: forms.py:279 73 | msgid "Maximum attachment size" 74 | msgstr "" 75 | 76 | #: forms.py:282 77 | msgid "Maximum attachment size in bytes (or KB, MB, GB if specified)" 78 | msgstr "" 79 | 80 | #: forms.py:285 81 | msgid "IMAP settings" 82 | msgstr "" 83 | 84 | #: forms.py:288 forms.py:308 85 | msgid "Server address" 86 | msgstr "" 87 | 88 | #: forms.py:290 89 | msgid "Address of your IMAP server" 90 | msgstr "" 91 | 92 | #: forms.py:294 93 | msgid "Use a secured connection" 94 | msgstr "" 95 | 96 | #: forms.py:296 97 | msgid "Use a secured connection to access IMAP server" 98 | msgstr "" 99 | 100 | #: forms.py:300 forms.py:324 101 | msgid "Server port" 102 | msgstr "" 103 | 104 | #: forms.py:302 105 | msgid "Listening port of your IMAP server" 106 | msgstr "" 107 | 108 | #: forms.py:305 109 | msgid "SMTP settings" 110 | msgstr "" 111 | 112 | #: forms.py:310 113 | msgid "Address of your SMTP server" 114 | msgstr "" 115 | 116 | #: forms.py:314 117 | msgid "Secured connection mode" 118 | msgstr "" 119 | 120 | #: forms.py:315 121 | msgid "None" 122 | msgstr "" 123 | 124 | #: forms.py:319 125 | msgid "Use a secured connection to access SMTP server" 126 | msgstr "" 127 | 128 | #: forms.py:326 129 | msgid "Listening port of your SMTP server" 130 | msgstr "" 131 | 132 | #: forms.py:330 133 | msgid "Authentication required" 134 | msgstr "" 135 | 136 | #: forms.py:332 137 | msgid "Server needs authentication" 138 | msgstr "" 139 | 140 | #: forms.py:339 141 | msgid "Display" 142 | msgstr "" 143 | 144 | #: forms.py:343 145 | msgid "Default message display mode" 146 | msgstr "" 147 | 148 | #: forms.py:345 149 | msgid "The default mode used when displaying a message" 150 | msgstr "" 151 | 152 | #: forms.py:351 153 | msgid "Enable HTML links display" 154 | msgstr "" 155 | 156 | #: forms.py:352 157 | msgid "Enable/Disable HTML links display" 158 | msgstr "" 159 | 160 | #: forms.py:357 161 | msgid "Number of displayed emails per page" 162 | msgstr "" 163 | 164 | #: forms.py:358 165 | msgid "Sets the maximum number of messages displayed in a page" 166 | msgstr "" 167 | 168 | #: forms.py:363 169 | msgid "Listing refresh rate" 170 | msgstr "" 171 | 172 | #: forms.py:364 173 | msgid "Automatic folder refresh rate (in seconds)" 174 | msgstr "" 175 | 176 | #: forms.py:369 177 | msgid "Folder container's width" 178 | msgstr "" 179 | 180 | #: forms.py:370 181 | msgid "The width of the folder list container" 182 | msgstr "" 183 | 184 | #: forms.py:373 185 | msgid "Folders" 186 | msgstr "" 187 | 188 | #: forms.py:377 189 | msgid "Trash folder" 190 | msgstr "" 191 | 192 | #: forms.py:378 193 | msgid "Folder where deleted messages go" 194 | msgstr "" 195 | 196 | #: forms.py:383 197 | msgid "Sent folder" 198 | msgstr "" 199 | 200 | #: forms.py:384 201 | msgid "Folder where copies of sent messages go" 202 | msgstr "" 203 | 204 | #: forms.py:389 205 | msgid "Drafts folder" 206 | msgstr "" 207 | 208 | #: forms.py:390 209 | msgid "Folder where drafts go" 210 | msgstr "" 211 | 212 | #: forms.py:394 213 | msgid "Junk folder" 214 | msgstr "" 215 | 216 | #: forms.py:395 217 | msgid "Folder where junk messages should go" 218 | msgstr "" 219 | 220 | #: forms.py:398 221 | msgid "Composing messages" 222 | msgstr "" 223 | 224 | #: forms.py:402 225 | msgid "Default editor" 226 | msgstr "" 227 | 228 | #: forms.py:404 229 | msgid "The default editor to use when composing a message" 230 | msgstr "" 231 | 232 | #: forms.py:410 233 | msgid "Signature text" 234 | msgstr "" 235 | 236 | #: forms.py:411 237 | msgid "User defined email signature" 238 | msgstr "" 239 | 240 | #: forms.py:431 241 | msgid "Value must be a positive integer (> 0)" 242 | msgstr "" 243 | 244 | #: handlers.py:20 245 | msgid "Webmail" 246 | msgstr "" 247 | 248 | #: lib/imapemail.py:53 249 | msgid "Add to contacts" 250 | msgstr "" 251 | 252 | #: lib/imapemail.py:274 253 | msgid "wrote:" 254 | msgstr "" 255 | 256 | #: lib/imapemail.py:341 lib/imapemail.py:344 257 | msgid "Original message" 258 | msgstr "" 259 | 260 | #: lib/imaputils.py:246 261 | msgid "Failed to retrieve hierarchy delimiter" 262 | msgstr "" 263 | 264 | #: lib/imaputils.py:282 265 | #, python-format 266 | msgid "Connection to IMAP server failed: %s" 267 | msgstr "" 268 | 269 | #: lib/imaputils.py:523 270 | msgid "Inbox" 271 | msgstr "" 272 | 273 | #: lib/imaputils.py:525 274 | msgid "Drafts" 275 | msgstr "" 276 | 277 | #: lib/imaputils.py:527 278 | msgid "Junk" 279 | msgstr "" 280 | 281 | #: lib/imaputils.py:529 282 | msgid "Sent" 283 | msgstr "" 284 | 285 | #: lib/imaputils.py:531 286 | msgid "Trash" 287 | msgstr "" 288 | 289 | #: modo_extension.py:18 290 | msgid "Simple IMAP webmail" 291 | msgstr "" 292 | 293 | #: templates/modoboa_webmail/attachments.html:8 294 | msgid "Attach" 295 | msgstr "" 296 | 297 | #: templates/modoboa_webmail/attachments.html:12 298 | msgid "Uploading.." 299 | msgstr "" 300 | 301 | #: templates/modoboa_webmail/compose_menubar.html:9 302 | #: templates/modoboa_webmail/headers.html:12 views.py:322 303 | msgid "Attachments" 304 | msgstr "" 305 | 306 | #: templates/modoboa_webmail/folder.html:6 307 | msgid "Parent mailbox" 308 | msgstr "" 309 | 310 | #: templates/modoboa_webmail/index.html:44 311 | msgid "Compose" 312 | msgstr "" 313 | 314 | #: templatetags/webmail_tags.py:26 templatetags/webmail_tags.py:99 315 | msgid "Back" 316 | msgstr "" 317 | 318 | #: templatetags/webmail_tags.py:32 319 | msgid "Reply" 320 | msgstr "" 321 | 322 | #: templatetags/webmail_tags.py:37 323 | msgid "Reply all" 324 | msgstr "" 325 | 326 | #: templatetags/webmail_tags.py:42 327 | msgid "Forward" 328 | msgstr "" 329 | 330 | #: templatetags/webmail_tags.py:50 templatetags/webmail_tags.py:118 331 | msgid "Delete" 332 | msgstr "" 333 | 334 | #: templatetags/webmail_tags.py:57 templatetags/webmail_tags.py:127 335 | msgid "Mark as spam" 336 | msgstr "" 337 | 338 | #: templatetags/webmail_tags.py:60 339 | msgid "Display options" 340 | msgstr "" 341 | 342 | #: templatetags/webmail_tags.py:64 343 | msgid "Activate links" 344 | msgstr "" 345 | 346 | #: templatetags/webmail_tags.py:67 347 | msgid "Disable links" 348 | msgstr "" 349 | 350 | #: templatetags/webmail_tags.py:70 351 | msgid "Show source" 352 | msgstr "" 353 | 354 | #: templatetags/webmail_tags.py:83 templatetags/webmail_tags.py:186 355 | msgid "Mark as not spam" 356 | msgstr "" 357 | 358 | #: templatetags/webmail_tags.py:104 359 | msgid "Send" 360 | msgstr "" 361 | 362 | #: templatetags/webmail_tags.py:130 363 | msgid "Actions" 364 | msgstr "" 365 | 366 | #: templatetags/webmail_tags.py:134 367 | msgid "Mark as read" 368 | msgstr "" 369 | 370 | #: templatetags/webmail_tags.py:139 371 | msgid "Mark as unread" 372 | msgstr "" 373 | 374 | #: templatetags/webmail_tags.py:144 375 | msgid "Mark as flagged" 376 | msgstr "" 377 | 378 | #: templatetags/webmail_tags.py:149 379 | msgid "Mark as unflagged" 380 | msgstr "" 381 | 382 | #: templatetags/webmail_tags.py:156 383 | msgid "Sort by" 384 | msgstr "" 385 | 386 | #: templatetags/webmail_tags.py:176 views.py:167 387 | msgid "Empty folder" 388 | msgstr "" 389 | 390 | #: templatetags/webmail_tags.py:256 views.py:204 391 | msgid "Create a new folder" 392 | msgstr "" 393 | 394 | #: templatetags/webmail_tags.py:264 395 | msgid "Edit the selected folder" 396 | msgstr "" 397 | 398 | #: templatetags/webmail_tags.py:269 399 | msgid "Remove the selected folder" 400 | msgstr "" 401 | 402 | #: templatetags/webmail_tags.py:273 403 | msgid "Compress folder" 404 | msgstr "" 405 | 406 | #: views.py:61 views.py:79 views.py:93 views.py:111 views.py:129 views.py:165 407 | #: views.py:179 views.py:257 views.py:280 views.py:486 views.py:534 408 | #: views.py:552 views.py:568 views.py:607 views.py:647 409 | msgid "Invalid request" 410 | msgstr "" 411 | 412 | #: views.py:99 413 | #, python-format 414 | msgid "%(count)d message deleted" 415 | msgid_plural "%(count)d messages deleted" 416 | msgstr[0] "" 417 | msgstr[1] "" 418 | 419 | #: views.py:142 views.py:153 420 | #, python-format 421 | msgid "%(count)d message marked" 422 | msgid_plural "%(count)d messages marked" 423 | msgstr[0] "" 424 | msgstr[1] "" 425 | 426 | #: views.py:197 427 | msgid "Folder created" 428 | msgstr "" 429 | 430 | #: views.py:207 431 | msgid "Create" 432 | msgstr "" 433 | 434 | #: views.py:223 views.py:259 435 | msgid "Edit folder" 436 | msgstr "" 437 | 438 | #: views.py:226 views.py:262 439 | msgid "Update" 440 | msgstr "" 441 | 442 | #: views.py:239 443 | msgid "Folder updated" 444 | msgstr "" 445 | 446 | #: views.py:312 447 | msgid "Failed to save attachment: " 448 | msgstr "" 449 | 450 | #: views.py:316 451 | #, python-format 452 | msgid "Attachment is too big (limit: %s)" 453 | msgstr "" 454 | 455 | #: views.py:339 456 | msgid "Bad query" 457 | msgstr "" 458 | 459 | #: views.py:351 460 | msgid "Failed to remove attachment: " 461 | msgstr "" 462 | 463 | #: views.py:356 464 | msgid "Unknown attachment" 465 | msgstr "" 466 | 467 | #: views.py:416 468 | msgid "Empty mailbox" 469 | msgstr "" 470 | 471 | #: views.py:559 472 | msgid "Message source" 473 | msgstr "" 474 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/en/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2017-05-19 10:28+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: static/modoboa_webmail/js/webmail.js:207 21 | msgid "No more message in this folder." 22 | msgstr "" 23 | 24 | #: static/modoboa_webmail/js/webmail.js:703 25 | msgid "Remove the selected folder?" 26 | msgstr "" 27 | 28 | #: static/modoboa_webmail/js/webmail.js:716 29 | msgid "Folder removed" 30 | msgstr "" 31 | 32 | #: static/modoboa_webmail/js/webmail.js:1029 33 | msgid "Message sent" 34 | msgstr "" 35 | 36 | #: static/modoboa_webmail/js/webmail.js:1362 37 | #, javascript-format 38 | msgid "Moving %s message" 39 | msgid_plural "Moving %s messages" 40 | msgstr[0] "" 41 | msgstr[1] "" 42 | 43 | #: static/modoboa_webmail/js/webmail.js:1434 44 | msgid "Contact added!" 45 | msgstr "" 46 | 47 | #: static/modoboa_webmail/js/webmail.js:1443 48 | msgid "A contact with this address already exists" 49 | msgstr "" 50 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/es/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # carlos araya , 2017 7 | # Pedro Gracia , 2011,2013 8 | # Pedro Gracia , 2010-2011 9 | msgid "" 10 | msgstr "" 11 | "Project-Id-Version: Modoboa\n" 12 | "Report-Msgid-Bugs-To: \n" 13 | "POT-Creation-Date: 2017-05-19 10:28+0200\n" 14 | "PO-Revision-Date: 2017-01-23 20:20+0000\n" 15 | "Last-Translator: carlos araya \n" 16 | "Language-Team: Spanish (http://www.transifex.com/tonio/modoboa/language/" 17 | "es/)\n" 18 | "Language: es\n" 19 | "MIME-Version: 1.0\n" 20 | "Content-Type: text/plain; charset=UTF-8\n" 21 | "Content-Transfer-Encoding: 8bit\n" 22 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 23 | 24 | #: static/modoboa_webmail/js/webmail.js:207 25 | #, fuzzy 26 | #| msgid "No more message in this mailbox." 27 | msgid "No more message in this folder." 28 | msgstr "No hay más mensajes en el buzón de correo." 29 | 30 | #: static/modoboa_webmail/js/webmail.js:703 31 | #, fuzzy 32 | #| msgid "Remove the selected mailbox?" 33 | msgid "Remove the selected folder?" 34 | msgstr "¿Eliminar el mailbox seleccionado?" 35 | 36 | #: static/modoboa_webmail/js/webmail.js:716 37 | #, fuzzy 38 | #| msgid "Mailbox removed" 39 | msgid "Folder removed" 40 | msgstr "Mailbox eliminado" 41 | 42 | #: static/modoboa_webmail/js/webmail.js:1029 43 | msgid "Message sent" 44 | msgstr "Mensaje enviado" 45 | 46 | #: static/modoboa_webmail/js/webmail.js:1362 47 | #, javascript-format 48 | msgid "Moving %s message" 49 | msgid_plural "Moving %s messages" 50 | msgstr[0] "Moviendo %s mensaje" 51 | msgstr[1] "Moviendo %s mensajes" 52 | 53 | #: static/modoboa_webmail/js/webmail.js:1434 54 | msgid "Contact added!" 55 | msgstr "¡Contacto añadido!" 56 | 57 | #: static/modoboa_webmail/js/webmail.js:1443 58 | msgid "A contact with this address already exists" 59 | msgstr "Ya existe un contacto con esta dirección" 60 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/fr/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Antoine Nguyen , 2017 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: Modoboa\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2017-05-19 10:28+0200\n" 12 | "PO-Revision-Date: 2017-05-19 08:31+0000\n" 13 | "Last-Translator: Antoine Nguyen \n" 14 | "Language-Team: French (http://www.transifex.com/tonio/modoboa/language/fr/)\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Language: fr\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 20 | 21 | #: static/modoboa_webmail/js/webmail.js:207 22 | msgid "No more message in this folder." 23 | msgstr "Plus aucun message dans ce dossier." 24 | 25 | #: static/modoboa_webmail/js/webmail.js:703 26 | msgid "Remove the selected folder?" 27 | msgstr "Supprimer le dossier sélectionné ?" 28 | 29 | #: static/modoboa_webmail/js/webmail.js:716 30 | msgid "Folder removed" 31 | msgstr "Dossier supprimé" 32 | 33 | #: static/modoboa_webmail/js/webmail.js:1029 34 | msgid "Message sent" 35 | msgstr "Message envoyé" 36 | 37 | #: static/modoboa_webmail/js/webmail.js:1362 38 | #, javascript-format 39 | msgid "Moving %s message" 40 | msgid_plural "Moving %s messages" 41 | msgstr[0] "Déplacement de %s message" 42 | msgstr[1] "Déplacement de %s messages" 43 | 44 | #: static/modoboa_webmail/js/webmail.js:1434 45 | msgid "Contact added!" 46 | msgstr "Contact ajouté !" 47 | 48 | #: static/modoboa_webmail/js/webmail.js:1443 49 | msgid "A contact with this address already exists" 50 | msgstr "Un contact avec cette adresse existe déjà" 51 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/it/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Modoboa\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2017-05-19 10:28+0200\n" 11 | "PO-Revision-Date: 2015-10-11 11:43+0000\n" 12 | "Last-Translator: Antoine Nguyen \n" 13 | "Language-Team: Italian (http://www.transifex.com/tonio/modoboa/language/" 14 | "it/)\n" 15 | "Language: it\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: static/modoboa_webmail/js/webmail.js:207 22 | #, fuzzy 23 | #| msgid "No more message in this mailbox." 24 | msgid "No more message in this folder." 25 | msgstr "Non ci sono messaggi in questa casella di posta elettronica." 26 | 27 | #: static/modoboa_webmail/js/webmail.js:703 28 | #, fuzzy 29 | #| msgid "Remove the selected mailbox?" 30 | msgid "Remove the selected folder?" 31 | msgstr "Rimuovere la casella di posta selezionata?" 32 | 33 | #: static/modoboa_webmail/js/webmail.js:716 34 | #, fuzzy 35 | #| msgid "Mailbox removed" 36 | msgid "Folder removed" 37 | msgstr "Casella di posta rimossa" 38 | 39 | #: static/modoboa_webmail/js/webmail.js:1029 40 | msgid "Message sent" 41 | msgstr "Messaggio inviato" 42 | 43 | #: static/modoboa_webmail/js/webmail.js:1362 44 | #, javascript-format 45 | msgid "Moving %s message" 46 | msgid_plural "Moving %s messages" 47 | msgstr[0] "Spostato %s messaggio" 48 | msgstr[1] "Spostati %s messaggi" 49 | 50 | #: static/modoboa_webmail/js/webmail.js:1434 51 | msgid "Contact added!" 52 | msgstr "" 53 | 54 | #: static/modoboa_webmail/js/webmail.js:1443 55 | msgid "A contact with this address already exists" 56 | msgstr "" 57 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/ja_JP/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # cwatanab , 2016 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: Modoboa\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2017-05-19 10:28+0200\n" 12 | "PO-Revision-Date: 2016-11-09 16:14+0000\n" 13 | "Last-Translator: cwatanab \n" 14 | "Language-Team: Japanese (Japan) (http://www.transifex.com/tonio/modoboa/" 15 | "language/ja_JP/)\n" 16 | "Language: ja_JP\n" 17 | "MIME-Version: 1.0\n" 18 | "Content-Type: text/plain; charset=UTF-8\n" 19 | "Content-Transfer-Encoding: 8bit\n" 20 | "Plural-Forms: nplurals=1; plural=0;\n" 21 | 22 | #: static/modoboa_webmail/js/webmail.js:207 23 | #, fuzzy 24 | #| msgid "No more message in this mailbox." 25 | msgid "No more message in this folder." 26 | msgstr "メールボックスに、これ以上メッセージはありません" 27 | 28 | #: static/modoboa_webmail/js/webmail.js:703 29 | #, fuzzy 30 | #| msgid "Remove the selected mailbox?" 31 | msgid "Remove the selected folder?" 32 | msgstr "選択されたメールボックスを削除しますか?" 33 | 34 | #: static/modoboa_webmail/js/webmail.js:716 35 | #, fuzzy 36 | #| msgid "Mailbox removed" 37 | msgid "Folder removed" 38 | msgstr "メールボックスは削除されました" 39 | 40 | #: static/modoboa_webmail/js/webmail.js:1029 41 | msgid "Message sent" 42 | msgstr "メッセージが送信されました" 43 | 44 | #: static/modoboa_webmail/js/webmail.js:1362 45 | #, javascript-format 46 | msgid "Moving %s message" 47 | msgid_plural "Moving %s messages" 48 | msgstr[0] " %s メッセージを移動しています" 49 | 50 | #: static/modoboa_webmail/js/webmail.js:1434 51 | msgid "Contact added!" 52 | msgstr "" 53 | 54 | #: static/modoboa_webmail/js/webmail.js:1443 55 | msgid "A contact with this address already exists" 56 | msgstr "" 57 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/nl_NL/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Ronald Otto , 2017 7 | # TuxBrother , 2014 8 | # Tuxis Internet Engineering V.O.F. , 2017 9 | msgid "" 10 | msgstr "" 11 | "Project-Id-Version: Modoboa\n" 12 | "Report-Msgid-Bugs-To: \n" 13 | "POT-Creation-Date: 2017-05-19 10:28+0200\n" 14 | "PO-Revision-Date: 2017-05-23 06:58+0000\n" 15 | "Last-Translator: Tuxis Internet Engineering V.O.F. \n" 16 | "Language-Team: Dutch (Netherlands) (http://www.transifex.com/tonio/modoboa/language/nl_NL/)\n" 17 | "MIME-Version: 1.0\n" 18 | "Content-Type: text/plain; charset=UTF-8\n" 19 | "Content-Transfer-Encoding: 8bit\n" 20 | "Language: nl_NL\n" 21 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 22 | 23 | #: static/modoboa_webmail/js/webmail.js:207 24 | msgid "No more message in this folder." 25 | msgstr "Niet meer berichten in deze map" 26 | 27 | #: static/modoboa_webmail/js/webmail.js:703 28 | msgid "Remove the selected folder?" 29 | msgstr "Geselecteerde map verwijderen?" 30 | 31 | #: static/modoboa_webmail/js/webmail.js:716 32 | msgid "Folder removed" 33 | msgstr "Map verwijderd" 34 | 35 | #: static/modoboa_webmail/js/webmail.js:1029 36 | msgid "Message sent" 37 | msgstr "Bericht verstuurd" 38 | 39 | #: static/modoboa_webmail/js/webmail.js:1362 40 | #, javascript-format 41 | msgid "Moving %s message" 42 | msgid_plural "Moving %s messages" 43 | msgstr[0] "%s Berichten verplaatsen" 44 | msgstr[1] "%s Berichten verplaatsen" 45 | 46 | #: static/modoboa_webmail/js/webmail.js:1434 47 | msgid "Contact added!" 48 | msgstr "Contactpersoon toegevoegd!" 49 | 50 | #: static/modoboa_webmail/js/webmail.js:1443 51 | msgid "A contact with this address already exists" 52 | msgstr "Er is al een contactpersoon met dit adres" 53 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/pt_BR/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Paulino Michelazzo , 2014-2015 7 | # Rafael Barretto , 2015 8 | msgid "" 9 | msgstr "" 10 | "Project-Id-Version: Modoboa\n" 11 | "Report-Msgid-Bugs-To: \n" 12 | "POT-Creation-Date: 2017-05-19 10:28+0200\n" 13 | "PO-Revision-Date: 2016-11-09 16:14+0000\n" 14 | "Last-Translator: Paulino Michelazzo \n" 15 | "Language-Team: Portuguese (Brazil) (http://www.transifex.com/tonio/modoboa/" 16 | "language/pt_BR/)\n" 17 | "Language: pt_BR\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 22 | 23 | #: static/modoboa_webmail/js/webmail.js:207 24 | #, fuzzy 25 | #| msgid "No more message in this mailbox." 26 | msgid "No more message in this folder." 27 | msgstr "Não existem mais mensagens nesta caixa." 28 | 29 | #: static/modoboa_webmail/js/webmail.js:703 30 | #, fuzzy 31 | #| msgid "Remove the selected mailbox?" 32 | msgid "Remove the selected folder?" 33 | msgstr "Remover a caixa de mensagens selecionada?" 34 | 35 | #: static/modoboa_webmail/js/webmail.js:716 36 | #, fuzzy 37 | #| msgid "Mailbox removed" 38 | msgid "Folder removed" 39 | msgstr "Caixa de mensagens removida" 40 | 41 | #: static/modoboa_webmail/js/webmail.js:1029 42 | msgid "Message sent" 43 | msgstr "Mensagem enviada" 44 | 45 | #: static/modoboa_webmail/js/webmail.js:1362 46 | #, javascript-format 47 | msgid "Moving %s message" 48 | msgid_plural "Moving %s messages" 49 | msgstr[0] "Movendo %s mensagem" 50 | msgstr[1] "Movendo %s mensagens" 51 | 52 | #: static/modoboa_webmail/js/webmail.js:1434 53 | msgid "Contact added!" 54 | msgstr "" 55 | 56 | #: static/modoboa_webmail/js/webmail.js:1443 57 | msgid "A contact with this address already exists" 58 | msgstr "" 59 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/pt_PT/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # lusitan , 2013 7 | # lusitan , 2013 8 | # Mike C. , 2012 9 | # Mike C. , 2012 10 | # Mike C. , 2014 11 | # Sandra Ribeiro , 2013 12 | msgid "" 13 | msgstr "" 14 | "Project-Id-Version: Modoboa\n" 15 | "Report-Msgid-Bugs-To: \n" 16 | "POT-Creation-Date: 2017-05-19 10:28+0200\n" 17 | "PO-Revision-Date: 2014-12-29 12:08+0000\n" 18 | "Last-Translator: Mike C. \n" 19 | "Language-Team: Portuguese (Portugal) (http://www.transifex.com/projects/p/" 20 | "modoboa/language/pt_PT/)\n" 21 | "Language: pt_PT\n" 22 | "MIME-Version: 1.0\n" 23 | "Content-Type: text/plain; charset=UTF-8\n" 24 | "Content-Transfer-Encoding: 8bit\n" 25 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 26 | 27 | #: static/modoboa_webmail/js/webmail.js:207 28 | #, fuzzy 29 | #| msgid "No more message in this mailbox." 30 | msgid "No more message in this folder." 31 | msgstr "Não existem mais mensagens nesta caixa de correio." 32 | 33 | #: static/modoboa_webmail/js/webmail.js:703 34 | #, fuzzy 35 | #| msgid "Remove the selected mailbox?" 36 | msgid "Remove the selected folder?" 37 | msgstr "Remover a caixa de correio seleccionada" 38 | 39 | #: static/modoboa_webmail/js/webmail.js:716 40 | #, fuzzy 41 | #| msgid "Mailbox removed" 42 | msgid "Folder removed" 43 | msgstr "Caixa removida" 44 | 45 | #: static/modoboa_webmail/js/webmail.js:1029 46 | msgid "Message sent" 47 | msgstr "Mensagem enviada" 48 | 49 | #: static/modoboa_webmail/js/webmail.js:1362 50 | #, javascript-format 51 | msgid "Moving %s message" 52 | msgid_plural "Moving %s messages" 53 | msgstr[0] "A mover %s mensagem" 54 | msgstr[1] "A mover %s mensagens" 55 | 56 | #: static/modoboa_webmail/js/webmail.js:1434 57 | msgid "Contact added!" 58 | msgstr "" 59 | 60 | #: static/modoboa_webmail/js/webmail.js:1443 61 | msgid "A contact with this address already exists" 62 | msgstr "" 63 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/ro_RO/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Andrei-Ilie Olteanu , 2018 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: Modoboa\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2017-05-19 10:28+0200\n" 12 | "PO-Revision-Date: 2018-06-12 14:31+0000\n" 13 | "Last-Translator: Andrei-Ilie Olteanu \n" 14 | "Language-Team: Romanian (Romania) (http://www.transifex.com/tonio/modoboa/language/ro_RO/)\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Language: ro_RO\n" 19 | "Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n" 20 | 21 | #: static/modoboa_webmail/js/webmail.js:207 22 | msgid "No more message in this folder." 23 | msgstr "Nu mai există mesaje în acest folder." 24 | 25 | #: static/modoboa_webmail/js/webmail.js:703 26 | msgid "Remove the selected folder?" 27 | msgstr "Șterge folderul selectat?" 28 | 29 | #: static/modoboa_webmail/js/webmail.js:716 30 | msgid "Folder removed" 31 | msgstr "Folder Șters" 32 | 33 | #: static/modoboa_webmail/js/webmail.js:1029 34 | msgid "Message sent" 35 | msgstr "Mesaj trimis" 36 | 37 | #: static/modoboa_webmail/js/webmail.js:1362 38 | #, javascript-format 39 | msgid "Moving %s message" 40 | msgid_plural "Moving %s messages" 41 | msgstr[0] "Mutare %s mesaj" 42 | msgstr[1] "Mutare %smesaj" 43 | msgstr[2] "Mutare %s mesaje" 44 | 45 | #: static/modoboa_webmail/js/webmail.js:1434 46 | msgid "Contact added!" 47 | msgstr "Contact adăugat" 48 | 49 | #: static/modoboa_webmail/js/webmail.js:1443 50 | msgid "A contact with this address already exists" 51 | msgstr "Un contact cu această adresă deja există" 52 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/ru/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Modoboa\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2018-08-21 09:34+0200\n" 11 | "PO-Revision-Date: 2018-04-15 15:58+0300\n" 12 | "Last-Translator: Antoine Nguyen \n" 13 | "Language-Team: Russian (http://www.transifex.com/projects/p/modoboa/language/" 14 | "ru/)\n" 15 | "Language: ru\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 20 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 21 | "X-Generator: Poedit 2.0.6\n" 22 | 23 | #: constants.py:7 lib/imapemail.py:329 24 | msgid "Date" 25 | msgstr "Дата" 26 | 27 | #: constants.py:8 28 | msgid "Sender" 29 | msgstr "Отправитель" 30 | 31 | #: constants.py:9 32 | msgid "Size" 33 | msgstr "Размер" 34 | 35 | #: constants.py:10 forms.py:113 lib/imapemail.py:327 36 | msgid "Subject" 37 | msgstr "Тема" 38 | 39 | #: exceptions.py:22 40 | msgid "Server response" 41 | msgstr "Ответ сервера" 42 | 43 | #: exceptions.py:37 44 | msgid "Unknown action" 45 | msgstr "Неизвестное действие" 46 | 47 | #: forms.py:94 48 | msgid "From" 49 | msgstr "От" 50 | 51 | #: forms.py:98 templates/modoboa_webmail/compose.html:11 52 | msgid "To" 53 | msgstr "Кому" 54 | 55 | #: forms.py:100 templates/modoboa_webmail/compose.html:28 56 | msgid "Cc" 57 | msgstr "Копия" 58 | 59 | #: forms.py:102 forms.py:108 templates/modoboa_webmail/compose.html:15 60 | msgid "Enter one or more addresses." 61 | msgstr "Введите один или более адресов." 62 | 63 | #: forms.py:106 templates/modoboa_webmail/compose.html:33 64 | msgid "Bcc" 65 | msgstr "Скрытая копия" 66 | 67 | #: forms.py:270 68 | msgid "Select a file" 69 | msgstr "Выберите файл" 70 | 71 | #: forms.py:276 72 | msgid "General" 73 | msgstr "Основные" 74 | 75 | #: forms.py:279 76 | msgid "Maximum attachment size" 77 | msgstr "Максимальный размер вложения" 78 | 79 | #: forms.py:282 80 | msgid "Maximum attachment size in bytes (or KB, MB, GB if specified)" 81 | msgstr "Максимальный размер вложения в байтах (или КБ. МБ, ГБ)" 82 | 83 | #: forms.py:285 84 | msgid "IMAP settings" 85 | msgstr "Настройки IMAP" 86 | 87 | #: forms.py:288 forms.py:308 88 | msgid "Server address" 89 | msgstr "Адрес сервера" 90 | 91 | #: forms.py:290 92 | msgid "Address of your IMAP server" 93 | msgstr "Адрес вашего IMAP сервера" 94 | 95 | #: forms.py:294 96 | msgid "Use a secured connection" 97 | msgstr "Использовать безопасное соединение" 98 | 99 | #: forms.py:296 100 | msgid "Use a secured connection to access IMAP server" 101 | msgstr "Использовать защищенное соединение с IMAP севером" 102 | 103 | #: forms.py:300 forms.py:324 104 | msgid "Server port" 105 | msgstr "Порт сервера" 106 | 107 | #: forms.py:302 108 | msgid "Listening port of your IMAP server" 109 | msgstr "Порт вашего IMAP сервера" 110 | 111 | #: forms.py:305 112 | msgid "SMTP settings" 113 | msgstr "Настройки SMTP" 114 | 115 | #: forms.py:310 116 | msgid "Address of your SMTP server" 117 | msgstr "Адрес вашего сервера SMTP" 118 | 119 | #: forms.py:314 120 | msgid "Secured connection mode" 121 | msgstr "Режим безопасного подключения" 122 | 123 | #: forms.py:315 124 | msgid "None" 125 | msgstr "Нет" 126 | 127 | #: forms.py:319 128 | msgid "Use a secured connection to access SMTP server" 129 | msgstr "Использовать защищенное соединение" 130 | 131 | #: forms.py:326 132 | msgid "Listening port of your SMTP server" 133 | msgstr "Порт вашего SMTP сервера" 134 | 135 | #: forms.py:330 136 | msgid "Authentication required" 137 | msgstr "Требуется аутентификация" 138 | 139 | #: forms.py:332 140 | msgid "Server needs authentication" 141 | msgstr "Сервер требует авторизации" 142 | 143 | #: forms.py:339 144 | msgid "Display" 145 | msgstr "Отображение" 146 | 147 | #: forms.py:343 148 | msgid "Default message display mode" 149 | msgstr "Отображение сообщений по умолчанию" 150 | 151 | #: forms.py:345 152 | msgid "The default mode used when displaying a message" 153 | msgstr "Для отображения сообщения используется режим по умолчанию" 154 | 155 | #: forms.py:351 156 | msgid "Enable HTML links display" 157 | msgstr "Разрешить показ HTML ссылок" 158 | 159 | #: forms.py:352 160 | msgid "Enable/Disable HTML links display" 161 | msgstr "Разрешить/Запретить показ HTML ссылок" 162 | 163 | #: forms.py:357 164 | msgid "Number of displayed emails per page" 165 | msgstr "Количество отображаемых сообщений на странице" 166 | 167 | #: forms.py:358 168 | msgid "Sets the maximum number of messages displayed in a page" 169 | msgstr "Максимальное количество сообщений на страницу" 170 | 171 | #: forms.py:363 172 | msgid "Listing refresh rate" 173 | msgstr "Частота обновления листинга" 174 | 175 | #: forms.py:364 176 | msgid "Automatic folder refresh rate (in seconds)" 177 | msgstr "Время обновления папок в секундах" 178 | 179 | #: forms.py:369 180 | msgid "Folder container's width" 181 | msgstr "Ширина окна папок" 182 | 183 | #: forms.py:370 184 | msgid "The width of the folder list container" 185 | msgstr "Ширина окна списка папок" 186 | 187 | #: forms.py:373 188 | msgid "Folders" 189 | msgstr "Папки" 190 | 191 | #: forms.py:377 192 | msgid "Trash folder" 193 | msgstr "Корзина" 194 | 195 | #: forms.py:378 196 | msgid "Folder where deleted messages go" 197 | msgstr "Папка для удаленных сообщений" 198 | 199 | #: forms.py:383 200 | msgid "Sent folder" 201 | msgstr "Папка отправленные" 202 | 203 | #: forms.py:384 204 | msgid "Folder where copies of sent messages go" 205 | msgstr "Папка для копий отправленных сообщений" 206 | 207 | #: forms.py:389 208 | msgid "Drafts folder" 209 | msgstr "Папка для черновиков" 210 | 211 | #: forms.py:390 212 | msgid "Folder where drafts go" 213 | msgstr "Папка для черновиков" 214 | 215 | #: forms.py:394 216 | msgid "Junk folder" 217 | msgstr "Папка для спама" 218 | 219 | #: forms.py:395 220 | msgid "Folder where junk messages should go" 221 | msgstr "Папка для спама" 222 | 223 | #: forms.py:398 224 | msgid "Composing messages" 225 | msgstr "Написать сообщение" 226 | 227 | #: forms.py:402 228 | msgid "Default editor" 229 | msgstr "Редактор по умолчанию" 230 | 231 | #: forms.py:404 232 | msgid "The default editor to use when composing a message" 233 | msgstr "Редактор по умолчанию для создания сообщения" 234 | 235 | #: forms.py:410 236 | msgid "Signature text" 237 | msgstr "Тест подписи" 238 | 239 | #: forms.py:411 240 | msgid "User defined email signature" 241 | msgstr "Пользовательская подпись" 242 | 243 | #: forms.py:431 244 | msgid "Value must be a positive integer (> 0)" 245 | msgstr "Значение может быть положительным целым числом" 246 | 247 | #: handlers.py:20 248 | msgid "Webmail" 249 | msgstr "Веб-почта" 250 | 251 | #: lib/imapemail.py:53 252 | msgid "Add to contacts" 253 | msgstr "Добавить в контакты" 254 | 255 | #: lib/imapemail.py:274 256 | msgid "wrote:" 257 | msgstr "пишет:" 258 | 259 | #: lib/imapemail.py:341 lib/imapemail.py:344 260 | msgid "Original message" 261 | msgstr "Исходное сообщение" 262 | 263 | #: lib/imaputils.py:246 264 | msgid "Failed to retrieve hierarchy delimiter" 265 | msgstr "Не удалось получить разделитель уровней иерархии" 266 | 267 | #: lib/imaputils.py:282 268 | #, python-format 269 | msgid "Connection to IMAP server failed: %s" 270 | msgstr "Подключение к IMAP серверу не удалось: %s" 271 | 272 | #: lib/imaputils.py:523 273 | msgid "Inbox" 274 | msgstr "Входящие" 275 | 276 | #: lib/imaputils.py:525 277 | msgid "Drafts" 278 | msgstr "Черновики" 279 | 280 | #: lib/imaputils.py:527 281 | msgid "Junk" 282 | msgstr "Спам" 283 | 284 | #: lib/imaputils.py:529 285 | msgid "Sent" 286 | msgstr "Отправленные" 287 | 288 | #: lib/imaputils.py:531 289 | msgid "Trash" 290 | msgstr "Корзина" 291 | 292 | #: modo_extension.py:18 293 | msgid "Simple IMAP webmail" 294 | msgstr "Веб-почта простой IMAP" 295 | 296 | #: templates/modoboa_webmail/attachments.html:8 297 | msgid "Attach" 298 | msgstr "Вложить" 299 | 300 | #: templates/modoboa_webmail/attachments.html:12 301 | msgid "Uploading.." 302 | msgstr "Загрузка..." 303 | 304 | #: templates/modoboa_webmail/compose_menubar.html:9 305 | #: templates/modoboa_webmail/headers.html:12 views.py:322 306 | msgid "Attachments" 307 | msgstr "Вложения" 308 | 309 | #: templates/modoboa_webmail/folder.html:6 310 | msgid "Parent mailbox" 311 | msgstr "Родительский почтовый ящик" 312 | 313 | #: templates/modoboa_webmail/index.html:44 314 | msgid "Compose" 315 | msgstr "Написать" 316 | 317 | #: templatetags/webmail_tags.py:26 templatetags/webmail_tags.py:99 318 | msgid "Back" 319 | msgstr "Назад" 320 | 321 | #: templatetags/webmail_tags.py:32 322 | msgid "Reply" 323 | msgstr "Ответить" 324 | 325 | #: templatetags/webmail_tags.py:37 326 | msgid "Reply all" 327 | msgstr "Ответить всем" 328 | 329 | #: templatetags/webmail_tags.py:42 330 | msgid "Forward" 331 | msgstr "Перенаправленые" 332 | 333 | #: templatetags/webmail_tags.py:50 templatetags/webmail_tags.py:118 334 | msgid "Delete" 335 | msgstr "Удалить" 336 | 337 | #: templatetags/webmail_tags.py:57 templatetags/webmail_tags.py:127 338 | msgid "Mark as spam" 339 | msgstr "Пометить, как спам" 340 | 341 | #: templatetags/webmail_tags.py:60 342 | msgid "Display options" 343 | msgstr "Показать параметры" 344 | 345 | #: templatetags/webmail_tags.py:64 346 | msgid "Activate links" 347 | msgstr "Разрешить ссылки" 348 | 349 | #: templatetags/webmail_tags.py:67 350 | msgid "Disable links" 351 | msgstr "Запретить ссылки" 352 | 353 | #: templatetags/webmail_tags.py:70 354 | msgid "Show source" 355 | msgstr "" 356 | 357 | #: templatetags/webmail_tags.py:83 templatetags/webmail_tags.py:186 358 | msgid "Mark as not spam" 359 | msgstr "Отменить пометку, как спам" 360 | 361 | #: templatetags/webmail_tags.py:104 362 | msgid "Send" 363 | msgstr "Послать" 364 | 365 | #: templatetags/webmail_tags.py:130 366 | msgid "Actions" 367 | msgstr "Действие" 368 | 369 | #: templatetags/webmail_tags.py:134 370 | msgid "Mark as read" 371 | msgstr "Пометить, как прочтенное" 372 | 373 | #: templatetags/webmail_tags.py:139 374 | msgid "Mark as unread" 375 | msgstr "Пометить, как не прочтенное" 376 | 377 | #: templatetags/webmail_tags.py:144 378 | msgid "Mark as flagged" 379 | msgstr "Пометить, как важное" 380 | 381 | #: templatetags/webmail_tags.py:149 382 | msgid "Mark as unflagged" 383 | msgstr "Пометить, как неважное" 384 | 385 | #: templatetags/webmail_tags.py:156 386 | msgid "Sort by" 387 | msgstr "Сортировать по" 388 | 389 | #: templatetags/webmail_tags.py:176 views.py:167 390 | msgid "Empty folder" 391 | msgstr "Очистить папку" 392 | 393 | #: templatetags/webmail_tags.py:256 views.py:204 394 | msgid "Create a new folder" 395 | msgstr "Создать новую папку" 396 | 397 | #: templatetags/webmail_tags.py:264 398 | msgid "Edit the selected folder" 399 | msgstr "Изменить выбранную папку" 400 | 401 | #: templatetags/webmail_tags.py:269 402 | msgid "Remove the selected folder" 403 | msgstr "Удалить выбранную папку" 404 | 405 | #: templatetags/webmail_tags.py:273 406 | msgid "Compress folder" 407 | msgstr "Сжать папку" 408 | 409 | #: views.py:61 views.py:79 views.py:93 views.py:111 views.py:129 views.py:165 410 | #: views.py:179 views.py:257 views.py:280 views.py:486 views.py:534 411 | #: views.py:552 views.py:568 views.py:607 views.py:647 412 | msgid "Invalid request" 413 | msgstr "Ошибочный запрос" 414 | 415 | #: views.py:99 416 | #, python-format 417 | msgid "%(count)d message deleted" 418 | msgid_plural "%(count)d messages deleted" 419 | msgstr[0] "%(count)d сообщение удалено" 420 | msgstr[1] "%(count)d сообщения удалено" 421 | msgstr[2] "%(count)d сообщений удалено" 422 | 423 | #: views.py:142 views.py:153 424 | #, python-format 425 | msgid "%(count)d message marked" 426 | msgid_plural "%(count)d messages marked" 427 | msgstr[0] "%(count)d сообщение помечено" 428 | msgstr[1] "%(count)d сообщения помечены" 429 | msgstr[2] "%(count)d сообщений помечено" 430 | 431 | #: views.py:197 432 | msgid "Folder created" 433 | msgstr "Папка создана" 434 | 435 | #: views.py:207 436 | msgid "Create" 437 | msgstr "Создать" 438 | 439 | #: views.py:223 views.py:259 440 | msgid "Edit folder" 441 | msgstr "Изменить папку" 442 | 443 | #: views.py:226 views.py:262 444 | msgid "Update" 445 | msgstr "Обновление" 446 | 447 | #: views.py:239 448 | msgid "Folder updated" 449 | msgstr "Папка обновлена" 450 | 451 | #: views.py:312 452 | msgid "Failed to save attachment: " 453 | msgstr "Ошибка записи вложения:" 454 | 455 | #: views.py:316 456 | #, python-format 457 | msgid "Attachment is too big (limit: %s)" 458 | msgstr "Вложения слишком велики (лимит: %s)" 459 | 460 | #: views.py:339 461 | msgid "Bad query" 462 | msgstr "Ошибочный запрос" 463 | 464 | #: views.py:351 465 | msgid "Failed to remove attachment: " 466 | msgstr "Не удалось удалить вложение:" 467 | 468 | #: views.py:356 469 | msgid "Unknown attachment" 470 | msgstr "Неизвестное вложение" 471 | 472 | #: views.py:416 473 | msgid "Empty mailbox" 474 | msgstr "Очистить почтовый ящик" 475 | 476 | #: views.py:559 477 | msgid "Message source" 478 | msgstr "" 479 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/ru/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Modoboa\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2017-05-19 10:28+0200\n" 11 | "PO-Revision-Date: 2018-04-15 16:01+0300\n" 12 | "Last-Translator: Antoine Nguyen \n" 13 | "Language-Team: Russian (http://www.transifex.com/projects/p/modoboa/language/" 14 | "ru/)\n" 15 | "Language: ru\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 20 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 21 | "X-Generator: Poedit 2.0.6\n" 22 | 23 | #: static/modoboa_webmail/js/webmail.js:207 24 | msgid "No more message in this folder." 25 | msgstr "В этой папке нет больше сообщений." 26 | 27 | #: static/modoboa_webmail/js/webmail.js:703 28 | msgid "Remove the selected folder?" 29 | msgstr "Удалить выбранные папки?" 30 | 31 | #: static/modoboa_webmail/js/webmail.js:716 32 | msgid "Folder removed" 33 | msgstr "Папка удалена" 34 | 35 | #: static/modoboa_webmail/js/webmail.js:1029 36 | msgid "Message sent" 37 | msgstr "Сообщение послано" 38 | 39 | #: static/modoboa_webmail/js/webmail.js:1362 40 | #, javascript-format 41 | msgid "Moving %s message" 42 | msgid_plural "Moving %s messages" 43 | msgstr[0] "Перемещается %s сообщение" 44 | msgstr[1] "Перемещается %s сообщения" 45 | msgstr[2] "Перемещается %s сообщений" 46 | 47 | #: static/modoboa_webmail/js/webmail.js:1434 48 | msgid "Contact added!" 49 | msgstr "Контакт добавлен!" 50 | 51 | #: static/modoboa_webmail/js/webmail.js:1443 52 | msgid "A contact with this address already exists" 53 | msgstr "Контакт с этим адресом уже существует" 54 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/sv/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Martin Persson , 2014,2017 7 | # Olle Gustafsson , 2013,2015-2018 8 | # Tomas Olsson , 2018 9 | msgid "" 10 | msgstr "" 11 | "Project-Id-Version: Modoboa\n" 12 | "Report-Msgid-Bugs-To: \n" 13 | "POT-Creation-Date: 2018-08-21 09:34+0200\n" 14 | "PO-Revision-Date: 2018-08-21 08:33+0000\n" 15 | "Last-Translator: Antoine Nguyen \n" 16 | "Language-Team: Swedish (http://www.transifex.com/tonio/modoboa/language/sv/)\n" 17 | "MIME-Version: 1.0\n" 18 | "Content-Type: text/plain; charset=UTF-8\n" 19 | "Content-Transfer-Encoding: 8bit\n" 20 | "Language: sv\n" 21 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 22 | 23 | #: constants.py:7 lib/imapemail.py:329 24 | msgid "Date" 25 | msgstr "Datum" 26 | 27 | #: constants.py:8 28 | msgid "Sender" 29 | msgstr "Avsändare" 30 | 31 | #: constants.py:9 32 | msgid "Size" 33 | msgstr "Storlek" 34 | 35 | #: constants.py:10 forms.py:113 lib/imapemail.py:327 36 | msgid "Subject" 37 | msgstr "Ämne" 38 | 39 | #: exceptions.py:22 40 | msgid "Server response" 41 | msgstr "Server svar" 42 | 43 | #: exceptions.py:37 44 | msgid "Unknown action" 45 | msgstr "Okänd åtgärd" 46 | 47 | #: forms.py:94 48 | msgid "From" 49 | msgstr "Från" 50 | 51 | #: forms.py:98 templates/modoboa_webmail/compose.html:11 52 | msgid "To" 53 | msgstr "Till" 54 | 55 | #: forms.py:100 templates/modoboa_webmail/compose.html:28 56 | msgid "Cc" 57 | msgstr "Cc" 58 | 59 | #: forms.py:102 forms.py:108 templates/modoboa_webmail/compose.html:15 60 | msgid "Enter one or more addresses." 61 | msgstr "Ange en eller fler adresser." 62 | 63 | #: forms.py:106 templates/modoboa_webmail/compose.html:33 64 | msgid "Bcc" 65 | msgstr "Bcc" 66 | 67 | #: forms.py:270 68 | msgid "Select a file" 69 | msgstr "Välj en fil" 70 | 71 | #: forms.py:276 72 | msgid "General" 73 | msgstr "Generellt" 74 | 75 | #: forms.py:279 76 | msgid "Maximum attachment size" 77 | msgstr "Maximal storlek för bilaga" 78 | 79 | #: forms.py:282 80 | msgid "Maximum attachment size in bytes (or KB, MB, GB if specified)" 81 | msgstr "Största storlek på bifogad fil i byte (eller KB, MB, GB om det anges)" 82 | 83 | #: forms.py:285 84 | msgid "IMAP settings" 85 | msgstr "IMAP inställningar" 86 | 87 | #: forms.py:288 forms.py:308 88 | msgid "Server address" 89 | msgstr "Server adress" 90 | 91 | #: forms.py:290 92 | msgid "Address of your IMAP server" 93 | msgstr "Adressen till din IMAP-server" 94 | 95 | #: forms.py:294 96 | msgid "Use a secured connection" 97 | msgstr "Använd säker anslutning" 98 | 99 | #: forms.py:296 100 | msgid "Use a secured connection to access IMAP server" 101 | msgstr "Använd säker anslutning mot IMAP-servern" 102 | 103 | #: forms.py:300 forms.py:324 104 | msgid "Server port" 105 | msgstr "Server port" 106 | 107 | #: forms.py:302 108 | msgid "Listening port of your IMAP server" 109 | msgstr "IMAP-server port" 110 | 111 | #: forms.py:305 112 | msgid "SMTP settings" 113 | msgstr "SMTP inställningar" 114 | 115 | #: forms.py:310 116 | msgid "Address of your SMTP server" 117 | msgstr "Adressen till din SMTP-server" 118 | 119 | #: forms.py:314 120 | msgid "Secured connection mode" 121 | msgstr "Använd en säker anslutning" 122 | 123 | #: forms.py:315 124 | msgid "None" 125 | msgstr "Ingen" 126 | 127 | #: forms.py:319 128 | msgid "Use a secured connection to access SMTP server" 129 | msgstr "Använd säker anslutning mot SMTP-servern" 130 | 131 | #: forms.py:326 132 | msgid "Listening port of your SMTP server" 133 | msgstr "SMTP-server port" 134 | 135 | #: forms.py:330 136 | msgid "Authentication required" 137 | msgstr "Autentisering krävs" 138 | 139 | #: forms.py:332 140 | msgid "Server needs authentication" 141 | msgstr "Servern behöver autentisering" 142 | 143 | #: forms.py:339 144 | msgid "Display" 145 | msgstr "Visning" 146 | 147 | #: forms.py:343 148 | msgid "Default message display mode" 149 | msgstr "Standard visningsläge för meddelande" 150 | 151 | #: forms.py:345 152 | msgid "The default mode used when displaying a message" 153 | msgstr "Standard visningsläge för meddelande" 154 | 155 | #: forms.py:351 156 | msgid "Enable HTML links display" 157 | msgstr "Aktivera visning av HTML länkar" 158 | 159 | #: forms.py:352 160 | msgid "Enable/Disable HTML links display" 161 | msgstr "Aktivera / inaktivera om HTML länkar skall visas" 162 | 163 | #: forms.py:357 164 | msgid "Number of displayed emails per page" 165 | msgstr "Antalet visade e-postmeddelanden per sida" 166 | 167 | #: forms.py:358 168 | msgid "Sets the maximum number of messages displayed in a page" 169 | msgstr "Anger det maximala antalet meddelanden som visas på en sida" 170 | 171 | #: forms.py:363 172 | msgid "Listing refresh rate" 173 | msgstr "Lyssning uppdateringsfrekvens" 174 | 175 | #: forms.py:364 176 | msgid "Automatic folder refresh rate (in seconds)" 177 | msgstr "Automatisk mapp uppdateringsfrekvens (i sekunder)" 178 | 179 | #: forms.py:369 180 | msgid "Folder container's width" 181 | msgstr "Brevlådornas behållares bredd" 182 | 183 | #: forms.py:370 184 | msgid "The width of the folder list container" 185 | msgstr "Bredden av brevlådans listbehållare" 186 | 187 | #: forms.py:373 188 | msgid "Folders" 189 | msgstr "Kataloger" 190 | 191 | #: forms.py:377 192 | msgid "Trash folder" 193 | msgstr "Papperskorg katalog" 194 | 195 | #: forms.py:378 196 | msgid "Folder where deleted messages go" 197 | msgstr "Katalog där raderade meddelanden hamnar" 198 | 199 | #: forms.py:383 200 | msgid "Sent folder" 201 | msgstr "Skickat katalog" 202 | 203 | #: forms.py:384 204 | msgid "Folder where copies of sent messages go" 205 | msgstr "Katalog där kopior av skickade meddelanden hamnar" 206 | 207 | #: forms.py:389 208 | msgid "Drafts folder" 209 | msgstr "Utkast" 210 | 211 | #: forms.py:390 212 | msgid "Folder where drafts go" 213 | msgstr "Katalog där dina utkast hamnar" 214 | 215 | #: forms.py:394 216 | msgid "Junk folder" 217 | msgstr "Skräpkatalog" 218 | 219 | #: forms.py:395 220 | msgid "Folder where junk messages should go" 221 | msgstr "Katalog där skräppost läggs" 222 | 223 | #: forms.py:398 224 | msgid "Composing messages" 225 | msgstr "Komponera meddelanden" 226 | 227 | #: forms.py:402 228 | msgid "Default editor" 229 | msgstr "Standard editor" 230 | 231 | #: forms.py:404 232 | msgid "The default editor to use when composing a message" 233 | msgstr "Standardredigerare att använda när du skriver ett meddelande" 234 | 235 | #: forms.py:410 236 | msgid "Signature text" 237 | msgstr "Signatur text" 238 | 239 | #: forms.py:411 240 | msgid "User defined email signature" 241 | msgstr "Användardefinierad e-post signatur" 242 | 243 | #: forms.py:431 244 | msgid "Value must be a positive integer (> 0)" 245 | msgstr "Måste vara ett positivt heltal (> 0)" 246 | 247 | #: handlers.py:20 248 | msgid "Webmail" 249 | msgstr "Webmail" 250 | 251 | #: lib/imapemail.py:53 252 | msgid "Add to contacts" 253 | msgstr "Lägg till kontakte" 254 | 255 | #: lib/imapemail.py:274 256 | msgid "wrote:" 257 | msgstr "skrev:" 258 | 259 | #: lib/imapemail.py:341 lib/imapemail.py:344 260 | msgid "Original message" 261 | msgstr "Orginalmeddelande" 262 | 263 | #: lib/imaputils.py:246 264 | msgid "Failed to retrieve hierarchy delimiter" 265 | msgstr "Det gick inte att hämta hierarki avgränsare" 266 | 267 | #: lib/imaputils.py:282 268 | #, python-format 269 | msgid "Connection to IMAP server failed: %s" 270 | msgstr "Anslutning till IMAP servern misslyckades: %s" 271 | 272 | #: lib/imaputils.py:523 273 | msgid "Inbox" 274 | msgstr "Inkorg" 275 | 276 | #: lib/imaputils.py:525 277 | msgid "Drafts" 278 | msgstr "Utkast" 279 | 280 | #: lib/imaputils.py:527 281 | msgid "Junk" 282 | msgstr "Skräp" 283 | 284 | #: lib/imaputils.py:529 285 | msgid "Sent" 286 | msgstr "Skickat" 287 | 288 | #: lib/imaputils.py:531 289 | msgid "Trash" 290 | msgstr "Papperskorg" 291 | 292 | #: modo_extension.py:18 293 | msgid "Simple IMAP webmail" 294 | msgstr "Enkel IMAP webmail" 295 | 296 | #: templates/modoboa_webmail/attachments.html:8 297 | msgid "Attach" 298 | msgstr "Bifoga" 299 | 300 | #: templates/modoboa_webmail/attachments.html:12 301 | msgid "Uploading.." 302 | msgstr "Laddar upp..." 303 | 304 | #: templates/modoboa_webmail/compose_menubar.html:9 305 | #: templates/modoboa_webmail/headers.html:12 views.py:322 306 | msgid "Attachments" 307 | msgstr "Bilagor" 308 | 309 | #: templates/modoboa_webmail/folder.html:6 310 | msgid "Parent mailbox" 311 | msgstr "Brevlådas förälder" 312 | 313 | #: templates/modoboa_webmail/index.html:44 314 | msgid "Compose" 315 | msgstr "Skriva" 316 | 317 | #: templatetags/webmail_tags.py:26 templatetags/webmail_tags.py:99 318 | msgid "Back" 319 | msgstr "Tillbaka" 320 | 321 | #: templatetags/webmail_tags.py:32 322 | msgid "Reply" 323 | msgstr "Svara" 324 | 325 | #: templatetags/webmail_tags.py:37 326 | msgid "Reply all" 327 | msgstr "Svara alla" 328 | 329 | #: templatetags/webmail_tags.py:42 330 | msgid "Forward" 331 | msgstr "Vidarebefordra" 332 | 333 | #: templatetags/webmail_tags.py:50 templatetags/webmail_tags.py:118 334 | msgid "Delete" 335 | msgstr "Radera" 336 | 337 | #: templatetags/webmail_tags.py:57 templatetags/webmail_tags.py:127 338 | msgid "Mark as spam" 339 | msgstr "Makera som skräppost" 340 | 341 | #: templatetags/webmail_tags.py:60 342 | msgid "Display options" 343 | msgstr "Visnings alternativ" 344 | 345 | #: templatetags/webmail_tags.py:64 346 | msgid "Activate links" 347 | msgstr "Aktivera länkar" 348 | 349 | #: templatetags/webmail_tags.py:67 350 | msgid "Disable links" 351 | msgstr "Avaktivera länkar" 352 | 353 | #: templatetags/webmail_tags.py:70 354 | msgid "Show source" 355 | msgstr "" 356 | 357 | #: templatetags/webmail_tags.py:83 templatetags/webmail_tags.py:186 358 | msgid "Mark as not spam" 359 | msgstr "Markera som ej skräppost" 360 | 361 | #: templatetags/webmail_tags.py:104 362 | msgid "Send" 363 | msgstr "Skicka" 364 | 365 | #: templatetags/webmail_tags.py:130 366 | msgid "Actions" 367 | msgstr "Åtgärder" 368 | 369 | #: templatetags/webmail_tags.py:134 370 | msgid "Mark as read" 371 | msgstr "Markera läst" 372 | 373 | #: templatetags/webmail_tags.py:139 374 | msgid "Mark as unread" 375 | msgstr "Markera oläst" 376 | 377 | #: templatetags/webmail_tags.py:144 378 | msgid "Mark as flagged" 379 | msgstr "Markera som flaggad" 380 | 381 | #: templatetags/webmail_tags.py:149 382 | msgid "Mark as unflagged" 383 | msgstr "Markera som oflaggat" 384 | 385 | #: templatetags/webmail_tags.py:156 386 | msgid "Sort by" 387 | msgstr "Sortera efter" 388 | 389 | #: templatetags/webmail_tags.py:176 views.py:167 390 | msgid "Empty folder" 391 | msgstr "Töm katalog" 392 | 393 | #: templatetags/webmail_tags.py:256 views.py:204 394 | msgid "Create a new folder" 395 | msgstr "Skapa ny katalog" 396 | 397 | #: templatetags/webmail_tags.py:264 398 | msgid "Edit the selected folder" 399 | msgstr "Redigera vald katalog" 400 | 401 | #: templatetags/webmail_tags.py:269 402 | msgid "Remove the selected folder" 403 | msgstr "Radera vald katalog" 404 | 405 | #: templatetags/webmail_tags.py:273 406 | msgid "Compress folder" 407 | msgstr "Komprimera katalog" 408 | 409 | #: views.py:61 views.py:79 views.py:93 views.py:111 views.py:129 views.py:165 410 | #: views.py:179 views.py:257 views.py:280 views.py:486 views.py:534 411 | #: views.py:552 views.py:568 views.py:607 views.py:647 412 | msgid "Invalid request" 413 | msgstr "Ogiltig förfrågan" 414 | 415 | #: views.py:99 416 | #, python-format 417 | msgid "%(count)d message deleted" 418 | msgid_plural "%(count)d messages deleted" 419 | msgstr[0] "%(count)d meddelande raderat" 420 | msgstr[1] "%(count)d meddelanden raderade" 421 | 422 | #: views.py:142 views.py:153 423 | #, python-format 424 | msgid "%(count)d message marked" 425 | msgid_plural "%(count)d messages marked" 426 | msgstr[0] "%(count)d message markerade" 427 | msgstr[1] "%(count)d meddelanden markerade" 428 | 429 | #: views.py:197 430 | msgid "Folder created" 431 | msgstr "Katalog skapad" 432 | 433 | #: views.py:207 434 | msgid "Create" 435 | msgstr "Skapa" 436 | 437 | #: views.py:223 views.py:259 438 | msgid "Edit folder" 439 | msgstr "Redigera katalog" 440 | 441 | #: views.py:226 views.py:262 442 | msgid "Update" 443 | msgstr "Uppdatera" 444 | 445 | #: views.py:239 446 | msgid "Folder updated" 447 | msgstr "Katalog uppdaterad" 448 | 449 | #: views.py:312 450 | msgid "Failed to save attachment: " 451 | msgstr "Det gick inte att spara bilaga:" 452 | 453 | #: views.py:316 454 | #, python-format 455 | msgid "Attachment is too big (limit: %s)" 456 | msgstr "Bilagan är för stor (max:% s)" 457 | 458 | #: views.py:339 459 | msgid "Bad query" 460 | msgstr "Dålig fråga" 461 | 462 | #: views.py:351 463 | msgid "Failed to remove attachment: " 464 | msgstr "Misslyckades med att radera bilaga:" 465 | 466 | #: views.py:356 467 | msgid "Unknown attachment" 468 | msgstr "Okänd bilaa" 469 | 470 | #: views.py:416 471 | msgid "Empty mailbox" 472 | msgstr "Tom brevlåda" 473 | 474 | #: views.py:559 475 | msgid "Message source" 476 | msgstr "" 477 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/sv/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Olle Gustafsson , 2013,2015,2017 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: Modoboa\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2017-05-19 10:28+0200\n" 12 | "PO-Revision-Date: 2017-09-22 09:57+0000\n" 13 | "Last-Translator: Olle Gustafsson \n" 14 | "Language-Team: Swedish (http://www.transifex.com/tonio/modoboa/language/sv/)\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Language: sv\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: static/modoboa_webmail/js/webmail.js:207 22 | msgid "No more message in this folder." 23 | msgstr "Inga fler meddelanden i den här katalogen." 24 | 25 | #: static/modoboa_webmail/js/webmail.js:703 26 | msgid "Remove the selected folder?" 27 | msgstr "Radera vald katalog?" 28 | 29 | #: static/modoboa_webmail/js/webmail.js:716 30 | msgid "Folder removed" 31 | msgstr "Katalog raderad" 32 | 33 | #: static/modoboa_webmail/js/webmail.js:1029 34 | msgid "Message sent" 35 | msgstr "Meddelande sänt" 36 | 37 | #: static/modoboa_webmail/js/webmail.js:1362 38 | #, javascript-format 39 | msgid "Moving %s message" 40 | msgid_plural "Moving %s messages" 41 | msgstr[0] "Flyttar %s meddelande" 42 | msgstr[1] "Flyttar %s meddelanden" 43 | 44 | #: static/modoboa_webmail/js/webmail.js:1434 45 | msgid "Contact added!" 46 | msgstr "Kontakt tillagd!" 47 | 48 | #: static/modoboa_webmail/js/webmail.js:1443 49 | msgid "A contact with this address already exists" 50 | msgstr "En kontakt med den här adressen finns redan" 51 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/zh_TW/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # akong , 2018 7 | # akong , 2018 8 | # freedom lee , 2018 9 | msgid "" 10 | msgstr "" 11 | "Project-Id-Version: Modoboa\n" 12 | "Report-Msgid-Bugs-To: \n" 13 | "POT-Creation-Date: 2018-08-21 09:34+0200\n" 14 | "PO-Revision-Date: 2018-10-13 13:37+0000\n" 15 | "Last-Translator: freedom lee \n" 16 | "Language-Team: Chinese (Taiwan) (http://www.transifex.com/tonio/modoboa/language/zh_TW/)\n" 17 | "MIME-Version: 1.0\n" 18 | "Content-Type: text/plain; charset=UTF-8\n" 19 | "Content-Transfer-Encoding: 8bit\n" 20 | "Language: zh_TW\n" 21 | "Plural-Forms: nplurals=1; plural=0;\n" 22 | 23 | #: constants.py:7 lib/imapemail.py:329 24 | msgid "Date" 25 | msgstr "日期" 26 | 27 | #: constants.py:8 28 | msgid "Sender" 29 | msgstr "寄件者" 30 | 31 | #: constants.py:9 32 | msgid "Size" 33 | msgstr "大小" 34 | 35 | #: constants.py:10 forms.py:113 lib/imapemail.py:327 36 | msgid "Subject" 37 | msgstr "主旨" 38 | 39 | #: exceptions.py:22 40 | msgid "Server response" 41 | msgstr "伺服器回應" 42 | 43 | #: exceptions.py:37 44 | msgid "Unknown action" 45 | msgstr "未知動作" 46 | 47 | #: forms.py:94 48 | msgid "From" 49 | msgstr "從" 50 | 51 | #: forms.py:98 templates/modoboa_webmail/compose.html:11 52 | msgid "To" 53 | msgstr "到" 54 | 55 | #: forms.py:100 templates/modoboa_webmail/compose.html:28 56 | msgid "Cc" 57 | msgstr "副本" 58 | 59 | #: forms.py:102 forms.py:108 templates/modoboa_webmail/compose.html:15 60 | msgid "Enter one or more addresses." 61 | msgstr "輸入一個或多個位址" 62 | 63 | #: forms.py:106 templates/modoboa_webmail/compose.html:33 64 | msgid "Bcc" 65 | msgstr "密件副本" 66 | 67 | #: forms.py:270 68 | msgid "Select a file" 69 | msgstr "選擇檔案" 70 | 71 | #: forms.py:276 72 | msgid "General" 73 | msgstr "一般" 74 | 75 | #: forms.py:279 76 | msgid "Maximum attachment size" 77 | msgstr "最大附件大小" 78 | 79 | #: forms.py:282 80 | msgid "Maximum attachment size in bytes (or KB, MB, GB if specified)" 81 | msgstr "最大附件大小的位元組 (假如有指定可以是 KB, MB, GB)" 82 | 83 | #: forms.py:285 84 | msgid "IMAP settings" 85 | msgstr "IMAP 設定" 86 | 87 | #: forms.py:288 forms.py:308 88 | msgid "Server address" 89 | msgstr "伺服器位址" 90 | 91 | #: forms.py:290 92 | msgid "Address of your IMAP server" 93 | msgstr "您的 IMAP 伺服器位址" 94 | 95 | #: forms.py:294 96 | msgid "Use a secured connection" 97 | msgstr "使用安全的連線" 98 | 99 | #: forms.py:296 100 | msgid "Use a secured connection to access IMAP server" 101 | msgstr "使用安全的連線來存取 IMAP 伺服器" 102 | 103 | #: forms.py:300 forms.py:324 104 | msgid "Server port" 105 | msgstr "伺服器連接埠" 106 | 107 | #: forms.py:302 108 | msgid "Listening port of your IMAP server" 109 | msgstr "您的 IMAP 伺服器的連接埠" 110 | 111 | #: forms.py:305 112 | msgid "SMTP settings" 113 | msgstr "SMTP 設定" 114 | 115 | #: forms.py:310 116 | msgid "Address of your SMTP server" 117 | msgstr "您的 SMTP 伺服器位址" 118 | 119 | #: forms.py:314 120 | msgid "Secured connection mode" 121 | msgstr "安全連線模式" 122 | 123 | #: forms.py:315 124 | msgid "None" 125 | msgstr "沒有" 126 | 127 | #: forms.py:319 128 | msgid "Use a secured connection to access SMTP server" 129 | msgstr "使用安全的連線來存取 SMTP 伺服器" 130 | 131 | #: forms.py:326 132 | msgid "Listening port of your SMTP server" 133 | msgstr "您的 SMTP 伺服器的連接埠" 134 | 135 | #: forms.py:330 136 | msgid "Authentication required" 137 | msgstr "必須驗證" 138 | 139 | #: forms.py:332 140 | msgid "Server needs authentication" 141 | msgstr "伺服器需要驗證" 142 | 143 | #: forms.py:339 144 | msgid "Display" 145 | msgstr "顯示" 146 | 147 | #: forms.py:343 148 | msgid "Default message display mode" 149 | msgstr "預設訊息顯示模式" 150 | 151 | #: forms.py:345 152 | msgid "The default mode used when displaying a message" 153 | msgstr "當正在顯示訊息時會使用預設模式" 154 | 155 | #: forms.py:351 156 | msgid "Enable HTML links display" 157 | msgstr "啟用 HTMK 顯示連結" 158 | 159 | #: forms.py:352 160 | msgid "Enable/Disable HTML links display" 161 | msgstr "啟用/停用 HTMK 顯示連結" 162 | 163 | #: forms.py:357 164 | msgid "Number of displayed emails per page" 165 | msgstr "每一頁顯示郵件數量" 166 | 167 | #: forms.py:358 168 | msgid "Sets the maximum number of messages displayed in a page" 169 | msgstr "設定最大每一頁顯示郵件數量" 170 | 171 | #: forms.py:363 172 | msgid "Listing refresh rate" 173 | msgstr "列出重整頻率" 174 | 175 | #: forms.py:364 176 | msgid "Automatic folder refresh rate (in seconds)" 177 | msgstr "資料夾自動重整頻率 (在幾秒)" 178 | 179 | #: forms.py:369 180 | msgid "Folder container's width" 181 | msgstr "資料夾寬度" 182 | 183 | #: forms.py:370 184 | msgid "The width of the folder list container" 185 | msgstr "資料夾列表寬度" 186 | 187 | #: forms.py:373 188 | msgid "Folders" 189 | msgstr "資料夾" 190 | 191 | #: forms.py:377 192 | msgid "Trash folder" 193 | msgstr "垃圾筒" 194 | 195 | #: forms.py:378 196 | msgid "Folder where deleted messages go" 197 | msgstr "已刪除郵件的資料夾" 198 | 199 | #: forms.py:383 200 | msgid "Sent folder" 201 | msgstr "寄件夾" 202 | 203 | #: forms.py:384 204 | msgid "Folder where copies of sent messages go" 205 | msgstr "已寄出郵件的資料夾" 206 | 207 | #: forms.py:389 208 | msgid "Drafts folder" 209 | msgstr "草稿" 210 | 211 | #: forms.py:390 212 | msgid "Folder where drafts go" 213 | msgstr "草稿郵件的資料夾" 214 | 215 | #: forms.py:394 216 | msgid "Junk folder" 217 | msgstr "垃圾信資料夾" 218 | 219 | #: forms.py:395 220 | msgid "Folder where junk messages should go" 221 | msgstr "垃圾郵件的資料夾" 222 | 223 | #: forms.py:398 224 | msgid "Composing messages" 225 | msgstr "編寫郵件" 226 | 227 | #: forms.py:402 228 | msgid "Default editor" 229 | msgstr "預設編輯器" 230 | 231 | #: forms.py:404 232 | msgid "The default editor to use when composing a message" 233 | msgstr "當編寫郵件時會使用預設編輯器" 234 | 235 | #: forms.py:410 236 | msgid "Signature text" 237 | msgstr "簽名文字" 238 | 239 | #: forms.py:411 240 | msgid "User defined email signature" 241 | msgstr "使用者定義郵件簽名" 242 | 243 | #: forms.py:431 244 | msgid "Value must be a positive integer (> 0)" 245 | msgstr "值必須是一個正的整數 (> 0)" 246 | 247 | #: handlers.py:20 248 | msgid "Webmail" 249 | msgstr "網頁郵件" 250 | 251 | #: lib/imapemail.py:53 252 | msgid "Add to contacts" 253 | msgstr "新增到通訊錄" 254 | 255 | #: lib/imapemail.py:274 256 | msgid "wrote:" 257 | msgstr "寫:" 258 | 259 | #: lib/imapemail.py:341 lib/imapemail.py:344 260 | msgid "Original message" 261 | msgstr "原始訊息" 262 | 263 | #: lib/imaputils.py:246 264 | msgid "Failed to retrieve hierarchy delimiter" 265 | msgstr "無法檢查層次分隔" 266 | 267 | #: lib/imaputils.py:282 268 | #, python-format 269 | msgid "Connection to IMAP server failed: %s" 270 | msgstr "連線到 IMAP 伺服器失敗: %s" 271 | 272 | #: lib/imaputils.py:523 273 | msgid "Inbox" 274 | msgstr "收件夾" 275 | 276 | #: lib/imaputils.py:525 277 | msgid "Drafts" 278 | msgstr "草稿" 279 | 280 | #: lib/imaputils.py:527 281 | msgid "Junk" 282 | msgstr "垃圾信" 283 | 284 | #: lib/imaputils.py:529 285 | msgid "Sent" 286 | msgstr "寄件夾" 287 | 288 | #: lib/imaputils.py:531 289 | msgid "Trash" 290 | msgstr "垃圾筒" 291 | 292 | #: modo_extension.py:18 293 | msgid "Simple IMAP webmail" 294 | msgstr "簡單 IMAP 網頁郵件" 295 | 296 | #: templates/modoboa_webmail/attachments.html:8 297 | msgid "Attach" 298 | msgstr "附件" 299 | 300 | #: templates/modoboa_webmail/attachments.html:12 301 | msgid "Uploading.." 302 | msgstr "上傳中.." 303 | 304 | #: templates/modoboa_webmail/compose_menubar.html:9 305 | #: templates/modoboa_webmail/headers.html:12 views.py:322 306 | msgid "Attachments" 307 | msgstr "附件" 308 | 309 | #: templates/modoboa_webmail/folder.html:6 310 | msgid "Parent mailbox" 311 | msgstr "主信箱" 312 | 313 | #: templates/modoboa_webmail/index.html:44 314 | msgid "Compose" 315 | msgstr "編寫" 316 | 317 | #: templatetags/webmail_tags.py:26 templatetags/webmail_tags.py:99 318 | msgid "Back" 319 | msgstr "上一步" 320 | 321 | #: templatetags/webmail_tags.py:32 322 | msgid "Reply" 323 | msgstr "回覆" 324 | 325 | #: templatetags/webmail_tags.py:37 326 | msgid "Reply all" 327 | msgstr "全部回覆" 328 | 329 | #: templatetags/webmail_tags.py:42 330 | msgid "Forward" 331 | msgstr "轉寄" 332 | 333 | #: templatetags/webmail_tags.py:50 templatetags/webmail_tags.py:118 334 | msgid "Delete" 335 | msgstr "刪除" 336 | 337 | #: templatetags/webmail_tags.py:57 templatetags/webmail_tags.py:127 338 | msgid "Mark as spam" 339 | msgstr "標記為垃圾信" 340 | 341 | #: templatetags/webmail_tags.py:60 342 | msgid "Display options" 343 | msgstr "顯示選項" 344 | 345 | #: templatetags/webmail_tags.py:64 346 | msgid "Activate links" 347 | msgstr "啟用連結" 348 | 349 | #: templatetags/webmail_tags.py:67 350 | msgid "Disable links" 351 | msgstr "停用連結" 352 | 353 | #: templatetags/webmail_tags.py:70 354 | msgid "Show source" 355 | msgstr "顯示來源" 356 | 357 | #: templatetags/webmail_tags.py:83 templatetags/webmail_tags.py:186 358 | msgid "Mark as not spam" 359 | msgstr "標記為正常信" 360 | 361 | #: templatetags/webmail_tags.py:104 362 | msgid "Send" 363 | msgstr "寄送" 364 | 365 | #: templatetags/webmail_tags.py:130 366 | msgid "Actions" 367 | msgstr "動作" 368 | 369 | #: templatetags/webmail_tags.py:134 370 | msgid "Mark as read" 371 | msgstr "標記為已讀取" 372 | 373 | #: templatetags/webmail_tags.py:139 374 | msgid "Mark as unread" 375 | msgstr "標記為未讀取" 376 | 377 | #: templatetags/webmail_tags.py:144 378 | msgid "Mark as flagged" 379 | msgstr "標記標籤" 380 | 381 | #: templatetags/webmail_tags.py:149 382 | msgid "Mark as unflagged" 383 | msgstr "取消標記標籤" 384 | 385 | #: templatetags/webmail_tags.py:156 386 | msgid "Sort by" 387 | msgstr "排序" 388 | 389 | #: templatetags/webmail_tags.py:176 views.py:167 390 | msgid "Empty folder" 391 | msgstr "空資料夾" 392 | 393 | #: templatetags/webmail_tags.py:256 views.py:204 394 | msgid "Create a new folder" 395 | msgstr "建立新的資料夾" 396 | 397 | #: templatetags/webmail_tags.py:264 398 | msgid "Edit the selected folder" 399 | msgstr "編輯已選擇的資料夾" 400 | 401 | #: templatetags/webmail_tags.py:269 402 | msgid "Remove the selected folder" 403 | msgstr "移除已選擇的資料夾" 404 | 405 | #: templatetags/webmail_tags.py:273 406 | msgid "Compress folder" 407 | msgstr "壓縮資料夾" 408 | 409 | #: views.py:61 views.py:79 views.py:93 views.py:111 views.py:129 views.py:165 410 | #: views.py:179 views.py:257 views.py:280 views.py:486 views.py:534 411 | #: views.py:552 views.py:568 views.py:607 views.py:647 412 | msgid "Invalid request" 413 | msgstr "無效請求" 414 | 415 | #: views.py:99 416 | #, python-format 417 | msgid "%(count)d message deleted" 418 | msgid_plural "%(count)d messages deleted" 419 | msgstr[0] "%(count)d 郵件已刪除" 420 | 421 | #: views.py:142 views.py:153 422 | #, python-format 423 | msgid "%(count)d message marked" 424 | msgid_plural "%(count)d messages marked" 425 | msgstr[0] "%(count)d 郵件已標記" 426 | 427 | #: views.py:197 428 | msgid "Folder created" 429 | msgstr "資料夾已建立" 430 | 431 | #: views.py:207 432 | msgid "Create" 433 | msgstr "建立" 434 | 435 | #: views.py:223 views.py:259 436 | msgid "Edit folder" 437 | msgstr "編輯資料夾" 438 | 439 | #: views.py:226 views.py:262 440 | msgid "Update" 441 | msgstr "更新" 442 | 443 | #: views.py:239 444 | msgid "Folder updated" 445 | msgstr "資料夾已更新" 446 | 447 | #: views.py:312 448 | msgid "Failed to save attachment: " 449 | msgstr "儲存附件失敗:" 450 | 451 | #: views.py:316 452 | #, python-format 453 | msgid "Attachment is too big (limit: %s)" 454 | msgstr "附件太大 (限制: %s)" 455 | 456 | #: views.py:339 457 | msgid "Bad query" 458 | msgstr "錯誤佇列" 459 | 460 | #: views.py:351 461 | msgid "Failed to remove attachment: " 462 | msgstr "移除附件失敗:" 463 | 464 | #: views.py:356 465 | msgid "Unknown attachment" 466 | msgstr "未知附件" 467 | 468 | #: views.py:416 469 | msgid "Empty mailbox" 470 | msgstr "空的信箱" 471 | 472 | #: views.py:559 473 | msgid "Message source" 474 | msgstr "消息來源" 475 | -------------------------------------------------------------------------------- /modoboa_webmail/locale/zh_TW/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # akong , 2018 7 | # Radium , 2017 8 | msgid "" 9 | msgstr "" 10 | "Project-Id-Version: Modoboa\n" 11 | "Report-Msgid-Bugs-To: \n" 12 | "POT-Creation-Date: 2017-05-19 10:28+0200\n" 13 | "PO-Revision-Date: 2018-06-28 08:45+0000\n" 14 | "Last-Translator: akong \n" 15 | "Language-Team: Chinese (Taiwan) (http://www.transifex.com/tonio/modoboa/language/zh_TW/)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Language: zh_TW\n" 20 | "Plural-Forms: nplurals=1; plural=0;\n" 21 | 22 | #: static/modoboa_webmail/js/webmail.js:207 23 | msgid "No more message in this folder." 24 | msgstr "沒有任何信件在這個資料夾" 25 | 26 | #: static/modoboa_webmail/js/webmail.js:703 27 | msgid "Remove the selected folder?" 28 | msgstr "移除已選擇的資料夾?" 29 | 30 | #: static/modoboa_webmail/js/webmail.js:716 31 | msgid "Folder removed" 32 | msgstr "資料夾已移除" 33 | 34 | #: static/modoboa_webmail/js/webmail.js:1029 35 | msgid "Message sent" 36 | msgstr "信件已發出" 37 | 38 | #: static/modoboa_webmail/js/webmail.js:1362 39 | #, javascript-format 40 | msgid "Moving %s message" 41 | msgid_plural "Moving %s messages" 42 | msgstr[0] "正在移動 %s 信件" 43 | 44 | #: static/modoboa_webmail/js/webmail.js:1434 45 | msgid "Contact added!" 46 | msgstr "聯絡人已新增!" 47 | 48 | #: static/modoboa_webmail/js/webmail.js:1443 49 | msgid "A contact with this address already exists" 50 | msgstr "這個聯絡人的郵件位址已存在" 51 | -------------------------------------------------------------------------------- /modoboa_webmail/modo_extension.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """Declare and register the webmail extension.""" 3 | 4 | from django.urls import reverse_lazy 5 | from django.utils.translation import gettext_lazy 6 | 7 | from modoboa.core.extensions import ModoExtension, exts_pool 8 | from modoboa.parameters import tools as param_tools 9 | 10 | from . import __version__ 11 | from . import forms 12 | 13 | 14 | class Webmail(ModoExtension): 15 | name = "modoboa_webmail" 16 | label = "Webmail" 17 | version = __version__ 18 | description = gettext_lazy("Simple IMAP webmail") 19 | needs_media = True 20 | url = "webmail" 21 | topredirection_url = reverse_lazy("modoboa_webmail:index") 22 | 23 | def load(self): 24 | param_tools.registry.add("global", forms.ParametersForm, "Webmail") 25 | param_tools.registry.add("user", forms.UserSettings, "Webmail") 26 | 27 | 28 | exts_pool.register_extension(Webmail) 29 | -------------------------------------------------------------------------------- /modoboa_webmail/static/modoboa_webmail/css/attachments.css: -------------------------------------------------------------------------------- 1 | #attachment_list { 2 | border: 1px solid #ccc; 3 | background-color: #eee; 4 | position: absolute; 5 | top: 140px; 6 | left: 20px; 7 | right: 20px; 8 | bottom: 35px; 9 | overflow-x: hidden; 10 | overflow-y: auto; 11 | padding: 2px; 12 | } 13 | 14 | #upload_status { 15 | position: relative; 16 | margin: 0 auto; 17 | font-size: 0.9em; 18 | width: 120px; 19 | font-style: italic; 20 | } 21 | 22 | #done_container { 23 | position: absolute; 24 | bottom: 0; 25 | width: 100%; 26 | text-align: center; 27 | } 28 | 29 | img { 30 | float: left; 31 | margin-right: 5px; 32 | } 33 | 34 | div.row label { 35 | width: 150px; 36 | } 37 | 38 | #id_attachment { 39 | width: 290px; 40 | } 41 | -------------------------------------------------------------------------------- /modoboa_webmail/static/modoboa_webmail/css/webmail.css: -------------------------------------------------------------------------------- 1 | /* MEDIA */ 2 | 3 | @media (min-width: 768px) { 4 | .main { 5 | padding-left: 0; 6 | padding-right: 0; 7 | } 8 | } 9 | 10 | @media (max-width: 1198px) { 11 | .container-fluid { 12 | padding: 0; 13 | } 14 | 15 | .display-up-1198-webmail { 16 | display: none; 17 | } 18 | .display-less-1198{ 19 | display: inherit; 20 | } 21 | 22 | } 23 | 24 | /* GENERAL */ 25 | 26 | .main { 27 | position: absolute; 28 | top: 51px; 29 | left: 0; 30 | right: 0; 31 | bottom: 0; 32 | overflow-x: hidden; 33 | } 34 | 35 | #listing { 36 | position: absolute; 37 | top: 75px; 38 | bottom: 0; 39 | left: 0; 40 | right: 0; 41 | overflow-y: auto; 42 | overflow-x: hidden; 43 | padding-left: 15px; 44 | padding-right: 15px; 45 | } 46 | 47 | #folders { 48 | top: 73px; 49 | left: 15px; 50 | right: 15px; 51 | overflow-x: auto; 52 | overflow-y: auto; 53 | } 54 | 55 | /* .sidebar .ui-resizable-handle { */ 56 | /* background-position: center center; */ 57 | /* background-repeat: no-repeat; */ 58 | /* } */ 59 | 60 | /* .sidebar .ui-resizable-n { */ 61 | /* top: 0px; */ 62 | /* } */ 63 | 64 | /* .sidebar .ui-resizable-s { */ 65 | /* bottom: 0px; */ 66 | /* } */ 67 | 68 | /* .sidebar .ui-resizable-e { */ 69 | /* right: 0px; */ 70 | /* background-image: url("../../pics/grippy.png"); */ 71 | /* width: 8px; */ 72 | /* } */ 73 | 74 | /* .sidebar .ui-resizable-w { */ 75 | /* left: 0px; */ 76 | /* background-image: url("../../pics/grippy.png"); */ 77 | /* width: 8px; */ 78 | /* } */ 79 | 80 | .nav-sidebar li > a { 81 | padding-top: 7px; 82 | padding-bottom: 7px; 83 | } 84 | 85 | .nav-sidebar li li { 86 | padding-left: 10px; 87 | } 88 | 89 | #mboxes_container a[name=loadfolder] { 90 | white-space: nowrap; 91 | overflow: auto; 92 | text-overflow: ellipsis; 93 | -o-text-overflow: ellipsis; 94 | } 95 | 96 | #mailcontent { 97 | position: absolute; 98 | top: 0; 99 | left: 0; 100 | border: 0; 101 | margin: 0; 102 | padding: 10px; 103 | width: 100%; 104 | } 105 | 106 | .unseen { 107 | font-weight: bold; 108 | } 109 | 110 | #left-toolbar { 111 | position: absolute; 112 | margin-left: 0px; 113 | width: 100%; 114 | left: 0; 115 | bottom: 0; 116 | } 117 | 118 | #quotabox { 119 | padding-top: 15px; 120 | } 121 | 122 | #quotabar { 123 | height: 5px; 124 | margin-bottom: 0; 125 | } 126 | 127 | #quotaraw { 128 | margin-left: 10px; 129 | } 130 | 131 | 132 | /* 133 | * Mailboxes list 134 | */ 135 | .email { 136 | border-top: 1px solid #ddd; 137 | padding-top: 10px; 138 | } 139 | 140 | .flag { 141 | cursor: pointer; 142 | margin-left: 10px; 143 | } 144 | 145 | div.email:hover { 146 | background-color: #f5f5f5; 147 | } 148 | 149 | .openable { 150 | cursor: pointer; 151 | } 152 | 153 | .leftcol-heading { 154 | margin-bottom: 15px; 155 | } 156 | 157 | #mboxactions { 158 | margin: 5px 0; 159 | } 160 | 161 | div.clickbox { 162 | position: absolute; 163 | left: 10px; 164 | top: 5px; 165 | width: 14px; 166 | height: 14px; 167 | z-index: 100; 168 | } 169 | 170 | div.collapsed { 171 | background-image: url("../../pics/collapsed.png"); 172 | background-repeat: no-repeat; 173 | } 174 | 175 | div.expanded { 176 | background-image: url("../../pics/expanded.png"); 177 | background-repeat: no-repeat; 178 | } 179 | 180 | #mboxes_container .nav .droppable div.clickbox { 181 | z-index: 800; 182 | } 183 | 184 | /* 185 | * Compose form 186 | */ 187 | #mailheader { 188 | position: relative; 189 | padding-bottom: 5px; 190 | } 191 | 192 | #mailheader label { 193 | text-align: left; 194 | } 195 | 196 | #mailheader .options { 197 | margin-bottom: 8px; 198 | } 199 | 200 | #attachments { 201 | position: relative; 202 | margin: 1px 0 5px 0; 203 | padding: 5px 0; 204 | cursor: pointer; 205 | width: 100%; 206 | } 207 | 208 | #attachments img { 209 | float: left; 210 | } 211 | 212 | list { 213 | position: absolute; 214 | top: 5px; 215 | left: 180px; 216 | font-weight: bold; 217 | } 218 | 219 | #body_container { 220 | position: static; 221 | /*height: 600px;*/ 222 | } 223 | 224 | #id_body { 225 | width: 100%; 226 | height: 100%; 227 | } 228 | 229 | /* 230 | * Attachments form 231 | */ 232 | #submit { 233 | margin: 0 auto; 234 | } 235 | 236 | #attachment_list { 237 | margin-top: 20px; 238 | height: 150px; 239 | overflow-x: hidden; 240 | overflow-y: auto; 241 | padding: 2px; 242 | } 243 | 244 | #attachment_list i, 245 | #attachment_list label { 246 | float: left; 247 | } 248 | 249 | /* 250 | * Drap & Drop 251 | */ 252 | .dragbox { 253 | padding : 5px; 254 | text-align : center; 255 | z-index : 1001; 256 | } 257 | 258 | /* 259 | * Folders form 260 | */ 261 | 262 | .modal-body { 263 | max-height: 300px; 264 | overflow-y: auto; 265 | } 266 | 267 | #folders2 li { 268 | position: relative; 269 | } 270 | 271 | #folders2 div.clickbox { 272 | left: 0; 273 | top: 0px; 274 | } 275 | 276 | #folders2 li li { 277 | margin-left: 10px; 278 | } 279 | 280 | #folders2 .active > a, 281 | #folders2 .active > a:hover, 282 | #folders2 .active > a:focus { 283 | color: #fff; 284 | background-color: #428bca; 285 | } 286 | 287 | /* 288 | * Table 289 | */ 290 | 291 | 292 | 293 | /* 294 | * Action buttons 295 | */ 296 | 297 | #menubar { 298 | padding-left: 15px; 299 | padding-bottom: 20px; 300 | z-index: 100; 301 | } 302 | 303 | #menubar .btn-toolbar ul.navbar-right{ 304 | margin-right: 3%; 305 | } 306 | 307 | #menubar .btn-toolbar .navbar-form{ 308 | margin: 0px; 309 | } 310 | 311 | #emailheaders { 312 | position: absolute; 313 | top: 0; 314 | left: 0; 315 | } -------------------------------------------------------------------------------- /modoboa_webmail/templates/modoboa_webmail/ask_password.html: -------------------------------------------------------------------------------- 1 | {% extends "fluid.html" %} 2 | 3 | {% load i18n form_tags %} 4 | 5 | {% block pagetitle %}{% trans "Password validation" %}{% endblock %} 6 | 7 | {% block container_content %} 8 |

{% trans "Validate password" %}
{% trans "Please confirm your password" %}

9 |
10 |
12 | {% csrf_token %} 13 | {% render_form form %} 14 |
15 | 16 |
17 |
18 | {% endblock container_content %} 19 | -------------------------------------------------------------------------------- /modoboa_webmail/templates/modoboa_webmail/attachments.html: -------------------------------------------------------------------------------- 1 | {% extends "common/generic_modal_form.html" %} 2 | {% load i18n %} 3 | {% load static %} 4 | {% block modalbody %} 5 | {{ block.super }} 6 | 7 |
8 | 9 |
10 | 11 | 14 | 16 |
17 | {% for att in attachments %} 18 |
19 |
20 | 22 | 23 | 24 | 25 |
26 |
27 | {% endfor %} 28 |
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /modoboa_webmail/templates/modoboa_webmail/compose.html: -------------------------------------------------------------------------------- 1 | {% load i18n form_tags %} 2 | 3 |
4 | {% csrf_token %} 5 | {{ form.origmsgid }} 6 |
7 |
8 | {% render_field form.from_ label_width="col-sm-1" %} 9 |
10 | {# handling to field #} 11 | 12 |
13 | 16 | {% if form.to.errors %} 17 | {% for error in form.to.errors %} 18 |

{{ error }}

19 | {% endfor %} 20 | {% endif %} 21 |
22 | 23 | {# handling cc and bcc buttons #} 24 | {% if not form.cc.value or not form.bcc.value %} 25 |
26 | {% if not form.cc.value %} 27 | 30 | {% endif %} 31 | {% if not form.bcc.value %} 32 | 35 | {% endif %} 36 |
37 | {% endif %} 38 |
39 | 40 | {# display cc field when required #} 41 | {% if form.cc.value %} 42 | {% render_field form.cc label_width="col-sm-1" %} 43 | {% else %} 44 | {% render_field form.cc label_width="col-sm-1" hidden=True %} 45 | {% endif %} 46 | 47 | {# display bcc field when required #} 48 | {% if form.bcc.value %} 49 | {% render_field form.bcc label_width="col-sm-1" %} 50 | {% else %} 51 | {% render_field form.bcc label_width="col-sm-1" hidden=True %} 52 | {% endif %} 53 | 54 | {% render_field form.subject label_width="col-sm-1" %} 55 | 56 |
57 | 58 |
59 | {{ form.body }} 60 |
61 | 62 |
63 |
64 | 65 | -------------------------------------------------------------------------------- /modoboa_webmail/templates/modoboa_webmail/compose_menubar.html: -------------------------------------------------------------------------------- 1 | {% extends "common/buttons_list.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block extra_buttons %} 6 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /modoboa_webmail/templates/modoboa_webmail/email_list.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load webmail_tags %} 3 | {% load static %} 4 | 5 | {% if with_top_div %}
{% endif %} 6 | {% for email in email_list %} 7 |
8 | 13 |
14 | {{ email.subject|parse_imap_header:"subject"|truncatechars:60 }} 15 |

16 | {{ email.from|parse_imap_header:"from" }} 17 |

18 |
19 |
20 | {{ email.date|parse_imap_header:"date" }} 21 |

{% if email.answered %}{% endif %}{% if email.forwarded %} {% endif %}{% if email.attachments %} {% endif %} {{ email.size|filesizeformat }}

22 |
23 |
24 | {% endfor %} 25 | {% if with_top_div %}
{% endif %} 26 | -------------------------------------------------------------------------------- /modoboa_webmail/templates/modoboa_webmail/folder.html: -------------------------------------------------------------------------------- 1 | {% extends "common/generic_modal_form.html" %} 2 | {% load i18n webmail_tags %} 3 | {% block extrafields %} 4 |
5 | 9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /modoboa_webmail/templates/modoboa_webmail/folders.html: -------------------------------------------------------------------------------- 1 | {% load i18n webmail_tags %} 2 | 5 | -------------------------------------------------------------------------------- /modoboa_webmail/templates/modoboa_webmail/headers.html: -------------------------------------------------------------------------------- 1 | {% load i18n lib_tags webmail_tags %} 2 | 3 | 4 | {% for hdr in headers %} 5 | 6 | 7 | 19 | 20 | {% endfor %} 21 | {% if attachments %} 22 | 23 | 24 | 29 | 30 | {% endif %} 31 |
{{ hdr.name|localize_header_name }} 8 | {% if hdr.safe %} 9 | {% if hdr.value|length > 100 %} 10 | {{ hdr.value|safe|truncatechars_html:100 }} 11 | {% else %} 12 | {{ hdr.value|safe }} 13 | {% endif %} 14 | {% elif hdr.value|length > 100 %} 15 | {{ hdr.value|truncatechars_html:100}} 16 | {% else %} 17 | {{ hdr.value}} 18 | {% endif %}
{% trans "Attachments" %} 25 | {% for key, fname in attachments.items %} 26 | {{ fname }} 27 | {% endfor %} 28 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /modoboa_webmail/templates/modoboa_webmail/index.html: -------------------------------------------------------------------------------- 1 | {% extends "twocols.html" %}{% load i18n lib_tags webmail_tags %} 2 | {% load static %} 3 | {% block pagetitle %}Webmail{% endblock %} 4 | {% block extra_css %} 5 | 6 | 7 | {% endblock %} 8 | 9 | {% block extra_js %} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 38 | {% endblock %} 39 | 40 | {% block leftcol %} 41 | 47 | 48 |
49 | {{ mboxes }} 50 |
51 | 52 |
53 |
54 |
55 | 58 | {% mboxes_menu %} 59 |
60 |
61 | {% if quota != -1 %} 62 |
63 |
64 |
{{ quota }}%
65 |
66 |
67 | {% endif %} 68 |
69 | {% endblock %} 70 | 71 | {% block apparea %} 72 | 73 |
74 | 75 | {% endblock %} 76 | 77 | -------------------------------------------------------------------------------- /modoboa_webmail/templates/modoboa_webmail/mail_source.html: -------------------------------------------------------------------------------- 1 | {% extends "common/generic_modal.html" %} 2 | 3 | {% block modal_css_classes %}modal-lg{% endblock %} 4 | 5 | {% block modalbody %} 6 |
 7 | {{ source }}
 8 | 
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /modoboa_webmail/templates/modoboa_webmail/main_action_bar.html: -------------------------------------------------------------------------------- 1 | {% extends "common/buttons_list.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block before_menubar %} 6 |
7 | 12 |
13 | {% include "common/email_searchbar.html" with extraopts=extraopts %} 14 |
15 |
16 | {% endblock %} 17 | 18 | {% block menubar_classes %}col-xs-7{% endblock %} 19 | -------------------------------------------------------------------------------- /modoboa_webmail/templates/modoboa_webmail/upload_done.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /modoboa_webmail/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modoboa/modoboa-webmail/5fd60386f4cb01d353b49d78592f7256ec570e53/modoboa_webmail/templatetags/__init__.py -------------------------------------------------------------------------------- /modoboa_webmail/templatetags/webmail_tags.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """Custom template tags.""" 3 | 4 | from six.moves.urllib.parse import urlencode 5 | 6 | from django import template 7 | from django.urls import reverse 8 | from django.template.loader import render_to_string 9 | from django.utils.encoding import smart_str 10 | from django.utils.html import escape 11 | from django.utils.safestring import mark_safe 12 | from django.utils.translation import gettext as _ 13 | 14 | from ..lib import imapheader, separate_mailbox 15 | from .. import constants 16 | 17 | register = template.Library() 18 | 19 | 20 | @register.simple_tag 21 | def viewmail_menu(selection, folder, user, mail_id=None): 22 | """Menu of the viewmail location.""" 23 | entries = [{ 24 | "name": "back", 25 | "url": "javascript:history.go(-1)", 26 | "img": "fa fa-arrow-left", 27 | "class": "btn-default", 28 | "label": _("Back") 29 | }, { 30 | "name": "reply", 31 | "url": "action=reply&mbox=%s&mailid=%s" % (folder, mail_id), 32 | "img": "fa fa-mail-reply", 33 | "class": "btn-primary", 34 | "label": _("Reply"), 35 | "menu": [{ 36 | "name": "replyall", 37 | "url": "action=reply&mbox=%s&mailid=%s&all=1" % (folder, mail_id), 38 | "img": "fa fa-mail-reply-all", 39 | "label": _("Reply all") 40 | }, { 41 | "name": "forward", 42 | "url": "action=forward&mbox=%s&mailid=%s" % (folder, mail_id), 43 | "img": "fa fa-mail-forward", 44 | "label": _("Forward") 45 | }] 46 | }, { 47 | "name": "delete", 48 | "img": "fa fa-trash", 49 | "class": "btn-danger", 50 | "url": u"{0}?mbox={1}&selection[]={2}".format( 51 | reverse("modoboa_webmail:mail_delete"), folder, mail_id), 52 | "title": _("Delete") 53 | }, { 54 | "name": "mark_as_junk", 55 | "img": "fa fa-fire", 56 | "class": "btn-warning", 57 | "url": u"{0}?mbox={1}&selection[]={2}".format( 58 | reverse("modoboa_webmail:mail_mark_as_junk"), folder, mail_id), 59 | "title": _("Mark as spam") 60 | }, { 61 | "name": "display_options", 62 | "title": _("Display options"), 63 | "img": "fa fa-cog", 64 | "menu": [{ 65 | "name": "activate_links", 66 | "label": _("Activate links") 67 | }, { 68 | "name": "disable_links", 69 | "label": _("Disable links") 70 | }, { 71 | "name": "show_source", 72 | "label": _("Show source"), 73 | "url": u"{}?mbox={}&mailid={}".format( 74 | reverse("modoboa_webmail:mailsource_get"), folder, mail_id) 75 | }] 76 | }] 77 | if folder == user.parameters.get_value("junk_folder"): 78 | entries[3] = { 79 | "name": "mark_as_not_junk", 80 | "img": "fa fa-thumbs-up", 81 | "class": "btn-success", 82 | "url": u"{0}?mbox={1}&selection[]={2}".format( 83 | reverse("modoboa_webmail:mail_mark_as_not_junk"), 84 | folder, mail_id), 85 | "title": _("Mark as not spam") 86 | } 87 | menu = render_to_string('common/buttons_list.html', 88 | {"selection": selection, "entries": entries, 89 | "user": user, "extraclasses": "pull-left"}) 90 | return menu 91 | 92 | 93 | @register.simple_tag 94 | def compose_menu(selection, backurl, user, **kwargs): 95 | """The menu of the compose action.""" 96 | entries = [ 97 | {"name": "back", 98 | "url": "javascript:history.go(-2);", 99 | "img": "fa fa-arrow-left", 100 | "class": "btn-default", 101 | "label": _("Back")}, 102 | {"name": "sendmail", 103 | "url": "", 104 | "img": "fa fa-send", 105 | "class": "btn-default btn-primary", 106 | "label": _("Send")}, 107 | ] 108 | context = { 109 | "selection": selection, "entries": entries, "user": user 110 | } 111 | context.update(kwargs) 112 | return render_to_string('modoboa_webmail/compose_menubar.html', context) 113 | 114 | 115 | @register.simple_tag 116 | def listmailbox_menu(selection, folder, user, **kwargs): 117 | """The menu of the listmailbox action.""" 118 | entries = [{ 119 | "name": "totrash", 120 | "title": _("Delete"), 121 | "class": "btn-danger", 122 | "img": "fa fa-trash", 123 | "url": reverse("modoboa_webmail:mail_delete") 124 | }, { 125 | "name": "mark_as_junk_multi", 126 | "img": "fa fa-fire", 127 | "class": "btn-warning", 128 | "url": reverse("modoboa_webmail:mail_mark_as_junk"), 129 | "title": _("Mark as spam") 130 | }, { 131 | "name": "actions", 132 | "label": _("Actions"), 133 | "class": "btn btn-default", 134 | "menu": [{ 135 | "name": "mark-read", 136 | "label": _("Mark as read"), 137 | "url": u"{0}?status=read".format( 138 | reverse("modoboa_webmail:mail_mark", args=[folder])) 139 | }, { 140 | "name": "mark-unread", 141 | "label": _("Mark as unread"), 142 | "url": u"{0}?status=unread".format( 143 | reverse("modoboa_webmail:mail_mark", args=[folder])) 144 | }, { 145 | "name": "mark-flagged", 146 | "label": _("Mark as flagged"), 147 | "url": u"{0}?status=flagged".format( 148 | reverse("modoboa_webmail:mail_mark", args=[folder])) 149 | }, { 150 | "name": "mark-unflagged", 151 | "label": _("Mark as unflagged"), 152 | "url": u"{0}?status=unflagged".format( 153 | reverse("modoboa_webmail:mail_mark", args=[folder])) 154 | }] 155 | }] 156 | sort_actions = [{ 157 | "header": True, 158 | "label": _("Sort by") 159 | }] 160 | current_order = kwargs.get("sort_order") 161 | for order in constants.SORT_ORDERS: 162 | entry = { 163 | "name": "sort_by_{}".format(order[0]), 164 | "label": order[1], 165 | "url": order[0], 166 | "class": "sort-order" 167 | } 168 | if current_order[1:] == order[0]: 169 | css = "fa fa-arrow-{}".format( 170 | "down" if current_order[0] == "-" else "up") 171 | entry.update({"img": css}) 172 | sort_actions.append(entry) 173 | entries[2]["menu"] += sort_actions 174 | if folder == user.parameters.get_value("trash_folder"): 175 | entries[0]["class"] += " disabled" 176 | entries[2]["menu"].insert(4, { 177 | "name": "empty", 178 | "label": _("Empty folder"), 179 | "url": u"{0}?name={1}".format( 180 | reverse("modoboa_webmail:trash_empty"), folder) 181 | }) 182 | elif folder == user.parameters.get_value("junk_folder"): 183 | entries[1] = { 184 | "name": "mark_as_not_junk_multi", 185 | "img": "fa fa-thumbs-up", 186 | "class": "btn-success", 187 | "url": reverse("modoboa_webmail:mail_mark_as_not_junk"), 188 | "title": _("Mark as not spam") 189 | } 190 | return render_to_string('modoboa_webmail/main_action_bar.html', { 191 | 'selection': selection, 'entries': entries, 'user': user, 'css': "nav", 192 | }) 193 | 194 | 195 | @register.simple_tag 196 | def print_mailboxes( 197 | tree, selected=None, withunseen=False, selectonly=False, 198 | hdelimiter='.'): 199 | """Display a tree of mailboxes and sub-mailboxes. 200 | 201 | :param tree: the mailboxes to display 202 | """ 203 | result = "" 204 | 205 | for mbox in tree: 206 | cssclass = "" 207 | name = mbox["path"] if "sub" in mbox else mbox["name"] 208 | label = ( 209 | mbox["label"] if "label" in mbox else 210 | separate_mailbox(mbox["name"], hdelimiter)[0]) 211 | if mbox.get("removed", False): 212 | cssclass = "disabled" 213 | elif selected == name: 214 | cssclass = "active" 215 | result += "
  • \n" % (name, cssclass) 216 | cssclass = "" 217 | extra_attrs = "" 218 | if withunseen and "unseen" in mbox: 219 | label += " (%d)" % mbox["unseen"] 220 | cssclass += " unseen" 221 | extra_attrs = ' data-toggle="%d"' % mbox["unseen"] 222 | 223 | if "sub" in mbox: 224 | if selected is not None and selected != name and selected.count( 225 | name): 226 | ul_state = "visible" 227 | div_state = "expanded" 228 | else: 229 | ul_state = "hidden" 230 | div_state = "collapsed" 231 | result += "
    " % div_state 232 | 233 | result += "" % ( 234 | "path" in mbox and mbox["path"] or mbox["name"], cssclass, 235 | 'selectfolder' if selectonly else 'loadfolder', extra_attrs 236 | ) 237 | 238 | iclass = mbox["class"] if "class" in mbox \ 239 | else "fa fa-folder" 240 | result += " %s" % (iclass, escape(label)) 241 | 242 | if "sub" in mbox and mbox["sub"]: 243 | result += "\n" 247 | result += "
  • \n" 248 | return mark_safe(result) 249 | 250 | 251 | @register.simple_tag 252 | def mboxes_menu(): 253 | """Mailboxes menu.""" 254 | entries = [ 255 | {"name": "newmbox", 256 | "url": reverse("modoboa_webmail:folder_add"), 257 | "img": "fa fa-plus", 258 | "label": _("Create a new folder"), 259 | "modal": True, 260 | "modalcb": "webmail.mboxform_cb", 261 | "closecb": "webmail.mboxform_close", 262 | "class": "btn-default btn-xs"}, 263 | {"name": "editmbox", 264 | "url": reverse("modoboa_webmail:folder_change"), 265 | "img": "fa fa-edit", 266 | "label": _("Edit the selected folder"), 267 | "class": "btn-default btn-xs"}, 268 | {"name": "removembox", 269 | "url": reverse("modoboa_webmail:folder_delete"), 270 | "img": "fa fa-trash", 271 | "label": _("Remove the selected folder"), 272 | "class": "btn-default btn-xs"}, 273 | {"name": "compress", 274 | "img": "fa fa-compress", 275 | "label": _("Compress folder"), 276 | "class": "btn-default btn-xs", 277 | "url": reverse("modoboa_webmail:folder_compress")} 278 | ] 279 | 280 | context = { 281 | "entries": entries, 282 | "css": "dropdown-menu", 283 | } 284 | return render_to_string('common/menu.html', context) 285 | 286 | 287 | @register.filter 288 | def parse_imap_header(value, header): 289 | """Simple template tag to display a IMAP header.""" 290 | safe = True 291 | try: 292 | value = getattr(imapheader, "parse_%s" % header)(value) 293 | except AttributeError: 294 | pass 295 | if header == "from": 296 | value = value[0] 297 | elif header == "subject": 298 | safe = False 299 | return value if not safe else mark_safe(value) 300 | 301 | 302 | @register.simple_tag 303 | def attachment_url(mbox, mail_id, fname, key): 304 | """Return full download url of an attachment.""" 305 | url = reverse("modoboa_webmail:attachment_get") 306 | params = { 307 | "mbox": mbox, 308 | "mailid": mail_id, 309 | "fname": smart_str(fname), 310 | "partnumber": key 311 | } 312 | url = "{}?{}".format(url, urlencode(params)) 313 | return url 314 | -------------------------------------------------------------------------------- /modoboa_webmail/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modoboa/modoboa-webmail/5fd60386f4cb01d353b49d78592f7256ec570e53/modoboa_webmail/tests/__init__.py -------------------------------------------------------------------------------- /modoboa_webmail/tests/data.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # flake8: noqa 4 | 5 | """Tests data.""" 6 | 7 | BODYSTRUCTURE_SAMPLE_1 = [ 8 | b'36 (FLAGS (\\Seen))', 9 | b'36 (UID 36 BODYSTRUCTURE (("text" "plain" ("charset" "UTF-8") NIL NIL "QUOTED-PRINTABLE" 959 29 NIL ("inline" NIL) NIL NIL)("text" "html" ("charset" "UTF-8") NIL NIL "QUOTED-PRINTABLE" 14695 322 NIL ("inline" NIL) NIL NIL) "alternative" ("boundary" "----=_Part_2437867_661044267.1501268072105") NIL NIL NIL))' 10 | ] 11 | 12 | BODYSTRUCTURE_SAMPLE_2 = [ 13 | (b'19 (UID 19 FLAGS (\\Seen $label4 user_flag-1) BODYSTRUCTURE (("text" "plain" ("charset" "ISO-8859-1" "format" "flowed") NIL NIL "7bit" 2 1 NIL NIL NIL NIL)("message" "rfc822" ("name*" "ISO-8859-1\'\'%5B%49%4E%53%43%52%49%50%54%49%4F%4E%5D%20%52%E9%63%E9%70%74%69%6F%6E%20%64%65%20%76%6F%74%72%65%20%64%6F%73%73%69%65%72%20%64%27%69%6E%73%63%72%69%70%74%69%6F%6E%20%46%72%65%65%20%48%61%75%74%20%44%E9%62%69%74") NIL NIL "8bit" 3632 ("Wed, 13 Dec 2006 20:30:02 +0100" {70}', 14 | b"[INSCRIPTION] R\xe9c\xe9ption de votre dossier d'inscription Free Haut D\xe9bit"), 15 | (b' (("Free Haut Debit" NIL "inscription" "freetelecom.fr")) (("Free Haut Debit" NIL "inscription" "freetelecom.fr")) ((NIL NIL "hautdebit" "freetelecom.fr")) ((NIL NIL "nguyen.antoine" "wanadoo.fr")) NIL NIL NIL "<20061213193125.9DA0919AC@dgroup2-2.proxad.net>") ("text" "plain" ("charset" "iso-8859-1") NIL NIL "8bit" 1428 38 NIL ("inline" NIL) NIL NIL) 76 NIL ("inline" ("filename*" "ISO-8859-1\'\'%5B%49%4E%53%43%52%49%50%54%49%4F%4E%5D%20%52%E9%63%E9%70%74%69%6F%6E%20%64%65%20%76%6F%74%72%65%20%64%6F%73%73%69%65%72%20%64%27%69%6E%73%63%72%69%70%74%69%6F%6E%20%46%72%65%65%20%48%61%75%74%20%44%E9%62%69%74")) NIL NIL) "mixed" ("boundary" "------------040706080908000209030901") NIL NIL NIL) BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)] {266}', 16 | b'Date: Tue, 19 Dec 2006 19:50:13 +0100\r\nFrom: Antoine Nguyen \r\nTo: Antoine Nguyen \r\nSubject: [Fwd: [INSCRIPTION] =?ISO-8859-1?Q?R=E9c=E9ption_de_votre_?=\r\n =?ISO-8859-1?Q?dossier_d=27inscription_Free_Haut_D=E9bit=5D?=\r\n\r\n'), 17 | b')' 18 | ] 19 | 20 | BODYSTRUCTURE_SAMPLE_3 = [ 21 | (b'58 (UID 123753 BODYSTRUCTURE ((("text" "plain" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 90 10 NIL NIL NIL NIL)("text" "html" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 1034 33 NIL NIL NIL NIL) "alternative" ("boundary" "_000_HE1PR10MB1642F2B8BA7FF8EAC0FC7AECD86E0HE1PR10MB1642EURP_") NIL NIL NIL)("application" "pdf" ("name" "=?iso-8859-1?Q?CV=5FAude=5FGIRODON=5FNGUYEN=5Fao=FBt2017_g=E9n=E9rique.pd?= =?iso-8859-1?Q?f?=") NIL {95}', '=?iso-8859-1?Q?CV=5FAude=5FGIRODON=5FNGUYEN=5Fao=FBt2017_g=E9n=E9rique.pd?=\n =?iso-8859-1?Q?f?='), 22 | b' "base64" 94130 NIL ("attachment" ("filename" "=?iso-8859-1?Q?CV=5FAude=5FGIRODON=5FNGUYEN=5Fao=FBt2017_g=E9n=E9rique.pd?= =?iso-8859-1?Q?f?=" "size" "68787" "creation-date" "Wed, 13 Sep 2017 08:50:03 GMT" "modification-date" "Wed, 13 Sep 2017 08:50:03 GMT")) NIL NIL) "mixed" ("boundary" "_004_HE1PR10MB1642F2B8BA7FF8EAC0FC7AECD86E0HE1PR10MB1642EURP_") NIL ("fr-FR") NIL))' 23 | ] 24 | 25 | BODYSTRUCTURE_4 = b'BODYSTRUCTURE ((("text" "plain" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 886 32 NIL NIL NIL NIL)("text" "html" ("charset" "us-ascii") NIL NIL "quoted-printable" 1208 16 NIL NIL NIL NIL) "alternative" ("boundary" "----=_NextPart_001_0003_01CCC564.B2F64FF0") NIL NIL NIL)("application" "octet-stream" ("name" "Carte Verte_2.pdf") NIL NIL "base64" 285610 NIL ("attachment" ("filename" "Carte Verte_2.pdf")) NIL NIL) "mixed" ("boundary" "----=_NextPart_000_0002_01CCC564.B2F64FF0") NIL NIL NIL)' 26 | 27 | BODYSTRUCTURE_SAMPLE_4 = [ 28 | (b'855 (UID 46931 ' + BODYSTRUCTURE_4 + b' BODY[HEADER.FIELDS (FROM TO CC DATE SUBJECT REPLY-TO MESSAGE-ID)] {235}', b'From: \r\nTo: \r\nCc: \r\nSubject: Notre contact du 28/12/2011 - 192175092\r\nDate: Wed, 28 Dec 2011 13:29:17 +0100\r\nMessage-ID: \r\n\r\n'), 29 | b')' 30 | ] 31 | 32 | BODYSTRUCTURE_ONLY_4 = [ 33 | (b'855 (UID 46931 ' + BODYSTRUCTURE_4), ')'] 34 | 35 | BODYSTRUCTURE_ONLY_5 = [ 36 | (b'855 (UID 46932 ' + BODYSTRUCTURE_4), ')'] 37 | 38 | BODY_PLAIN_4 = [ 39 | (b'855 (UID 46931 BODY[1.1] {25}', b'This is a test message.\r\n'), b')'] 40 | 41 | BODYSTRUCTURE_SAMPLE_5 = [ 42 | (b'856 (UID 46936 BODYSTRUCTURE (("text" "plain" ("charset" "ISO-8859-1") NIL NIL "quoted-printable" 724 22 NIL NIL NIL NIL)("text" "html" ("charset" "ISO-8859-1") NIL NIL "quoted-printable" 2662 48 NIL NIL NIL NIL) "alternative" ("boundary" "----=_Part_1326887_254624357.1325083973970") NIL NIL NIL) BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)] {258}', 'Date: Wed, 28 Dec 2011 15:52:53 +0100 (CET)\r\nFrom: =?ISO-8859-1?Q?Malakoff_M=E9d=E9ric?= \r\nTo: Antoine Nguyen \r\nSubject: =?ISO-8859-1?Q?Votre_inscription_au_grand_Jeu_Malakoff_M=E9d=E9ric?=\r\n\r\n'), 43 | b')' 44 | ] 45 | 46 | BODYSTRUCTURE_SAMPLE_6 = [ 47 | (b'123 (UID 3 BODYSTRUCTURE (((("text" "plain" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 1266 30 NIL NIL NIL NIL)("text" "html" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 8830 227 NIL NIL NIL NIL) "alternative" ("boundary" "_000_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_") NIL NIL NIL)("image" "png" ("name" "image005.png") "" "image005.png" "base64" 7464 NIL ("inline" ("filename" "image005.png" "size" "5453" "creation-date" "Tue, 06 Sep 2011 13:33:49 GMT" "modification-date" "Tue, 06 Sep 2011 13:33:49 GMT")) NIL NIL)("image" "jpeg" ("name" "image006.jpg") "" "image006.jpg" "base64" 2492 NIL ("inline" ("filename" "image006.jpg" "size" "1819" "creation-date" "Tue, 06 Sep 2011 13:33:49 GMT" "modification-date" "Tue, 06 Sep 2011 13:33:49 GMT")) NIL NIL) "related" ("boundary" "_006_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_" "type" "multipart/alternative") NIL NIL NIL)("application" "pdf" ("name" "bilan assurance CIC.PDF") NIL "bilan assurance CIC.PDF" "base64" 459532 NIL ("attachment" ("filename" "bilan assurance CIC.PDF" "size" "335811" "creation-date" "Fri, 16 Sep 2011 12:45:23 GMT" "modification-date" "Fri, 16 Sep 2011 12:45:23 GMT")) NIL NIL)(("text" "plain" ("charset" "utf-8") NIL NIL "quoted-printable" 1389 29 NIL NIL NIL NIL)("text" "html" ("charset" "utf-8") NIL NIL "quoted-printable" 1457 27 NIL NIL NIL NIL) "alternative" ("boundary" "===============0775904800==") ("inline" NIL) NIL NIL) "mixed" ("boundary" "_007_152AC7ECD1F8AB43A9AD95DBDDCA3118082C09GKIMA24cmcicfr_") NIL ("fr-FR") NIL)', 48 | ), 49 | b')' 50 | ] 51 | 52 | BODYSTRUCTURE_SAMPLE_7 = [ 53 | (b'856 (UID 11111 BODYSTRUCTURE ((("text" "plain" ("charset" "UTF-8") NIL NIL "7bit" 0 0 NIL NIL NIL NIL) "mixed" ("boundary" "----=_Part_407172_3159001.1321948277321") NIL NIL NIL)("application" "octet-stream" ("name" "26274308.pdf") NIL NIL "base64" 14906 NIL ("attachment" ("filename" "(26274308.pdf")) NIL NIL) "mixed" ("boundary" "----=_Part_407171_9686991.1321948277321") NIL NIL NIL)',) 54 | , 55 | b')' 56 | ] 57 | 58 | BODYSTRUCTURE_SAMPLE_8 = [ 59 | (b'1 (UID 947 BODYSTRUCTURE ("text" "html" ("charset" "utf-8") NIL NIL "8bit" 889 34 NIL NIL NIL NIL) BODY[HEADER.FIELDS (FROM TO CC DATE SUBJECT)] {80}', 'From: Antoine Nguyen \r\nDate: Sat, 26 Mar 2016 11:45:49 +0100\r\n\r\n'), 60 | b')' 61 | ] 62 | 63 | BODYSTRUCTURE_SAMPLE_9 = [ 64 | (b'855 (UID 46932 ' + BODYSTRUCTURE_4 + b' BODY[HEADER.FIELDS (FROM TO CC DATE SUBJECT)] {235}', b'From: \r\nTo: \r\nCc: \r\nSubject: Notre contact du 28/12/2011 - 192175092\r\nDate: Wed, 28 Dec 2011 13:29:17 +0100\r\nMessage-ID: \r\n\r\n'), 65 | b')' 66 | ] 67 | 68 | BODYSTRUCTURE_SAMPLE_10 = [ 69 | (b'855 (UID 46932 ' + BODYSTRUCTURE_4 + b' BODY[1.1] {10})', 70 | b'XXXXXXXX\r\n', 71 | b'BODY[2] {10}', 72 | b'XXXXXXXX\r\n', 73 | b')',) 74 | ] 75 | 76 | BODYSTRUCTURE_EMPTY_MAIL = [ 77 | b'33 (UID 33 BODYSTRUCTURE (("text" "plain" ("charset" "us-ascii") NIL NIL "quoted-printable" 0 0 NIL NIL NIL NIL)("application" "zip" ("name" "hotmail.com!ngyn.org!1435428000!1435514400.zip") NIL NIL "base64" 978 NIL ("attachment" ("filename" "hotmail.com!ngyn.org!1435428000!1435514400.zip")) NIL NIL) "mixed" ("boundary" "--boundary_32281_b52cf564-2a50-4f96-aeb0-ef5f83b05463") NIL NIL NIL))' 78 | ] 79 | 80 | EMPTY_BODY = [(b'33 (UID 33 BODY[1] {0}', b''), b')'] 81 | 82 | COMPLETE_MAIL = [ 83 | (b'109 (UID 133872 BODY[] {2504}', b'Return-Path: \r\nDelivered-To: test1@test.org\r\nReceived: from mail.testsrv.org\r\n\tby nelson.ngyn.org (Dovecot) with LMTP id E9jPFPHHe1u1bQAABvoInA\r\n\tfor ; Tue, 21 Aug 2018 10:06:09 +0200\r\nReceived: from [192.168.0.12] (unknown [109.9.172.169])\r\n\t(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))\r\n\t(No client certificate requested)\r\n\tby mail.testsrv.org (Postfix) with ESMTPSA id 2B279E00D2\r\n\tfor ; Tue, 21 Aug 2018 10:06:09 +0200 (CEST)\r\nDKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=test.org; s=test;\r\n\tt=1534838769; h=from:from:sender:reply-to:subject:subject:date:date:\r\n\t message-id:message-id:to:to:cc:mime-version:mime-version:\r\n\t content-type:content-type:\r\n\t content-transfer-encoding:content-transfer-encoding:in-reply-to:\r\n\t references; bh=EVfAHeUMDygbJe0SkMWJHjgXGjtiTLZnMQbyWqzsrCY=;\r\n\tb=fRc4i+WbPkbdD1BNfV9TqsZZ9nTbnHn6CKbH4nwrQnmUYQvvFUcpXC8PBURyglyLBAqPKb\r\n\tF6Dq7TnvcYdkIpR0XyMko+XB/qt8Wj0J0tC8bFxfknN8yNqj32SQAjtLrsjtzEgC1LkcLo\r\n\tcXU+FQbZR0D8EL7YD/nnsbO4mYnf3as=\r\nTo: Antoine Nguyen \r\nFrom: Antoine Nguyen \r\nSubject: Simple message\r\nMessage-ID: <7ed6e950-9e2b-b0fc-9e66-5b8358453b8d@test.org>\r\nDate: Tue, 21 Aug 2018 10:06:08 +0200\r\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101\r\n Thunderbird/52.9.1\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: 7bit\r\nContent-Language: fr-FR\r\nARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=test.org;\r\n\ts=test; t=1534838769; h=from:from:sender:reply-to:subject:subject:date:date:\r\n\t message-id:message-id:to:to:cc:mime-version:mime-version:\r\n\t content-type:content-type:\r\n\t content-transfer-encoding:content-transfer-encoding:in-reply-to:\r\n\t references; bh=EVfAHeUMDygbJe0SkMWJHjgXGjtiTLZnMQbyWqzsrCY=;\r\n\tb=ZV1ndumHftEnhq8Z7zujDS9WUlpJmjrTIEzXsIy8+1HhZ6AEcmur5r8SmS/xfUvpoScNvF\r\n\tlYAHIdbKPX54Fpq7rZVgIhuy8vfcRGprHxH9DMhk3kHiuZLsJT1F6UdPwTYSGAWBTbg1u2\r\n\tHu2VoeSYBV5PrsINvPr1QCbo+GqTlJU=\r\nARC-Seal: i=1; s=test; d=test.org; t=1534838769; a=rsa-sha256; cv=none;\r\n\tb=CE7Q0SnkLMHx82eOtanv4pCtl3fhaPqOAYb3Nqxi5lqwDcO80vTMfGUwOv6r3Exv9Cnj3+nCxejzXtrO4TTKesL9bamOBf+veRYTLfy8wjRIfgEVXS/amOjf9u4UzGgwxL1HbhHWbCIqJhaVwrPZaHZwA9XPmboqGVQNP3i2nH0=\r\nARC-Authentication-Results: i=1; ORIGINATING;\r\n\tauth=pass smtp.auth=test1@test.org smtp.mailfrom=test1@test.org\r\nAuthentication-Results: ORIGINATING;\r\n\tauth=pass smtp.auth=test1@test.org smtp.mailfrom=test1@test.org\r\n\r\nHello!\r\n\r\n'), b')' 84 | ] 85 | -------------------------------------------------------------------------------- /modoboa_webmail/tests/test_fetch_parser.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """FETCH parser tests.""" 4 | 5 | from __future__ import print_function 6 | 7 | import unittest 8 | 9 | import six 10 | 11 | from modoboa_webmail.lib.fetch_parser import FetchResponseParser 12 | 13 | from . import data 14 | 15 | 16 | def dump_bodystructure(fp, bs, depth=0): 17 | """Dump a parsed BODYSTRUCTURE.""" 18 | indentation = " " * (depth * 4) 19 | for mp in bs: 20 | if isinstance(mp, list): 21 | if isinstance(mp[0], list): 22 | print("{}multipart/{}".format(indentation, mp[1]), file=fp) 23 | dump_bodystructure(fp, mp, depth + 1) 24 | else: 25 | dump_bodystructure(fp, mp, depth) 26 | elif isinstance(mp, dict): 27 | if isinstance(mp["struct"][0], list): 28 | print("{}multipart/{}".format( 29 | indentation, mp["struct"][1]), file=fp) 30 | dump_bodystructure(fp, mp["struct"][0], depth + 1) 31 | else: 32 | print("{}{}/{}".format( 33 | indentation, *mp["struct"][:2]), file=fp) 34 | fp.seek(0) 35 | result = fp.read() 36 | return result 37 | 38 | 39 | class FetchParserTestCase(unittest.TestCase): 40 | """Test FETCH parser.""" 41 | 42 | def setUp(self): 43 | """Setup test env.""" 44 | self.parser = FetchResponseParser() 45 | 46 | def _test_bodystructure_output(self, bs, expected): 47 | """.""" 48 | r = self.parser.parse(bs) 49 | fp = six.StringIO() 50 | output = dump_bodystructure(fp, r[list(r.keys())[0]]["BODYSTRUCTURE"]) 51 | fp.close() 52 | self.assertEqual(output, expected) 53 | return r 54 | 55 | def test_parse_bodystructure(self): 56 | """Test the parsing of several responses containing BS.""" 57 | self._test_bodystructure_output( 58 | data.BODYSTRUCTURE_SAMPLE_1, """multipart/alternative 59 | text/plain 60 | text/html 61 | """) 62 | self._test_bodystructure_output( 63 | data.BODYSTRUCTURE_SAMPLE_2, """multipart/mixed 64 | text/plain 65 | message/rfc822 66 | """) 67 | self._test_bodystructure_output( 68 | data.BODYSTRUCTURE_SAMPLE_3, """multipart/mixed 69 | multipart/alternative 70 | text/plain 71 | text/html 72 | application/pdf 73 | """) 74 | self._test_bodystructure_output( 75 | data.BODYSTRUCTURE_SAMPLE_4, """multipart/mixed 76 | multipart/alternative 77 | text/plain 78 | text/html 79 | application/octet-stream 80 | """) 81 | self._test_bodystructure_output( 82 | data.BODYSTRUCTURE_SAMPLE_5, """multipart/alternative 83 | text/plain 84 | text/html 85 | """) 86 | self._test_bodystructure_output( 87 | data.BODYSTRUCTURE_SAMPLE_6, """multipart/mixed 88 | multipart/related 89 | multipart/alternative 90 | text/plain 91 | text/html 92 | image/png 93 | image/jpeg 94 | application/pdf 95 | multipart/alternative 96 | text/plain 97 | text/html 98 | """) 99 | self._test_bodystructure_output( 100 | data.BODYSTRUCTURE_SAMPLE_7, """multipart/mixed 101 | multipart/mixed 102 | text/plain 103 | application/octet-stream 104 | """) 105 | self._test_bodystructure_output( 106 | data.BODYSTRUCTURE_SAMPLE_8, "text/html\n") 107 | -------------------------------------------------------------------------------- /modoboa_webmail/urls.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from django.urls import path 3 | 4 | from . import views 5 | 6 | app_name = 'modoboa_webmail' 7 | 8 | urlpatterns = [ 9 | path('', views.index, name="index"), 10 | path('submailboxes', views.submailboxes, name="submailboxes_get"), 11 | path('getmailcontent', views.getmailcontent, name="mailcontent_get"), 12 | path('getmailsource', views.getmailsource, name="mailsource_get"), 13 | path('unseenmsgs', views.check_unseen_messages, 14 | name="unseen_messages_check"), 15 | 16 | path('delete/', views.delete, name="mail_delete"), 17 | path('move/', views.move, name="mail_move"), 18 | path('mark//', views.mark, name="mail_mark"), 19 | path('mark_as_junk/', views.mark_as_junk, name="mail_mark_as_junk"), 20 | path('mark_as_not_junk/', views.mark_as_not_junk, 21 | name="mail_mark_as_not_junk"), 22 | 23 | path('newfolder/', views.newfolder, name="folder_add"), 24 | path('editfolder/', views.editfolder, name="folder_change"), 25 | path('delfolder/', views.delfolder, name="folder_delete"), 26 | path('compressfolder/', views.folder_compress, name="folder_compress"), 27 | path('emptytrash/', views.empty, name="trash_empty"), 28 | 29 | path('attachments/', views.attachments, name="attachment_list"), 30 | path('delattachment/', views.delattachment, name="attachment_delete"), 31 | path('getattachment/', views.getattachment, name="attachment_get"), 32 | path('password/', views.get_plain_password, name="get_plain_password") 33 | ] 34 | -------------------------------------------------------------------------------- /modoboa_webmail/validators.py: -------------------------------------------------------------------------------- 1 | """Custom form validators.""" 2 | 3 | from email.utils import getaddresses 4 | 5 | from django.utils.encoding import force_str 6 | from django.core.validators import validate_email 7 | 8 | 9 | class EmailListValidator(object): 10 | 11 | """Validate a list of email.""" 12 | 13 | def __call__(self, value): 14 | value = force_str(value) 15 | addresses = getaddresses([value]) 16 | [validate_email(email) for name, email in addresses if email] 17 | 18 | 19 | validate_email_list = EmailListValidator() 20 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Profiled execution. 11 | profile=no 12 | 13 | # Add files or directories to the blacklist. They should be base names, not 14 | # paths. 15 | ignore=CVS,migrations 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=yes 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins= 23 | 24 | [MESSAGES CONTROL] 25 | 26 | # Enable the message, report, category or checker with the given id(s). You can 27 | # either give multiple identifier separated by comma (,) or put this option 28 | # multiple time. See also the "--disable" option for examples. 29 | #enable= 30 | 31 | # Disable the message, report, category or checker with the given id(s). You 32 | # can either give multiple identifiers separated by comma (,) or put this 33 | # option multiple times (only on the command line, not in the configuration 34 | # file where it should appear only once).You can also use "--disable=all" to 35 | # disable everything first and then reenable specific checks. For example, if 36 | # you want to run only the similarities checker, you can use "--disable=all 37 | # --enable=similarities". If you want to run only the classes checker, but have 38 | # no Warning level messages displayed, use"--disable=all --enable=classes 39 | # --disable=W" 40 | disable=no-init,no-member,locally-disabled,too-few-public-methods,too-many-public-methods,too-many-ancestors 41 | 42 | 43 | [REPORTS] 44 | 45 | # Set the output format. Available formats are text, parseable, colorized, msvs 46 | # (visual studio) and html. You can also give a reporter class, eg 47 | # mypackage.mymodule.MyReporterClass. 48 | output-format=text 49 | 50 | # Put messages in a separate file for each module / package specified on the 51 | # command line instead of printing them on stdout. Reports (if any) will be 52 | # written in a file name "pylint_global.[txt|html]". 53 | files-output=no 54 | 55 | # Tells whether to display a full report or only the messages 56 | reports=yes 57 | 58 | # Python expression which should return a note less than 10 (10 is the highest 59 | # note). You have access to the variables errors warning, statement which 60 | # respectively contain the number of errors / warnings messages and the total 61 | # number of statements analyzed. This is used by the global evaluation report 62 | # (RP0004). 63 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 64 | 65 | # Add a comment according to your evaluation note. This is used by the global 66 | # evaluation report (RP0004). 67 | comment=no 68 | 69 | # Template used to display messages. This is a python new-style format string 70 | # used to format the message information. See doc for all details 71 | #msg-template= 72 | 73 | 74 | [LOGGING] 75 | 76 | # Logging modules to check that the string format arguments are in logging 77 | # function parameter format 78 | logging-modules=logging 79 | 80 | 81 | [FORMAT] 82 | 83 | # Maximum number of characters on a single line. 84 | max-line-length=80 85 | 86 | # Regexp for a line that is allowed to be longer than the limit. 87 | ignore-long-lines=^\s*(# )??$ 88 | 89 | # Allow the body of an if to be on the same line as the test if there is no 90 | # else. 91 | single-line-if-stmt=no 92 | 93 | # List of optional constructs for which whitespace checking is disabled 94 | no-space-check=trailing-comma,dict-separator 95 | 96 | # Maximum number of lines in a module 97 | max-module-lines=1000 98 | 99 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 100 | # tab). 101 | indent-string=' ' 102 | 103 | # Number of spaces of indent required inside a hanging or continued line. 104 | indent-after-paren=4 105 | 106 | 107 | [SIMILARITIES] 108 | 109 | # Minimum lines number of a similarity. 110 | min-similarity-lines=4 111 | 112 | # Ignore comments when computing similarities. 113 | ignore-comments=yes 114 | 115 | # Ignore docstrings when computing similarities. 116 | ignore-docstrings=yes 117 | 118 | # Ignore imports when computing similarities. 119 | ignore-imports=no 120 | 121 | 122 | [MISCELLANEOUS] 123 | 124 | # List of note tags to take in consideration, separated by a comma. 125 | notes=FIXME,XXX,TODO 126 | 127 | 128 | [BASIC] 129 | 130 | # Required attributes for module, separated by a comma 131 | required-attributes= 132 | 133 | # List of builtins function names that should not be used, separated by a comma 134 | bad-functions=map,filter,apply,input,file 135 | 136 | # Good variable names which should always be accepted, separated by a comma 137 | good-names=i,j,k,ex,Run,_,tz,logger,pk,qs 138 | 139 | # Bad variable names which should always be refused, separated by a comma 140 | bad-names=foo,bar,baz,toto,tutu,tata 141 | 142 | # Colon-delimited sets of names that determine each other's naming style when 143 | # the name regexes allow several styles. 144 | name-group= 145 | 146 | # Include a hint for the correct naming format with invalid-name 147 | include-naming-hint=no 148 | 149 | # Regular expression matching correct function names 150 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 151 | 152 | # Naming hint for function names 153 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 154 | 155 | # Regular expression matching correct variable names 156 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 157 | 158 | # Naming hint for variable names 159 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 160 | 161 | # Regular expression matching correct constant names 162 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 163 | 164 | # Naming hint for constant names 165 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 166 | 167 | # Regular expression matching correct attribute names 168 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 169 | 170 | # Naming hint for attribute names 171 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 172 | 173 | # Regular expression matching correct argument names 174 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 175 | 176 | # Naming hint for argument names 177 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 178 | 179 | # Regular expression matching correct class attribute names 180 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 181 | 182 | # Naming hint for class attribute names 183 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 184 | 185 | # Regular expression matching correct inline iteration names 186 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 187 | 188 | # Naming hint for inline iteration names 189 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 190 | 191 | # Regular expression matching correct class names 192 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 193 | 194 | # Naming hint for class names 195 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 196 | 197 | # Regular expression matching correct module names 198 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 199 | 200 | # Naming hint for module names 201 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 202 | 203 | # Regular expression matching correct method names 204 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 205 | 206 | # Naming hint for method names 207 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 208 | 209 | # Regular expression which should only match function or class names that do 210 | # not require a docstring. 211 | no-docstring-rgx=__.*__ 212 | 213 | # Minimum line length for functions/classes that require docstrings, shorter 214 | # ones are exempt. 215 | docstring-min-length=-1 216 | 217 | 218 | [TYPECHECK] 219 | 220 | # Tells whether missing members accessed in mixin class should be ignored. A 221 | # mixin class is detected if its name ends with "mixin" (case insensitive). 222 | ignore-mixin-members=yes 223 | 224 | # List of module names for which member attributes should not be checked 225 | # (useful for modules/projects where namespaces are manipulated during runtime 226 | # and thus existing member attributes cannot be deduced by static analysis 227 | ignored-modules= 228 | 229 | # List of classes names for which member attributes should not be checked 230 | # (useful for classes with attributes dynamically set). 231 | ignored-classes=SQLObject 232 | 233 | # When zope mode is activated, add a predefined set of Zope acquired attributes 234 | # to generated-members. 235 | zope=no 236 | 237 | # List of members which are set dynamically and missed by pylint inference 238 | # system, and so shouldn't trigger E0201 when accessed. Python regular 239 | # expressions are accepted. 240 | generated-members=REQUEST,acl_users,aq_parent 241 | 242 | 243 | [VARIABLES] 244 | 245 | # Tells whether we should check for unused import in __init__ files. 246 | init-import=no 247 | 248 | # A regular expression matching the name of dummy variables (i.e. expectedly 249 | # not used). 250 | dummy-variables-rgx=_$|dummy 251 | 252 | # List of additional names supposed to be defined in builtins. Remember that 253 | # you should avoid to define new builtins when possible. 254 | additional-builtins= 255 | 256 | 257 | [DESIGN] 258 | 259 | # Maximum number of arguments for function / method 260 | max-args=5 261 | 262 | # Argument names that match this expression will be ignored. Default to name 263 | # with leading underscore 264 | ignored-argument-names=_.* 265 | 266 | # Maximum number of locals for function / method body 267 | max-locals=15 268 | 269 | # Maximum number of return / yield for function / method body 270 | max-returns=6 271 | 272 | # Maximum number of branch for function / method body 273 | max-branches=12 274 | 275 | # Maximum number of statements in function / method body 276 | max-statements=50 277 | 278 | # Maximum number of parents for a class (see R0901). 279 | max-parents=7 280 | 281 | # Maximum number of attributes for a class (see R0902). 282 | max-attributes=7 283 | 284 | # Minimum number of public methods for a class (see R0903). 285 | min-public-methods=2 286 | 287 | # Maximum number of public methods for a class (see R0904). 288 | max-public-methods=20 289 | 290 | 291 | [CLASSES] 292 | 293 | # List of interface methods to ignore, separated by a comma. This is used for 294 | # instance to not check methods defines in Zope's Interface base class. 295 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 296 | 297 | # List of method names used to declare (i.e. assign) instance attributes. 298 | defining-attr-methods=__init__,__new__,setUp 299 | 300 | # List of valid names for the first argument in a class method. 301 | valid-classmethod-first-arg=cls 302 | 303 | # List of valid names for the first argument in a metaclass class method. 304 | valid-metaclass-classmethod-first-arg=mcs 305 | 306 | 307 | [IMPORTS] 308 | 309 | # Deprecated modules which should not be used, separated by a comma 310 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 311 | 312 | # Create a graph of every (i.e. internal and external) dependencies in the 313 | # given file (report RP0402 must not be disabled) 314 | import-graph= 315 | 316 | # Create a graph of external dependencies in the given file (report RP0402 must 317 | # not be disabled) 318 | ext-import-graph= 319 | 320 | # Create a graph of internal dependencies in the given file (report RP0402 must 321 | # not be disabled) 322 | int-import-graph= 323 | 324 | 325 | [EXCEPTIONS] 326 | 327 | # Exceptions that will emit a warning when being caught. Defaults to 328 | # "Exception" 329 | overgeneral-exceptions=Exception 330 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | chardet 2 | lxml 3 | modoboa>=2.3.0 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | [pep8] 4 | max-line-length = 80 5 | exclude = migrations 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | A setuptools based setup module. 6 | 7 | See: 8 | https://packaging.python.org/en/latest/distributing.html 9 | """ 10 | 11 | import io 12 | from os import path 13 | try: 14 | from pip.req import parse_requirements 15 | except ImportError: 16 | # pip >= 10 17 | from pip._internal.req import parse_requirements 18 | from setuptools import setup, find_packages 19 | 20 | 21 | def get_requirements(requirements_file): 22 | """Use pip to parse requirements file.""" 23 | requirements = [] 24 | if path.isfile(requirements_file): 25 | for req in parse_requirements(requirements_file, session="hack"): 26 | try: 27 | # check markers, such as 28 | # 29 | # rope_py3k ; python_version >= '3.0' 30 | # 31 | if req.match_markers(): 32 | requirements.append(str(req.req)) 33 | except AttributeError: 34 | # pip >= 20.0.2 35 | requirements.append(req.requirement) 36 | return requirements 37 | 38 | 39 | if __name__ == "__main__": 40 | HERE = path.abspath(path.dirname(__file__)) 41 | INSTALL_REQUIRES = get_requirements(path.join(HERE, "requirements.txt")) 42 | 43 | with io.open(path.join(HERE, "README.rst"), encoding="utf-8") as readme: 44 | LONG_DESCRIPTION = readme.read() 45 | 46 | def local_scheme(version): 47 | """Skip the local version (eg. +xyz of 0.6.1.dev4+gdf99fe2) 48 | to be able to upload to Test PyPI""" 49 | return "" 50 | 51 | setup( 52 | name="modoboa-webmail", 53 | description="The webmail of Modoboa", 54 | long_description=LONG_DESCRIPTION, 55 | license="MIT", 56 | url="http://modoboa.org/", 57 | author="Antoine Nguyen", 58 | author_email="tonio@ngyn.org", 59 | classifiers=[ 60 | "Development Status :: 5 - Production/Stable", 61 | "Environment :: Web Environment", 62 | "Framework :: Django :: 4.2", 63 | "Intended Audience :: System Administrators", 64 | "License :: OSI Approved :: MIT License", 65 | "Operating System :: OS Independent", 66 | "Programming Language :: Python :: 3.8", 67 | "Programming Language :: Python :: 3.9", 68 | "Programming Language :: Python :: 3.10", 69 | "Programming Language :: Python :: 3.11", 70 | "Topic :: Communications :: Email", 71 | "Topic :: Internet :: WWW/HTTP", 72 | ], 73 | keywords="email webmail", 74 | packages=find_packages(exclude=["docs", "test_project"]), 75 | include_package_data=True, 76 | zip_safe=False, 77 | install_requires=INSTALL_REQUIRES, 78 | use_scm_version={"local_scheme": local_scheme}, 79 | setup_requires=["setuptools_scm"], 80 | ) 81 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | factory-boy<3.4.0 2 | testfixtures==7.2.2 3 | psycopg[binary]>=3.1.8 4 | mysqlclient<2.2.5 5 | -------------------------------------------------------------------------------- /test_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import unicode_literals 4 | 5 | import os 6 | import sys 7 | 8 | if __name__ == "__main__": 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError: 13 | # The above import may fail for some other reason. Ensure that the 14 | # issue is really that Django is missing to avoid masking other 15 | # exceptions on Python 2. 16 | try: 17 | import django # noqa 18 | except ImportError: 19 | raise ImportError( 20 | "Couldn't import Django. Are you sure it's installed and " 21 | "available on your PYTHONPATH environment variable? Did you " 22 | "forget to activate a virtual environment?" 23 | ) 24 | raise 25 | execute_from_command_line(sys.argv) 26 | -------------------------------------------------------------------------------- /test_project/test_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modoboa/modoboa-webmail/5fd60386f4cb01d353b49d78592f7256ec570e53/test_project/test_project/__init__.py -------------------------------------------------------------------------------- /test_project/test_project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for test_project project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/2.2/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/2.2/ref/settings/ 9 | """ 10 | 11 | from logging.handlers import SysLogHandler 12 | import os 13 | 14 | from modoboa.test_settings import * # noqa 15 | 16 | 17 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 18 | BASE_DIR = os.path.realpath(os.path.dirname(os.path.dirname(__file__))) 19 | 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = "!8o(-dbbl3e+*bh7nx-^xysdt)1gso*%@4ze4-9_9o+i&t--u_" 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = "DEBUG" in os.environ 29 | 30 | ALLOWED_HOSTS = [ 31 | "api", 32 | "api-unsecured", 33 | "127.0.0.1", 34 | "localhost", 35 | ] 36 | 37 | INTERNAL_IPS = ["127.0.0.1"] 38 | 39 | SITE_ID = 1 40 | 41 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField" 42 | 43 | # A list of all the people who get code error notifications. When DEBUG=False 44 | # and a view raises an exception, Django will email these people with the full 45 | # exception information. 46 | # See https://docs.djangoproject.com/en/dev/ref/settings/#admins 47 | # ADMINS = [('Administrator', 'admin@example.net')] 48 | 49 | # The email address that error messages come from, such as those sent to ADMINS 50 | # SERVER_EMAIL = 'webmaster@example.net' 51 | EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" 52 | 53 | # Security settings 54 | 55 | X_FRAME_OPTIONS = "SAMEORIGIN" 56 | CSRF_COOKIE_SECURE = False 57 | SESSION_COOKIE_SECURE = False 58 | 59 | # Application definition 60 | 61 | INSTALLED_APPS = ( 62 | "django.contrib.auth", 63 | "django.contrib.contenttypes", 64 | "django.contrib.sessions", 65 | "django.contrib.messages", 66 | "django.contrib.sites", 67 | "django.contrib.staticfiles", 68 | "reversion", 69 | "ckeditor", 70 | "ckeditor_uploader", 71 | "oauth2_provider", 72 | "corsheaders", 73 | "rest_framework", 74 | "rest_framework.authtoken", 75 | "drf_spectacular", 76 | "django_otp", 77 | "django_otp.plugins.otp_totp", 78 | "django_otp.plugins.otp_static", 79 | "django_rename_app", 80 | "django_rq", 81 | ) 82 | 83 | # A dedicated place to register Modoboa applications 84 | # Do not delete it. 85 | # Do not change the order. 86 | MODOBOA_APPS = ( 87 | "modoboa", 88 | "modoboa.core", 89 | "modoboa.lib", 90 | "modoboa.admin", 91 | "modoboa.transport", 92 | "modoboa.relaydomains", 93 | "modoboa.limits", 94 | "modoboa.parameters", 95 | "modoboa.dnstools", 96 | "modoboa.policyd", 97 | "modoboa.maillog", 98 | "modoboa.pdfcredentials", 99 | "modoboa.dmarc", 100 | "modoboa.imap_migration", 101 | "modoboa.postfix_autoreply", 102 | "modoboa.sievefilters", 103 | # Modoboa extensions here. 104 | "modoboa_webmail", 105 | ) 106 | 107 | try: 108 | import ldap # noqa: F401 109 | except ImportError: 110 | pass 111 | else: 112 | MODOBOA_APPS += ("modoboa.ldapsync",) 113 | 114 | INSTALLED_APPS += MODOBOA_APPS 115 | 116 | AUTH_USER_MODEL = "core.User" 117 | 118 | MIDDLEWARE = ( 119 | "django.contrib.sessions.middleware.SessionMiddleware", 120 | "django.middleware.locale.LocaleMiddleware", 121 | "x_forwarded_for.middleware.XForwardedForMiddleware", 122 | "corsheaders.middleware.CorsMiddleware", 123 | "django.middleware.common.CommonMiddleware", 124 | "django.middleware.csrf.CsrfViewMiddleware", 125 | "django.contrib.auth.middleware.AuthenticationMiddleware", 126 | "django.contrib.messages.middleware.MessageMiddleware", 127 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 128 | "modoboa.core.middleware.LocalConfigMiddleware", 129 | "modoboa.lib.middleware.AjaxLoginRedirect", 130 | "modoboa.lib.middleware.CommonExceptionCatcher", 131 | "modoboa.lib.middleware.RequestCatcherMiddleware", 132 | ) 133 | 134 | AUTHENTICATION_BACKENDS = ( 135 | # 'modoboa.lib.authbackends.LDAPBackend', 136 | # 'modoboa.lib.authbackends.SMTPBackend', 137 | "django.contrib.auth.backends.ModelBackend", 138 | "modoboa.imap_migration.auth_backends.IMAPBackend", 139 | ) 140 | 141 | # SMTP authentication 142 | # AUTH_SMTP_SERVER_ADDRESS = 'localhost' 143 | # AUTH_SMTP_SERVER_PORT = 25 144 | # AUTH_SMTP_SECURED_MODE = None # 'ssl' or 'starttls' are accepted 145 | 146 | 147 | TEMPLATES = [ 148 | { 149 | "BACKEND": "django.template.backends.django.DjangoTemplates", 150 | "DIRS": [], 151 | "APP_DIRS": True, 152 | "OPTIONS": { 153 | "context_processors": [ 154 | "django.template.context_processors.debug", 155 | "django.template.context_processors.request", 156 | "django.contrib.auth.context_processors.auth", 157 | "django.template.context_processors.i18n", 158 | "django.template.context_processors.media", 159 | "django.template.context_processors.static", 160 | "django.template.context_processors.tz", 161 | "django.contrib.messages.context_processors.messages", 162 | "modoboa.core.context_processors.top_notifications", 163 | "modoboa.core.context_processors.new_admin_url", 164 | ], 165 | "debug": DEBUG, 166 | }, 167 | }, 168 | ] 169 | 170 | ROOT_URLCONF = "test_project.urls" 171 | 172 | WSGI_APPLICATION = "test_project.wsgi.application" 173 | 174 | CORS_ORIGIN_ALLOW_ALL = True 175 | 176 | # Internationalization 177 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 178 | 179 | LANGUAGE_CODE = "en" 180 | 181 | TIME_ZONE = "UTC" 182 | 183 | USE_I18N = True 184 | 185 | USE_L10N = True 186 | 187 | USE_TZ = True 188 | 189 | # Static files (CSS, JavaScript, Images) 190 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 191 | 192 | STATIC_URL = "/sitestatic/" 193 | STATIC_ROOT = os.path.join(BASE_DIR, "sitestatic") 194 | STATICFILES_DIRS = (os.path.join(BASE_DIR, "..", "modoboa", "bower_components"),) 195 | 196 | MEDIA_URL = "/media/" 197 | MEDIA_ROOT = os.path.join(BASE_DIR, "media") 198 | 199 | # oAuth2 settings 200 | 201 | OAUTH2_PROVIDER = { 202 | "OIDC_ENABLED": True, 203 | "OIDC_RP_INITIATED_LOGOUT_ENABLED": True, 204 | "OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT": True, 205 | "OIDC_RSA_PRIVATE_KEY": os.environ.get("OIDC_RSA_PRIVATE_KEY"), 206 | "SCOPES": { 207 | "openid": "OpenID Connect scope", 208 | "read": "Read scope", 209 | "write": "Write scope", 210 | "introspection": "Introspect token scope", 211 | }, 212 | "DEFAULT_SCOPES": ["openid", "read", "write"], 213 | } 214 | 215 | # Rest framework settings 216 | 217 | REST_FRAMEWORK = { 218 | "DEFAULT_THROTTLE_RATES": { 219 | "user": "200/minute", 220 | "ddos": "10/second", 221 | "ddos_lesser": "200/minute", 222 | "login": "10/minute", 223 | "password_recovery_request": "11/hour", 224 | "password_recovery_totp_check": "25/hour", 225 | "password_recovery_apply": "25/hour", 226 | }, 227 | "DEFAULT_AUTHENTICATION_CLASSES": ( 228 | "oauth2_provider.contrib.rest_framework.OAuth2Authentication", 229 | "rest_framework.authentication.TokenAuthentication", 230 | "rest_framework.authentication.SessionAuthentication", 231 | ), 232 | "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", 233 | "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning", 234 | } 235 | 236 | # Uncomment if you need a custom sub url for new_admin interface 237 | # NEW_ADMIN_URL = 'new-admin' 238 | 239 | SPECTACULAR_SETTINGS = { 240 | "SCHEMA_PATH_PREFIX": r"/api/v[0-9]", 241 | "TITLE": "Modoboa API", 242 | "VERSION": None, 243 | "SERVE_PERMISSIONS": ["rest_framework.permissions.IsAuthenticated"], 244 | } 245 | 246 | # Modoboa settings 247 | # MODOBOA_CUSTOM_LOGO = os.path.join(MEDIA_URL, "custom_logo.png") 248 | 249 | # DOVECOT_LOOKUP_PATH = ('/path/to/dovecot', ) 250 | DOVECOT_USER = "root" 251 | 252 | 253 | MODOBOA_API_URL = "https://api.modoboa.org/1/" 254 | 255 | PID_FILE_STORAGE_PATH = "/tmp" 256 | 257 | # REDIS 258 | 259 | REDIS_HOST = os.environ.get("REDIS_HOST", "127.0.0.1") 260 | REDIS_PORT = os.environ.get("REDIS_PORT", 6379) 261 | REDIS_QUOTA_DB = 0 262 | REDIS_URL = "redis://{}:{}/{}".format(REDIS_HOST, REDIS_PORT, REDIS_QUOTA_DB) 263 | 264 | # RQ 265 | 266 | RQ_QUEUES = { 267 | "dkim": { 268 | "URL": REDIS_URL, 269 | }, 270 | "modoboa": { 271 | "URL": REDIS_URL, 272 | }, 273 | } 274 | 275 | # CACHE 276 | 277 | CACHES = { 278 | "default": { 279 | "BACKEND": "django.core.cache.backends.redis.RedisCache", 280 | "LOCATION": f"redis://{REDIS_HOST}:{REDIS_PORT}", 281 | } 282 | } 283 | 284 | # Password validation 285 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 286 | 287 | AUTH_PASSWORD_VALIDATORS = [ 288 | { 289 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 290 | }, 291 | { 292 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 293 | }, 294 | { 295 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 296 | }, 297 | { 298 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 299 | }, 300 | { 301 | "NAME": "modoboa.core.password_validation.ComplexityValidator", 302 | "OPTIONS": {"upper": 1, "lower": 1, "digits": 1, "specials": 0}, 303 | }, 304 | ] 305 | 306 | # CKeditor 307 | 308 | CKEDITOR_UPLOAD_PATH = "uploads/" 309 | 310 | CKEDITOR_IMAGE_BACKEND = "pillow" 311 | 312 | CKEDITOR_RESTRICT_BY_USER = True 313 | 314 | CKEDITOR_BROWSE_SHOW_DIRS = True 315 | 316 | CKEDITOR_ALLOW_NONIMAGE_FILES = False 317 | 318 | CKEDITOR_CONFIGS = { 319 | "default": { 320 | "allowedContent": True, 321 | "toolbar": "Modoboa", 322 | "width": None, 323 | "toolbar_Modoboa": [ 324 | ["Bold", "Italic", "Underline"], 325 | ["JustifyLeft", "JustifyCenter", "JustifyRight", "JustifyBlock"], 326 | ["BidiLtr", "BidiRtl", "Language"], 327 | ["NumberedList", "BulletedList", "-", "Outdent", "Indent"], 328 | ["Undo", "Redo"], 329 | ["Link", "Unlink", "Anchor", "-", "Smiley"], 330 | ["TextColor", "BGColor", "-", "Source"], 331 | ["Font", "FontSize"], 332 | [ 333 | "Image", 334 | ], 335 | ["SpellChecker"], 336 | ], 337 | }, 338 | } 339 | 340 | # Logging configuration 341 | 342 | LOGGING = { 343 | "version": 1, 344 | "formatters": { 345 | "syslog": {"format": "%(name)s: %(levelname)s %(message)s"}, 346 | "django.server": { 347 | "()": "django.utils.log.ServerFormatter", 348 | "format": "[%(server_time)s] %(message)s", 349 | }, 350 | }, 351 | "handlers": { 352 | "mail-admins": { 353 | "level": "ERROR", 354 | "class": "django.utils.log.AdminEmailHandler", 355 | "include_html": True, 356 | }, 357 | "syslog-auth": { 358 | "class": "logging.handlers.SysLogHandler", 359 | "facility": SysLogHandler.LOG_AUTH, 360 | "formatter": "syslog", 361 | }, 362 | "modoboa": { 363 | "class": "modoboa.core.loggers.SQLHandler", 364 | }, 365 | "console": {"class": "logging.StreamHandler"}, 366 | "django.server": { 367 | "level": "INFO", 368 | "class": "logging.StreamHandler", 369 | "formatter": "django.server", 370 | }, 371 | }, 372 | "loggers": { 373 | "django": {"handlers": ["mail-admins"], "level": "ERROR", "propagate": False}, 374 | "modoboa.auth": { 375 | "handlers": ["syslog-auth", "modoboa"], 376 | "level": "INFO", 377 | "propagate": False, 378 | }, 379 | "modoboa.admin": {"handlers": ["modoboa"], "level": "INFO", "propagate": False}, 380 | "django.server": { 381 | "handlers": ["django.server"], 382 | "level": "INFO", 383 | "propagate": False, 384 | }, 385 | # 'django_auth_ldap': { 386 | # 'level': 'DEBUG', 387 | # 'handlers': ['console'] 388 | # }, 389 | }, 390 | } 391 | 392 | SILENCED_SYSTEM_CHECKS = [ 393 | "security.W019", # modoboa uses iframes to display e-mails 394 | ] 395 | 396 | DISABLE_DASHBOARD_EXTERNAL_QUERIES = False 397 | 398 | # Load settings from extensions 399 | 400 | LDAP_SERVER_PORT = os.environ.get("LDAP_SERVER_PORT", 3389) 401 | -------------------------------------------------------------------------------- /test_project/test_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include 2 | from django.urls import re_path 3 | 4 | urlpatterns = [ 5 | re_path(r'', include('modoboa.urls')), 6 | ] 7 | -------------------------------------------------------------------------------- /test_project/test_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for test_project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | from __future__ import unicode_literals 11 | 12 | import os 13 | 14 | from django.core.wsgi import get_wsgi_application 15 | 16 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") 17 | 18 | application = get_wsgi_application() 19 | --------------------------------------------------------------------------------