From c0fa5393bd8622a22e2caa31f2ba1ebb7bec3d53 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 7 Nov 2024 12:45:07 +0530 Subject: [PATCH] ci: migrate from `black` to `ruff` (#27) --- .editorconfig | 21 + .eslintrc | 124 +++++ .github/helper/flake8.conf | 74 --- .github/workflows/ci.yml | 108 +++++ .github/workflows/{linters.yml => linter.yml} | 53 ++- .pre-commit-config.yaml | 58 ++- commitlint.config.js | 2 +- frontend/postcss.config.js | 8 +- frontend/src/components/AppSidebar.vue | 9 +- .../src/components/Controls/Autocomplete.vue | 19 +- frontend/src/components/Controls/Link.vue | 3 +- .../components/Controls/MultiselectInput.vue | 371 ++++++++------- frontend/src/components/EmojiPicker.vue | 174 ++++--- frontend/src/components/HeaderActions.vue | 30 +- frontend/src/components/Icons/Logo.vue | 36 +- frontend/src/components/MailDate.vue | 60 +-- frontend/src/components/MailDetails.vue | 294 ++++++------ frontend/src/components/Modals/SendMail.vue | 440 +++++++++--------- frontend/src/components/SidebarDetail.vue | 73 ++- frontend/src/components/SidebarLink.vue | 115 +++-- frontend/src/components/UserDropdown.vue | 16 +- frontend/src/main.js | 6 +- frontend/src/pages/Home.vue | 54 +-- frontend/src/pages/Inbox.vue | 182 ++++---- frontend/src/pages/Sent.vue | 177 +++---- frontend/src/router.js | 52 +-- frontend/src/socket.js | 39 +- frontend/src/stores/session.js | 96 ++-- frontend/src/stores/user.js | 25 +- frontend/src/translation.js | 53 +-- frontend/src/utils/composables.js | 154 +++--- frontend/src/utils/dialogs.js | 46 +- frontend/src/utils/index.js | 106 ++--- frontend/tailwind.config.js | 30 +- frontend/vite.config.js | 72 +-- mail/api/auth.py | 13 +- mail/api/inbound.py | 36 +- mail/api/mail.py | 15 +- mail/api/outbound.py | 4 +- mail/api/webhook.py | 12 +- .../doctype/incoming_mail/incoming_mail.js | 54 ++- .../doctype/incoming_mail/incoming_mail.py | 34 +- .../incoming_mail/incoming_mail_list.js | 14 +- mail/mail/doctype/mail_alias/mail_alias.js | 26 +- mail/mail/doctype/mail_alias/mail_alias.py | 11 +- .../mail_alias_mailbox/mail_alias_mailbox.py | 4 +- .../mail/doctype/mail_contact/mail_contact.js | 12 +- .../mail/doctype/mail_contact/mail_contact.py | 13 +- mail/mail/doctype/mail_domain/mail_domain.js | 99 ++-- mail/mail/doctype/mail_domain/mail_domain.py | 11 +- .../doctype/mail_recipient/mail_recipient.py | 2 +- .../doctype/mail_settings/mail_settings.py | 9 +- .../mail_sync_history/mail_sync_history.py | 4 +- mail/mail/doctype/mailbox/mailbox.js | 78 ++-- mail/mail/doctype/mailbox/mailbox.py | 9 +- .../doctype/outgoing_mail/outgoing_mail.js | 211 +++++---- .../doctype/outgoing_mail/outgoing_mail.py | 144 +++--- .../outgoing_mail/outgoing_mail_list.js | 22 +- mail/mail/report/mail_tracker/mail_tracker.js | 16 +- mail/mail/report/mail_tracker/mail_tracker.py | 11 +- .../report/outbound_delay/outbound_delay.js | 4 +- .../report/outbound_delay/outbound_delay.py | 16 +- .../outgoing_mail_summary.py | 8 +- mail/mail_server.py | 21 +- mail/overrides.py | 16 +- mail/tasks.py | 5 +- mail/utils/__init__.py | 31 +- mail/utils/cache.py | 7 +- mail/utils/email_parser.py | 15 +- mail/utils/query.py | 7 +- mail/utils/user.py | 12 +- mail/utils/validation.py | 17 +- mail/www/mail.html | 21 - pyproject.toml | 38 +- yarn.lock | 4 + 75 files changed, 2248 insertions(+), 2018 deletions(-) create mode 100644 .editorconfig create mode 100644 .eslintrc delete mode 100644 .github/helper/flake8.conf create mode 100644 .github/workflows/ci.yml rename .github/workflows/{linters.yml => linter.yml} (51%) delete mode 100644 mail/www/mail.html create mode 100644 yarn.lock diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..73816f19 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# Root editor config file +root = true + +# Common settings +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +# python, js indentation settings +[{*.py,*.js,*.vue,*.css,*.scss,*.html}] +indent_style = tab +indent_size = 4 +max_line_length = 99 + +# JSON files - mostly doctype schema files +[{*.json}] +insert_final_newline = false +indent_style = space +indent_size = 1 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..c5e7d683 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,124 @@ +{ + "env": { + "browser": true, + "node": true, + "es2022": true + }, + "parserOptions": { + "sourceType": "module" + }, + "extends": "eslint:recommended", + "rules": { + "indent": "off", + "brace-style": "off", + "no-mixed-spaces-and-tabs": "off", + "no-useless-escape": "off", + "space-unary-ops": ["error", { "words": true }], + "linebreak-style": "off", + "quotes": ["off"], + "semi": "off", + "camelcase": "off", + "no-unused-vars": "off", + "no-console": ["warn"], + "no-extra-boolean-cast": ["off"], + "no-control-regex": ["off"], + }, + "root": true, + "globals": { + "frappe": true, + "Vue": true, + "SetVueGlobals": true, + "__": true, + "repl": true, + "Class": true, + "locals": true, + "cint": true, + "cstr": true, + "cur_frm": true, + "cur_dialog": true, + "cur_page": true, + "cur_list": true, + "cur_tree": true, + "msg_dialog": true, + "is_null": true, + "in_list": true, + "has_common": true, + "posthog": true, + "has_words": true, + "validate_email": true, + "open_web_template_values_editor": true, + "validate_name": true, + "validate_phone": true, + "validate_url": true, + "get_number_format": true, + "format_number": true, + "format_currency": true, + "comment_when": true, + "open_url_post": true, + "toTitle": true, + "lstrip": true, + "rstrip": true, + "strip": true, + "strip_html": true, + "replace_all": true, + "flt": true, + "precision": true, + "CREATE": true, + "AMEND": true, + "CANCEL": true, + "copy_dict": true, + "get_number_format_info": true, + "strip_number_groups": true, + "print_table": true, + "Layout": true, + "web_form_settings": true, + "$c": true, + "$a": true, + "$i": true, + "$bg": true, + "$y": true, + "$c_obj": true, + "refresh_many": true, + "refresh_field": true, + "toggle_field": true, + "get_field_obj": true, + "get_query_params": true, + "unhide_field": true, + "hide_field": true, + "set_field_options": true, + "getCookie": true, + "getCookies": true, + "get_url_arg": true, + "md5": true, + "$": true, + "jQuery": true, + "moment": true, + "hljs": true, + "Awesomplete": true, + "Sortable": true, + "Showdown": true, + "Taggle": true, + "Gantt": true, + "Slick": true, + "Webcam": true, + "PhotoSwipe": true, + "PhotoSwipeUI_Default": true, + "io": true, + "JsBarcode": true, + "L": true, + "Chart": true, + "DataTable": true, + "Cypress": true, + "cy": true, + "it": true, + "describe": true, + "expect": true, + "context": true, + "before": true, + "beforeEach": true, + "after": true, + "qz": true, + "localforage": true, + "extend_cscript": true + } +} diff --git a/.github/helper/flake8.conf b/.github/helper/flake8.conf deleted file mode 100644 index eb1b9d8c..00000000 --- a/.github/helper/flake8.conf +++ /dev/null @@ -1,74 +0,0 @@ -[flake8] -ignore = - B001, - B007, - B009, - B010, - B950, - E101, - E111, - E114, - E116, - E117, - E121, - E122, - E123, - E124, - E125, - E126, - E127, - E128, - E131, - E201, - E202, - E203, - E211, - E221, - E222, - E223, - E224, - E225, - E226, - E228, - E231, - E241, - E242, - E251, - E261, - E262, - E265, - E266, - E271, - E272, - E273, - E274, - E301, - E302, - E303, - E305, - E306, - E402, - E501, - E502, - E701, - E702, - E703, - E741, - F401, - F403, - F405, - W191, - W291, - W292, - W293, - W391, - W503, - W504, - E711, - E129, - F841, - E713, - E712, - - -max-line-length = 200 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..0661d0ca --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,108 @@ + +name: CI + +on: + push: + branches: + - develop + pull_request: + +concurrency: + group: develop-mail-${{ github.event.number }} + cancel-in-progress: true + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + name: Server + + services: + redis-cache: + image: redis:alpine + ports: + - 13000:6379 + redis-queue: + image: redis:alpine + ports: + - 11000:6379 + mariadb: + image: mariadb:10.6 + env: + MYSQL_ROOT_PASSWORD: root + ports: + - 3306:3306 + options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + steps: + - name: Clone + uses: actions/checkout@v3 + + - name: Find tests + run: | + echo "Finding tests" + grep -rn "def test" > /dev/null + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18 + check-latest: true + + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: 'echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT' + + - uses: actions/cache@v3 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install MariaDB Client + run: | + sudo apt update + sudo apt-get install mariadb-client-10.6 + + - name: Setup + run: | + pip install frappe-bench + bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench + mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" + mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" + + - name: Install + working-directory: /home/runner/frappe-bench + run: | + bench get-app mail $GITHUB_WORKSPACE + bench setup requirements --dev + bench new-site --db-root-password root --admin-password admin test_site + bench --site test_site install-app mail + bench build + env: + CI: 'Yes' + + - name: Run Tests + working-directory: /home/runner/frappe-bench + run: | + bench --site test_site set-config allow_tests true + bench --site test_site run-tests --app mail + env: + TYPE: server diff --git a/.github/workflows/linters.yml b/.github/workflows/linter.yml similarity index 51% rename from .github/workflows/linters.yml rename to .github/workflows/linter.yml index f36897f4..6d38c64a 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linter.yml @@ -1,3 +1,4 @@ + name: Linters on: @@ -8,31 +9,12 @@ permissions: contents: read concurrency: - group: commitcheck-frappe-${{ github.event_name }}-${{ github.event.number }} + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - commit-lint: - name: "Semantic Commits" - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 200 - - uses: actions/setup-node@v4 - with: - node-version: 18 - check-latest: true - - - name: Check commit titles - run: | - npm install @commitlint/cli @commitlint/config-conventional - npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} - linter: - name: "Semgrep Rules" + name: 'Frappe Linter' runs-on: ubuntu-latest if: github.event_name == 'pull_request' @@ -40,8 +22,9 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: '3.10' cache: pip + - uses: pre-commit/action@v3.0.0 - name: Download Semgrep rules run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules @@ -50,3 +33,29 @@ jobs: run: | pip install semgrep semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness + + deps-vulnerable-check: + name: 'Vulnerable Dependency Check' + runs-on: ubuntu-latest + + steps: + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - uses: actions/checkout@v4 + + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Install and run pip-audit + run: | + pip install pip-audit + cd ${GITHUB_WORKSPACE} + pip-audit --desc on . diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 40e979c9..a2dcdd3a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,8 @@ -exclude: "node_modules|.git" +exclude: 'node_modules|.git' default_stages: [commit] fail_fast: false + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 @@ -19,21 +20,50 @@ repos: - id: check-yaml - id: debug-statements - - repo: https://github.com/frappe/black - rev: 951ccf4d5bb0d692b457a5ebc4215d755618eb68 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.2.0 hooks: - - id: black - additional_dependencies: ["click==8.0.4"] + - id: ruff + name: "Run ruff linter and apply fixes" + args: ["--fix"] + + - id: ruff-format + name: "Format Python code" + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.7.1 + hooks: + - id: prettier + types_or: [javascript, vue, scss] + # Ignore any files that might contain jinja / bundles + exclude: | + (?x)^( + mail/public/dist/.*| + .*node_modules.*| + .*boilerplate.*| + mail/templates/includes/.*| + mail/public/js/lib/.* + )$ + - - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v8.44.0 hooks: - - id: flake8 - additional_dependencies: ["flake8-bugbear"] - args: ["--config", ".github/helper/flake8.conf"] - exclude: ".*setup.py$" + - id: eslint + types_or: [javascript] + args: ['--quiet'] + # Ignore any files that might contain jinja / bundles + exclude: | + (?x)^( + mail/public/dist/.*| + cypress/.*| + .*node_modules.*| + .*boilerplate.*| + mail/templates/includes/.*| + mail/public/js/lib/.* + )$ ci: - autoupdate_schedule: weekly - skip: [] - submodules: false + autoupdate_schedule: weekly + skip: [] + submodules: false diff --git a/commitlint.config.js b/commitlint.config.js index 616dcef4..09de8b82 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -22,4 +22,4 @@ module.exports = { ], ], }, -}; \ No newline at end of file +}; diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 33ad091d..1b69d43b 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -1,6 +1,6 @@ module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, } diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue index 3042dded..8ed7c5ef 100644 --- a/frontend/src/components/AppSidebar.vue +++ b/frontend/src/components/AppSidebar.vue @@ -27,9 +27,12 @@ > diff --git a/frontend/src/components/Controls/Autocomplete.vue b/frontend/src/components/Controls/Autocomplete.vue index 81f5358b..60da92fb 100644 --- a/frontend/src/components/Controls/Autocomplete.vue +++ b/frontend/src/components/Controls/Autocomplete.vue @@ -50,10 +50,7 @@ - +
\ No newline at end of file + diff --git a/frontend/src/components/Icons/Logo.vue b/frontend/src/components/Icons/Logo.vue index 13173dbb..fa8d201b 100644 --- a/frontend/src/components/Icons/Logo.vue +++ b/frontend/src/components/Icons/Logo.vue @@ -1,13 +1,25 @@ \ No newline at end of file + + + + + + + + + + + + diff --git a/frontend/src/components/MailDate.vue b/frontend/src/components/MailDate.vue index f2c17dcc..f8d690dd 100644 --- a/frontend/src/components/MailDate.vue +++ b/frontend/src/components/MailDate.vue @@ -1,42 +1,46 @@ \ No newline at end of file + diff --git a/frontend/src/components/MailDetails.vue b/frontend/src/components/MailDetails.vue index d458626c..020fc3b1 100644 --- a/frontend/src/components/MailDetails.vue +++ b/frontend/src/components/MailDetails.vue @@ -1,164 +1,184 @@ \ No newline at end of file + diff --git a/frontend/src/components/Modals/SendMail.vue b/frontend/src/components/Modals/SendMail.vue index b67b96ab..71113142 100644 --- a/frontend/src/components/Modals/SendMail.vue +++ b/frontend/src/components/Modals/SendMail.vue @@ -1,95 +1,96 @@ + + + \ No newline at end of file + diff --git a/frontend/src/components/SidebarDetail.vue b/frontend/src/components/SidebarDetail.vue index 147027e0..3c6a6a41 100644 --- a/frontend/src/components/SidebarDetail.vue +++ b/frontend/src/components/SidebarDetail.vue @@ -1,48 +1,47 @@ \ No newline at end of file + diff --git a/frontend/src/components/SidebarLink.vue b/frontend/src/components/SidebarLink.vue index c7485eb0..0c712a6e 100644 --- a/frontend/src/components/SidebarLink.vue +++ b/frontend/src/components/SidebarLink.vue @@ -1,31 +1,52 @@ diff --git a/frontend/src/components/UserDropdown.vue b/frontend/src/components/UserDropdown.vue index a7625c51..75dedce8 100644 --- a/frontend/src/components/UserDropdown.vue +++ b/frontend/src/components/UserDropdown.vue @@ -28,18 +28,14 @@
{{ branding.data?.brand_name }} Mail
-
+
{{ convertToTitleCase(userResource.data?.full_name) }}
@@ -62,13 +58,7 @@ import Logo from '@/components/Icons/Logo.vue' import { sessionStore } from '@/stores/session' import { Dropdown } from 'frappe-ui' -import { - ChevronDown, - LogIn, - LogOut, - User, - ArrowRightLeft, -} from 'lucide-vue-next' +import { ChevronDown, LogIn, LogOut, User, ArrowRightLeft } from 'lucide-vue-next' import { useRouter } from 'vue-router' import { convertToTitleCase } from '../utils' import { userStore } from '@/stores/user' diff --git a/frontend/src/main.js b/frontend/src/main.js index 2d192cbb..aabb38fb 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -8,11 +8,7 @@ import dayjs from '@/utils/dayjs' import { userStore } from './stores/user' import translationPlugin from './translation' import { initSocket } from './socket' -import { - setConfig, - frappeRequest, - pageMetaPlugin, -} from 'frappe-ui' +import { setConfig, frappeRequest, pageMetaPlugin } from 'frappe-ui' let pinia = createPinia() let app = createApp(App) diff --git a/frontend/src/pages/Home.vue b/frontend/src/pages/Home.vue index eb65c045..89eff8a0 100644 --- a/frontend/src/pages/Home.vue +++ b/frontend/src/pages/Home.vue @@ -1,39 +1,35 @@ diff --git a/frontend/src/pages/Inbox.vue b/frontend/src/pages/Inbox.vue index ec418b76..521c16d1 100644 --- a/frontend/src/pages/Inbox.vue +++ b/frontend/src/pages/Inbox.vue @@ -1,109 +1,127 @@ \ No newline at end of file + diff --git a/frontend/src/pages/Sent.vue b/frontend/src/pages/Sent.vue index 2bafea74..8d5a73f1 100644 --- a/frontend/src/pages/Sent.vue +++ b/frontend/src/pages/Sent.vue @@ -1,108 +1,127 @@ \ No newline at end of file + return [ + { + label: 'Sent', + route: { name: 'Sent' }, + }, + ] +}) + diff --git a/frontend/src/router.js b/frontend/src/router.js index 34cafc8d..83bdd9b6 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -3,39 +3,39 @@ import { userStore } from '@/stores/user' import { sessionStore } from '@/stores/session' const routes = [ - { - path: '/', - redirect: { - name: 'Inbox', - }, - }, - { - path: '/inbox', - name: 'Inbox', - component: () => import('@/pages/Inbox.vue'), - }, - { - path: '/sent', - name: 'Sent', - component: () => import('@/pages/Sent.vue'), - } + { + path: '/', + redirect: { + name: 'Inbox', + }, + }, + { + path: '/inbox', + name: 'Inbox', + component: () => import('@/pages/Inbox.vue'), + }, + { + path: '/sent', + name: 'Sent', + component: () => import('@/pages/Sent.vue'), + }, ] let router = createRouter({ - history: createWebHistory('/mail'), - routes, + history: createWebHistory('/mail'), + routes, }) router.beforeEach(async (to, from, next) => { - const { userResource } = userStore() - const { isLoggedIn } = sessionStore() + const { userResource } = userStore() + const { isLoggedIn } = sessionStore() - isLoggedIn && (await userResource.promise) + isLoggedIn && (await userResource.promise) - if (!isLoggedIn) { - window.location.href = '/login' - } else { - next() - } + if (!isLoggedIn) { + window.location.href = '/login' + } else { + next() + } }) export default router diff --git a/frontend/src/socket.js b/frontend/src/socket.js index ec346aab..5fff1423 100644 --- a/frontend/src/socket.js +++ b/frontend/src/socket.js @@ -4,25 +4,24 @@ import { getCachedListResource } from 'frappe-ui/src/resources/listResource' import { getCachedResource } from 'frappe-ui/src/resources/resources' export function initSocket() { - let host = window.location.hostname - let siteName = window.site_name || host - let port = window.location.port ? `:${socketio_port}` : '' - let protocol = port ? 'http' : 'https' - let url = `${protocol}://${host}${port}/${siteName}` + let host = window.location.hostname + let siteName = window.site_name || host + let port = window.location.port ? `:${socketio_port}` : '' + let protocol = port ? 'http' : 'https' + let url = `${protocol}://${host}${port}/${siteName}` - let socket = io(url, { - withCredentials: true, - reconnectionAttempts: 5, - }) - socket.on('refetch_resource', (data) => { - if (data.cache_key) { - let resource = - getCachedResource(data.cache_key) || - getCachedListResource(data.cache_key) - if (resource) { - resource.reload() - } - } - }) - return socket + let socket = io(url, { + withCredentials: true, + reconnectionAttempts: 5, + }) + socket.on('refetch_resource', (data) => { + if (data.cache_key) { + let resource = + getCachedResource(data.cache_key) || getCachedListResource(data.cache_key) + if (resource) { + resource.reload() + } + } + }) + return socket } diff --git a/frontend/src/stores/session.js b/frontend/src/stores/session.js index 4d89b33a..8c7c4253 100644 --- a/frontend/src/stores/session.js +++ b/frontend/src/stores/session.js @@ -5,58 +5,58 @@ import router from '@/router' import { ref, computed } from 'vue' export const sessionStore = defineStore('mail-session', () => { - let { userResource } = userStore() + let { userResource } = userStore() - function sessionUser() { - let cookies = new URLSearchParams(document.cookie.split('; ').join('&')) - let _sessionUser = cookies.get('user_id') - if (_sessionUser === 'Guest') { - _sessionUser = null - } - return _sessionUser - } + function sessionUser() { + let cookies = new URLSearchParams(document.cookie.split('; ').join('&')) + let _sessionUser = cookies.get('user_id') + if (_sessionUser === 'Guest') { + _sessionUser = null + } + return _sessionUser + } - let user = ref(sessionUser()) - const isLoggedIn = computed(() => !!user.value) + let user = ref(sessionUser()) + const isLoggedIn = computed(() => !!user.value) - const login = createResource({ - url: 'login', - onError() { - throw new Error('Invalid email or password') - }, - onSuccess() { - userResource.reload() - user.value = sessionUser() - login.reset() - router.replace({ path: '/' }) - }, - }) + const login = createResource({ + url: 'login', + onError() { + throw new Error('Invalid email or password') + }, + onSuccess() { + userResource.reload() + user.value = sessionUser() + login.reset() + router.replace({ path: '/' }) + }, + }) - const logout = createResource({ - url: 'logout', - onSuccess() { - sessionStorage.removeItem("currentIncomingMail") - sessionStorage.removeItem("currentOutgoingMail") - userResource.reset() - user.value = null - window.location.reload() - }, - }) + const logout = createResource({ + url: 'logout', + onSuccess() { + sessionStorage.removeItem('currentIncomingMail') + sessionStorage.removeItem('currentOutgoingMail') + userResource.reset() + user.value = null + window.location.reload() + }, + }) - const branding = createResource({ - url: 'mail.api.mail.get_branding', - cache: 'brand', - auto: true, - onSuccess(data) { - document.querySelector("link[rel='icon']").href = data.favicon - }, - }) + const branding = createResource({ + url: 'mail.api.mail.get_branding', + cache: 'brand', + auto: true, + onSuccess(data) { + document.querySelector("link[rel='icon']").href = data.favicon + }, + }) - return { - user, - isLoggedIn, - login, - logout, - branding, - } + return { + user, + isLoggedIn, + login, + logout, + branding, + } }) diff --git a/frontend/src/stores/user.js b/frontend/src/stores/user.js index d850e845..f38e73b8 100644 --- a/frontend/src/stores/user.js +++ b/frontend/src/stores/user.js @@ -1,18 +1,19 @@ +import router from '@/router' import { defineStore } from 'pinia' import { createResource } from 'frappe-ui' export const userStore = defineStore('mail-users', () => { - let userResource = createResource({ - url: 'mail.api.mail.get_user_info', - onError(error) { - if (error && error.exc_type === 'AuthenticationError') { - router.push('/login') - } - }, - auto: true - }) + let userResource = createResource({ + url: 'mail.api.mail.get_user_info', + onError(error) { + if (error && error.exc_type === 'AuthenticationError') { + router.push('/login') + } + }, + auto: true, + }) - return { - userResource, - } + return { + userResource, + } }) diff --git a/frontend/src/translation.js b/frontend/src/translation.js index 1796e064..2ed3e8b0 100644 --- a/frontend/src/translation.js +++ b/frontend/src/translation.js @@ -1,40 +1,35 @@ import { createResource } from 'frappe-ui' export default function translationPlugin(app) { - app.config.globalProperties.__ = translate - window.__ = translate - if (!window.translatedMessages) fetchTranslations() + app.config.globalProperties.__ = translate + window.__ = translate + if (!window.translatedMessages) fetchTranslations() } function translate(message) { - let translatedMessages = window.translatedMessages || {} - let translatedMessage = translatedMessages[message] || message + let translatedMessages = window.translatedMessages || {} + let translatedMessage = translatedMessages[message] || message - const hasPlaceholders = /{\d+}/.test(message) - if (!hasPlaceholders) { - return translatedMessage - } - return { - format: function (...args) { - return translatedMessage.replace( - /{(\d+)}/g, - function (match, number) { - return typeof args[number] != 'undefined' - ? args[number] - : match - } - ) - }, - } + const hasPlaceholders = /{\d+}/.test(message) + if (!hasPlaceholders) { + return translatedMessage + } + return { + format: function (...args) { + return translatedMessage.replace(/{(\d+)}/g, function (match, number) { + return typeof args[number] != 'undefined' ? args[number] : match + }) + }, + } } function fetchTranslations(lang) { - createResource({ - url: 'mail.api.mail.get_translations', - cache: 'translations', - auto: true, - transform: (data) => { - window.translatedMessages = data - }, - }) + createResource({ + url: 'mail.api.mail.get_translations', + cache: 'translations', + auto: true, + transform: (data) => { + window.translatedMessages = data + }, + }) } diff --git a/frontend/src/utils/composables.js b/frontend/src/utils/composables.js index c53346b5..4ef97408 100644 --- a/frontend/src/utils/composables.js +++ b/frontend/src/utils/composables.js @@ -1,100 +1,100 @@ import { onMounted, onUnmounted, reactive, ref, watch } from 'vue' export function useScreenSize() { - const size = reactive({ - width: window.innerWidth, - height: window.innerHeight, - }) + const size = reactive({ + width: window.innerWidth, + height: window.innerHeight, + }) - const onResize = () => { - size.width = window.innerWidth - size.height = window.innerHeight - } + const onResize = () => { + size.width = window.innerWidth + size.height = window.innerHeight + } - onMounted(() => { - window.addEventListener('resize', onResize) - }) + onMounted(() => { + window.addEventListener('resize', onResize) + }) - onUnmounted(() => { - window.removeEventListener('resize', onResize) - }) + onUnmounted(() => { + window.removeEventListener('resize', onResize) + }) - return size + return size } // write a composable for detecting swipe gestures in mobile devices export function useSwipe() { - const swipe = reactive({ - initialX: null, - initialY: null, - currentX: null, - currentY: null, - diffX: null, - diffY: null, - absDiffX: null, - absDiffY: null, - direction: null, - }) + const swipe = reactive({ + initialX: null, + initialY: null, + currentX: null, + currentY: null, + diffX: null, + diffY: null, + absDiffX: null, + absDiffY: null, + direction: null, + }) - const onTouchStart = (e) => { - swipe.initialX = e.touches[0].clientX - swipe.initialY = e.touches[0].clientY - swipe.direction = null - swipe.diffX = null - swipe.diffY = null - swipe.absDiffX = null - swipe.absDiffY = null - } + const onTouchStart = (e) => { + swipe.initialX = e.touches[0].clientX + swipe.initialY = e.touches[0].clientY + swipe.direction = null + swipe.diffX = null + swipe.diffY = null + swipe.absDiffX = null + swipe.absDiffY = null + } - const onTouchMove = (e) => { - swipe.currentX = e.touches[0].clientX - swipe.currentY = e.touches[0].clientY + const onTouchMove = (e) => { + swipe.currentX = e.touches[0].clientX + swipe.currentY = e.touches[0].clientY - swipe.diffX = swipe.initialX - swipe.currentX - swipe.diffY = swipe.initialY - swipe.currentY + swipe.diffX = swipe.initialX - swipe.currentX + swipe.diffY = swipe.initialY - swipe.currentY - swipe.absDiffX = Math.abs(swipe.diffX) - swipe.absDiffY = Math.abs(swipe.diffY) - } + swipe.absDiffX = Math.abs(swipe.diffX) + swipe.absDiffY = Math.abs(swipe.diffY) + } - const onTouchEnd = (e) => { - let { diffX, diffY, absDiffX, absDiffY } = swipe - if (absDiffX > absDiffY) { - if (diffX > 0) { - swipe.direction = 'left' - } else { - swipe.direction = 'right' - } - } else { - if (diffY > 0) { - swipe.direction = 'up' - } else { - swipe.direction = 'down' - } - } - } + const onTouchEnd = (e) => { + let { diffX, diffY, absDiffX, absDiffY } = swipe + if (absDiffX > absDiffY) { + if (diffX > 0) { + swipe.direction = 'left' + } else { + swipe.direction = 'right' + } + } else { + if (diffY > 0) { + swipe.direction = 'up' + } else { + swipe.direction = 'down' + } + } + } - onMounted(() => { - window.addEventListener('touchstart', onTouchStart) - window.addEventListener('touchend', onTouchEnd) - window.addEventListener('touchmove', onTouchMove) - }) + onMounted(() => { + window.addEventListener('touchstart', onTouchStart) + window.addEventListener('touchend', onTouchEnd) + window.addEventListener('touchmove', onTouchMove) + }) - onUnmounted(() => { - window.removeEventListener('touchstart', onTouchStart) - window.removeEventListener('touchend', onTouchEnd) - window.removeEventListener('touchmove', onTouchMove) - }) + onUnmounted(() => { + window.removeEventListener('touchstart', onTouchStart) + window.removeEventListener('touchend', onTouchEnd) + window.removeEventListener('touchmove', onTouchMove) + }) - return swipe + return swipe } export function useLocalStorage(key, initialValue) { - let value = ref(null) - let storedValue = localStorage.getItem(key) - value.value = storedValue ? JSON.parse(storedValue) : initialValue + let value = ref(null) + let storedValue = localStorage.getItem(key) + value.value = storedValue ? JSON.parse(storedValue) : initialValue - watch(value, (newValue) => { - localStorage.setItem(key, JSON.stringify(newValue)) - }) - return value + watch(value, (newValue) => { + localStorage.setItem(key, JSON.stringify(newValue)) + }) + return value } diff --git a/frontend/src/utils/dialogs.js b/frontend/src/utils/dialogs.js index 922e6a27..24e1b4ea 100644 --- a/frontend/src/utils/dialogs.js +++ b/frontend/src/utils/dialogs.js @@ -4,32 +4,28 @@ import { h, reactive, ref } from 'vue' let dialogs = ref([]) export let Dialogs = { - name: 'Dialogs', - render() { - return dialogs.value.map((dialog) => { - return h( - Dialog, - { - options: dialog, - modelValue: dialog.show, - 'onUpdate:modelValue': (val) => (dialog.show = val), - }, - () => [ - h( - 'p', - { class: 'text-p-base text-gray-700' }, - dialog.message - ), - h(ErrorMessage, { class: 'mt-2', message: dialog.error }), - ] - ) - }) - }, + name: 'Dialogs', + render() { + return dialogs.value.map((dialog) => { + return h( + Dialog, + { + options: dialog, + modelValue: dialog.show, + 'onUpdate:modelValue': (val) => (dialog.show = val), + }, + () => [ + h('p', { class: 'text-p-base text-gray-700' }, dialog.message), + h(ErrorMessage, { class: 'mt-2', message: dialog.error }), + ] + ) + }) + }, } export function createDialog(options) { - let dialog = reactive(options) - dialog.key = `dialog-${Math.random().toString(36).slice(2, 9)}` - dialogs.value.push(dialog) - dialog.show = true + let dialog = reactive(options) + dialog.key = `dialog-${Math.random().toString(36).slice(2, 9)}` + dialogs.value.push(dialog) + dialog.show = true } diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js index 1e51c709..dd68433b 100644 --- a/frontend/src/utils/index.js +++ b/frontend/src/utils/index.js @@ -1,66 +1,65 @@ import { useTimeAgo } from '@vueuse/core' export function convertToTitleCase(str) { - if (!str) { - return '' - } + if (!str) { + return '' + } - return str - .toLowerCase() - .split(' ') - .map(function (word) { - return word.charAt(0).toUpperCase().concat(word.substr(1)) - }) - .join(' ') + return str + .toLowerCase() + .split(' ') + .map(function (word) { + return word.charAt(0).toUpperCase().concat(word.substr(1)) + }) + .join(' ') } export function getSidebarLinks() { - return [ - { - label: 'Inbox', - icon: 'Inbox', - to: 'Inbox', - activeFor: ['Inbox'], - }, - { - label: 'Sent', - icon: 'Send', - to: 'Sent', - activeFor: ['Sent'], - }, - ] + return [ + { + label: 'Inbox', + icon: 'Inbox', + to: 'Inbox', + activeFor: ['Inbox'], + }, + { + label: 'Sent', + icon: 'Send', + to: 'Sent', + activeFor: ['Sent'], + }, + ] } export function formatNumber(number) { - return number.toLocaleString('en-IN', { - maximumFractionDigits: 0, - }) + return number.toLocaleString('en-IN', { + maximumFractionDigits: 0, + }) } -export function startResizing (event) { - const startX = event.clientX; - const sidebar = document.getElementsByClassName("mailSidebar")[0]; - const startWidth = sidebar.offsetWidth; +export function startResizing(event) { + const startX = event.clientX + const sidebar = document.getElementsByClassName('mailSidebar')[0] + const startWidth = sidebar.offsetWidth - const onMouseMove = (event) => { - const diff = event.clientX - startX; - let newWidth = startWidth + diff; - if (newWidth < 200) { - newWidth = 200; - } - sidebar.style.width = newWidth + "px"; - } + const onMouseMove = (event) => { + const diff = event.clientX - startX + let newWidth = startWidth + diff + if (newWidth < 200) { + newWidth = 200 + } + sidebar.style.width = newWidth + 'px' + } - const onMouseUp = () => { - document.removeEventListener("mousemove", onMouseMove); - document.removeEventListener("mouseup", onMouseUp); - } + const onMouseUp = () => { + document.removeEventListener('mousemove', onMouseMove) + document.removeEventListener('mouseup', onMouseUp) + } - document.addEventListener("mousemove", onMouseMove); - document.addEventListener("mouseup", onMouseUp); + document.addEventListener('mousemove', onMouseMove) + document.addEventListener('mouseup', onMouseUp) } - export function singularize(word) { const endings = { ves: 'fe', @@ -71,18 +70,15 @@ export function singularize(word) { es: 'e', s: '', } - return word.replace( - new RegExp(`(${Object.keys(endings).join('|')})$`), - (r) => endings[r] - ) + return word.replace(new RegExp(`(${Object.keys(endings).join('|')})$`), (r) => endings[r]) } export function timeAgo(date) { - return useTimeAgo(date).value + return useTimeAgo(date).value } export function validateEmail(email) { - let regExp = - /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - return regExp.test(email) -} \ No newline at end of file + let regExp = + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + return regExp.test(email) +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 1097dedd..42f3f334 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,18 +1,16 @@ module.exports = { - presets: [ - require('frappe-ui/src/utils/tailwind.config') - ], - content: [ - "./index.html", - "./src/**/*.{vue,js,ts,jsx,tsx}", - "./node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}", - ], - theme: { - extend: { - strokeWidth: { - 1.5: '1.5', - }, - }, - }, - plugins: [], + presets: [require('frappe-ui/src/utils/tailwind.config')], + content: [ + './index.html', + './src/**/*.{vue,js,ts,jsx,tsx}', + './node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}', + ], + theme: { + extend: { + strokeWidth: { + 1.5: '1.5', + }, + }, + }, + plugins: [], } diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 9b05fc00..eceeabcc 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -5,37 +5,43 @@ import frappeui from 'frappe-ui/vite' // https://vitejs.dev/config/ export default defineConfig({ - plugins: [ - frappeui(), - vue({ - script: { - defineModel: true, - propsDestructure: true, - }, - }), - ], - resolve: { - alias: { - '@': path.resolve(__dirname, 'src'), - }, - }, - build: { - outDir: `../mail/public/frontend`, - emptyOutDir: true, - commonjsOptions: { - include: [/tailwind.config.js/, /node_modules/], - }, - sourcemap: true, - target: 'es2015', - rollupOptions: { - output: { - manualChunks: { - 'frappe-ui': ['frappe-ui'], - }, - }, - }, - }, - optimizeDeps: { - include: ['frappe-ui > feather-icons', 'showdown', 'engine.io-client', 'prosemirror-state', 'prosemirror-view'], - }, + plugins: [ + frappeui(), + vue({ + script: { + defineModel: true, + propsDestructure: true, + }, + }), + ], + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + }, + }, + build: { + outDir: `../mail/public/frontend`, + emptyOutDir: true, + commonjsOptions: { + include: [/tailwind.config.js/, /node_modules/], + }, + sourcemap: true, + target: 'es2015', + rollupOptions: { + output: { + manualChunks: { + 'frappe-ui': ['frappe-ui'], + }, + }, + }, + }, + optimizeDeps: { + include: [ + 'frappe-ui > feather-icons', + 'showdown', + 'engine.io-client', + 'prosemirror-state', + 'prosemirror-view', + ], + }, }) diff --git a/mail/api/auth.py b/mail/api/auth.py index 3b7ba29b..fe297963 100644 --- a/mail/api/auth.py +++ b/mail/api/auth.py @@ -1,5 +1,6 @@ import frappe from frappe import _ + from mail.utils.user import has_role, is_mailbox_owner from mail.utils.validation import ( validate_mailbox_for_incoming, @@ -8,9 +9,7 @@ @frappe.whitelist(methods=["POST"]) -def validate( - mailbox: str | None = None, for_inbound: bool = False, for_outbound: bool = False -) -> None: +def validate(mailbox: str | None = None, for_inbound: bool = False, for_outbound: bool = False) -> None: """Validates the mailbox for inbound and outbound emails.""" if mailbox: @@ -30,9 +29,7 @@ def validate_user() -> None: user = frappe.session.user if not has_role(user, "Mailbox User"): - frappe.throw( - _("User {0} is not allowed to access mailboxes.").format(frappe.bold(user)) - ) + frappe.throw(_("User {0} is not allowed to access mailboxes.").format(frappe.bold(user))) def validate_mailbox(mailbox: str) -> None: @@ -42,7 +39,5 @@ def validate_mailbox(mailbox: str) -> None: if not is_mailbox_owner(mailbox, user): frappe.throw( - _("Mailbox {0} is not associated with user {1}").format( - frappe.bold(mailbox), frappe.bold(user) - ) + _("Mailbox {0} is not associated with user {1}").format(frappe.bold(mailbox), frappe.bold(user)) ) diff --git a/mail/api/inbound.py b/mail/api/inbound.py index a5690f57..dd008480 100644 --- a/mail/api/inbound.py +++ b/mail/api/inbound.py @@ -1,14 +1,16 @@ -import pytz -import frappe -from frappe import _ from datetime import datetime -from typing import TYPE_CHECKING from email.utils import formataddr +from typing import TYPE_CHECKING + +import frappe +import pytz +from frappe import _ +from frappe.utils import cint, convert_utc_to_system_timezone, now + +from mail.api.auth import validate_mailbox, validate_user +from mail.mail.doctype.mail_sync_history.mail_sync_history import get_mail_sync_history from mail.utils import convert_to_utc -from mail.api.auth import validate_user, validate_mailbox from mail.utils.validation import validate_mailbox_for_incoming -from frappe.utils import now, cint, convert_utc_to_system_timezone -from mail.mail.doctype.mail_sync_history.mail_sync_history import get_mail_sync_history if TYPE_CHECKING: from mail.mail.doctype.mail_sync_history.mail_sync_history import MailSyncHistory @@ -31,12 +33,8 @@ def pull( source = get_source() last_synced_at = convert_to_system_timezone(last_synced_at) sync_history = get_mail_sync_history(source, frappe.session.user, mailbox) - result = get_incoming_mails( - mailbox, limit, last_synced_at or sync_history.last_synced_at - ) - update_mail_sync_history( - sync_history, result["last_synced_at"], result["last_synced_mail"] - ) + result = get_incoming_mails(mailbox, limit, last_synced_at or sync_history.last_synced_at) + update_mail_sync_history(sync_history, result["last_synced_at"], result["last_synced_mail"]) result["last_synced_at"] = convert_to_utc(result["last_synced_at"]) return result @@ -59,12 +57,8 @@ def pull_raw( source = get_source() last_synced_at = convert_to_system_timezone(last_synced_at) sync_history = get_mail_sync_history(source, frappe.session.user, mailbox) - result = get_raw_incoming_mails( - mailbox, limit, last_synced_at or sync_history.last_synced_at - ) - update_mail_sync_history( - sync_history, result["last_synced_at"], result["last_synced_mail"] - ) + result = get_raw_incoming_mails(mailbox, limit, last_synced_at or sync_history.last_synced_at) + update_mail_sync_history(sync_history, result["last_synced_at"], result["last_synced_mail"]) result["last_synced_at"] = convert_to_utc(result["last_synced_at"]) return result @@ -73,9 +67,7 @@ def pull_raw( def validate_max_sync_limit(limit: int) -> None: """Validates if the limit is within the maximum limit set in the Mail Settings.""" - max_sync_limit = cint( - frappe.db.get_single_value("Mail Settings", "max_sync_via_api", cache=True) - ) + max_sync_limit = cint(frappe.db.get_single_value("Mail Settings", "max_sync_via_api", cache=True)) if limit > max_sync_limit: frappe.throw(_("Cannot fetch more than {0} emails at a time.").format(max_sync_limit)) diff --git a/mail/api/mail.py b/mail/api/mail.py index c3569bee..da5d3051 100644 --- a/mail/api/mail.py +++ b/mail/api/mail.py @@ -1,8 +1,9 @@ import re + import frappe from bs4 import BeautifulSoup -from frappe.utils import is_html from frappe.translate import get_all_translations +from frappe.utils import is_html @frappe.whitelist(allow_guest=True) @@ -139,9 +140,7 @@ def add_to_thread(mail): thread.append(mail) if mail.in_reply_to_mail_name: - reply_mail = get_mail_details( - mail.in_reply_to_mail_name, mail.in_reply_to_mail_type, False - ) + reply_mail = get_mail_details(mail.in_reply_to_mail_name, mail.in_reply_to_mail_type, False) add_to_thread(reply_mail) if mail.message_id: replica_name = find_replica(mail, mail.mail_type) @@ -197,9 +196,7 @@ def get_thread(mail, thread): visited.add(mail.name) if mail.in_reply_to_mail_name: - reply_mail = get_mail_details( - mail.in_reply_to_mail_name, mail.in_reply_to_mail_type, True - ) + reply_mail = get_mail_details(mail.in_reply_to_mail_name, mail.in_reply_to_mail_type, True) get_thread(reply_mail, thread) replica = find_replica(mail, mail.mail_type) @@ -262,9 +259,7 @@ def remove_duplicates_and_sort(thread) -> list: seen = set() thread = [x for x in thread if x["name"] not in seen and not seen.add(x["name"])] - thread = [ - x for x in thread if x["message_id"] not in seen and not seen.add(x["message_id"]) - ] + thread = [x for x in thread if x["message_id"] not in seen and not seen.add(x["message_id"])] thread.sort(key=lambda x: x.creation) return thread diff --git a/mail/api/outbound.py b/mail/api/outbound.py index f92e399d..32436d80 100644 --- a/mail/api/outbound.py +++ b/mail/api/outbound.py @@ -1,7 +1,9 @@ import json +from email.utils import parseaddr + import frappe from frappe import _ -from email.utils import parseaddr + from mail.mail.doctype.outgoing_mail.outgoing_mail import create_outgoing_mail diff --git a/mail/api/webhook.py b/mail/api/webhook.py index be044de5..1ecd3a95 100644 --- a/mail/api/webhook.py +++ b/mail/api/webhook.py @@ -1,7 +1,9 @@ import json + import frappe from frappe import _ -from frappe.utils import get_datetime, convert_utc_to_system_timezone +from frappe.utils import convert_utc_to_system_timezone, get_datetime + from mail.mail.doctype.incoming_mail.incoming_mail import process_incoming_mail @@ -15,9 +17,7 @@ def update_delivery_status() -> None: doc._update_delivery_status(data, notify_update=True) except Exception: error_log = frappe.get_traceback(with_context=False) - frappe.log_error( - title=f"Update Delivery Status - {data['outgoing_mail']}", message=error_log - ) + frappe.log_error(title=f"Update Delivery Status - {data['outgoing_mail']}", message=error_log) @frappe.whitelist(methods=["POST"], allow_guest=True) @@ -40,9 +40,7 @@ def receive_email() -> None: is_spam=data["is_spam"], ) last_synced_at = convert_utc_to_system_timezone(get_datetime(data["processed_at"])) - frappe.db.set_single_value( - "Mail Settings", "last_synced_at", last_synced_at, update_modified=False - ) + frappe.db.set_single_value("Mail Settings", "last_synced_at", last_synced_at, update_modified=False) except Exception: error_log = frappe.get_traceback(with_context=False) frappe.log_error(title=f"Receive Email - {data['domain_name']}", message=error_log) diff --git a/mail/mail/doctype/incoming_mail/incoming_mail.js b/mail/mail/doctype/incoming_mail/incoming_mail.js index cea508f0..4b51e824 100644 --- a/mail/mail/doctype/incoming_mail/incoming_mail.js +++ b/mail/mail/doctype/incoming_mail/incoming_mail.js @@ -3,37 +3,45 @@ frappe.ui.form.on("Incoming Mail", { refresh(frm) { - frm.trigger("add_actions"); + frm.trigger("add_actions"); }, - add_actions(frm) { - if (frm.doc.docstatus === 1) { - frm.add_custom_button(__("Reply"), () => { - frm.trigger("reply"); - }, __("Actions")); - frm.add_custom_button(__("Reply All"), () => { - frm.trigger("reply_all"); - }, __("Actions")); - } - }, + add_actions(frm) { + if (frm.doc.docstatus === 1) { + frm.add_custom_button( + __("Reply"), + () => { + frm.trigger("reply"); + }, + __("Actions") + ); + frm.add_custom_button( + __("Reply All"), + () => { + frm.trigger("reply_all"); + }, + __("Actions") + ); + } + }, - reply(frm) { - frappe.model.open_mapped_doc({ + reply(frm) { + frappe.model.open_mapped_doc({ method: "mail.mail.doctype.incoming_mail.incoming_mail.reply_to_mail", frm: frm, - args: { - all: false, - }, + args: { + all: false, + }, }); - }, + }, - reply_all(frm) { - frappe.model.open_mapped_doc({ + reply_all(frm) { + frappe.model.open_mapped_doc({ method: "mail.mail.doctype.incoming_mail.incoming_mail.reply_to_mail", frm: frm, - args: { - all: true, - }, + args: { + all: true, + }, }); - } + }, }); diff --git a/mail/mail/doctype/incoming_mail/incoming_mail.py b/mail/mail/doctype/incoming_mail/incoming_mail.py index 908c722c..e08fb7c0 100644 --- a/mail/mail/doctype/incoming_mail/incoming_mail.py +++ b/mail/mail/doctype/incoming_mail/incoming_mail.py @@ -2,22 +2,24 @@ # For license information, please see license.txt import time +from email.utils import parseaddr +from typing import TYPE_CHECKING + import frappe from frappe import _ -from uuid_utils import uuid7 -from typing import TYPE_CHECKING -from email.utils import parseaddr -from frappe.query_builder import Interval from frappe.model.document import Document +from frappe.query_builder import Interval from frappe.query_builder.functions import Now from frappe.utils import now, time_diff_in_seconds -from mail.utils.cache import get_postmaster_for_domain -from mail.mail_server import get_mail_server_inbound_api -from mail.utils.email_parser import EmailParser, extract_ip_and_host +from uuid_utils import uuid7 + from mail.mail.doctype.mail_contact.mail_contact import create_mail_contact from mail.mail.doctype.outgoing_mail.outgoing_mail import create_outgoing_mail -from mail.utils.user import is_mailbox_owner, is_system_manager, get_user_mailboxes -from mail.utils import parse_iso_datetime, get_in_reply_to_mail, add_or_update_tzinfo +from mail.mail_server import get_mail_server_inbound_api +from mail.utils import add_or_update_tzinfo, get_in_reply_to_mail, parse_iso_datetime +from mail.utils.cache import get_postmaster_for_domain +from mail.utils.email_parser import EmailParser, extract_ip_and_host +from mail.utils.user import get_user_mailboxes, is_mailbox_owner, is_system_manager if TYPE_CHECKING: from mail.mail.doctype.outgoing_mail.outgoing_mail import OutgoingMail @@ -25,7 +27,6 @@ class IncomingMail(Document): def autoname(self) -> None: - self.name = str(uuid7()) def validate(self) -> None: @@ -72,9 +73,7 @@ def process(self) -> None: self.from_ip, self.from_host = extract_ip_and_host(parser.get_header("Received")) self.received_at = parse_iso_datetime(parser.get_header("Received-At")) self.in_reply_to = parser.get_in_reply_to() - self.in_reply_to_mail_type, self.in_reply_to_mail_name = get_in_reply_to_mail( - self.in_reply_to - ) + self.in_reply_to_mail_type, self.in_reply_to_mail_name = get_in_reply_to_mail(self.in_reply_to) parser.save_attachments(self.doctype, self.name, is_private=True) self.body_html, self.body_plain = parser.get_body() @@ -120,7 +119,6 @@ def create_mail_contact(self) -> None: """Creates the mail contact.""" if frappe.get_cached_value("Mailbox", self.receiver, "create_mail_contact"): - user = frappe.get_cached_value("Mailbox", self.receiver, "user") create_mail_contact(user, self.sender, self.display_name) @@ -204,9 +202,7 @@ def reply_to_mail(source_name, target_doc=None) -> "OutgoingMail": def delete_rejected_mails() -> None: """Called by the scheduler to delete the rejected mails based on the retention.""" - retention_days = frappe.db.get_single_value( - "Mail Settings", "rejected_mail_retention", cache=True - ) + retention_days = frappe.db.get_single_value("Mail Settings", "rejected_mail_retention", cache=True) IM = frappe.qb.DocType("Incoming Mail") ( frappe.qb.from_(IM) @@ -288,9 +284,7 @@ def process_incoming_mail(incoming_mail_log: str, message: str, is_spam: bool) - def is_active_domain(domain_name: str) -> bool: """Returns True if the domain is active, otherwise False.""" - return bool( - frappe.db.exists("Mail Domain", {"domain_name": domain_name, "enabled": 1}) - ) + return bool(frappe.db.exists("Mail Domain", {"domain_name": domain_name, "enabled": 1})) def is_mail_alias(alias: str) -> bool: """Returns True if the mail alias exists, otherwise False.""" diff --git a/mail/mail/doctype/incoming_mail/incoming_mail_list.js b/mail/mail/doctype/incoming_mail/incoming_mail_list.js index 4b2e040d..59cea55b 100644 --- a/mail/mail/doctype/incoming_mail/incoming_mail_list.js +++ b/mail/mail/doctype/incoming_mail/incoming_mail_list.js @@ -5,15 +5,15 @@ frappe.listview_settings["Incoming Mail"] = { refresh: (listview) => { listview.page.add_inner_button("Fetch Emails", () => { fetch_emails_from_mail_server(listview); - });; + }); }, get_indicator: (doc) => { const status_colors = { - "Draft": "grey", - "Rejected": "red", - "Accepted": "green", - "Cancelled": "red", + Draft: "grey", + Rejected: "red", + Accepted: "green", + Cancelled: "red", }; return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; }, @@ -29,6 +29,6 @@ function fetch_emails_from_mail_server(listview) { message: __("{0} job has been created.", [__("Fetch Emails").bold()]), indicator: "green", }); - } + }, }); -} \ No newline at end of file +} diff --git a/mail/mail/doctype/mail_alias/mail_alias.js b/mail/mail/doctype/mail_alias/mail_alias.js index b5fa1bd4..2e53d535 100644 --- a/mail/mail/doctype/mail_alias/mail_alias.js +++ b/mail/mail/doctype/mail_alias/mail_alias.js @@ -3,21 +3,21 @@ frappe.ui.form.on("Mail Alias", { setup(frm) { - frm.trigger("set_queries"); - }, + frm.trigger("set_queries"); + }, - set_queries(frm) { + set_queries(frm) { frm.set_query("domain_name", () => ({ - filters: { - "enabled": 1, - "is_verified": 1, - } - })); + filters: { + enabled: 1, + is_verified: 1, + }, + })); - frm.set_query("mailbox", "mailboxes", (doc) => { + frm.set_query("mailbox", "mailboxes", (doc) => { let filters = { - "domain_name": doc.domain_name || " ", - "incoming": 1, + domain_name: doc.domain_name || " ", + incoming: 1, }; let selected_mailboxes = frm.doc.mailboxes.map((d) => d.mailbox); @@ -29,7 +29,5 @@ frappe.ui.form.on("Mail Alias", { filters: filters, }; }); - }, + }, }); - - diff --git a/mail/mail/doctype/mail_alias/mail_alias.py b/mail/mail/doctype/mail_alias/mail_alias.py index 527d6334..bd0637c2 100644 --- a/mail/mail/doctype/mail_alias/mail_alias.py +++ b/mail/mail/doctype/mail_alias/mail_alias.py @@ -4,9 +4,10 @@ import frappe from frappe import _ from frappe.model.document import Document + from mail.utils.validation import ( - validate_domain_is_enabled_and_verified, is_valid_email_for_domain, + validate_domain_is_enabled_and_verified, validate_mailbox_for_incoming, ) @@ -38,14 +39,10 @@ def validate_mailboxes(self) -> None: for mailbox in self.mailboxes: if mailbox.mailbox == self.alias: - frappe.throw( - _("Row #{0}: Mailbox cannot be the same as the alias.").format(mailbox.idx) - ) + frappe.throw(_("Row #{0}: Mailbox cannot be the same as the alias.").format(mailbox.idx)) elif mailbox.mailbox in mailboxes: frappe.throw( - _("Row #{0}: Duplicate mailbox {1}.").format( - mailbox.idx, frappe.bold(mailbox.mailbox) - ) + _("Row #{0}: Duplicate mailbox {1}.").format(mailbox.idx, frappe.bold(mailbox.mailbox)) ) validate_mailbox_for_incoming(mailbox.mailbox) diff --git a/mail/mail/doctype/mail_alias_mailbox/mail_alias_mailbox.py b/mail/mail/doctype/mail_alias_mailbox/mail_alias_mailbox.py index 93d25c74..116d988b 100644 --- a/mail/mail/doctype/mail_alias_mailbox/mail_alias_mailbox.py +++ b/mail/mail/doctype/mail_alias_mailbox/mail_alias_mailbox.py @@ -10,6 +10,4 @@ class MailAliasMailbox(Document): def on_doctype_update() -> None: - frappe.db.add_unique( - "Mail Alias Mailbox", ["parent", "mailbox"], constraint_name="unique_parent_mailbox" - ) + frappe.db.add_unique("Mail Alias Mailbox", ["parent", "mailbox"], constraint_name="unique_parent_mailbox") diff --git a/mail/mail/doctype/mail_contact/mail_contact.js b/mail/mail/doctype/mail_contact/mail_contact.js index 34ab810d..83c63ff1 100644 --- a/mail/mail/doctype/mail_contact/mail_contact.js +++ b/mail/mail/doctype/mail_contact/mail_contact.js @@ -3,12 +3,12 @@ frappe.ui.form.on("Mail Contact", { refresh(frm) { - frm.trigger("set_user"); + frm.trigger("set_user"); }, - set_user(frm) { - if (frm.doc.__islocal && !frm.doc.user && frappe.session.user) { - frm.set_value("user", frappe.session.user); - } - }, + set_user(frm) { + if (frm.doc.__islocal && !frm.doc.user && frappe.session.user) { + frm.set_value("user", frappe.session.user); + } + }, }); diff --git a/mail/mail/doctype/mail_contact/mail_contact.py b/mail/mail/doctype/mail_contact/mail_contact.py index f43c1984..b35fdd4f 100644 --- a/mail/mail/doctype/mail_contact/mail_contact.py +++ b/mail/mail/doctype/mail_contact/mail_contact.py @@ -4,6 +4,7 @@ import frappe from frappe import _ from frappe.model.document import Document + from mail.utils.user import is_system_manager @@ -26,18 +27,14 @@ def validate_duplicate_contact(self) -> None: """Validates if the contact is duplicate.""" if frappe.db.exists("Mail Contact", {"user": self.user, "email": self.email}): - frappe.throw( - _("Mail Contact with email {0} already exists.").format(frappe.bold(self.email)) - ) + frappe.throw(_("Mail Contact with email {0} already exists.").format(frappe.bold(self.email))) def create_mail_contact(user: str, email: str, display_name: str | None = None) -> None: """Creates the mail contact.""" if mail_contact := frappe.db.exists("Mail Contact", {"user": user, "email": email}): - current_display_name = frappe.get_cached_value( - "Mail Contact", mail_contact, "display_name" - ) + current_display_name = frappe.get_cached_value("Mail Contact", mail_contact, "display_name") if display_name != current_display_name: frappe.db.set_value("Mail Contact", mail_contact, "display_name", display_name) else: @@ -66,6 +63,4 @@ def get_permission_query_condition(user: str | None = None) -> str: def on_doctype_update() -> None: - frappe.db.add_unique( - "Mail Contact", ["user", "email"], constraint_name="unique_user_email" - ) + frappe.db.add_unique("Mail Contact", ["user", "email"], constraint_name="unique_user_email") diff --git a/mail/mail/doctype/mail_domain/mail_domain.js b/mail/mail/doctype/mail_domain/mail_domain.js index de4fb872..558e60f5 100644 --- a/mail/mail/doctype/mail_domain/mail_domain.js +++ b/mail/mail/doctype/mail_domain/mail_domain.js @@ -3,55 +3,62 @@ frappe.ui.form.on("Mail Domain", { refresh(frm) { - frm.trigger("add_actions"); + frm.trigger("add_actions"); }, - add_actions(frm) { - if (!frm.doc.__islocal) { - frm.add_custom_button(__("Verify DNS Records"), () => { - frm.trigger("verify_dns_records"); - }, __("Actions")); + add_actions(frm) { + if (!frm.doc.__islocal) { + frm.add_custom_button( + __("Verify DNS Records"), + () => { + frm.trigger("verify_dns_records"); + }, + __("Actions") + ); - frm.add_custom_button(__("Refresh DNS Records"), () => { - frappe.confirm( - __("Are you certain you wish to proceed?"), - () => frm.trigger("refresh_dns_records") - ) - }, __("Actions")); - } - }, + frm.add_custom_button( + __("Refresh DNS Records"), + () => { + frappe.confirm(__("Are you certain you wish to proceed?"), () => + frm.trigger("refresh_dns_records") + ); + }, + __("Actions") + ); + } + }, - verify_dns_records(frm) { - frappe.call({ - doc: frm.doc, - method: "verify_dns_records", - args: { - save: true, - }, - freeze: true, - freeze_message: __("Verifying DNS Records..."), - callback: (r) => { - if (!r.exc) { - frm.refresh(); - } - } - }); - }, + verify_dns_records(frm) { + frappe.call({ + doc: frm.doc, + method: "verify_dns_records", + args: { + save: true, + }, + freeze: true, + freeze_message: __("Verifying DNS Records..."), + callback: (r) => { + if (!r.exc) { + frm.refresh(); + } + }, + }); + }, - refresh_dns_records(frm) { - frappe.call({ - doc: frm.doc, - method: "refresh_dns_records", - args: { - save: true, - }, - freeze: true, - freeze_message: __("Refreshing DNS Records..."), - callback: (r) => { - if (!r.exc) { - frm.refresh(); - } - } - }); - }, + refresh_dns_records(frm) { + frappe.call({ + doc: frm.doc, + method: "refresh_dns_records", + args: { + save: true, + }, + freeze: true, + freeze_message: __("Refreshing DNS Records..."), + callback: (r) => { + if (!r.exc) { + frm.refresh(); + } + }, + }); + }, }); diff --git a/mail/mail/doctype/mail_domain/mail_domain.py b/mail/mail/doctype/mail_domain/mail_domain.py index e25f207a..23a33b32 100644 --- a/mail/mail/doctype/mail_domain/mail_domain.py +++ b/mail/mail/doctype/mail_domain/mail_domain.py @@ -4,8 +4,9 @@ import frappe from frappe import _ from frappe.model.document import Document -from mail.mail_server import get_mail_server_domain_api + from mail.mail.doctype.mailbox.mailbox import create_postmaster_mailbox +from mail.mail_server import get_mail_server_domain_api class MailDomain(Document): @@ -84,14 +85,10 @@ def verify_dns_records(self, save: bool = True) -> None: if not errors: self.is_verified = 1 - frappe.msgprint( - _("DNS Records verified successfully."), indicator="green", alert=True - ) + frappe.msgprint(_("DNS Records verified successfully."), indicator="green", alert=True) else: self.is_verified = 0 - frappe.msgprint( - errors, title="DNS Verification Failed", indicator="red", as_list=True - ) + frappe.msgprint(errors, title="DNS Verification Failed", indicator="red", as_list=True) if save: self.save() diff --git a/mail/mail/doctype/mail_recipient/mail_recipient.py b/mail/mail/doctype/mail_recipient/mail_recipient.py index 4e59f4b5..a25e1bcc 100644 --- a/mail/mail/doctype/mail_recipient/mail_recipient.py +++ b/mail/mail/doctype/mail_recipient/mail_recipient.py @@ -2,8 +2,8 @@ # For license information, please see license.txt import frappe -from uuid_utils import uuid7 from frappe.model.document import Document +from uuid_utils import uuid7 class MailRecipient(Document): diff --git a/mail/mail/doctype/mail_settings/mail_settings.py b/mail/mail/doctype/mail_settings/mail_settings.py index 1f043e6d..5e2fa60f 100644 --- a/mail/mail/doctype/mail_settings/mail_settings.py +++ b/mail/mail/doctype/mail_settings/mail_settings.py @@ -3,10 +3,11 @@ import frappe from frappe import _ -from frappe.utils import cint +from frappe.core.api.file import get_max_file_size from frappe.model.document import Document +from frappe.utils import cint + from mail.mail_server import MailServerAuthAPI -from frappe.core.api.file import get_max_file_size class MailSettings(Document): @@ -61,6 +62,4 @@ def validate_mail_settings() -> None: for field in mandatory_fields: if not mail_settings.get(field): field_label = frappe.get_meta("Mail Settings").get_label(field) - frappe.throw( - _("Please set the {0} in the Mail Settings.").format(frappe.bold(field_label)) - ) + frappe.throw(_("Please set the {0} in the Mail Settings.").format(frappe.bold(field_label))) diff --git a/mail/mail/doctype/mail_sync_history/mail_sync_history.py b/mail/mail/doctype/mail_sync_history/mail_sync_history.py index 4b176938..051ecb42 100644 --- a/mail/mail/doctype/mail_sync_history/mail_sync_history.py +++ b/mail/mail/doctype/mail_sync_history/mail_sync_history.py @@ -45,9 +45,7 @@ def create_mail_sync_history( def get_mail_sync_history(source: str, user: str, mailbox: str) -> "MailSyncHistory": """Returns the Mail Sync History for the given source, user and mailbox.""" - if name := frappe.db.exists( - "Mail Sync History", {"source": source, "user": user, "mailbox": mailbox} - ): + if name := frappe.db.exists("Mail Sync History", {"source": source, "user": user, "mailbox": mailbox}): return frappe.get_doc("Mail Sync History", name) return create_mail_sync_history(source, user, mailbox, commit=True) diff --git a/mail/mail/doctype/mailbox/mailbox.js b/mail/mail/doctype/mailbox/mailbox.js index 9617d339..ebb0dfb4 100644 --- a/mail/mail/doctype/mailbox/mailbox.js +++ b/mail/mail/doctype/mailbox/mailbox.js @@ -3,50 +3,60 @@ frappe.ui.form.on("Mailbox", { setup(frm) { - frm.trigger("set_queries"); - }, + frm.trigger("set_queries"); + }, - refresh(frm) { - frm.trigger("add_actions"); + refresh(frm) { + frm.trigger("add_actions"); }, set_queries(frm) { frm.set_query("domain_name", () => ({ - filters: { - "enabled": 1, - "is_verified": 1, - } - })); + filters: { + enabled: 1, + is_verified: 1, + }, + })); frm.set_query("user", () => ({ - query: "mail.utils.query.get_users_with_mailbox_user_role", - filters: { + query: "mail.utils.query.get_users_with_mailbox_user_role", + filters: { enabled: 1, role: "Mailbox User", }, - })); - }, + })); + }, - add_actions(frm) { - if (frm.doc.__islocal || !has_common(frappe.user_roles, ["Administrator", "System Manager"])) return; + add_actions(frm) { + if ( + frm.doc.__islocal || + !has_common(frappe.user_roles, ["Administrator", "System Manager"]) + ) + return; - frm.add_custom_button(__("Delete Incoming Mails"), () => { - frappe.confirm( - __("Are you certain you wish to proceed?"), - () => frm.trigger("delete_incoming_mails") - ) - }, __("Actions")); + frm.add_custom_button( + __("Delete Incoming Mails"), + () => { + frappe.confirm(__("Are you certain you wish to proceed?"), () => + frm.trigger("delete_incoming_mails") + ); + }, + __("Actions") + ); - frm.add_custom_button(__("Delete Outgoing Mails"), () => { - frappe.confirm( - __("Are you certain you wish to proceed?"), - () => frm.trigger("delete_outgoing_mails") - ) - }, __("Actions")); - }, + frm.add_custom_button( + __("Delete Outgoing Mails"), + () => { + frappe.confirm(__("Are you certain you wish to proceed?"), () => + frm.trigger("delete_outgoing_mails") + ); + }, + __("Actions") + ); + }, - delete_incoming_mails(frm) { - frappe.call({ + delete_incoming_mails(frm) { + frappe.call({ method: "mail.mail.doctype.mailbox.mailbox.delete_incoming_mails", args: { mailbox: frm.doc.name, @@ -54,10 +64,10 @@ frappe.ui.form.on("Mailbox", { freeze: true, freeze_message: __("Deleting Incoming Mails..."), }); - }, + }, - delete_outgoing_mails(frm) { - frappe.call({ + delete_outgoing_mails(frm) { + frappe.call({ method: "mail.mail.doctype.mailbox.mailbox.delete_outgoing_mails", args: { mailbox: frm.doc.name, @@ -65,5 +75,5 @@ frappe.ui.form.on("Mailbox", { freeze: true, freeze_message: __("Deleting Outgoing Mails..."), }); - }, + }, }); diff --git a/mail/mail/doctype/mailbox/mailbox.py b/mail/mail/doctype/mailbox/mailbox.py index d0b0c233..5b87a57c 100644 --- a/mail/mail/doctype/mailbox/mailbox.py +++ b/mail/mail/doctype/mailbox/mailbox.py @@ -3,12 +3,13 @@ import frappe from frappe import _ -from mail.utils.cache import delete_cache from frappe.model.document import Document + +from mail.utils.cache import delete_cache from mail.utils.user import has_role, is_system_manager from mail.utils.validation import ( - validate_domain_is_enabled_and_verified, is_valid_email_for_domain, + validate_domain_is_enabled_and_verified, ) @@ -46,9 +47,7 @@ def validate_user(self) -> None: frappe.throw(_("User is mandatory.")) if not has_role(self.user, "Mailbox User") and not is_system_manager(self.user): - frappe.throw( - _("User {0} does not have Mailbox User role.").format(frappe.bold(self.user)) - ) + frappe.throw(_("User {0} does not have Mailbox User role.").format(frappe.bold(self.user))) def validate_email(self) -> None: """Validates the email address.""" diff --git a/mail/mail/doctype/outgoing_mail/outgoing_mail.js b/mail/mail/doctype/outgoing_mail/outgoing_mail.js index 935b64be..cfc19137 100644 --- a/mail/mail/doctype/outgoing_mail/outgoing_mail.js +++ b/mail/mail/doctype/outgoing_mail/outgoing_mail.js @@ -2,128 +2,147 @@ // For license information, please see license.txt frappe.ui.form.on("Outgoing Mail", { - setup(frm) { - frm.trigger("set_queries"); - }, + setup(frm) { + frm.trigger("set_queries"); + }, refresh(frm) { - frm.trigger("hide_amend_button"); - frm.trigger("add_actions"); - frm.trigger("set_sender"); + frm.trigger("hide_amend_button"); + frm.trigger("add_actions"); + frm.trigger("set_sender"); }, - set_queries(frm) { - frm.set_query("sender", () => ({ - query: "mail.utils.query.get_sender", - })); - }, + set_queries(frm) { + frm.set_query("sender", () => ({ + query: "mail.utils.query.get_sender", + })); + }, - hide_amend_button(frm) { + hide_amend_button(frm) { if (frm.doc.docstatus == 2) { - frm.page.btn_primary.hide() + frm.page.btn_primary.hide(); } }, - add_actions(frm) { - if (frm.doc.docstatus === 1) { - if (frm.doc.status === "Pending") { - frm.add_custom_button(__("Transfer Now"), () => { - frm.trigger("transfer_to_mail_server"); - }, __("Actions")); - } - else if (frm.doc.status === "Failed" && frm.doc.failed_count < 3) { - frm.add_custom_button(__("Retry"), () => { - frm.trigger("retry_failed"); - }, __("Actions")); - } - else if (["Queued", "Deferred"].includes(frm.doc.status)) { - frm.add_custom_button(__("Fetch Delivery Status"), () => { - frm.trigger("fetch_and_update_delivery_statuses"); - }, __("Actions")); - } - else if (frm.doc.status === "Sent") { - frm.add_custom_button(__("Reply"), () => { - frm.trigger("reply"); - }, __("Actions")); - frm.add_custom_button(__("Reply All"), () => { - frm.trigger("reply_all"); - }, __("Actions")); - } - } - }, + add_actions(frm) { + if (frm.doc.docstatus === 1) { + if (frm.doc.status === "Pending") { + frm.add_custom_button( + __("Transfer Now"), + () => { + frm.trigger("transfer_to_mail_server"); + }, + __("Actions") + ); + } else if (frm.doc.status === "Failed" && frm.doc.failed_count < 3) { + frm.add_custom_button( + __("Retry"), + () => { + frm.trigger("retry_failed"); + }, + __("Actions") + ); + } else if (["Queued", "Deferred"].includes(frm.doc.status)) { + frm.add_custom_button( + __("Fetch Delivery Status"), + () => { + frm.trigger("fetch_and_update_delivery_statuses"); + }, + __("Actions") + ); + } else if (frm.doc.status === "Sent") { + frm.add_custom_button( + __("Reply"), + () => { + frm.trigger("reply"); + }, + __("Actions") + ); + frm.add_custom_button( + __("Reply All"), + () => { + frm.trigger("reply_all"); + }, + __("Actions") + ); + } + } + }, - set_sender(frm) { - if (!frm.doc.sender) { - frappe.call({ - method: "mail.mail.doctype.outgoing_mail.outgoing_mail.get_default_sender", - callback: (r) => { - if (r.message) { - frm.set_value("sender", r.message); - } - } - }); - } - }, + set_sender(frm) { + if (!frm.doc.sender) { + frappe.call({ + method: "mail.mail.doctype.outgoing_mail.outgoing_mail.get_default_sender", + callback: (r) => { + if (r.message) { + frm.set_value("sender", r.message); + } + }, + }); + } + }, - transfer_to_mail_server(frm) { - frappe.call({ - doc: frm.doc, - method: "transfer_to_mail_server", - freeze: true, - freeze_message: __("Transferring..."), - callback: (r) => { - if (!r.exc) { - frm.refresh(); - } - } - }); - }, + transfer_to_mail_server(frm) { + frappe.call({ + doc: frm.doc, + method: "transfer_to_mail_server", + freeze: true, + freeze_message: __("Transferring..."), + callback: (r) => { + if (!r.exc) { + frm.refresh(); + } + }, + }); + }, - retry_failed(frm) { - frappe.call({ - doc: frm.doc, - method: "retry_failed", - freeze: true, - freeze_message: __("Retrying..."), - callback: (r) => { - if (!r.exc) { - frm.refresh(); - } - } - }); - }, + retry_failed(frm) { + frappe.call({ + doc: frm.doc, + method: "retry_failed", + freeze: true, + freeze_message: __("Retrying..."), + callback: (r) => { + if (!r.exc) { + frm.refresh(); + } + }, + }); + }, - fetch_and_update_delivery_statuses(frm) { + fetch_and_update_delivery_statuses(frm) { frappe.call({ method: "mail.tasks.enqueue_fetch_and_update_delivery_statuses", freeze: true, freeze_message: __("Creating Job..."), callback: () => { - frappe.show_alert({ - message: __("{0} job has been created.", [__("Fetch Delivery Statuses").bold()]), - indicator: "green", - }); - } + frappe.show_alert({ + message: __("{0} job has been created.", [ + __("Fetch Delivery Statuses").bold(), + ]), + indicator: "green", + }); + }, }); }, - reply(frm) { - frappe.model.open_mapped_doc({ + reply(frm) { + frappe.model.open_mapped_doc({ method: "mail.mail.doctype.outgoing_mail.outgoing_mail.reply_to_mail", frm: frm, - args: { - all: false, - }, + args: { + all: false, + }, }); - }, + }, - reply_all(frm) { - frappe.model.open_mapped_doc({ + reply_all(frm) { + frappe.model.open_mapped_doc({ method: "mail.mail.doctype.outgoing_mail.outgoing_mail.reply_to_mail", frm: frm, - args: { - all: true, - }, + args: { + all: true, + }, }); - } + }, }); diff --git a/mail/mail/doctype/outgoing_mail/outgoing_mail.py b/mail/mail/doctype/outgoing_mail/outgoing_mail.py index d8e5d6a8..9d611aa9 100644 --- a/mail/mail/doctype/outgoing_mail/outgoing_mail.py +++ b/mail/mail/doctype/outgoing_mail/outgoing_mail.py @@ -3,47 +3,49 @@ import json import time -import frappe -from frappe import _ -from re import finditer -from uuid_utils import uuid7 +from email import message_from_string, policy +from email.encoders import encode_base64 from email.message import Message -from mimetypes import guess_type -from dkim import sign as dkim_sign -from email.mime.base import MIMEBase -from email.mime.text import MIMEText from email.mime.audio import MIMEAudio +from email.mime.base import MIMEBase from email.mime.image import MIMEImage -from email.encoders import encode_base64 -from frappe.query_builder import Interval -from frappe.model.document import Document -from urllib.parse import parse_qs, urlparse -from email import policy, message_from_string from email.mime.multipart import MIMEMultipart -from mail.utils.email_parser import EmailParser -from frappe.utils.file_manager import save_file -from mail.utils.cache import get_user_default_mailbox -from mail.mail_server import get_mail_server_outbound_api -from mail.utils.validation import validate_mailbox_for_outgoing -from frappe.query_builder.functions import Now, IfNull, GroupConcat -from email.utils import parseaddr, formataddr, formatdate, make_msgid -from mail.mail.doctype.mail_contact.mail_contact import create_mail_contact -from mail.utils.user import get_user_mailboxes, is_mailbox_owner, is_system_manager -from mail.utils import ( - get_in_reply_to, - convert_html_to_text, - get_in_reply_to_mail, - parsedate_to_datetime, -) +from email.mime.text import MIMEText +from email.utils import formataddr, formatdate, make_msgid, parseaddr +from mimetypes import guess_type +from re import finditer +from urllib.parse import parse_qs, urlparse + +import frappe +from dkim import sign as dkim_sign +from frappe import _ +from frappe.model.document import Document +from frappe.query_builder import Interval +from frappe.query_builder.functions import GroupConcat, IfNull, Now from frappe.utils import ( + convert_utc_to_system_timezone, flt, - now, get_datetime, get_datetime_str, + now, time_diff_in_seconds, validate_email_address, - convert_utc_to_system_timezone, ) +from frappe.utils.file_manager import save_file +from uuid_utils import uuid7 + +from mail.mail.doctype.mail_contact.mail_contact import create_mail_contact +from mail.mail_server import get_mail_server_outbound_api +from mail.utils import ( + convert_html_to_text, + get_in_reply_to, + get_in_reply_to_mail, + parsedate_to_datetime, +) +from mail.utils.cache import get_user_default_mailbox +from mail.utils.email_parser import EmailParser +from mail.utils.user import get_user_mailboxes, is_mailbox_owner, is_system_manager +from mail.utils.validation import validate_mailbox_for_outgoing class OutgoingMail(Document): @@ -136,9 +138,7 @@ def validate_sender(self) -> None: user = frappe.session.user if not is_mailbox_owner(self.sender, user) and not is_system_manager(user): frappe.throw( - _("You are not allowed to send mail from mailbox {0}.").format( - frappe.bold(self.sender) - ) + _("You are not allowed to send mail from mailbox {0}.").format(frappe.bold(self.sender)) ) validate_mailbox_for_outgoing(self.sender) @@ -160,9 +160,7 @@ def validate_in_reply_to(self) -> None: ) ) - self.in_reply_to = get_in_reply_to( - self.in_reply_to_mail_type, self.in_reply_to_mail_name - ) + self.in_reply_to = get_in_reply_to(self.in_reply_to_mail_type, self.in_reply_to_mail_name) if not self.in_reply_to: frappe.throw( _("In Reply To Mail {0} - {1} does not exist.").format( @@ -187,9 +185,7 @@ def validate_recipients(self) -> None: if validate_email_address(recipient.email) != recipient.email: frappe.throw( - _("Row #{0}: Invalid recipient {1}.").format( - recipient.idx, frappe.bold(recipient.email) - ) + _("Row #{0}: Invalid recipient {1}.").format(recipient.idx, frappe.bold(recipient.email)) ) type_email = (recipient.type, recipient.email) @@ -210,9 +206,7 @@ def validate_custom_headers(self) -> None: max_headers = self.runtime.mail_settings.max_headers if len(self.custom_headers) > max_headers: frappe.throw( - _( - "Custom Headers limit exceeded ({0}). Maximum {1} custom header(s) allowed." - ).format( + _("Custom Headers limit exceeded ({0}). Maximum {1} custom header(s) allowed.").format( frappe.bold(len(self.custom_headers)), frappe.bold(max_headers) ) ) @@ -223,9 +217,7 @@ def validate_custom_headers(self) -> None: header.key = f"X-{header.key}" if header.key.upper().startswith("X-FM-"): - frappe.throw( - _("Custom header {0} is not allowed.").format(frappe.bold(header.key)) - ) + frappe.throw(_("Custom header {0} is not allowed.").format(frappe.bold(header.key))) if header.key in custom_headers: frappe.throw( @@ -243,9 +235,7 @@ def load_attachments(self) -> None: self.attachments = ( frappe.qb.from_(FILE) .select(FILE.name, FILE.file_name, FILE.file_url, FILE.is_private, FILE.file_size) - .where( - (FILE.attached_to_doctype == self.doctype) & (FILE.attached_to_name == self.name) - ) + .where((FILE.attached_to_doctype == self.doctype) & (FILE.attached_to_name == self.name)) ).run(as_dict=True) for attachment in self.attachments: @@ -418,9 +408,7 @@ def _add_attachments(message: MIMEMultipart | Message) -> None: part.set_payload(content) encode_base64(part) - part.add_header( - "Content-Disposition", f'{attachment.type}; filename="{file.file_name}"' - ) + part.add_header("Content-Disposition", f'{attachment.type}; filename="{file.file_name}"') part.add_header("Content-ID", f"<{attachment.name}>") message.attach(part) @@ -482,9 +470,7 @@ def create_mail_contacts(self) -> None: if self.runtime.mailbox.create_mail_contact: for recipient in self.recipients: - create_mail_contact( - self.runtime.mailbox.user, recipient.email, recipient.display_name - ) + create_mail_contact(self.runtime.mailbox.user, recipient.email, recipient.display_name) def get_dkim_domain_selector_and_private_key(self) -> tuple[str, str, str]: """Returns the DKIM domain, selector, and private key.""" @@ -505,13 +491,9 @@ def _add_recipient(self, type: str, recipient: str | list[str]) -> None: if not email: frappe.throw(_("Invalid format for recipient {0}.").format(frappe.bold(rcpt))) - self.append( - "recipients", {"type": type, "email": email, "display_name": display_name} - ) + self.append("recipients", {"type": type, "email": email, "display_name": display_name}) - def _get_recipients( - self, type: str | None = None, as_list: bool = False - ) -> str | list[str]: + def _get_recipients(self, type: str | None = None, as_list: bool = False) -> str | list[str]: """Returns the recipients.""" recipients = [] @@ -569,9 +551,7 @@ def _replace_image_url_with_content_id(self) -> str: return body_html - def _get_attachment_content_id( - self, file_url: str, set_as_inline: bool = False - ) -> str | None: + def _get_attachment_content_id(self, file_url: str, set_as_inline: bool = False) -> str | None: """Returns the attachment content ID.""" if file_url: @@ -614,9 +594,7 @@ def _update_delivery_status(self, data: dict, notify_update: bool = False) -> No """Update Delivery Status.""" if self.token != data["token"]: - msg = _("Invalid token ({0}) for outgoing mail ({1}).").format( - data["token"], self.name - ) + msg = _("Invalid token ({0}) for outgoing mail ({1}).").format(data["token"], self.name) self.add_comment("Comment", msg) frappe.throw(msg) elif self.docstatus != 1: @@ -630,12 +608,10 @@ def _update_delivery_status(self, data: dict, notify_update: bool = False) -> No for rcpt in self.recipients: if _rcpt := recipients_map.get(rcpt.email): rcpt.status = _rcpt["status"] - rcpt.action_at = convert_utc_to_system_timezone( - get_datetime(_rcpt["action_at"]) - ).replace(tzinfo=None) - rcpt.action_after = time_diff_in_seconds( - rcpt.action_at, self.transfer_completed_at + rcpt.action_at = convert_utc_to_system_timezone(get_datetime(_rcpt["action_at"])).replace( + tzinfo=None ) + rcpt.action_after = time_diff_in_seconds(rcpt.action_at, self.transfer_completed_at) rcpt.retries = _rcpt["retries"] rcpt.response = _rcpt["response"] rcpt.db_update() @@ -702,9 +678,7 @@ def transfer_to_mail_server(self) -> None: token = outbound_api.send(self.name, recipients, message) transfer_completed_at = now() - transfer_completed_after = time_diff_in_seconds( - transfer_completed_at, transfer_started_at - ) + transfer_completed_after = time_diff_in_seconds(transfer_completed_at, transfer_started_at) self._db_set( token=token, status="Queued", @@ -845,9 +819,9 @@ def delete_newsletters() -> None: fields=["name", "newsletter_retention"], order_by="newsletter_retention", ): - newsletter_retention_and_mail_domains_map.setdefault( - mail_domain["newsletter_retention"], [] - ).append(mail_domain["name"]) + newsletter_retention_and_mail_domains_map.setdefault(mail_domain["newsletter_retention"], []).append( + mail_domain["name"] + ) for retention_days, mail_domains in newsletter_retention_and_mail_domains_map.items(): OM = frappe.qb.DocType("Outgoing Mail") @@ -913,11 +887,7 @@ def transfer_emails_to_mail_server() -> None: OM.submitted_at, GroupConcat(MR.email).as_("recipients"), ) - .where( - (OM.docstatus == 1) - & (OM.failed_count < 3) - & (OM.status.isin(["Pending", "Failed"])) - ) + .where((OM.docstatus == 1) & (OM.failed_count < 3) & (OM.status.isin(["Pending", "Failed"]))) .groupby(OM.name) .orderby(OM.submitted_at) .limit(batch_size) @@ -934,9 +904,7 @@ def transfer_emails_to_mail_server() -> None: for mail in mails: try: transfer_started_at = now() - transfer_started_after = time_diff_in_seconds( - transfer_started_at, mail["submitted_at"] - ) + transfer_started_after = time_diff_in_seconds(transfer_started_at, mail["submitted_at"]) token = outbound_api.send(mail["name"], mail["recipients"], mail["message"]) @@ -1000,11 +968,7 @@ def fetch_and_update_delivery_statuses() -> None: OM.name.as_("outgoing_mail"), OM.token, ) - .where( - (OM.docstatus == 1) - & (IfNull(OM.token, "") != "") - & (OM.status.isin(statuses_to_update)) - ) + .where((OM.docstatus == 1) & (IfNull(OM.token, "") != "") & (OM.status.isin(statuses_to_update))) .orderby(OM.submitted_at) .limit(batch_size) ) diff --git a/mail/mail/doctype/outgoing_mail/outgoing_mail_list.js b/mail/mail/doctype/outgoing_mail/outgoing_mail_list.js index 02696b38..15fd1231 100644 --- a/mail/mail/doctype/outgoing_mail/outgoing_mail_list.js +++ b/mail/mail/doctype/outgoing_mail/outgoing_mail_list.js @@ -4,18 +4,18 @@ frappe.listview_settings["Outgoing Mail"] = { get_indicator: (doc) => { const status_colors = { - "Draft": "grey", - "Pending": "yellow", - "Queuing": "yellow", - "Failed": "red", - "Queued": "blue", - "Blocked": "red", - "Deferred": "orange", - "Bounced": "pink", + Draft: "grey", + Pending: "yellow", + Queuing: "yellow", + Failed: "red", + Queued: "blue", + Blocked: "red", + Deferred: "orange", + Bounced: "pink", "Partially Sent": "purple", - "Sent": "green", - "Cancelled": "red", + Sent: "green", + Cancelled: "red", }; return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; }, -}; \ No newline at end of file +}; diff --git a/mail/mail/report/mail_tracker/mail_tracker.js b/mail/mail/report/mail_tracker/mail_tracker.js index b201329e..ba47f2c8 100644 --- a/mail/mail/report/mail_tracker/mail_tracker.js +++ b/mail/mail/report/mail_tracker/mail_tracker.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.query_reports["Mail Tracker"] = { - "filters": [ + filters: [ { fieldname: "from_date", label: __("From Date"), @@ -32,7 +32,17 @@ frappe.query_reports["Mail Tracker"] = { fieldname: "status", label: __("Status"), fieldtype: "Select", - options: ["", "Pending", "Failed", "Queued", "Blocked", "Deferred", "Bounced", "Partially Sent", "Sent"], + options: [ + "", + "Pending", + "Failed", + "Queued", + "Blocked", + "Deferred", + "Bounced", + "Partially Sent", + "Sent", + ], }, { fieldname: "domain_name", @@ -56,5 +66,5 @@ frappe.query_reports["Mail Tracker"] = { label: __("Tracking ID"), fieldtype: "Data", }, - ] + ], }; diff --git a/mail/mail/report/mail_tracker/mail_tracker.py b/mail/mail/report/mail_tracker/mail_tracker.py index 4fd4b7f8..fe8c4d78 100644 --- a/mail/mail/report/mail_tracker/mail_tracker.py +++ b/mail/mail/report/mail_tracker/mail_tracker.py @@ -3,9 +3,10 @@ import frappe from frappe import _ +from frappe.query_builder import Criterion, Order from frappe.query_builder.functions import Date -from frappe.query_builder import Order, Criterion -from mail.utils.user import has_role, is_system_manager, get_user_mailboxes + +from mail.utils.user import get_user_mailboxes, has_role, is_system_manager def execute(filters: dict | None = None) -> tuple: @@ -113,11 +114,7 @@ def get_data(filters: dict | None = None) -> list[list]: .orderby(OM.submitted_at, order=Order.desc) ) - if ( - not filters.get("name") - and not filters.get("message_id") - and not filters.get("tracking_id") - ): + if not filters.get("name") and not filters.get("message_id") and not filters.get("tracking_id"): query = query.where( (Date(OM.submitted_at) >= Date(filters.get("from_date"))) & (Date(OM.submitted_at) <= Date(filters.get("to_date"))) diff --git a/mail/mail/report/outbound_delay/outbound_delay.js b/mail/mail/report/outbound_delay/outbound_delay.js index c22e1c46..eeca9cf5 100644 --- a/mail/mail/report/outbound_delay/outbound_delay.js +++ b/mail/mail/report/outbound_delay/outbound_delay.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.query_reports["Outbound Delay"] = { - "filters": [ + filters: [ { fieldname: "from_date", label: __("From Date"), @@ -68,5 +68,5 @@ frappe.query_reports["Outbound Delay"] = { fieldtype: "Check", default: 0, }, - ] + ], }; diff --git a/mail/mail/report/outbound_delay/outbound_delay.py b/mail/mail/report/outbound_delay/outbound_delay.py index 3372be75..be4eeb04 100644 --- a/mail/mail/report/outbound_delay/outbound_delay.py +++ b/mail/mail/report/outbound_delay/outbound_delay.py @@ -1,16 +1,17 @@ # Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt + import frappe from frappe import _ -from typing import Tuple -from frappe.utils import flt -from frappe.query_builder import Order, Criterion +from frappe.query_builder import Criterion, Order from frappe.query_builder.functions import Date, IfNull -from mail.utils.user import has_role, is_system_manager, get_user_mailboxes +from frappe.utils import flt + +from mail.utils.user import get_user_mailboxes, has_role, is_system_manager -def execute(filters: dict | None = None) -> Tuple[list, list]: +def execute(filters: dict | None = None) -> tuple[list, list]: columns = get_columns() data = get_data(filters) summary = get_summary(data) @@ -144,10 +145,7 @@ def get_data(filters: dict | None = None) -> list[list]: (OM.transfer_started_after + OM.transfer_completed_after).as_("transfer_delay"), MR.action_after.as_("action_delay"), ( - OM.submitted_after - + OM.transfer_started_after - + OM.transfer_completed_after - + MR.action_after + OM.submitted_after + OM.transfer_started_after + OM.transfer_completed_after + MR.action_after ).as_("total_delay"), OM.domain_name, OM.ip_address, diff --git a/mail/mail/report/outgoing_mail_summary/outgoing_mail_summary.py b/mail/mail/report/outgoing_mail_summary/outgoing_mail_summary.py index 0efdb8d2..ca5ae9e0 100644 --- a/mail/mail/report/outgoing_mail_summary/outgoing_mail_summary.py +++ b/mail/mail/report/outgoing_mail_summary/outgoing_mail_summary.py @@ -2,12 +2,14 @@ # For license information, please see license.txt import json +from datetime import datetime + import frappe from frappe import _ -from datetime import datetime -from frappe.query_builder import Order, Criterion +from frappe.query_builder import Criterion, Order from frappe.query_builder.functions import Date, IfNull -from mail.utils.user import has_role, is_system_manager, get_user_mailboxes + +from mail.utils.user import get_user_mailboxes, has_role, is_system_manager def execute(filters: dict | None = None) -> tuple: diff --git a/mail/mail_server.py b/mail/mail_server.py index 710b41a9..881ec683 100644 --- a/mail/mail_server.py +++ b/mail/mail_server.py @@ -1,8 +1,9 @@ -import frappe from typing import Any from urllib.parse import urljoin + +import frappe from frappe.frappeclient import FrappeClient, FrappeOAuth2Client -from frappe.utils import get_datetime, convert_utc_to_system_timezone +from frappe.utils import convert_utc_to_system_timezone, get_datetime class MailServerAPI: @@ -19,9 +20,7 @@ def __init__( self.api_key = api_key self.api_secret = api_secret self.access_token = access_token - self.client = self.get_client( - self.server, self.api_key, self.api_secret, self.access_token - ) + self.client = self.get_client(self.server, self.api_key, self.api_secret, self.access_token) @staticmethod def get_client( @@ -87,9 +86,7 @@ def validate(self) -> None: class MailServerDomainAPI(MailServerAPI): """Class to manage domains in the Frappe Mail Server.""" - def add_or_update_domain( - self, domain_name: str, mail_client_host: str | None = None - ) -> dict: + def add_or_update_domain(self, domain_name: str, mail_client_host: str | None = None) -> dict: """Adds or updates a domain in the Frappe Mail Server.""" endpoint = "/api/method/mail_server.api.domain.add_or_update_domain" @@ -142,17 +139,13 @@ def fetch_delivery_statuses(self, data: list[dict]) -> list[dict]: class MailServerInboundAPI(MailServerAPI): """Class to receive inbound emails from the Frappe Mail Server.""" - def fetch( - self, limit: int = 100, last_synced_at: str | None = None - ) -> dict[str, list[dict] | str]: + def fetch(self, limit: int = 100, last_synced_at: str | None = None) -> dict[str, list[dict] | str]: """Fetches inbound emails from the Frappe Mail Server.""" endpoint = "/api/method/mail_server.api.inbound.fetch" data = {"limit": limit, "last_synced_at": last_synced_at} result = self.request("GET", endpoint=endpoint, data=data) - result["last_synced_at"] = convert_utc_to_system_timezone( - get_datetime(result["last_synced_at"]) - ) + result["last_synced_at"] = convert_utc_to_system_timezone(get_datetime(result["last_synced_at"])) return result diff --git a/mail/overrides.py b/mail/overrides.py index c026b407..ac50c452 100644 --- a/mail/overrides.py +++ b/mail/overrides.py @@ -1,22 +1,16 @@ import frappe from frappe import _ -from frappe.utils import flt, cint +from frappe.utils import cint, flt def validate_file(doc, method): """Validates attachment attached to Outgoing Mail and Incoming Mail.""" def _throw(msg, raise_exception=True, indicator="red", alert=True): - frappe.msgprint( - msg, raise_exception=raise_exception, indicator=indicator, alert=alert - ) - - if ( - doc.attached_to_doctype in ["Outgoing Mail", "Incoming Mail"] and doc.attached_to_name - ): - docstatus = cint( - frappe.db.get_value(doc.attached_to_doctype, doc.attached_to_name, "docstatus") - ) + frappe.msgprint(msg, raise_exception=raise_exception, indicator=indicator, alert=alert) + + if doc.attached_to_doctype in ["Outgoing Mail", "Incoming Mail"] and doc.attached_to_name: + docstatus = cint(frappe.db.get_value(doc.attached_to_doctype, doc.attached_to_name, "docstatus")) if method == "validate": if doc.is_new() and docstatus > 0: diff --git a/mail/tasks.py b/mail/tasks.py index de566426..d0dd267c 100644 --- a/mail/tasks.py +++ b/mail/tasks.py @@ -1,10 +1,11 @@ import frappe -from mail.utils import enqueue_job + from mail.mail.doctype.incoming_mail.incoming_mail import fetch_emails_from_mail_server from mail.mail.doctype.outgoing_mail.outgoing_mail import ( - transfer_emails_to_mail_server, fetch_and_update_delivery_statuses, + transfer_emails_to_mail_server, ) +from mail.utils import enqueue_job def enqueue_transfer_emails_to_mail_server() -> None: diff --git a/mail/utils/__init__.py b/mail/utils/__init__.py index d414df5d..d4574ad4 100644 --- a/mail/utils/__init__.py +++ b/mail/utils/__init__.py @@ -1,14 +1,15 @@ import re -import pytz -import frappe -from frappe import _ -from typing import Callable -from bs4 import BeautifulSoup +from collections.abc import Callable from datetime import datetime -from frappe.utils.caching import request_cache -from frappe.utils.background_jobs import get_jobs from email.utils import parsedate_to_datetime as parsedate + +import frappe +import pytz +from bs4 import BeautifulSoup +from frappe import _ from frappe.utils import get_datetime, get_datetime_str, get_system_timezone +from frappe.utils.background_jobs import get_jobs +from frappe.utils.caching import request_cache @request_cache @@ -47,9 +48,7 @@ def get_in_reply_to( """Returns message_id of the mail to which the given mail is a reply to.""" if in_reply_to_mail_type and in_reply_to_mail_name: - return frappe.get_cached_value( - in_reply_to_mail_type, in_reply_to_mail_name, "message_id" - ) + return frappe.get_cached_value(in_reply_to_mail_type, in_reply_to_mail_name, "message_id") return None @@ -63,9 +62,7 @@ def enqueue_job(method: str | Callable, **kwargs) -> None: frappe.enqueue(method, **kwargs) -def convert_to_utc( - date_time: datetime | str, from_timezone: str | None = None -) -> "datetime": +def convert_to_utc(date_time: datetime | str, from_timezone: str | None = None) -> "datetime": """Converts the given datetime to UTC timezone.""" dt = get_datetime(date_time) @@ -76,9 +73,7 @@ def convert_to_utc( return dt.astimezone(pytz.utc) -def parsedate_to_datetime( - date_header: str, to_timezone: str | None = None -) -> "datetime": +def parsedate_to_datetime(date_header: str, to_timezone: str | None = None) -> "datetime": """Returns datetime object from parsed date header.""" dt = parsedate(date_header) @@ -96,9 +91,7 @@ def parse_iso_datetime( if not to_timezone: to_timezone = get_system_timezone() - dt = datetime.fromisoformat(datetime_str.replace("Z", "+00:00")).astimezone( - pytz.timezone(to_timezone) - ) + dt = datetime.fromisoformat(datetime_str.replace("Z", "+00:00")).astimezone(pytz.timezone(to_timezone)) return get_datetime_str(dt) if as_str else dt diff --git a/mail/utils/cache.py b/mail/utils/cache.py index 0172a1e1..08c1adf0 100644 --- a/mail/utils/cache.py +++ b/mail/utils/cache.py @@ -1,11 +1,10 @@ +from typing import Any + import frappe from frappe import _ -from typing import Any -def _get_or_set( - name: str, getter: callable, expires_in_sec: int | None = 60 * 60 -) -> Any | None: +def _get_or_set(name: str, getter: callable, expires_in_sec: int | None = 60 * 60) -> Any | None: """Get or set a value in the cache.""" value = frappe.cache.get_value(name) diff --git a/mail/utils/email_parser.py b/mail/utils/email_parser.py index b6341c21..0409a484 100644 --- a/mail/utils/email_parser.py +++ b/mail/utils/email_parser.py @@ -1,13 +1,14 @@ import re -from email import policy +from email import message_from_string, policy +from email.header import decode_header, make_header +from email.utils import parseaddr from typing import TYPE_CHECKING from urllib.parse import unquote -from email.utils import parseaddr -from email import message_from_string -from mail.utils import parsedate_to_datetime + from frappe.utils import cint, get_datetime_str from frappe.utils.file_manager import save_file -from email.header import decode_header, make_header + +from mail.utils import parsedate_to_datetime if TYPE_CHECKING: from email.message import Message @@ -98,9 +99,7 @@ def get_recipients(self, types: str | list | None = None) -> list[dict]: return recipients - def save_attachments( - self, doctype: str, docname: str, is_private: bool = True - ) -> None: + def save_attachments(self, doctype: str, docname: str, is_private: bool = True) -> None: """Saves the attachments of the email.""" def save_attachment( diff --git a/mail/utils/query.py b/mail/utils/query.py index 75325a59..dfd4003f 100644 --- a/mail/utils/query.py +++ b/mail/utils/query.py @@ -1,6 +1,7 @@ import frappe -from frappe.query_builder import Order, Criterion -from mail.utils.user import has_role, is_system_manager, get_user_mailboxes +from frappe.query_builder import Criterion, Order + +from mail.utils.user import get_user_mailboxes, has_role, is_system_manager @frappe.whitelist() @@ -74,7 +75,7 @@ def get_outgoing_mails( if not conditions: return [] - query = query.where((Criterion.any(conditions))) + query = query.where(Criterion.any(conditions)) return query.run(as_dict=False) diff --git a/mail/utils/user.py b/mail/utils/user.py index f1be42c7..7c929ff7 100644 --- a/mail/utils/user.py +++ b/mail/utils/user.py @@ -1,6 +1,8 @@ -import frappe from typing import Literal + +import frappe from frappe.utils.caching import request_cache + from mail.utils.cache import get_user_incoming_mailboxes, get_user_outgoing_mailboxes @@ -11,9 +13,7 @@ def is_system_manager(user: str) -> bool: return user == "Administrator" or has_role(user, "System Manager") -def get_user_mailboxes( - user: str, type: Literal["Incoming", "Outgoing"] | None = None -) -> list: +def get_user_mailboxes(user: str, type: Literal["Incoming", "Outgoing"] | None = None) -> list: """Returns the list of mailboxes associated with the user.""" if type and type in ["Incoming", "Outgoing"]: @@ -22,9 +22,7 @@ def get_user_mailboxes( else: return get_user_outgoing_mailboxes(user) - unique_mailboxes = set(get_user_incoming_mailboxes(user)) | set( - get_user_outgoing_mailboxes(user) - ) + unique_mailboxes = set(get_user_incoming_mailboxes(user)) | set(get_user_outgoing_mailboxes(user)) return list(unique_mailboxes) diff --git a/mail/utils/validation.py b/mail/utils/validation.py index 3f017551..6de6274d 100644 --- a/mail/utils/validation.py +++ b/mail/utils/validation.py @@ -1,4 +1,5 @@ import re + import frappe from frappe import _ from frappe.utils.caching import request_cache @@ -10,9 +11,7 @@ def is_valid_host(host: str) -> bool: return bool(re.compile(r"^[a-zA-Z0-9_-]+$").match(host)) -def is_valid_email_for_domain( - email: str, domain_name: str, raise_exception: bool = False -) -> bool: +def is_valid_email_for_domain(email: str, domain_name: str, raise_exception: bool = False) -> bool: """Returns True if the email domain matches with the given domain else False.""" email_domain = email.split("@")[1] @@ -36,9 +35,7 @@ def validate_domain_is_enabled_and_verified(domain_name: str) -> None: if frappe.flags.ingore_domain_validation: return - enabled, is_verified = frappe.db.get_value( - "Mail Domain", domain_name, ["enabled", "is_verified"] - ) + enabled, is_verified = frappe.db.get_value("Mail Domain", domain_name, ["enabled", "is_verified"]) if not enabled: frappe.throw(_("Domain {0} is disabled.").format(frappe.bold(domain_name))) @@ -55,9 +52,7 @@ def validate_mailbox_for_outgoing(mailbox: str) -> None: if not enabled: frappe.throw(_("Mailbox {0} is disabled.").format(frappe.bold(mailbox))) elif not outgoing: - frappe.throw( - _("Mailbox {0} is not allowed for Outgoing Mail.").format(frappe.bold(mailbox)) - ) + frappe.throw(_("Mailbox {0} is not allowed for Outgoing Mail.").format(frappe.bold(mailbox))) @request_cache @@ -69,6 +64,4 @@ def validate_mailbox_for_incoming(mailbox: str) -> None: if not enabled: frappe.throw(_("Mailbox {0} is disabled.").format(frappe.bold(mailbox))) elif not incoming: - frappe.throw( - _("Mailbox {0} is not allowed for Incoming Mail.").format(frappe.bold(mailbox)) - ) + frappe.throw(_("Mailbox {0} is not allowed for Incoming Mail.").format(frappe.bold(mailbox))) diff --git a/mail/www/mail.html b/mail/www/mail.html deleted file mode 100644 index 6129a1d5..00000000 --- a/mail/www/mail.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - Frappe UI App - - - - - - -
-
-
- - - - - diff --git a/pyproject.toml b/pyproject.toml index 8203fb5d..f8265355 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,11 +3,12 @@ name = "mail" authors = [ { name = "Frappe Technologies Pvt. Ltd.", email = "developers@frappe.io"} ] -description = "Mail" +description = "Frappe Mail" requires-python = ">=3.10" readme = "README.md" dynamic = ["version"] dependencies = [ + # "frappe~=15.0.0" # Installed and managed by bench. "uuid-utils~=0.6.1", "dkimpy~=1.1.5", ] @@ -19,3 +20,38 @@ build-backend = "flit_core.buildapi" # These dependencies are only installed when developer mode is enabled [tool.bench.dev-dependencies] # package_name = "~=1.1.0" + +[tool.ruff] +line-length = 110 +target-version = "py310" + +[tool.ruff.lint] +select = [ + "F", + "E", + "W", + "I", + "UP", + "B", +] +ignore = [ + "B017", # assertRaises(Exception) - should be more specific + "B018", # useless expression, not assigned to anything + "B023", # function doesn't bind loop variable - will have last iteration's value + "B904", # raise inside except without from + "E101", # indentation contains mixed spaces and tabs + "E402", # module level import not at top of file + "E501", # line too long + "E741", # ambiguous variable name + "F401", # "unused" imports + "F403", # can't detect undefined names from * import + "F405", # can't detect undefined names from * import + "F722", # syntax error in forward type annotation + "W191", # indentation contains tabs +] +typing-modules = ["frappe.types.DF"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "tab" +docstring-code-format = true diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000..fb57ccd1 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +