├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature-request.yml └── workflows │ ├── lint-backend.yaml │ ├── lint-frontend.yaml │ ├── test-backend.yaml │ └── test-frontend.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE.md ├── README.md ├── backend ├── .env.example ├── Dockerfile ├── aomail │ ├── admin.py │ ├── administration │ │ └── dashboard.py │ ├── ai_providers │ │ ├── anthropic │ │ │ └── client.py │ │ ├── deepseek │ │ │ └── client.py │ │ ├── google │ │ │ └── client.py │ │ ├── groq │ │ │ └── client.py │ │ ├── llm_functions.py │ │ ├── mistral │ │ │ └── client.py │ │ ├── openai │ │ │ └── client.py │ │ ├── prompts.py │ │ └── utils.py │ ├── analytics │ │ ├── dashboard.py │ │ └── statistics.py │ ├── apps.py │ ├── assets │ │ └── default-agent-icon.png │ ├── authentication │ │ ├── authentication.py │ │ └── signup.py │ ├── constants.py │ ├── controllers │ │ ├── agents.py │ │ ├── artificial_intelligence.py │ │ ├── categories.py │ │ ├── custom_categorization.py │ │ ├── emails.py │ │ ├── filters.py │ │ ├── labels.py │ │ ├── preferences.py │ │ ├── rules.py │ │ ├── search_api_emails.py │ │ ├── search_emails.py │ │ ├── search_labels.py │ │ ├── search_rules.py │ │ ├── signatures.py │ │ └── views.py │ ├── email_providers │ │ ├── google │ │ │ ├── authentication.py │ │ │ ├── compose_email.py │ │ │ ├── email_operations.py │ │ │ ├── labels.py │ │ │ ├── profile.py │ │ │ ├── troubleshooting.py │ │ │ └── webhook.py │ │ ├── imap │ │ │ ├── authentication.py │ │ │ ├── email_operations.py │ │ │ ├── emails_sync.py │ │ │ ├── profile.py │ │ │ └── utils.py │ │ ├── microsoft │ │ │ ├── authentication.py │ │ │ ├── compose_email.py │ │ │ ├── email_operations.py │ │ │ ├── labels.py │ │ │ ├── profile.py │ │ │ ├── troubleshooting.py │ │ │ └── webhook.py │ │ ├── smtp │ │ │ ├── authentication.py │ │ │ └── compose_email.py │ │ └── utils.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_initial.py │ │ ├── 0003_remove_rule_action_reply_recipients.py │ │ ├── 0004_preference_llm_provider_preference_model.py │ │ ├── 0005_preference_categorize_and_summarize_email_prompt_and_more.py │ │ ├── 0006_emailserverconfig_socialapi_imap_config_and_more.py │ │ ├── 0007_socialapi_last_fetched_date.py │ │ └── __init__.py │ ├── models.py │ ├── payment_providers │ │ └── stripe.py │ ├── schedule_tasks.py │ ├── urls.py │ └── utils │ │ ├── ai_memory.py │ │ ├── email_processing.py │ │ ├── security.py │ │ ├── serializers.py │ │ └── tree_knowledge.py ├── config │ ├── asgi.py │ ├── settings.py │ └── urls.py ├── entrypoint.sh ├── manage.py ├── requirements.txt ├── templates │ ├── ai_failed_conv.html │ ├── ai_failed_email.html │ ├── password_reset_email.html │ └── unsubscribe_failure.html └── tests │ ├── conftest.py │ ├── test_ai_providers_utils.py │ ├── test_email_processing_utils.py │ ├── test_email_providers_utils.py │ ├── test_google_llm.py │ ├── test_imap_utils.py │ ├── test_security.py │ └── test_signup.py ├── docker-compose.yml ├── frontend ├── .eslintrc.js ├── Dockerfile ├── babel.config.js ├── nginx.conf ├── package.json ├── postcss.config.js ├── public │ ├── index.html │ └── logo-aomail.png ├── src │ ├── App.vue │ ├── assets │ │ ├── ao-happy.png │ │ ├── ao-neutral.png │ │ ├── ao-new-idea.png │ │ ├── ao-prompt-error.png │ │ ├── css │ │ │ ├── fonts.css │ │ │ ├── quill.css │ │ │ └── tailwind.css │ │ ├── eye-icon.svg │ │ ├── fonts │ │ │ ├── pxiByp8kv8JHgFVrLDz8Z1JlFc-K.woff2 │ │ │ ├── pxiByp8kv8JHgFVrLDz8Z1xlFQ.woff2 │ │ │ ├── pxiByp8kv8JHgFVrLEj6Z1JlFc-K.woff2 │ │ │ ├── pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2 │ │ │ ├── pxiByp8kv8JHgFVrLFj_Z1JlFc-K.woff2 │ │ │ ├── pxiByp8kv8JHgFVrLFj_Z1xlFQ.woff2 │ │ │ ├── pxiByp8kv8JHgFVrLGT9Z1JlFc-K.woff2 │ │ │ ├── pxiByp8kv8JHgFVrLGT9Z1xlFQ.woff2 │ │ │ ├── pxiEyp8kv8JHgFVrJJfecg.woff2 │ │ │ └── pxiEyp8kv8JHgFVrJJnecmNE.woff2 │ │ ├── js │ │ │ ├── plateform.d.ts │ │ │ └── plateform.js │ │ ├── logo-aomail.png │ │ ├── logos │ │ │ ├── apple.svg │ │ │ ├── bouygues.svg │ │ │ ├── fastmail.svg │ │ │ ├── free.svg │ │ │ ├── gmx.svg │ │ │ ├── google.svg │ │ │ ├── la_poste.svg │ │ │ ├── microsoft.svg │ │ │ ├── orange.svg │ │ │ ├── other.svg │ │ │ ├── sfr.svg │ │ │ └── yahoo.svg │ │ └── user.png │ ├── declaration.d.ts │ ├── global │ │ ├── components │ │ │ ├── Conversation │ │ │ │ └── ChatBubble.vue │ │ │ ├── CreateAgentModal.vue │ │ │ ├── EmailItem.vue │ │ │ ├── ImapSmtpModal.vue │ │ │ ├── ImportantEmails.vue │ │ │ ├── InformativeEmails.vue │ │ │ ├── ManualEmail │ │ │ │ ├── ManualEmail.vue │ │ │ │ ├── RecipientInputRow.vue │ │ │ │ ├── RecipientItem.vue │ │ │ │ ├── RecipientsSection.vue │ │ │ │ ├── ScheduleSendModal.vue │ │ │ │ ├── SelectedEmailChoiceButton.vue │ │ │ │ ├── SendEmailButtons.vue │ │ │ │ └── SubjectAttachmentsSection.vue │ │ │ ├── Navbar.vue │ │ │ ├── NotificationTimer.vue │ │ │ ├── ResizableDivider.vue │ │ │ ├── SeeMailModal.vue │ │ │ ├── SendAiInstructionButton.vue │ │ │ ├── SignatureModal.vue │ │ │ ├── UpdateAgentModal.vue │ │ │ └── UselessEmails.vue │ │ ├── const.ts │ │ ├── emailProviders.ts │ │ ├── fetchData.ts │ │ ├── filters.ts │ │ ├── formatters.ts │ │ ├── popUp.ts │ │ ├── preferences.ts │ │ ├── security.ts │ │ └── types.ts │ ├── i18n │ │ ├── en.json │ │ ├── fr.json │ │ └── index.ts │ ├── main.ts │ ├── pages │ │ ├── AiAssistant │ │ │ ├── AiAssistant.vue │ │ │ ├── components │ │ │ │ └── PrioritizationGuidelines.vue │ │ │ └── utils │ │ │ │ └── jobs.ts │ │ ├── Analytics │ │ │ ├── Analytics.vue │ │ │ └── components │ │ │ │ ├── AomailAdvancedTab.vue │ │ │ │ ├── CharTitle.vue │ │ │ │ ├── DashboardTab.vue │ │ │ │ └── EmailProvidersAdvancedTab.vue │ │ ├── Answer │ │ │ ├── Answer.vue │ │ │ └── components │ │ │ │ └── AiEmail.vue │ │ ├── CustomCategorization │ │ │ ├── CustomCategorization.vue │ │ │ └── components │ │ │ │ ├── ChatInput.vue │ │ │ │ └── Conversation.vue │ │ ├── Errors │ │ │ ├── 401NotAuthorized.vue │ │ │ └── 404NotFound.vue │ │ ├── Inbox │ │ │ ├── Inbox.vue │ │ │ ├── components │ │ │ │ ├── AssistantChat.vue │ │ │ │ ├── Categories.vue │ │ │ │ ├── CategoryDeletionModal.vue │ │ │ │ ├── Conversation.vue │ │ │ │ ├── Filters.vue │ │ │ │ ├── NewCategoryModal.vue │ │ │ │ ├── NewFilterModal.vue │ │ │ │ ├── ReadEmails.vue │ │ │ │ ├── SearchBar.vue │ │ │ │ ├── UpdateCategoryModal.vue │ │ │ │ └── UpdateFilterModal.vue │ │ │ └── utils │ │ │ │ └── types.ts │ │ ├── Labels │ │ │ ├── Labels.vue │ │ │ ├── components │ │ │ │ ├── ActionButtons.vue │ │ │ │ ├── Filters.vue │ │ │ │ ├── Label.vue │ │ │ │ └── SearchMenu.vue │ │ │ └── utils │ │ │ │ └── types.ts │ │ ├── Login │ │ │ └── Login.vue │ │ ├── Logout │ │ │ └── Logout.vue │ │ ├── New │ │ │ ├── New.vue │ │ │ └── components │ │ │ │ └── AiEmail.vue │ │ ├── PromptCustomization │ │ │ ├── PromptCustomization.vue │ │ │ └── components │ │ │ │ └── Prompts.vue │ │ ├── ReplyLater │ │ │ └── ReplyLater.vue │ │ ├── ResetPassword │ │ │ ├── PasswordResetLink.vue │ │ │ └── ResetPasswordForm.vue │ │ ├── Rules │ │ │ ├── Rules.vue │ │ │ ├── components │ │ │ │ ├── Filters.vue │ │ │ │ ├── NewRuleModal.vue │ │ │ │ ├── Rule.vue │ │ │ │ ├── SearchBar.vue │ │ │ │ ├── TagInput.vue │ │ │ │ └── UpdateRuleModal.vue │ │ │ └── utils │ │ │ │ └── types.ts │ │ ├── Search │ │ │ ├── Search.vue │ │ │ ├── components │ │ │ │ ├── AiSearchMenu.vue │ │ │ │ ├── AomailFilters.vue │ │ │ │ ├── ApiEmailModal.vue │ │ │ │ ├── ApiFilters.vue │ │ │ │ ├── EmailItem.vue │ │ │ │ ├── EmailList.vue │ │ │ │ └── SearchMenu.vue │ │ │ └── utils │ │ │ │ └── types.ts │ │ ├── Settings │ │ │ ├── Settings.vue │ │ │ ├── components │ │ │ │ ├── AccountDeletionModal.vue │ │ │ │ ├── AddUserDescriptionModal.vue │ │ │ │ ├── LanguageSelection.vue │ │ │ │ ├── MyAccountMenu.vue │ │ │ │ ├── PreferencesMenu.vue │ │ │ │ ├── ThemeSelection.vue │ │ │ │ ├── TimeZoneSelection.vue │ │ │ │ ├── TroubleshootingMenuModal.vue │ │ │ │ ├── UnlinkEmailModal.vue │ │ │ │ ├── UpdateUserDescriptionModal.vue │ │ │ │ ├── UserCredentialsUpdateSection.vue │ │ │ │ └── UserEmailLinked.vue │ │ │ └── utils │ │ │ │ └── types.ts │ │ ├── Signup │ │ │ ├── SignUp.vue │ │ │ ├── SignUpLink.vue │ │ │ └── components │ │ │ │ ├── CategoriesForm.vue │ │ │ │ ├── CredentialsForm.vue │ │ │ │ ├── EmailLinkForm.vue │ │ │ │ ├── NewCategoryModal.vue │ │ │ │ ├── StepMarker.vue │ │ │ │ ├── StepsTracker.vue │ │ │ │ └── UpdateCategoryModal.vue │ │ ├── Subscription │ │ │ ├── Subscription.vue │ │ │ └── utils │ │ │ │ └── types.ts │ │ └── Transfer │ │ │ ├── Transfer.vue │ │ │ └── components │ │ │ └── AiEmail.vue │ └── router │ │ ├── router.ts │ │ └── routes.ts ├── tailwind.config.js ├── tsconfig.json └── vue.config.js ├── pytest.ini └── start.sh /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a bug report 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Before opening a new issue, please do a search of existing issues. 9 | 10 | If you need help, contact us on [Discord](https://discord.com/invite/JxbPZNDd). 11 | - type: textarea 12 | attributes: 13 | label: To Reproduce 14 | description: | 15 | A detailed, step-by-step description of how to reproduce the issue is required. 16 | Please ensure your report includes clear instructions using numbered lists. 17 | 18 | If possible, provide a link to a repository or project where the issue can be reproduced. 19 | placeholder: | 20 | 1. Create a application 21 | 2. Click X 22 | 3. Y will happen 23 | validations: 24 | required: true 25 | - type: textarea 26 | attributes: 27 | label: Current vs. Expected behavior 28 | description: A clear and concise description of what the bug is, and what you expected to happen. 29 | placeholder: "Following the steps from the previous section, I expected A to happen, but I observed B instead" 30 | validations: 31 | required: true 32 | - type: textarea 33 | attributes: 34 | label: Additional context 35 | description: | 36 | Any extra information that might help us investigate. 37 | placeholder: | 38 | I tested on a DigitalOcean VPS with Ubuntu 20.04 and Docker version 20.10.12. 39 | 40 | - type: dropdown 41 | attributes: 42 | label: Will you send a PR to fix it? 43 | description: Let us know if you are planning to submit a pull request to address this issue. 44 | 45 | options: 46 | - "Yes" 47 | - "No" 48 | - "Maybe, need help" 49 | validations: 50 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Discord Server 4 | url: https://discord.com/invite/JxbPZNDd 5 | about: Please ask your questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a new feature or improvement to the project 3 | labels: ["enhancement"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Before opening a new issue, please do a search of existing requested features. 9 | 10 | If you need help, contact us on [Discord](https://discord.com/invite/JxbPZNDd). 11 | - type: textarea 12 | attributes: 13 | label: What problem will this feature address? 14 | description: A clear and concise description of what the problem is. 15 | placeholder: | 16 | I'm always frustrated when I can't do X 17 | validations: 18 | required: true 19 | - type: textarea 20 | attributes: 21 | label: Describe the solution you'd like 22 | description: A clear and concise description of what you want to happen. 23 | placeholder: Add X to the core 24 | validations: 25 | required: true 26 | - type: textarea 27 | attributes: 28 | label: Describe alternatives you've considered 29 | description: A clear and concise description of any alternative solutions or features you've considered. 30 | placeholder: | 31 | Maybe use Y as a workaround? 32 | validations: 33 | required: true 34 | - type: textarea 35 | attributes: 36 | label: Additional context 37 | description: Add any other context or screenshots about the feature request here. 38 | validations: 39 | required: false 40 | 41 | - type: dropdown 42 | attributes: 43 | label: Will you send a PR to implement it? 44 | description: Let us know if you are planning to submit a pull request to implement this feature. 45 | options: 46 | - "Yes" 47 | - "No" 48 | - "Maybe, need help" 49 | validations: 50 | required: true -------------------------------------------------------------------------------- /.github/workflows/lint-backend.yaml: -------------------------------------------------------------------------------- 1 | name: Lint Backend Code 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | defaults: 9 | run: 10 | working-directory: ./backend 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Setup Python 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: '3.11' 17 | - name: Run black formatter 18 | run: | 19 | pip install git+https://github.com/psf/black 20 | python -m black . --exclude 'aomail/migrations/*|aomail/urls.py' 21 | - name: Commit changes 22 | uses: stefanzweifel/git-auto-commit-action@v4 23 | with: 24 | commit_message: Apply backend code formatting changes 25 | branch: ${{ github.head_ref }} -------------------------------------------------------------------------------- /.github/workflows/lint-frontend.yaml: -------------------------------------------------------------------------------- 1 | name: Lint Frontend Code 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | format: 7 | runs-on: ubuntu-latest 8 | defaults: 9 | run: 10 | working-directory: ./frontend 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | ref: ${{ github.head_ref }} 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: "16.x" 18 | - run: npm install 19 | - run: npm run format 20 | - name: Commit changes 21 | uses: stefanzweifel/git-auto-commit-action@v4 22 | with: 23 | commit_message: Apply frontend code formatting changes 24 | branch: ${{ github.head_ref }} -------------------------------------------------------------------------------- /.github/workflows/test-backend.yaml: -------------------------------------------------------------------------------- 1 | name: Run Backend Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | run-backend-tests: 11 | strategy: 12 | fail-fast: false 13 | 14 | name: Run Backend Tests 15 | runs-on: ubuntu-latest 16 | defaults: 17 | run: 18 | working-directory: ./backend 19 | 20 | services: 21 | postgres: 22 | image: postgres 23 | env: 24 | POSTGRES_PASSWORD: postgres 25 | options: >- 26 | --health-cmd pg_isready 27 | --health-interval 10s 28 | --health-timeout 5s 29 | --health-retries 5 30 | ports: 31 | - 5432:5432 32 | 33 | steps: 34 | - name: Checkout code 35 | uses: actions/checkout@v4 36 | 37 | - name: Setup Python 38 | uses: actions/setup-python@v5 39 | with: 40 | python-version: '3.11' 41 | 42 | - name: Install dependencies 43 | run: | 44 | python -m pip install --upgrade pip 45 | pip install -r requirements.txt 46 | 47 | - name: Run tests 48 | run: | 49 | export DJANGO_SETTINGS_MODULE=config.settings 50 | python -m pytest 51 | env: 52 | GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} 53 | DJANGO_SECRET_KEY: "testing-secret-key" 54 | DJANGO_DB_ENGINE: django.db.backends.postgresql 55 | DJANGO_DB_NAME: postgres 56 | DJANGO_DB_USER: postgres 57 | DJANGO_DB_PASSWORD: postgres 58 | DJANGO_DB_HOST: localhost 59 | DJANGO_DB_PORT: 5432 60 | -------------------------------------------------------------------------------- /.github/workflows/test-frontend.yaml: -------------------------------------------------------------------------------- 1 | name: Run Frontend Build Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run-frontend-build-test: 7 | strategy: 8 | fail-fast: false 9 | 10 | name: Run Frontend Build Test 11 | runs-on: ubuntu-latest 12 | defaults: 13 | run: 14 | working-directory: ./frontend 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - run: | 19 | npm install 20 | npm run build 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Auto-created files 2 | .DS_Store 3 | node_modules 4 | /dist 5 | db.sqlite3 6 | 7 | # Local environment files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | backend.log* 17 | aomail-cron.log* 18 | 19 | # Editor directories and files 20 | .idea 21 | .vscode 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | __pycache__/ 28 | # migrations/ 29 | media/ 30 | trees/ 31 | LogoAugmentAI_export4.png 32 | package-lock.json 33 | .py_env/ 34 | dist/ 35 | backend/logger.json 36 | 37 | # Credentials 38 | build.sh 39 | start_augustin_dev.sh 40 | start_theo_dev.sh 41 | backend/creds/ 42 | .env 43 | py_env/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | frontend/src/assets/* -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "tabWidth": 4, 4 | "printWidth": 120, 5 | "bracketSpacing": true, 6 | "htmlWhitespaceSensitivity": "ignore" 7 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | ## Core License (AGPLv3) 4 | 5 | Copyright 2025 Augustin ROLET & Théo HUBERT 6 | 7 | Licensed under the AGPLv3 License, Version 3.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | https://www.gnu.org/licenses/agpl-3.0.en.html 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and limitations under the License. 17 | 18 | ## Additional Terms for Specific Features 19 | 20 | The following additional terms apply to all features of Aomail. In the event of a conflict, these provisions shall take precedence over those in the AGPLv3 License: 21 | 22 | - **Self-Hosted Version Free**: All features of Aomail will always be free to use in the self-hosted version. 23 | - **Restriction on Resale**: Aomail cannot be sold or offered as a service by any party other than the copyright holders without prior written consent. 24 | - **Modification Distribution**: Any modifications made to Aomail by a third party must be distributed freely and cannot be sold or offered as a service. 25 | 26 | For further inquiries or permissions, please contact us directly. -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | # LLM API KEYS 2 | ANTHROPIC_API_KEY="" 3 | GEMINI_API_KEY="" 4 | MISTRAL_API_KEY="" 5 | OPENAI_API_KEY="" 6 | DEEPSEEK_API_KEY="" 7 | GROQ_API_KEY="" 8 | 9 | # ENCRYPTION KEYS 10 | SOCIAL_API_REFRESH_TOKEN_KEY="" 11 | EMAIL_ONE_LINE_SUMMARY_KEY="" 12 | EMAIL_SHORT_SUMMARY_KEY="" 13 | EMAIL_HTML_CONTENT_KEY="" 14 | 15 | # DJANGO CREDENTIALS 16 | DOMAIN="localhost" # or your DNS domain (e.g. "aomail.ai") 17 | DJANGO_SECRET_KEY="" 18 | DJANGO_DB_ENGINE="django.db.backends.postgresql" 19 | DJANGO_DB_NAME="aomaildb" 20 | DJANGO_DB_USER="user" 21 | DJANGO_DB_PASSWORD="password" 22 | DJANGO_DB_HOST="db" 23 | DJANGO_DB_PORT="5432" 24 | 25 | # EMAIL CREDENTIALS (for alerts) 26 | EMAIL_NO_REPLY="" 27 | EMAIL_NO_REPLY_PASSWORD="" 28 | EMAIL_ADMIN="" 29 | 30 | # EMAIL PROVIDER WEBHOOKS 31 | GOOGLE_TOPIC_NAME="" # optional - only if you want to use PubSub 32 | GOOGLE_PROJECT_ID="" 33 | GOOGLE_CLIENT_ID="" 34 | GOOGLE_CLIENT_SECRET="" 35 | 36 | MICROSOFT_CLIENT_ID="" 37 | MICROSOFT_CLIENT_SECRET="" 38 | MICROSOFT_TENANT_ID="" 39 | MICROSOFT_CLIENT_STATE="" 40 | 41 | # STRIPE CREDENTIALS 42 | STRIPE_PUBLISHABLE_KEY="" 43 | STRIPE_SECRET_KEY="" 44 | STRIPE_AOMAIL_WEBHOOK_SECRET="" -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Base stage for building the application 2 | FROM python:3.11 AS base 3 | WORKDIR /app 4 | RUN apt-get update && apt-get install -y cron 5 | COPY ./requirements.txt . 6 | RUN pip install --no-cache-dir -r requirements.txt 7 | COPY entrypoint.sh /usr/local/bin/ 8 | RUN chmod +x /usr/local/bin/entrypoint.sh 9 | COPY . . 10 | 11 | # Development mode 12 | FROM base AS development 13 | ENTRYPOINT ["entrypoint.sh"] 14 | EXPOSE 8000 15 | 16 | # Production mode 17 | FROM base AS production 18 | ENTRYPOINT ["entrypoint.sh"] 19 | EXPOSE 8000 20 | -------------------------------------------------------------------------------- /backend/aomail/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Category, Agent 3 | 4 | # Register your models here. 5 | admin.site.register(Category) 6 | admin.site.register(Agent) 7 | -------------------------------------------------------------------------------- /backend/aomail/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AomailConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "aomail" 7 | -------------------------------------------------------------------------------- /backend/aomail/assets/default-agent-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/backend/aomail/assets/default-agent-icon.png -------------------------------------------------------------------------------- /backend/aomail/controllers/custom_categorization.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module to handle the logic of email categorization and prioritization customization. 3 | 4 | Endpoints: 5 | - ✅ review_user_description: Reviews and validates the user-provided description for email categorization. 6 | - ✅ generate_categories_scratch: Generates categories based on user-provided topics for email classification. 7 | - ✅ generate_prioritization_scratch: Generates email prioritization guidance based on user input for better categorization. 8 | """ 9 | 10 | import json 11 | from rest_framework.decorators import api_view 12 | from aomail.utils.security import block_user, subscription 13 | from django.http import HttpRequest 14 | from aomail.constants import ALLOWED_PLANS 15 | from aomail.ai_providers import llm_functions 16 | from aomail.ai_providers.utils import update_tokens_stats 17 | from rest_framework import status 18 | from rest_framework.response import Response 19 | from aomail.models import Preference 20 | 21 | 22 | @api_view(["POST"]) 23 | @block_user 24 | @subscription(ALLOWED_PLANS) 25 | def review_user_description(request: HttpRequest) -> dict: 26 | """ 27 | Reviews and validates the user-provided description for email categorization. 28 | 29 | Args: 30 | request (HttpRequest): HTTP request object containing the user description. 31 | Expects a JSON body with: 32 | user_description (str): The description provided by the user for categorizing emails. 33 | 34 | Returns: 35 | Response: A JSON response with validation result and feedback message. 36 | """ 37 | parameters: dict = json.loads(request.body) 38 | user_description: str = parameters["description"] 39 | preference = Preference.objects.get(user=request.user) 40 | result = llm_functions.review_user_description( 41 | user_description, preference.llm_provider, preference.llm_model 42 | ) 43 | update_tokens_stats(request.user, result) 44 | 45 | return Response(result, status=status.HTTP_200_OK) 46 | 47 | 48 | @api_view(["POST"]) 49 | @block_user 50 | @subscription(ALLOWED_PLANS) 51 | def generate_categories_scratch(request: HttpRequest) -> dict: 52 | """ 53 | Generates email categories based on user-provided topics for automatic email classification. 54 | 55 | Args: 56 | request (HttpRequest): HTTP request object containing the user topics. 57 | Expects a JSON body with: 58 | userTopics (str | list): A list or string of topics provided by the user. 59 | chatHistory (list): A list of messages between user and AI. 60 | 61 | Returns: 62 | Response: A JSON response with generated categories, descriptions, and feedback. 63 | """ 64 | parameters: dict = json.loads(request.body) 65 | user_topics: str = parameters["userTopics"] 66 | chat_history: str = parameters.get("chatHistory") 67 | preference = Preference.objects.get(user=request.user) 68 | result = llm_functions.generate_categories_scratch( 69 | user_topics, 70 | chat_history, 71 | preference.llm_provider, 72 | preference.llm_model, 73 | ) 74 | update_tokens_stats(request.user, result) 75 | 76 | return Response(result, status=status.HTTP_200_OK) 77 | 78 | 79 | @api_view(["POST"]) 80 | @block_user 81 | @subscription(ALLOWED_PLANS) 82 | def generate_prioritization_scratch(request: HttpRequest) -> dict: 83 | """ 84 | Generates email prioritization guidance based on user-provided input to optimize categorization. 85 | 86 | Args: 87 | request (HttpRequest): HTTP request object containing the following JSON body: 88 | - userInput (dict | str): A dictionary or string with user-provided prioritization guidelines. 89 | 90 | Returns: 91 | Response: A JSON response with: 92 | - important (str): Enhanced description of important emails. 93 | - informative (str): Enhanced description of informative emails. 94 | - useless (str): Enhanced description of useless emails. 95 | """ 96 | parameters: dict = json.loads(request.body) 97 | user_input: str = parameters["userInput"] 98 | preference = Preference.objects.get(user=request.user) 99 | result = llm_functions.generate_prioritization_scratch( 100 | user_input, preference.llm_provider, preference.llm_model 101 | ) 102 | update_tokens_stats(request.user, result) 103 | 104 | return Response(result, status=status.HTTP_200_OK) 105 | -------------------------------------------------------------------------------- /backend/aomail/email_providers/google/troubleshooting.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handles regrant consent logic and data synchronization for Google API. 3 | 4 | Endpoints: 5 | - ✅ check_connectivity: Checks if Aomail is connected to the user's Google account. 6 | - ✅ synchronize: Synchronizes Aomail database with Google servers. 7 | """ 8 | 9 | import json 10 | import logging 11 | from concurrent.futures import ThreadPoolExecutor, as_completed 12 | from django.http import HttpRequest 13 | from rest_framework.response import Response 14 | from rest_framework import status 15 | from rest_framework.decorators import api_view 16 | from aomail.utils.security import subscription 17 | from aomail.constants import ( 18 | ALLOWED_PLANS, 19 | ) 20 | from aomail.models import Email, SocialAPI, Subscription 21 | from aomail.email_providers.google.authentication import ( 22 | authenticate_service, 23 | fetch_email_ids_since, 24 | ) 25 | from aomail.email_providers.utils import email_to_db 26 | from aomail.email_providers.google.webhook import ( 27 | check_and_resubscribe_to_missing_resources, 28 | ) 29 | 30 | 31 | LOGGER = logging.getLogger(__name__) 32 | 33 | 34 | @api_view(["POST"]) 35 | @subscription(ALLOWED_PLANS) 36 | def check_connectivity(request: HttpRequest) -> Response: 37 | """ 38 | Checks the connectivity status of a user's linked email account. 39 | 40 | Args: 41 | request (HttpRequest): The HTTP request object containing the user's email. 42 | 43 | Returns: 44 | Response: Contains the token validity status and the number of missed emails. 45 | """ 46 | parameters: dict = json.loads(request.body) 47 | email = parameters["email"] 48 | user = request.user 49 | 50 | services = authenticate_service(user, email, ["gmail"]) 51 | if not services: 52 | return Response( 53 | {"isTokenValid": False}, 54 | status=status.HTTP_200_OK, 55 | ) 56 | 57 | subscription = Subscription.objects.get(user=user) 58 | start_date = subscription.created_at 59 | email_ids = fetch_email_ids_since(services["gmail"], email, start_date) 60 | 61 | nb_missed_emails = 0 62 | 63 | for email_id in email_ids: 64 | if not Email.objects.filter(user=user, provider_id=email_id).exists(): 65 | nb_missed_emails += 1 66 | 67 | return Response( 68 | {"isTokenValid": True, "nbMissedEmails": nb_missed_emails}, 69 | status=status.HTTP_200_OK, 70 | ) 71 | 72 | 73 | @api_view(["POST"]) 74 | @subscription(ALLOWED_PLANS) 75 | def synchronize(request: HttpRequest) -> Response: 76 | """ 77 | Synchronizes Aomail DB with Google servers by fetching and processing email IDs. 78 | 79 | Args: 80 | request (HttpRequest): The HTTP request object containing the user's email and 81 | the number of missed emails. 82 | 83 | Returns: 84 | Response: Contains the number of emails processed from the user's inbox. 85 | """ 86 | parameters: dict = json.loads(request.body) 87 | email = parameters["email"] 88 | nb_missed_emails = parameters["nbMissedEmails"] 89 | user = request.user 90 | 91 | # Resubscribe user to email notifications in case 92 | social_api = SocialAPI.objects.get(user=user, email=email) 93 | check_and_resubscribe_to_missing_resources(user, email) 94 | 95 | services = authenticate_service(user, email, ["gmail"]) 96 | 97 | subscription = Subscription.objects.get(user=user) 98 | start_date = subscription.created_at 99 | email_ids = fetch_email_ids_since(services["gmail"], email, start_date) 100 | 101 | LOGGER.info( 102 | f"Starting to process {len(email_ids)} emails for user ID: {user.id} and social API ID: {social_api.id}" 103 | ) 104 | 105 | nb_processed_emails = 0 106 | with ThreadPoolExecutor(max_workers=10) as executor: 107 | future_to_email_id = {} 108 | 109 | for email_id in email_ids: 110 | if Email.objects.filter(provider_id=email_id).exists(): 111 | continue 112 | 113 | future = executor.submit(email_to_db, social_api, email_id) 114 | future_to_email_id[future] = email_id 115 | 116 | for future in as_completed(future_to_email_id): 117 | email_id = future_to_email_id[future] 118 | try: 119 | if future.result(): # Increment count if email_to_db returns True 120 | nb_processed_emails += 1 121 | except Exception as e: 122 | LOGGER.error(f"Error processing email ID {email_id}: {str(e)}") 123 | 124 | LOGGER.info( 125 | f"All emails have been processed. Processed: {nb_processed_emails}, Missed: {nb_missed_emails}" 126 | ) 127 | 128 | return Response( 129 | {"nbProcessedEmails": nb_processed_emails}, 130 | status=status.HTTP_200_OK, 131 | ) 132 | -------------------------------------------------------------------------------- /backend/aomail/email_providers/imap/authentication.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains the authentication logic for the IMAP protocol. 3 | """ 4 | 5 | import logging 6 | from imap_tools import MailBox, MailBoxUnencrypted, BaseMailBox 7 | from aomail.utils import security 8 | from aomail.constants import SOCIAL_API_REFRESH_TOKEN_KEY 9 | 10 | 11 | LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | def validate_imap_connection( 15 | email_address: str, 16 | app_password_encrypted: str, 17 | imap_host: str, 18 | imap_port: int, 19 | imap_encryption: str, 20 | ) -> bool: 21 | mailbox = connect_to_imap( 22 | email_address, app_password_encrypted, imap_host, imap_port, imap_encryption 23 | ) 24 | if mailbox: 25 | mailbox.logout() 26 | return True 27 | return False 28 | 29 | 30 | def connect_to_imap( 31 | email_address: str, 32 | app_password_encrypted: str, 33 | imap_host: str, 34 | imap_port: int, 35 | imap_encryption: str, 36 | ) -> BaseMailBox | None: 37 | try: 38 | app_password = security.decrypt_text( 39 | SOCIAL_API_REFRESH_TOKEN_KEY, app_password_encrypted 40 | ) 41 | use_tls = imap_encryption == "tls" 42 | LOGGER.info( 43 | f"Validating IMAP (Encryption: {imap_encryption}) connection for {email_address} on {imap_host}:{imap_port}" 44 | ) 45 | 46 | mailbox_class = MailBox if use_tls else MailBoxUnencrypted 47 | mailbox = mailbox_class(imap_host, imap_port) 48 | 49 | LOGGER.info( 50 | f"Logging in to IMAP server for {email_address} on {imap_host}:{imap_port}" 51 | ) 52 | mailbox.login(email_address, app_password) 53 | LOGGER.info( 54 | f"Logged in to IMAP server for {email_address} on {imap_host}:{imap_port}" 55 | ) 56 | return mailbox 57 | 58 | except Exception as e: 59 | LOGGER.error( 60 | f"Error validating IMAP connection for {email_address} on {imap_host}:{imap_port}: {e}" 61 | ) 62 | return None 63 | -------------------------------------------------------------------------------- /backend/aomail/email_providers/imap/emails_sync.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | from concurrent.futures import ThreadPoolExecutor, as_completed 4 | import threading 5 | from aomail.email_providers.utils import email_to_db 6 | from aomail.models import Email, SocialAPI 7 | from django.contrib.auth.models import User 8 | from aomail.email_providers.imap.authentication import connect_to_imap 9 | 10 | 11 | LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | def save_emails_to_db(user: User): 15 | """Saves email from last sync date to db 16 | 17 | Args: 18 | user (User): The user object containing user and email data. 19 | """ 20 | social_apis = SocialAPI.objects.filter(user=user) 21 | for social_api in social_apis: 22 | if social_api.imap_config: 23 | threading.Thread(target=process_emails, args=(social_api,)).start() 24 | 25 | 26 | def process_emails(social_api: SocialAPI): 27 | """Processes emails from the social API. 28 | 29 | Args: 30 | social_api (SocialAPI): The social API object containing user and email data. 31 | """ 32 | mailbox = connect_to_imap( 33 | social_api.email, 34 | social_api.imap_config.app_password, 35 | social_api.imap_config.host, 36 | social_api.imap_config.port, 37 | social_api.imap_config.encryption, 38 | ) 39 | 40 | if not mailbox: 41 | return {} 42 | 43 | last_email_fetched_date = social_api.last_fetched_date.strftime("%d-%b-%Y") 44 | start_time = datetime.datetime.now() 45 | 46 | nb_processed_emails = 0 47 | with ThreadPoolExecutor(max_workers=10) as executor: 48 | future_to_email_id = {} 49 | 50 | # if possible fetch only the ids as we will fetch the data twice otherwise its fine and not a big deal 51 | for email in mailbox.fetch( 52 | criteria=f"SINCE {last_email_fetched_date}", mark_seen=False 53 | ): 54 | message_id = email.headers.get("message-id") 55 | email_id = message_id[0].split("<")[1].split(">")[0] 56 | 57 | if Email.objects.filter(provider_id=email_id).exists(): 58 | continue 59 | 60 | future = executor.submit(email_to_db, social_api, email_id) 61 | future_to_email_id[future] = email_id 62 | 63 | for future in as_completed(future_to_email_id): 64 | email_id = future_to_email_id[future] 65 | try: 66 | if future.result(): # Increment count if email_to_db returns True 67 | nb_processed_emails += 1 68 | except Exception as e: 69 | LOGGER.error(f"Error processing email ID {email_id}: {str(e)}") 70 | 71 | LOGGER.info( 72 | f"All {nb_processed_emails} emails have been processed for {social_api.email} and type_api {social_api.type_api}" 73 | ) 74 | 75 | social_api.last_fetched_date = start_time 76 | -------------------------------------------------------------------------------- /backend/aomail/email_providers/imap/profile.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | import time 4 | from django.contrib.auth.models import User 5 | from aomail.utils import email_processing 6 | from aomail.email_providers.imap.authentication import connect_to_imap 7 | from aomail.models import SocialAPI 8 | 9 | 10 | LOGGER = logging.getLogger(__name__) 11 | 12 | 13 | def get_data(social_api: SocialAPI) -> dict: 14 | """ 15 | Retrieves email statistics for a given IMAP social API. 16 | 17 | Args: 18 | social_api (SocialAPI): The social API object containing user and email data. 19 | 20 | Returns: 21 | dict: A dictionary containing email statistics. 22 | """ 23 | mailbox = connect_to_imap( 24 | social_api.email, 25 | social_api.imap_config.app_password, 26 | social_api.imap_config.host, 27 | social_api.imap_config.port, 28 | social_api.imap_config.encryption, 29 | ) 30 | 31 | num_emails_received = 0 32 | num_emails_read = 0 33 | num_emails_archived = 0 34 | num_emails_starred = 0 35 | num_emails_sent = 0 36 | 37 | if mailbox: 38 | num_emails_received = len(mailbox.numbers()) 39 | num_emails_read = len(mailbox.numbers(criteria="SEEN")) 40 | # not quite accurate though 41 | num_emails_archived = len(mailbox.numbers(criteria="DELETED")) 42 | num_emails_starred = len(mailbox.numbers(criteria="FLAGGED")) 43 | # old date to be sure to get all sent emails 44 | num_emails_sent = len(mailbox.numbers(criteria=f"SENTSINCE 01-Jan-1990")) 45 | mailbox.logout() 46 | 47 | return { 48 | "num_emails_received": num_emails_received, 49 | "num_emails_read": num_emails_read, 50 | "num_emails_archived": num_emails_archived, 51 | "num_emails_starred": num_emails_starred, 52 | "num_emails_sent": num_emails_sent, 53 | } 54 | 55 | 56 | def set_all_contacts(social_api: SocialAPI): 57 | """Saves all contacts by fetching all emails from user's inbox""" 58 | mailbox = connect_to_imap( 59 | social_api.email, 60 | social_api.imap_config.app_password, 61 | social_api.imap_config.host, 62 | social_api.imap_config.port, 63 | social_api.imap_config.encryption, 64 | ) 65 | if not mailbox: 66 | return 67 | 68 | nb_contact_saved = 0 69 | start = time.time() 70 | LOGGER.info( 71 | f"Saving contacts of: {social_api.email} and type_api: {social_api.type_api} for user ID: {social_api.user.id}" 72 | ) 73 | 74 | for email in mailbox.fetch(mark_seen=False): 75 | if email_processing.save_email_sender( 76 | social_api.user, email.from_values.name, email.from_values.email 77 | ): 78 | nb_contact_saved += 1 79 | 80 | for to_email in email.to_values: 81 | if email_processing.save_email_sender( 82 | social_api.user, to_email.name, to_email.email 83 | ): 84 | nb_contact_saved += 1 85 | 86 | for cc_email in email.cc_values: 87 | if email_processing.save_email_sender( 88 | social_api.user, cc_email.name, cc_email.email 89 | ): 90 | nb_contact_saved += 1 91 | 92 | for bcc_email in email.bcc_values: 93 | if email_processing.save_email_sender( 94 | social_api.user, bcc_email.name, bcc_email.email 95 | ): 96 | nb_contact_saved += 1 97 | 98 | formatted_time = str(datetime.timedelta(seconds=time.time() - start)) 99 | LOGGER.info( 100 | f"Saved {nb_contact_saved} contacts of: {social_api.email} for user ID: {social_api.user.id} in {formatted_time}" 101 | ) 102 | mailbox.logout() 103 | -------------------------------------------------------------------------------- /backend/aomail/email_providers/imap/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utils module for IMAP operations 3 | """ 4 | 5 | 6 | def get_imap_email_id(message_id: tuple) -> str: 7 | """ 8 | Extracts the email ID from a message ID string. 9 | 10 | Args: 11 | message_id (tuple): The message ID string. 12 | 13 | Returns: 14 | str: The email ID. 15 | """ 16 | return message_id[0].split("<")[1].split(">")[0] 17 | -------------------------------------------------------------------------------- /backend/aomail/email_providers/microsoft/troubleshooting.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handles regrant consent logic and data synchronization for Microsoft Graph API. 3 | 4 | Endpoints: 5 | - ✅ check_connectivity: Checks if Aomail is connected to the user's Microsoft account. 6 | - ✅ synchronize: Synchronizes Aomail database with Microsoft servers. 7 | """ 8 | 9 | import json 10 | import logging 11 | from concurrent.futures import ThreadPoolExecutor, as_completed 12 | from django.http import HttpRequest 13 | from rest_framework.response import Response 14 | from rest_framework import status 15 | from rest_framework.decorators import api_view 16 | from aomail.utils.security import subscription 17 | from aomail.constants import ( 18 | ALLOWED_PLANS, 19 | ) 20 | from aomail.models import Email, SocialAPI, Subscription 21 | from aomail.email_providers.microsoft.authentication import ( 22 | fetch_email_ids_since, 23 | refresh_access_token, 24 | ) 25 | from aomail.email_providers.utils import email_to_db 26 | from aomail.email_providers.microsoft.webhook import ( 27 | check_and_resubscribe_to_missing_resources, 28 | ) 29 | 30 | 31 | LOGGER = logging.getLogger(__name__) 32 | 33 | 34 | @api_view(["POST"]) 35 | @subscription(ALLOWED_PLANS) 36 | def check_connectivity(request: HttpRequest) -> Response: 37 | """ 38 | Checks the connectivity status of a user's linked email account. 39 | 40 | Args: 41 | request (HttpRequest): The HTTP request object containing the user's email. 42 | 43 | Returns: 44 | Response: Contains the token validity status and the number of missed emails. 45 | """ 46 | parameters: dict = json.loads(request.body) 47 | email = parameters["email"] 48 | user = request.user 49 | check_and_resubscribe_to_missing_resources(user, email) 50 | 51 | subscription = Subscription.objects.get(user=user) 52 | start_date = subscription.created_at 53 | social_api = SocialAPI.objects.get(user=user, email=email) 54 | 55 | access_token = refresh_access_token(social_api) 56 | 57 | if not access_token: 58 | return Response( 59 | { 60 | "isTokenValid": False, 61 | }, 62 | status=status.HTTP_200_OK, 63 | ) 64 | 65 | email_ids = fetch_email_ids_since(access_token, start_date) 66 | 67 | nb_missed_emails = 0 68 | for email_id in email_ids: 69 | if not Email.objects.filter(user=user, provider_id=email_id).exists(): 70 | nb_missed_emails += 1 71 | 72 | return Response( 73 | {"isTokenValid": True, "nbMissedEmails": nb_missed_emails}, 74 | status=status.HTTP_200_OK, 75 | ) 76 | 77 | 78 | @api_view(["POST"]) 79 | @subscription(ALLOWED_PLANS) 80 | def synchronize(request: HttpRequest) -> Response: 81 | """ 82 | Synchronizes Aomail DB with Microsoft Graph API by fetching and processing email IDs. 83 | 84 | Args: 85 | request (HttpRequest): The HTTP request object containing the user's email and 86 | the number of missed emails. 87 | 88 | Returns: 89 | Response: Contains the number of emails processed from the user's inbox. 90 | """ 91 | parameters: dict = json.loads(request.body) 92 | email = parameters["email"] 93 | nb_missed_emails = parameters["nbMissedEmails"] 94 | user = request.user 95 | 96 | subscription = Subscription.objects.get(user=user) 97 | start_date = subscription.created_at 98 | social_api = SocialAPI.objects.get(user=user, email=email) 99 | 100 | access_token = refresh_access_token(social_api) 101 | email_ids = fetch_email_ids_since(access_token, start_date) 102 | 103 | LOGGER.info( 104 | f"Starting to process {len(email_ids)} emails for user ID: {user.id} and social API ID: {social_api.id}" 105 | ) 106 | 107 | nb_processed_emails = 0 108 | with ThreadPoolExecutor(max_workers=10) as executor: 109 | future_to_email_id = {} 110 | 111 | for email_id in email_ids: 112 | if Email.objects.filter(provider_id=email_id).exists(): 113 | continue 114 | 115 | future = executor.submit(email_to_db, social_api, email_id) 116 | future_to_email_id[future] = email_id 117 | 118 | for future in as_completed(future_to_email_id): 119 | email_id = future_to_email_id[future] 120 | try: 121 | if future.result(): # Increment count if email_to_db returns True 122 | nb_processed_emails += 1 123 | except Exception as e: 124 | LOGGER.error(f"Error processing email ID {email_id}: {str(e)}") 125 | 126 | LOGGER.info( 127 | f"All emails have been processed. Processed: {nb_processed_emails}, Missed: {nb_missed_emails}" 128 | ) 129 | 130 | return Response( 131 | {"nbProcessedEmails": nb_processed_emails}, 132 | status=status.HTTP_200_OK, 133 | ) 134 | -------------------------------------------------------------------------------- /backend/aomail/email_providers/smtp/authentication.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains the authentication logic for the SMTP protocol. 3 | """ 4 | 5 | import logging 6 | from smtplib import SMTP, SMTP_SSL 7 | from aomail.constants import SOCIAL_API_REFRESH_TOKEN_KEY 8 | from aomail.utils import security 9 | 10 | 11 | LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | def validate_smtp_connection( 15 | email_address: str, 16 | app_password_encrypted: str, 17 | smtp_host: str, 18 | smtp_port: int, 19 | smtp_encryption: str, 20 | ) -> bool: 21 | smtp = connect_to_smtp( 22 | email_address, app_password_encrypted, smtp_host, smtp_port, smtp_encryption 23 | ) 24 | if smtp: 25 | smtp.quit() 26 | return True 27 | return False 28 | 29 | 30 | def connect_to_smtp( 31 | email_address: str, 32 | app_password_encrypted: str, 33 | smtp_host: str, 34 | smtp_port: int, 35 | smtp_encryption: str, 36 | ) -> SMTP | SMTP_SSL | None: 37 | try: 38 | app_password = security.decrypt_text( 39 | SOCIAL_API_REFRESH_TOKEN_KEY, app_password_encrypted 40 | ) 41 | LOGGER.info( 42 | f"Validating SMTP (Encryption: {smtp_encryption}) connection for {email_address} on {smtp_host}:{smtp_port}" 43 | ) 44 | 45 | smtp_class = SMTP_SSL if smtp_encryption == "ssl" else SMTP 46 | smtp = smtp_class(smtp_host, smtp_port) 47 | if smtp_encryption == "tls": 48 | LOGGER.info(f"Starting TLS connection") 49 | smtp.ehlo() 50 | smtp.starttls() 51 | 52 | LOGGER.info( 53 | f"Logging in to SMTP server for {email_address} on {smtp_host}:{smtp_port}" 54 | ) 55 | smtp.login(email_address, app_password) 56 | LOGGER.info( 57 | f"Logged in to SMTP server for {email_address} on {smtp_host}:{smtp_port}" 58 | ) 59 | return smtp 60 | 61 | except Exception as e: 62 | LOGGER.error( 63 | f"Error validating SMTP connection for {email_address} on {smtp_host}:{smtp_port}: {e}" 64 | ) 65 | return None 66 | -------------------------------------------------------------------------------- /backend/aomail/email_providers/smtp/compose_email.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handles email composition and transfer operations using SMTP protocol. 3 | 4 | Endpoints: 5 | - ✅ send_email: Sends an email. 6 | """ 7 | 8 | import logging 9 | from rest_framework import status 10 | from django.http import HttpRequest 11 | from rest_framework.decorators import api_view 12 | from rest_framework.response import Response 13 | from rest_framework.decorators import api_view 14 | from email.mime.multipart import MIMEMultipart 15 | from email.mime.text import MIMEText 16 | from aomail.utils.security import subscription 17 | from aomail.constants import ALLOW_ALL 18 | from aomail.models import SocialAPI 19 | from aomail.email_providers.smtp.authentication import connect_to_smtp 20 | 21 | LOGGER = logging.getLogger(__name__) 22 | 23 | 24 | @api_view(["POST"]) 25 | @subscription(ALLOW_ALL) 26 | def send_email(request: HttpRequest) -> Response: 27 | """ 28 | Sends an email using the Gmail API. 29 | 30 | Args: 31 | request (HttpRequest): HTTP request object containing POST data with email details. 32 | 33 | Returns: 34 | Response: Response indicating success or error. 35 | """ 36 | user = request.user 37 | email = request.POST.get("email") 38 | social_api = SocialAPI.objects.get(user=user, email=email) 39 | 40 | smtp_connection = connect_to_smtp( 41 | social_api.email, 42 | social_api.smtp_config.app_password, 43 | social_api.smtp_config.host, 44 | social_api.smtp_config.port, 45 | social_api.smtp_config.encryption, 46 | ) 47 | 48 | if not smtp_connection: 49 | return Response( 50 | {"error": "Internal server error"}, 51 | status=status.HTTP_500_INTERNAL_SERVER_ERROR, 52 | ) 53 | 54 | msg = MIMEMultipart("alternative") 55 | msg["Subject"] = request.POST.get("subject") 56 | msg["From"] = social_api.email 57 | 58 | msg.attach(MIMEText(request.POST.get("message"), "html")) 59 | 60 | smtp_connection.sendmail( 61 | social_api.email, 62 | request.POST.getlist("to") 63 | + request.POST.getlist("cc") 64 | + request.POST.getlist("bcc"), 65 | msg.as_string(), 66 | ) 67 | smtp_connection.quit() 68 | 69 | return Response({"message": "Email sent successfully!"}, status=status.HTTP_200_OK) 70 | -------------------------------------------------------------------------------- /backend/aomail/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.6 on 2025-02-08 22:20 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ] 10 | 11 | operations = [ 12 | ] -------------------------------------------------------------------------------- /backend/aomail/migrations/0003_remove_rule_action_reply_recipients.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.6 on 2025-02-24 11:28 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('aomail', '0002_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='rule', 15 | name='action_reply_recipients', 16 | ), 17 | ] -------------------------------------------------------------------------------- /backend/aomail/migrations/0004_preference_llm_provider_preference_model.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.6 on 2025-02-25 14:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('aomail', '0003_remove_rule_action_reply_recipients'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='preference', 15 | name='llm_provider', 16 | field=models.CharField(default='google', max_length=50), 17 | ), 18 | migrations.AddField( 19 | model_name='preference', 20 | name='llm_model', 21 | field=models.CharField(max_length=50, null=True), 22 | ), 23 | ] -------------------------------------------------------------------------------- /backend/aomail/migrations/0005_preference_categorize_and_summarize_email_prompt_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.6 on 2025-02-26 14:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('aomail', '0004_preference_llm_provider_preference_model'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='preference', 15 | name='categorize_and_summarize_email_prompt', 16 | field=models.TextField(max_length=2000, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name='preference', 20 | name='generate_email_prompt', 21 | field=models.TextField(max_length=1000, null=True), 22 | ), 23 | migrations.AddField( 24 | model_name='preference', 25 | name='generate_email_response_prompt', 26 | field=models.TextField(max_length=1000, null=True), 27 | ), 28 | migrations.AddField( 29 | model_name='preference', 30 | name='generate_response_keywords_prompt', 31 | field=models.TextField(max_length=1000, null=True), 32 | ), 33 | migrations.AddField( 34 | model_name='preference', 35 | name='improve_email_draft_prompt', 36 | field=models.TextField(max_length=1000, null=True), 37 | ), 38 | migrations.AddField( 39 | model_name='preference', 40 | name='improve_email_response_prompt', 41 | field=models.TextField(max_length=1000, null=True), 42 | ), 43 | ] -------------------------------------------------------------------------------- /backend/aomail/migrations/0006_emailserverconfig_socialapi_imap_config_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.6 on 2025-03-06 14:08 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('aomail', '0005_preference_categorize_and_summarize_email_prompt_and_more'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='EmailServerConfig', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('app_password', models.CharField(max_length=2000)), 19 | ('host', models.CharField(max_length=30)), 20 | ('port', models.IntegerField()), 21 | ('encryption', models.CharField(max_length=10)), 22 | ], 23 | ), 24 | migrations.AddField( 25 | model_name='socialapi', 26 | name='imap_config', 27 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='imap_config', to='aomail.emailserverconfig'), 28 | ), 29 | migrations.AddField( 30 | model_name='socialapi', 31 | name='smtp_config', 32 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='smtp_config', to='aomail.emailserverconfig'), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /backend/aomail/migrations/0007_socialapi_last_fetched_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.6 on 2025-03-07 16:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('aomail', '0006_emailserverconfig_socialapi_imap_config_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='socialapi', 15 | name='last_fetched_date', 16 | field=models.DateTimeField(auto_now=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/aomail/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/backend/aomail/migrations/__init__.py -------------------------------------------------------------------------------- /backend/aomail/schedule_tasks.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for handling scheduled tasks related to file handling in Django settings. 3 | """ 4 | 5 | import logging 6 | import time 7 | from django.utils import timezone 8 | from datetime import timedelta 9 | from aomail.models import GoogleListener 10 | from aomail.email_providers.google import webhook as google_webhook 11 | from aomail.constants import ENV 12 | 13 | 14 | LOGGER = logging.getLogger(__name__) 15 | 16 | 17 | def renew_gmail_subscriptions(): 18 | """ 19 | Resubscribe all Gmail accounts where their subscription is about to expire. 20 | 21 | This function checks for all GoogleListener instances where the last_modified 22 | date is older than or equal to 3 days from today and attempts to resubscribe 23 | the associated Gmail accounts to email notifications. 24 | """ 25 | start_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S") 26 | LOGGER.info( 27 | f"{start_time} - [{ENV}] Starting the subscription renewal process for Gmail accounts." 28 | ) 29 | start_time = time.time() 30 | 31 | # Calculate the datetime from which all subscriptions need to be renewed 32 | today = timezone.now() 33 | renewal_threshold = today - timedelta(days=3) 34 | 35 | # Get all GoogleListener instances that need to be renewed 36 | google_listeners = GoogleListener.objects.filter( 37 | last_modified__lte=renewal_threshold 38 | ).exclude(social_api__user__subscription__is_block=True) 39 | 40 | nb_subrenew = 0 41 | 42 | for google_listener in google_listeners: 43 | social_api = google_listener.social_api 44 | 45 | if not social_api: 46 | continue 47 | 48 | user = social_api.user 49 | email = social_api.email 50 | 51 | subscribed = google_webhook.subscribe_to_email_notifications(user, email) 52 | if not subscribed: 53 | LOGGER.critical( 54 | f"Failed to renew the subscription for user ID: {user.id}, SocialAPI ID: {social_api.id}" 55 | ) 56 | else: 57 | google_listener.last_modified = today 58 | google_listener.save() 59 | nb_subrenew += 1 60 | LOGGER.info( 61 | f"Successfully renewed the subscription for user ID: {user.id}, SocialAPI ID: {social_api.id}" 62 | ) 63 | 64 | elapsed_time = time.time() - start_time 65 | hours, remainder = divmod(elapsed_time, 3600) 66 | minutes, seconds = divmod(remainder, 60) 67 | formatted_time = f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}" 68 | LOGGER.info(f"Renewed {nb_subrenew} subscriptions in {formatted_time}.") 69 | -------------------------------------------------------------------------------- /backend/config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for Aomail project. 3 | 4 | This module configures ASGI for the Aomail application to handle both 5 | synchronous HTTP and asynchronous WebSocket connections. 6 | 7 | Documentation: 8 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ 9 | """ 10 | 11 | import os 12 | from django.core.asgi import get_asgi_application 13 | 14 | 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") 16 | 17 | application = get_asgi_application() 18 | -------------------------------------------------------------------------------- /backend/config/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | Aomail Project base URL Configuration. 3 | """ 4 | 5 | from django.contrib import admin 6 | from django.urls import path, include 7 | 8 | 9 | urlpatterns = [ 10 | path("admin/", admin.site.urls), 11 | path("aomail/", include("aomail.urls")), 12 | ] 13 | -------------------------------------------------------------------------------- /backend/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Define color codes 4 | RED='\033[0;31m' 5 | GREEN='\033[0;32m' 6 | YELLOW='\033[1;33m' 7 | BLUE='\033[0;34m' 8 | NC='\033[0m' # No Color 9 | 10 | # Function to print messages in color 11 | print_message() { 12 | COLOR=$1 13 | MESSAGE=$2 14 | echo "${COLOR}===> ${MESSAGE}${NC}" 15 | } 16 | 17 | # Create the folder backend/media/pictures if it doesn't exist 18 | print_message $BLUE "Creating essential folders..." 19 | 20 | # Check and create media/pictures folder 21 | if [ ! -d "media/pictures" ]; then 22 | mkdir -p media/pictures 23 | print_message $GREEN "Created media/pictures folder." 24 | else 25 | print_message $GREEN "media/pictures folder already exists." 26 | fi 27 | 28 | # Check and create media/labels folder 29 | if [ ! -d "media/labels" ]; then 30 | mkdir -p media/labels 31 | print_message $GREEN "Created media/labels folder." 32 | else 33 | print_message $GREEN "media/labels folder already exists." 34 | fi 35 | 36 | # Apply database migrations 37 | print_message $BLUE "Applying database migrations..." 38 | python manage.py makemigrations && python manage.py migrate 39 | if [ $? -eq 0 ]; then 40 | print_message $GREEN "Database migrations applied successfully." 41 | else 42 | print_message $RED "Failed to apply database migrations." 43 | exit 1 44 | fi 45 | 46 | # Start cron service 47 | print_message $BLUE "Starting Cron service..." 48 | cron 49 | if [ $? -eq 0 ]; then 50 | print_message $GREEN "Cron service started successfully." 51 | else 52 | print_message $RED "Failed to start Cron service." 53 | exit 1 54 | fi 55 | 56 | # Set cron tasks 57 | print_message $BLUE "Setting cron tasks..." 58 | python manage.py crontab add 59 | if [ $? -eq 0 ]; then 60 | print_message $GREEN "Cron tasks set successfully." 61 | else 62 | print_message $RED "Failed to set cron tasks." 63 | exit 1 64 | fi 65 | 66 | # Display active cron jobs 67 | print_message $BLUE "Active cron jobs:" 68 | python manage.py crontab show 69 | 70 | # Start the Django application 71 | print_message $BLUE "Starting Django application..." 72 | exec "$@" 73 | -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | uvicorn 2 | gunicorn 3 | chardet 4 | pypdf 5 | reportlab 6 | pillow 7 | groq 8 | stripe 9 | django-crontab 10 | python-json-logger 11 | aiohttp 12 | aiosignal 13 | annotated-types 14 | anthropic 15 | anyio 16 | asgiref 17 | attrs 18 | beautifulsoup4 19 | bs4 20 | cachetools 21 | certifi 22 | cffi 23 | charset-normalizer 24 | colorama 25 | cryptography 26 | dataclasses-json 27 | defusedxml 28 | distro 29 | dj-rest-auth 30 | Django 31 | django-allauth 32 | django-cors-headers 33 | django-extensions 34 | djangorestframework 35 | djangorestframework-simplejwt 36 | filelock 37 | frozenlist 38 | fsspec 39 | google-api-core 40 | google-api-python-client 41 | google-auth 42 | google-auth-httplib2 43 | google-auth-oauthlib 44 | googleapis-common-protos 45 | greenlet 46 | h11 47 | httpcore 48 | httplib2 49 | httpx 50 | huggingface-hub 51 | idna 52 | jsonpatch 53 | jsonpointer 54 | langchain 55 | langchain_anthropic 56 | langchain-community 57 | langchain-core 58 | langsmith 59 | marshmallow 60 | mistralai 61 | msal 62 | multidict 63 | mypy-extensions 64 | numpy 65 | oauthlib 66 | openai 67 | orjson 68 | packaging 69 | pandas 70 | protobuf 71 | pyarrow 72 | pyasn1 73 | pyasn1-modules 74 | pycparser 75 | pydantic 76 | pydantic_core 77 | PyJWT 78 | pyparsing 79 | python-dateutil 80 | python3-openid 81 | pytz 82 | psycopg2-binary 83 | PyYAML 84 | requests 85 | requests-oauthlib 86 | rest-framework-simplejwt 87 | rsa 88 | six 89 | sniffio 90 | soupsieve 91 | SQLAlchemy 92 | sqlparse 93 | tenacity 94 | tokenizers 95 | tqdm 96 | typing-inspect 97 | typing_extensions 98 | tzdata 99 | uritemplate 100 | urllib3 101 | yarl 102 | google-generativeai 103 | pytest-django 104 | imap-tools -------------------------------------------------------------------------------- /backend/templates/ai_failed_conv.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ title }} 9 | 10 | 11 | 12 |
13 |

{{ title }}

14 |

An error occurred while attempting to generate a new body response.

15 |

Details:

16 |
    17 |
  • Attempt Number: {{ attempt_number }}
  • 18 |
  • Error: {{ error }}
  • 19 |
  • User: {{ user }}
  • 20 |
21 |

Please investigate urgently.

22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /backend/templates/ai_failed_email.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Critical Alert: Email Processing Failure 9 | 10 | 11 | 12 |
13 |

Critical Alert: AI Email Processing Failure

14 |

An error occurred while attempting to process an email with AI.

15 |

Details:

16 |
    17 |
  • Eror: {{ error }}
  • 18 |
  • Email: {{ email }}
  • 19 |
  • Email Provider: {{ email_provider }}
  • 20 |
  • User: {{ user }}
  • 21 |
22 |

Please investigate urgently.

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /backend/templates/password_reset_email.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Password Reset 8 | 9 | 10 | 11 | 12 | 13 | 46 | 47 |
14 | 16 | 17 | 37 | 38 | 39 | 43 | 44 |
18 |

Password Reset

19 |

You have requested to reset your password. Click the 20 | button below to proceed:

21 | 23 | 24 | 30 | 31 |
26 | Reset 28 | Password 29 |
32 |

If you did not request a password reset, please ignore this 33 | email.

34 |

This link is valid for 1 hour. 35 |

36 |
40 |

For any assistance, please contact our support team at {{ email }}.

42 |
45 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /backend/templates/unsubscribe_failure.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ title }} 9 | 10 | 11 | 12 |
13 |

{{ title }}

14 |

An error occurred while attempting to unsubscribe from Microsoft.

15 |

Details:

16 |
    17 |
  • Attempt Number: {{ attempt_number }}
  • 18 |
  • Subscription ID: {{ subscription_id }}
  • 19 |
  • Email Provider: {{ email_provider }}
  • 20 |
  • User: {{ user }}
  • 21 |
22 |

Please investigate urgently.

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /backend/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.contrib.auth.models import User 3 | from aomail.models import SocialAPI, Statistics 4 | 5 | 6 | @pytest.fixture 7 | def user(): 8 | user, _ = User.objects.get_or_create(username="testuser", password="testpassword") 9 | return user 10 | 11 | 12 | @pytest.fixture 13 | def social_api(user: User): 14 | social_api, _ = SocialAPI.objects.get_or_create( 15 | user=user, 16 | email="testuser@example.com", 17 | type_api="google", 18 | user_description="user_description", 19 | access_token="access_token", 20 | refresh_token="refresh_token", 21 | ) 22 | return social_api 23 | 24 | 25 | @pytest.fixture 26 | def statistics(user: User): 27 | statistics, _ = Statistics.objects.get_or_create(user=user) 28 | return statistics 29 | -------------------------------------------------------------------------------- /backend/tests/test_ai_providers_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | from aomail.ai_providers.utils import extract_json_from_response, count_corrections 4 | from django.contrib.auth.models import User 5 | from aomail.models import Statistics 6 | from aomail.ai_providers.utils import update_tokens_stats 7 | 8 | 9 | def test_extract_json_from_response(): 10 | response_text = """ 11 | Content: ```json 12 | { 13 | "response": "Hello, world!" 14 | } 15 | ``` 16 | """ 17 | assert extract_json_from_response(response_text) == {"response": "Hello, world!"} 18 | response_text = """ 19 | Content: ``` 20 | { 21 | "response": "Hello, world!" 22 | } 23 | ``` 24 | """ 25 | assert extract_json_from_response(response_text) == {"response": "Hello, world!"} 26 | response_text = """ 27 | { 28 | "response": "Hello, world!" 29 | } 30 | """ 31 | assert extract_json_from_response(response_text) == {"response": "Hello, world!"} 32 | 33 | 34 | def test_extract_json_from_response_errors(): 35 | with pytest.raises(json.JSONDecodeError): 36 | extract_json_from_response("```Hello, world!") 37 | with pytest.raises(json.JSONDecodeError): 38 | extract_json_from_response("") 39 | with pytest.raises(json.JSONDecodeError): 40 | extract_json_from_response("```{'key': 'value}```") 41 | with pytest.raises(json.JSONDecodeError): 42 | extract_json_from_response("```{Hello, world!}```") 43 | with pytest.raises(json.JSONDecodeError): 44 | extract_json_from_response("```{Hello, world!```") 45 | with pytest.raises(json.JSONDecodeError): 46 | extract_json_from_response("```json{Hello, world!```") 47 | 48 | 49 | def test_count_corrections(): 50 | assert ( 51 | count_corrections( 52 | "Hello, world!", "Hello, world!", "Hello, world!", "Hello, world!" 53 | ) 54 | == 0 55 | ) 56 | assert ( 57 | count_corrections( 58 | "Hello, world!", "Hello, world!", "Hello world!", "Hello world!" 59 | ) 60 | == 2 61 | ) 62 | assert ( 63 | count_corrections( 64 | "Original subject", "Original body", "Corrected subject", "Corrected body" 65 | ) 66 | == 2 67 | ) 68 | 69 | 70 | @pytest.fixture 71 | def test_update_tokens_stats(user: User, statistics: Statistics): 72 | update_tokens_stats(user, {"tokens_input": 10, "tokens_output": 20}) 73 | statistics.refresh_from_db() 74 | assert statistics.nb_tokens_input == 10 75 | assert statistics.nb_tokens_output == 20 76 | -------------------------------------------------------------------------------- /backend/tests/test_email_processing_utils.py: -------------------------------------------------------------------------------- 1 | from aomail.utils.email_processing import ( 2 | camel_to_snake, 3 | is_no_reply_email, 4 | preprocess_email, 5 | validate_email_address, 6 | snake_to_camel, 7 | contains_html, 8 | concat_text, 9 | ) 10 | 11 | 12 | def test_validate_email_address(): 13 | assert validate_email_address("test.test@gmail.com") == True 14 | assert validate_email_address("faKeEmail@outlook.fr") == True 15 | assert validate_email_address("faKe239Email@outlook.fr") == True 16 | assert validate_email_address("") == False 17 | assert validate_email_address("test.test.test") == False 18 | 19 | 20 | def test_camel_to_snake(): 21 | assert camel_to_snake("nbEmailsReceived") == "nb_emails_received" 22 | assert camel_to_snake("camelCase") == "camel_case" 23 | assert camel_to_snake("ABC") == "a_b_c" 24 | assert camel_to_snake("alreadysnakecase") == "alreadysnakecase" 25 | assert camel_to_snake("") == "" 26 | 27 | 28 | def test_snake_to_camel(): 29 | assert snake_to_camel("nb_emails_received") == "nbEmailsReceived" 30 | assert snake_to_camel("camel_case") == "camelCase" 31 | assert snake_to_camel("already_snake") == "alreadySnake" 32 | assert snake_to_camel("") == "" 33 | assert snake_to_camel("_hidden") == "Hidden" 34 | 35 | 36 | def test_is_no_reply_email(): 37 | assert is_no_reply_email("no-reply@gmail.com") == True 38 | assert is_no_reply_email("donotreply@gmail.com") == True 39 | assert is_no_reply_email("do-not-reply@gmail.com") == True 40 | assert is_no_reply_email("noreply@gmail.com") == True 41 | assert is_no_reply_email("test.test@gmail.com") == False 42 | assert is_no_reply_email("faKeEmail@outlook.fr") == False 43 | assert is_no_reply_email("faKe239Email@outlook.fr") == False 44 | 45 | 46 | def test_preprocess_email(): 47 | assert preprocess_email("") == "" 48 | assert preprocess_email("https://aomail.ai") == "https://aomail.ai" 49 | assert preprocess_email("http://aomail.ai") == "http://aomail.ai" 50 | assert preprocess_email("random text\r\nend of text") == "random text\nend of text" 51 | assert ( 52 | preprocess_email("random text\n\n\n\n\nend of text") 53 | == "random text\n\nend of text" 54 | ) 55 | assert preprocess_email("[image:[myImageStuff]") == "" 56 | assert preprocess_email(" spaces at ends ") == "spaces at ends" 57 | assert preprocess_email("http://link.com some text") == "http://link.com some text" 58 | assert ( 59 | preprocess_email("mixed\r\nline\nending\r\nstyles") 60 | == "mixed\nline\nending\nstyles" 61 | ) 62 | 63 | 64 | def test_contains_html(): 65 | assert contains_html("
test
") == True 66 | assert contains_html("plain text") == False 67 | assert contains_html("  with entity") == True 68 | assert contains_html("") == True 69 | assert contains_html(b"bytes test") == True 70 | 71 | 72 | def test_concat_text(): 73 | assert concat_text(None, "first") == "first" 74 | assert concat_text("existing", "append") == "existingappend" 75 | assert concat_text(None, b"bytes text") == "bytes text" 76 | assert concat_text("existing", b"bytes append") == "existingbytes append" 77 | -------------------------------------------------------------------------------- /backend/tests/test_imap_utils.py: -------------------------------------------------------------------------------- 1 | from aomail.email_providers.imap.utils import get_imap_email_id 2 | 3 | 4 | def test_get_imap_email_id(): 5 | assert ( 6 | get_imap_email_id(("",)) 7 | == "Bhit1ZtWVkWqyXNbQwdXcw@notifications.google.com" 8 | ) 9 | assert ( 10 | get_imap_email_id(("",)) 11 | == "fKV5TKolhqO--dQMhbnSCg@notifications.google.com" 12 | ) 13 | assert ( 14 | get_imap_email_id( 15 | ( 16 | "\r\n ", 17 | ) 18 | ) 19 | == "DU0PR10MB7052CBFED79FC823749C55E2DDD12@DU0PR10MB7052.EURPRD10.PROD.OUTLOOK.COM" 20 | ) 21 | assert ( 22 | get_imap_email_id( 23 | ("",) 24 | ) 25 | == "AC700000000107DF4B9116B80D4efs_mkt_prod2@don.efs.sante.fr" 26 | ) 27 | assert ( 28 | get_imap_email_id( 29 | ( 30 | "\r\n ", 31 | ) 32 | ) 33 | == "AS8P251MB08879403671AEC41C692F13084D62@AS8P251MB0887.EURP251.PROD.OUTLOOK.COM" 34 | ) 35 | -------------------------------------------------------------------------------- /backend/tests/test_security.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from aomail.utils.security import encrypt_text, decrypt_text 3 | 4 | 5 | @pytest.fixture 6 | def encryption_key(): 7 | return "XP6XNlULLDpZnZvskYE_dvJ3PPpXsmtFAv37Dlt3ak4=" 8 | 9 | 10 | def test_encrypt_text(encryption_key: str): 11 | assert encrypt_text(encryption_key, "test") != "test" 12 | 13 | 14 | def test_decrypt_text(encryption_key: str): 15 | assert decrypt_text(encryption_key, encrypt_text(encryption_key, "test")) == "test" 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | image: postgres:13-alpine 4 | environment: 5 | POSTGRES_USER: ${POSTGRES_USER} 6 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 7 | POSTGRES_DB: ${POSTGRES_DB} 8 | volumes: 9 | - postgres_data:/var/lib/postgresql/data 10 | healthcheck: 11 | test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER}" ] 12 | interval: 10s 13 | timeout: 5s 14 | retries: 5 15 | networks: 16 | - backend_network 17 | ports: 18 | - "${DB_PORT}:5432" 19 | restart: unless-stopped 20 | 21 | backend_dev: 22 | build: 23 | context: ./backend 24 | target: development 25 | environment: 26 | - FRONTEND_PORT=${FRONTEND_PORT} 27 | - BACKEND_PORT=${BACKEND_PORT} 28 | - ENV=${ENV} 29 | - DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} 30 | ports: 31 | - "${BACKEND_PORT}:8000" 32 | depends_on: 33 | db: 34 | condition: service_healthy 35 | command: [ "python", "manage.py", "runserver", "0.0.0.0:8000" ] 36 | networks: 37 | - backend_network 38 | volumes: 39 | - ./backend:/app 40 | - /home/prod/prod/backend/media/agent_icon:/app/media/agent_icon 41 | - /home/prod/prod/backend/media/pictures:/app/media/pictures 42 | - /home/prod/prod/backend/media/labels:/app/media/labels 43 | restart: unless-stopped 44 | 45 | backend_prod: 46 | build: 47 | context: ./backend 48 | target: production 49 | environment: 50 | - FRONTEND_PORT=${FRONTEND_PORT} 51 | - BACKEND_PORT=${BACKEND_PORT} 52 | - ENV=${ENV} 53 | - DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} 54 | ports: 55 | - "${BACKEND_PORT}:8000" 56 | depends_on: 57 | db: 58 | condition: service_healthy 59 | command: [ "gunicorn", "config.asgi:application", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000" ] 60 | networks: 61 | - backend_network 62 | volumes: 63 | - /home/prod/prod/backend/media/agent_icon:/app/media/agent_icon 64 | - /home/prod/prod/backend/media/pictures:/app/media/pictures 65 | - /home/prod/prod/backend/media/labels:/app/media/labels 66 | restart: unless-stopped 67 | 68 | frontend_dev: 69 | build: 70 | context: ./frontend 71 | target: development 72 | args: 73 | VUE_APP_ENV: ${ENV} 74 | environment: 75 | - NODE_ENV=${NODE_ENV} 76 | - VUE_APP_ENV=${ENV} 77 | - ALLOWED_HOSTS=${ALLOWED_HOSTS} 78 | - VUE_APP_BASE_URL=${BASE_URL} 79 | - VUE_APP_API_BASE_URL=${API_BASE_URL} 80 | ports: 81 | - "${FRONTEND_PORT}:${SERVER_FRONTEND_PORT}" 82 | depends_on: 83 | - backend_dev 84 | networks: 85 | - frontend_network 86 | volumes: 87 | - ./frontend:/app 88 | restart: unless-stopped 89 | 90 | frontend_prod: 91 | build: 92 | context: ./frontend 93 | target: production 94 | args: 95 | VUE_APP_ENV: ${ENV} 96 | VUE_APP_BASE_URL: ${BASE_URL} 97 | VUE_APP_API_BASE_URL: ${API_BASE_URL} 98 | environment: 99 | - NODE_ENV=${NODE_ENV} 100 | - VUE_APP_ENV=${ENV} 101 | - VUE_APP_BASE_URL=${BASE_URL} 102 | - VUE_APP_API_BASE_URL=${API_BASE_URL} 103 | ports: 104 | - "${FRONTEND_PORT}:${SERVER_FRONTEND_PORT}" 105 | depends_on: 106 | - backend_prod 107 | networks: 108 | - frontend_network 109 | restart: unless-stopped 110 | 111 | volumes: 112 | postgres_data: 113 | 114 | 115 | networks: 116 | frontend_network: 117 | backend_network: 118 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ["plugin:vue/vue3-essential", "eslint:recommended", "@vue/typescript/recommended"], 7 | parserOptions: { 8 | ecmaVersion: 2020, 9 | }, 10 | globals: { 11 | defineProps: "readonly", 12 | defineEmits: "readonly", 13 | defineExpose: "readonly", 14 | withDefaults: "readonly", 15 | }, 16 | rules: { 17 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 18 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", 19 | "vue/multi-word-component-names": "off", 20 | "@typescript-eslint/no-explicit-any": "off", 21 | }, 22 | ignorePatterns: ["src/assets/*"], 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Base stage for building the application 2 | FROM node:lts-alpine AS base 3 | WORKDIR /app 4 | COPY package*.json ./ 5 | RUN npm install 6 | COPY . . 7 | 8 | # Development mode 9 | FROM base AS development 10 | EXPOSE 8080 11 | CMD ["npm", "run", "serve"] 12 | 13 | # Production mode 14 | FROM base AS build 15 | ARG VUE_APP_ENV 16 | ARG VUE_APP_BASE_URL 17 | ARG VUE_APP_API_BASE_URL 18 | RUN npm run build 19 | 20 | FROM nginx:alpine AS production 21 | COPY --from=build /app/dist /usr/share/nginx/html 22 | COPY nginx.conf /etc/nginx/conf.d/default.conf 23 | EXPOSE 80 24 | CMD ["nginx", "-g", "daemon off;"] 25 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"], 3 | }; 4 | -------------------------------------------------------------------------------- /frontend/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | # root directory for static files 5 | root /usr/share/nginx/html; 6 | index index.html index.htm; 7 | 8 | # If the request does not match a file, serve index.html 9 | location / { 10 | try_files $uri $uri/ /index.html; 11 | } 12 | 13 | # Serve static files directly 14 | location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { 15 | try_files $uri =404; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aomail", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "format": "prettier --write --config ../.prettierrc.json --ignore-path ../.prettierignore --ignore-unknown '**/*.!(json)'", 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "@fortawesome/fontawesome-free": "^6.6.0", 13 | "@headlessui/vue": "^1.7.22", 14 | "@heroicons/vue": "^2.1.5", 15 | "@stripe/stripe-js": "^4.8.0", 16 | "@vuepic/vue-datepicker": "^9.0.2", 17 | "chart.js": "^4.4.7", 18 | "core-js": "^3.8.3", 19 | "echarts": "^5.6.0", 20 | "moment-timezone": "^0.5.45", 21 | "pdf-lib": "^1.17.1", 22 | "quill": "^2.0.2", 23 | "tippy.js": "^6.3.7", 24 | "vue": "^3.2.13", 25 | "vue-flag-icon": "^2.1.0", 26 | "vue-i18n": "^9.13.1", 27 | "vue-multiselect": "^3.2.0", 28 | "vue-router": "^4.0.3" 29 | }, 30 | "devDependencies": { 31 | "@tailwindcss/forms": "^0.5.7", 32 | "@types/node": "^22.1.0", 33 | "@types/vue-i18n": "^7.0.0", 34 | "@typescript-eslint/eslint-plugin": "^5.62.0", 35 | "@typescript-eslint/parser": "^5.62.0", 36 | "@vue/cli-plugin-babel": "~5.0.0", 37 | "@vue/cli-plugin-eslint": "~5.0.0", 38 | "@vue/cli-plugin-router": "~5.0.0", 39 | "@vue/cli-plugin-typescript": "~5.0.0", 40 | "@vue/cli-service": "~5.0.0", 41 | "@vue/compiler-sfc": "^3.4.35", 42 | "@vue/eslint-config-typescript": "^9.1.0", 43 | "@vue/runtime-core": "^3.4.35", 44 | "autoprefixer": "^10.4.20", 45 | "eslint": "^7.32.0", 46 | "eslint-config-prettier": "^8.3.0", 47 | "eslint-plugin-prettier": "^4.0.0", 48 | "eslint-plugin-vue": "^8.7.1", 49 | "postcss": "^8.4.40", 50 | "prettier": "^2.4.1", 51 | "tailwindcss": "^3.4.7", 52 | "terser-webpack-plugin": "^5.3.0", 53 | "typescript": "~4.5.5", 54 | "webpack-obfuscator": "^3.2.0" 55 | }, 56 | "browserslist": [ 57 | "> 1%", 58 | "last 2 versions", 59 | "not dead", 60 | "not ie 11" 61 | ] 62 | } -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Aomail 11 | 12 | 13 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /frontend/public/logo-aomail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/frontend/public/logo-aomail.png -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /frontend/src/assets/ao-happy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/frontend/src/assets/ao-happy.png -------------------------------------------------------------------------------- /frontend/src/assets/ao-neutral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/frontend/src/assets/ao-neutral.png -------------------------------------------------------------------------------- /frontend/src/assets/ao-new-idea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/frontend/src/assets/ao-new-idea.png -------------------------------------------------------------------------------- /frontend/src/assets/ao-prompt-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/frontend/src/assets/ao-prompt-error.png -------------------------------------------------------------------------------- /frontend/src/assets/css/fonts.css: -------------------------------------------------------------------------------- 1 | /* latin-ext */ 2 | @font-face { 3 | font-family: 'Poppins'; 4 | font-style: normal; 5 | font-weight: 200; 6 | font-display: swap; 7 | src: url('../fonts/pxiByp8kv8JHgFVrLFj_Z1JlFc-K.woff2') format('woff2'); 8 | unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; 9 | } 10 | /* latin */ 11 | @font-face { 12 | font-family: 'Poppins'; 13 | font-style: normal; 14 | font-weight: 200; 15 | font-display: swap; 16 | src: url('../fonts/pxiByp8kv8JHgFVrLFj_Z1xlFQ.woff2') format('woff2'); 17 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 18 | } 19 | /* latin-ext */ 20 | @font-face { 21 | font-family: 'Poppins'; 22 | font-style: normal; 23 | font-weight: 300; 24 | font-display: swap; 25 | src: url('../fonts/pxiByp8kv8JHgFVrLDz8Z1JlFc-K.woff2') format('woff2'); 26 | unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; 27 | } 28 | /* latin */ 29 | @font-face { 30 | font-family: 'Poppins'; 31 | font-style: normal; 32 | font-weight: 300; 33 | font-display: swap; 34 | src: url('../fonts/pxiByp8kv8JHgFVrLDz8Z1xlFQ.woff2') format('woff2'); 35 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 36 | } 37 | /* latin-ext */ 38 | @font-face { 39 | font-family: 'Poppins'; 40 | font-style: normal; 41 | font-weight: 400; 42 | font-display: swap; 43 | src: url('../fonts/pxiEyp8kv8JHgFVrJJnecmNE.woff2') format('woff2'); 44 | unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; 45 | } 46 | /* latin */ 47 | @font-face { 48 | font-family: 'Poppins'; 49 | font-style: normal; 50 | font-weight: 400; 51 | font-display: swap; 52 | src: url('../fonts/pxiEyp8kv8JHgFVrJJfecg.woff2') format('woff2'); 53 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 54 | } 55 | /* latin-ext */ 56 | @font-face { 57 | font-family: 'Poppins'; 58 | font-style: normal; 59 | font-weight: 500; 60 | font-display: swap; 61 | src: url('../fonts/pxiByp8kv8JHgFVrLGT9Z1JlFc-K.woff2') format('woff2'); 62 | unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; 63 | } 64 | /* latin */ 65 | @font-face { 66 | font-family: 'Poppins'; 67 | font-style: normal; 68 | font-weight: 500; 69 | font-display: swap; 70 | src: url('../fonts/pxiByp8kv8JHgFVrLGT9Z1xlFQ.woff2') format('woff2'); 71 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 72 | } 73 | /* latin-ext */ 74 | @font-face { 75 | font-family: 'Poppins'; 76 | font-style: normal; 77 | font-weight: 600; 78 | font-display: swap; 79 | src: url('../fonts/pxiByp8kv8JHgFVrLEj6Z1JlFc-K.woff2') format('woff2'); 80 | unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; 81 | } 82 | /* latin */ 83 | @font-face { 84 | font-family: 'Poppins'; 85 | font-style: normal; 86 | font-weight: 600; 87 | font-display: swap; 88 | src: url('../fonts/pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2') format('woff2'); 89 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 90 | } -------------------------------------------------------------------------------- /frontend/src/assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; 4 | 5 | .ql-container { 6 | border: none !important; 7 | border-radius: 0 !important; 8 | box-shadow: none !important; 9 | background-color: #ffffff !important; 10 | min-height: 200px !important; 11 | font-size: 14px !important; 12 | line-height: 1.5 !important; 13 | color: #1f2937 !important; 14 | padding: 1rem !important; 15 | } 16 | 17 | .ql-editor { 18 | padding: 0 !important; 19 | min-height: 200px !important; 20 | font-size: 14px !important; 21 | line-height: 1.5 !important; 22 | color: #1f2937 !important; 23 | } 24 | 25 | .ql-snow { 26 | border: none !important; 27 | border-radius: 8px !important; 28 | overflow: visible !important; 29 | } 30 | 31 | .ql-snow .ql-toolbar { 32 | border: none !important; 33 | background-color: #f8fafc !important; 34 | border-top-left-radius: 8px !important; 35 | border-top-right-radius: 8px !important; 36 | position: relative !important; 37 | z-index: 2 !important; 38 | } 39 | 40 | .ql-snow .ql-container { 41 | border: none !important; 42 | background-color: #ffffff !important; 43 | border-bottom-left-radius: 8px !important; 44 | border-bottom-right-radius: 8px !important; 45 | } 46 | 47 | .ql-snow .ql-picker { 48 | position: relative !important; 49 | z-index: 3 !important; 50 | } 51 | 52 | .ql-snow .ql-picker-options { 53 | position: absolute !important; 54 | z-index: 50 !important; 55 | } 56 | 57 | .ql-snow .ql-picker-label:hover .ql-stroke, 58 | .ql-snow .ql-picker-item:hover .ql-stroke, 59 | .ql-snow .ql-picker-item.ql-selected .ql-stroke, 60 | .ql-snow .ql-picker-label.ql-active .ql-stroke, 61 | .ql-snow .ql-picker-label.ql-expanded .ql-stroke, 62 | .ql-snow .ql-toolbar button:hover .ql-stroke, 63 | .ql-snow .ql-toolbar button:hover .ql-fill, 64 | .ql-snow .ql-toolbar button.ql-active .ql-stroke, 65 | .ql-snow .ql-toolbar button.ql-active .ql-fill { 66 | stroke: #4f46e5; 67 | fill: #4f46e5; 68 | } 69 | 70 | #dynamicTextarea { 71 | overflow-y: hidden; 72 | resize: none; 73 | } 74 | 75 | ::-webkit-scrollbar { 76 | width: 8px; 77 | margin-right: 10px; 78 | } 79 | 80 | ::-webkit-scrollbar-track { 81 | border-radius: 0.75rem; 82 | background-color: rgba(240, 240, 240, 0.5); 83 | } 84 | 85 | ::-webkit-scrollbar-thumb { 86 | background-color: darkgrey; 87 | border-radius: 10px; 88 | } 89 | 90 | ::-webkit-scrollbar-thumb:hover { 91 | background-color: grey; 92 | } 93 | 94 | #NewMailAI::-webkit-scrollbar { 95 | margin-right: 100px; 96 | } 97 | 98 | #firstMainColumn, 99 | #secondMainColumn { 100 | flex: 1; 101 | transition: flex 0.3s ease; 102 | } 103 | 104 | @keyframes spin { 105 | 0% { 106 | transform: rotate(0deg); 107 | } 108 | 109 | 100% { 110 | transform: rotate(360deg); 111 | } 112 | } 113 | 114 | .loading-spinner { 115 | border: 4px solid rgba(0, 0, 0, 0.3); 116 | border-top: 4px solid #000; 117 | border-radius: 50%; 118 | width: 40px; 119 | height: 40px; 120 | animation: spin 2s linear infinite; 121 | } 122 | 123 | .fade-enter-active { 124 | transition: opacity 0.3s ease; 125 | } 126 | 127 | .fade-enter { 128 | opacity: 0; 129 | } 130 | 131 | .custom-scrollbar::-webkit-scrollbar-track { 132 | margin: 4px; 133 | } 134 | -------------------------------------------------------------------------------- /frontend/src/assets/eye-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 17 | 18 | 19 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/pxiByp8kv8JHgFVrLDz8Z1JlFc-K.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/frontend/src/assets/fonts/pxiByp8kv8JHgFVrLDz8Z1JlFc-K.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/pxiByp8kv8JHgFVrLDz8Z1xlFQ.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/frontend/src/assets/fonts/pxiByp8kv8JHgFVrLDz8Z1xlFQ.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/pxiByp8kv8JHgFVrLEj6Z1JlFc-K.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/frontend/src/assets/fonts/pxiByp8kv8JHgFVrLEj6Z1JlFc-K.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/frontend/src/assets/fonts/pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/pxiByp8kv8JHgFVrLFj_Z1JlFc-K.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/frontend/src/assets/fonts/pxiByp8kv8JHgFVrLFj_Z1JlFc-K.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/pxiByp8kv8JHgFVrLFj_Z1xlFQ.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/frontend/src/assets/fonts/pxiByp8kv8JHgFVrLFj_Z1xlFQ.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/pxiByp8kv8JHgFVrLGT9Z1JlFc-K.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/frontend/src/assets/fonts/pxiByp8kv8JHgFVrLGT9Z1JlFc-K.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/pxiByp8kv8JHgFVrLGT9Z1xlFQ.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/frontend/src/assets/fonts/pxiByp8kv8JHgFVrLGT9Z1xlFQ.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/pxiEyp8kv8JHgFVrJJfecg.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/frontend/src/assets/fonts/pxiEyp8kv8JHgFVrJJfecg.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/pxiEyp8kv8JHgFVrJJnecmNE.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/frontend/src/assets/fonts/pxiEyp8kv8JHgFVrJJnecmNE.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/js/plateform.d.ts: -------------------------------------------------------------------------------- 1 | declare module './assets/js/plateform.js'; -------------------------------------------------------------------------------- /frontend/src/assets/logo-aomail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/frontend/src/assets/logo-aomail.png -------------------------------------------------------------------------------- /frontend/src/assets/logos/apple.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | apple [#173] 9 | Created with Sketch. 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend/src/assets/logos/fastmail.svg: -------------------------------------------------------------------------------- 1 | 16 | 21 | 26 | 31 | 37 | -------------------------------------------------------------------------------- /frontend/src/assets/logos/gmx.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /frontend/src/assets/logos/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/src/assets/logos/la_poste.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 20221102_lp_logotype_bleu_vertical_rvb-63652c442d623-ai 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 33 | 37 | 41 | 45 | 47 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /frontend/src/assets/logos/microsoft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/src/assets/logos/orange.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/src/assets/logos/sfr.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | image/svg+xml 14 | 15 | 16 | 17 | 18 | 19 | 20 | 27 | 29 | 32 | -------------------------------------------------------------------------------- /frontend/src/assets/logos/yahoo.svg: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/src/assets/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aomail-ai/aomail-app/d5977a0eba391d3fc00b7281478f56d7331ea93c/frontend/src/assets/user.png -------------------------------------------------------------------------------- /frontend/src/declaration.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const value: string; 3 | export default value; 4 | } 5 | 6 | declare module "*.svg" { 7 | const value: string; 8 | export default value; 9 | } 10 | 11 | declare module "@heroicons/vue/*" { 12 | import { DefineComponent } from "vue"; 13 | const component: DefineComponent; 14 | export default component; 15 | } 16 | 17 | declare module "@heroicons/vue/24/outline/*" { 18 | import { DefineComponent } from "vue"; 19 | const component: DefineComponent; 20 | export default component; 21 | } 22 | 23 | declare module "moment-timezone" { 24 | import moment from "moment"; 25 | export = moment; 26 | } 27 | 28 | declare module "*.vue" { 29 | import { DefineComponent } from "vue"; 30 | const component: DefineComponent; 31 | export default component; 32 | } 33 | 34 | declare module "vue3-timepicker" { 35 | import { Component } from "vue"; 36 | const TimePicker: Component; 37 | export default TimePicker; 38 | } 39 | 40 | declare module "vue-flag-icon"; 41 | -------------------------------------------------------------------------------- /frontend/src/global/components/Conversation/ChatBubble.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 55 | -------------------------------------------------------------------------------- /frontend/src/global/components/ManualEmail/ManualEmail.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 54 | -------------------------------------------------------------------------------- /frontend/src/global/components/ManualEmail/RecipientItem.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 92 | -------------------------------------------------------------------------------- /frontend/src/global/components/ManualEmail/RecipientsSection.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 32 | -------------------------------------------------------------------------------- /frontend/src/global/components/ManualEmail/SelectedEmailChoiceButton.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 67 | -------------------------------------------------------------------------------- /frontend/src/global/components/NotificationTimer.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 117 | -------------------------------------------------------------------------------- /frontend/src/global/components/ResizableDivider.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | -------------------------------------------------------------------------------- /frontend/src/global/components/SendAiInstructionButton.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 73 | -------------------------------------------------------------------------------- /frontend/src/global/const.ts: -------------------------------------------------------------------------------- 1 | export const VERSION = "v 1.5"; 2 | export const BASE_URL = process.env.VUE_APP_BASE_URL; 3 | export const API_BASE_URL = process.env.VUE_APP_API_BASE_URL; 4 | export const PASSWORD_MIN_LENGTH = 8; 5 | export const PASSWORD_MAX_LENGTH = 128; 6 | export const MICROSOFT = "microsoft"; 7 | export const GOOGLE = "google"; 8 | export const YAHOO = "yahoo"; 9 | export const APPLE = "apple"; 10 | export const IMPORTANT = "important"; 11 | export const INFORMATIVE = "informative"; 12 | export const USELESS = "useless"; 13 | export const DEFAULT_CATEGORY = "Others"; 14 | export const ANSWER_REQUIRED = "Answer Required"; 15 | export const MIGHT_REQUIRE_ANSWER = "Might Require Answer"; 16 | export const NO_ANSWER_REQUIRED = "No Answer Required"; 17 | export const HIGHLY_RELEVANT = "Highly Relevant"; 18 | export const POSSIBLY_RELEVANT = "Possibly Relevant"; 19 | export const NOT_RELEVANT = "Not Relevant"; 20 | export const SPAM = "spam"; 21 | export const SCAM = "scam"; 22 | export const NEWSLETTER = "newsletter"; 23 | export const NOTIFICATION = "notification"; 24 | export const MEETING = "meeting"; 25 | export const POP_UP_ERROR_COLOR = "bg-red-200/[.89] border border-red-400"; 26 | export const POP_UP_SUCCESS_COLOR = "bg-green-200/[.89] border border-green-400"; 27 | export const AOMAIL_SEARCH_KEY = "aomail"; 28 | export const API_SEARCH_KEY = "api"; 29 | export const ALLOWED_THEMES = ["light", "dark"]; 30 | export const ALLOWED_LANGUAGES = ["french", "american", "german", "russian", "spanish", "chinese", "indian"] as const; 31 | export type AllowedLanguageType = (typeof ALLOWED_LANGUAGES)[number]; 32 | export const UNAUTHENTICATED_URLS = [ 33 | `${BASE_URL}`, 34 | `${BASE_URL}signup`, 35 | `${BASE_URL}signup-link`, 36 | `${BASE_URL}password-reset-link`, 37 | `${BASE_URL}reset-password-form`, 38 | `${BASE_URL}not-authorized`, 39 | ]; 40 | export const STRIPE_PUBLISHABLE_KEY = 41 | "pk_live_51Q9kHvK8H3QtVm1poy56IRXxMg7VUycAoIz7v6WWS1RoupyTQjO1MvNt1o6H3xAHW9fylKUyEFbnJgB5KYhuKDra00ob4EvFBO"; 42 | export const SUBSCRIPTION_MANAGEMENT_URL = "https://billing.stripe.com/p/login/6oE9AK5iOe0ycs8fYY"; 43 | -------------------------------------------------------------------------------- /frontend/src/global/emailProviders.ts: -------------------------------------------------------------------------------- 1 | export const pinnedProviders = ["google", "microsoft", "apple", "yahoo", "other"]; 2 | 3 | export const knownProviders = [ 4 | { 5 | name: "Fastmail", 6 | typeApi: "fastmail", 7 | link: "https://app.fastmail.com/settings/security/apps/new", 8 | imapHost: "imap.fastmail.com", 9 | imapPort: 993, 10 | imapEncryption: "tls", 11 | smtpHost: "smtp.fastmail.com", 12 | smtpPort: 465, 13 | smtpEncryption: "ssl", 14 | }, 15 | { 16 | name: "GMX", 17 | typeApi: "gmx", 18 | link: "https://support.gmx.com/pop-imap/toggle.html", 19 | imapHost: "imap.gmx.com", 20 | imapPort: 993, 21 | imapEncryption: "tls", 22 | smtpHost: "mail.gmx.com", 23 | smtpPort: 465, 24 | smtpEncryption: "ssl", 25 | }, 26 | { 27 | name: "Orange", 28 | typeApi: "orange", 29 | link: "https://assistance.orange.fr/ordinateurs-peripheriques/installer-et-utiliser/l-utilisation-du-mail-et-du-cloud/mail-orange/le-mail-orange-nouvelle-version/parametrer-la-boite-mail/mail-orange-comment-acceder-a-sa-boite-mail-orange-depuis-une-application-ou-un-logiciel-de-messagerie-non-fourni-par-orange_434630-964290#onglet1", 30 | imapHost: "imap.orange.fr", 31 | imapPort: 993, 32 | imapEncryption: "tls", 33 | smtpHost: "smtp.orange.fr", 34 | smtpPort: 465, 35 | smtpEncryption: "ssl", 36 | }, 37 | { 38 | name: "La Poste", 39 | typeApi: "la_poste", 40 | link: "https://aide.laposte.net/contents/comment-parametrer-un-logiciel-de-messagerie-pour-envoyer-et-recevoir-mes-courriers-electroniques", 41 | imapHost: "imap.laposte.net", 42 | imapPort: 993, 43 | imapEncryption: "tls", 44 | smtpHost: "smtp.laposte.net", 45 | smtpPort: 587, 46 | smtpEncryption: "tls", 47 | }, 48 | { 49 | name: "Free", 50 | typeApi: "free", 51 | link: "https://assistance.free.fr/articles/609", 52 | imapHost: "imap.free.fr", 53 | imapPort: 993, 54 | imapEncryption: "tls", 55 | smtpHost: "smtp.free.fr", 56 | smtpPort: 587, 57 | smtpEncryption: "tls", 58 | }, 59 | { 60 | name: "Bouygues", 61 | typeApi: "bouygues", 62 | link: "https://www.assistance.bouyguestelecom.fr/s/article/logiciel-messagerie-emails-bbox", 63 | imapHost: "imap.bbox.fr", 64 | imapPort: 993, 65 | imapEncryption: "tls", 66 | smtpHost: "smtp.bbox.fr", 67 | smtpPort: 465, 68 | smtpEncryption: "ssl", 69 | }, 70 | { 71 | name: "SFR", 72 | typeApi: "sfr", 73 | link: "https://assistance.sfr.fr/sfrmail-appli/sfrmail/configurer-messagerie-recevoir-email-sfr.html", 74 | imapHost: "imap.sfr.fr", 75 | imapPort: 993, 76 | imapEncryption: "tls", 77 | smtpHost: "smtp.sfr.fr", 78 | smtpPort: 465, 79 | smtpEncryption: "ssl", 80 | }, 81 | { 82 | name: "Microsoft", 83 | typeApi: "microsoft", 84 | link: "https://support.microsoft.com/en-us/office/pop-imap-and-smtp-settings-for-outlook-com-d088b986-291d-42b8-9564-9c414e2aa040", 85 | imapHost: "outlook.office365.com", 86 | imapPort: 993, 87 | imapEncryption: "tls", 88 | smtpHost: "smtp-mail.outlook.com", 89 | smtpPort: 587, 90 | smtpEncryption: "tls", 91 | }, 92 | { 93 | name: "Google", 94 | typeApi: "google", 95 | link: "https://myaccount.google.com/u/1/apppasswords", 96 | imapHost: "imap.gmail.com", 97 | imapPort: 993, 98 | imapEncryption: "tls", 99 | smtpHost: "smtp.gmail.com", 100 | smtpPort: 587, 101 | smtpEncryption: "tls", 102 | }, 103 | { 104 | name: "Yahoo", 105 | typeApi: "yahoo", 106 | link: "https://help.yahoo.com/kb/generate-password-sln15241.html#cont1", 107 | imapHost: "imap.mail.yahoo.com", 108 | imapPort: 993, 109 | imapEncryption: "tls", 110 | smtpHost: "smtp.mail.yahoo.com", 111 | smtpPort: 587, 112 | smtpEncryption: "tls", 113 | }, 114 | { 115 | name: "Apple", 116 | typeApi: "apple", 117 | link: "https://support.apple.com/en-us/102525", 118 | imapHost: "imap.mail.me.com", 119 | imapPort: 993, 120 | imapEncryption: "tls", 121 | smtpHost: "smtp.mail.me.com", 122 | smtpPort: 587, 123 | smtpEncryption: "tls", 124 | }, 125 | { 126 | name: "Other", 127 | typeApi: "other", 128 | link: "", 129 | imapHost: "", 130 | imapPort: 993, 131 | imapEncryption: "tls", 132 | smtpHost: "", 133 | smtpPort: 465, 134 | smtpEncryption: "ssl", 135 | }, 136 | ]; 137 | -------------------------------------------------------------------------------- /frontend/src/global/filters.ts: -------------------------------------------------------------------------------- 1 | import { postData } from "./fetchData"; 2 | import { Filter } from "./types"; 3 | import { i18n } from "@/global/preferences"; 4 | 5 | export async function createDefaultFilters(categoryName: string) { 6 | const ImportantEmailsFilter: Filter = { 7 | name: "🚨 " + i18n.global.t("constants.ruleModalConstants.important"), 8 | important: true, 9 | informative: false, 10 | category: categoryName, 11 | useless: false, 12 | read: false, 13 | notification: false, 14 | newsletter: false, 15 | spam: false, 16 | scam: false, 17 | meeting: false, 18 | }; 19 | 20 | const InformativeEmailsFilter: Filter = { 21 | name: "ℹ️ " + i18n.global.t("constants.ruleModalConstants.informative"), 22 | important: false, 23 | informative: true, 24 | category: categoryName, 25 | useless: false, 26 | read: false, 27 | notification: false, 28 | newsletter: false, 29 | spam: false, 30 | scam: false, 31 | meeting: false, 32 | }; 33 | 34 | await postData("create_filter/", ImportantEmailsFilter); 35 | await postData("create_filter/", InformativeEmailsFilter); 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/global/formatters.ts: -------------------------------------------------------------------------------- 1 | import { i18n, timezoneSelected } from "./preferences"; 2 | 3 | export function formatInteger(count: number): string { 4 | if (isNaN(count) || count === null) { 5 | return "N/A"; 6 | } 7 | 8 | if (count >= 1_000_000_000) { 9 | return `${(count / 1_000_000_000).toFixed(1)}B`; 10 | } else if (count >= 1_000_000) { 11 | return `${(count / 1_000_000).toFixed(1)}M`; 12 | } else if (count >= 1_000) { 13 | return count.toLocaleString(); 14 | } else { 15 | return count.toString(); 16 | } 17 | } 18 | 19 | export function formatFloat(value: number): string { 20 | if (isNaN(value) || value === null) { 21 | return "N/A"; 22 | } 23 | 24 | if (value >= 1_000_000_000) { 25 | return `${(value / 1_000_000_000).toFixed(2)}B`; 26 | } else if (value >= 1_000_000) { 27 | return `${(value / 1_000_000).toFixed(2)}M`; 28 | } else if (value >= 1_000) { 29 | return `${(value / 1_000).toFixed(2)}K`; 30 | } else { 31 | return value.toFixed(2); 32 | } 33 | } 34 | 35 | export const formatSentDateAndTime = (sentDateString: string, sentTimeString: string) => { 36 | const sentDateAndTimeString = `${sentDateString}T${sentTimeString}:00Z`; 37 | const sentDateObject = new Date(sentDateAndTimeString); 38 | 39 | const formattedSentDateAndTime = sentDateObject.toLocaleString(i18n.global.locale, { 40 | timeZone: timezoneSelected.value, 41 | year: "numeric", 42 | month: "long", 43 | day: "numeric", 44 | hour: "2-digit", 45 | minute: "2-digit", 46 | }); 47 | 48 | return formattedSentDateAndTime; 49 | }; 50 | 51 | export const formatSentDate = (sentDateString: string) => { 52 | const sentDateObject = new Date(`${sentDateString}T00:00:00Z`); 53 | 54 | const formattedSentDate = sentDateObject.toLocaleDateString(i18n.global.locale, { 55 | timeZone: timezoneSelected.value, 56 | year: "numeric", 57 | month: "long", 58 | day: "numeric", 59 | weekday: "long", 60 | }); 61 | 62 | return formattedSentDate; 63 | }; 64 | 65 | export const formatSentTime = (sentDateString: string, sentTimeString: string) => { 66 | const sentDateTimeString = `${sentDateString}T${sentTimeString}:00Z`; 67 | const sentDateTimeObject = new Date(sentDateTimeString); 68 | 69 | const formattedSentTime = sentDateTimeObject.toLocaleTimeString(i18n.global.locale, { 70 | timeZone: timezoneSelected.value, 71 | hour: "2-digit", 72 | minute: "2-digit", 73 | }); 74 | 75 | return formattedSentTime; 76 | }; 77 | -------------------------------------------------------------------------------- /frontend/src/global/popUp.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from "vue"; 2 | import { POP_UP_ERROR_COLOR, POP_UP_SUCCESS_COLOR } from "./const"; 3 | 4 | export function displayErrorPopup( 5 | showNotification: Ref, 6 | notificationTitle: Ref, 7 | notificationMessage: Ref, 8 | backgroundColor: Ref, 9 | title: string, 10 | message: string 11 | ) { 12 | backgroundColor.value = POP_UP_ERROR_COLOR; 13 | notificationTitle.value = title; 14 | notificationMessage.value = message; 15 | showNotification.value = true; 16 | 17 | setTimeout(() => { 18 | showNotification.value = false; 19 | }, 4000); 20 | } 21 | 22 | export function displaySuccessPopup( 23 | showNotification: Ref, 24 | notificationTitle: Ref, 25 | notificationMessage: Ref, 26 | backgroundColor: Ref, 27 | title: string, 28 | message: string 29 | ) { 30 | backgroundColor.value = POP_UP_SUCCESS_COLOR; 31 | notificationTitle.value = title; 32 | notificationMessage.value = message; 33 | showNotification.value = true; 34 | 35 | setTimeout(() => { 36 | showNotification.value = false; 37 | }, 4000); 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/global/preferences.ts: -------------------------------------------------------------------------------- 1 | import { ALLOWED_LANGUAGES, ALLOWED_THEMES, UNAUTHENTICATED_URLS } from "./const"; 2 | import { ref } from "vue"; 3 | import { createI18n, I18n } from "vue-i18n"; 4 | import messages from "@/i18n"; 5 | import { getData } from "./fetchData"; 6 | 7 | type UserPreferenceResponse = { 8 | language?: string; 9 | theme?: string; 10 | timezone?: string; 11 | error?: string; 12 | }; 13 | 14 | const languageSelected = ref("american"); 15 | export const themeSelected = ref("light"); 16 | export const timezoneSelected = ref("UTC"); 17 | 18 | const fetchUserPreference = async ( 19 | endpoint: string, 20 | key: keyof UserPreferenceResponse, 21 | allowedValues?: string[] 22 | ): Promise => { 23 | const storedValue = localStorage.getItem(key); 24 | 25 | if (storedValue && allowedValues?.includes(storedValue)) { 26 | return storedValue; 27 | } 28 | 29 | const result = await getData(`${endpoint}`); 30 | 31 | if (!result.success) { 32 | return null; 33 | } 34 | 35 | if (result.data[key] !== undefined) { 36 | const value = result.data[key]; 37 | if (typeof value === "string") { 38 | localStorage.setItem(key, value); 39 | return value; 40 | } 41 | } 42 | 43 | return null; 44 | }; 45 | 46 | const isUnAuthenticatedUrl = (url: string) => { 47 | return UNAUTHENTICATED_URLS.some((baseUrl) => url.split("?")[0] === baseUrl); 48 | }; 49 | 50 | export const initializePreferences = async (i18n: I18n) => { 51 | const currentUrl = window.location.href; 52 | 53 | if (!isUnAuthenticatedUrl(currentUrl)) { 54 | const language = await fetchUserPreference("user/preferences/language/", "language", [...ALLOWED_LANGUAGES]); 55 | if (language) { 56 | languageSelected.value = language; 57 | i18n.global.locale = language; 58 | } 59 | const theme = await fetchUserPreference("user/preferences/theme/", "theme", ALLOWED_THEMES); 60 | if (theme) { 61 | themeSelected.value = theme; 62 | } 63 | const timezone = await fetchUserPreference("user/preferences/timezone/", "timezone"); 64 | if (timezone) { 65 | timezoneSelected.value = timezone; 66 | } 67 | } 68 | }; 69 | 70 | export const i18n = createI18n({ legacy: true, locale: languageSelected.value, fallbackLocale: "american", messages }); 71 | -------------------------------------------------------------------------------- /frontend/src/global/security.ts: -------------------------------------------------------------------------------- 1 | import router from "@/router/router"; 2 | import { API_BASE_URL } from "./const"; 3 | 4 | interface AuthResponse { 5 | isAuthenticated: boolean; 6 | isActive: boolean; 7 | } 8 | 9 | interface RefreshTokenResponse { 10 | accessToken: string; 11 | } 12 | 13 | export async function isUserAuthenticated(): Promise { 14 | const accessToken = localStorage.getItem("accessToken"); 15 | if (!accessToken) { 16 | return false; 17 | } 18 | 19 | try { 20 | const requestOptions: RequestInit = { 21 | method: "GET", 22 | headers: { 23 | Authorization: `Bearer ${accessToken}`, 24 | }, 25 | }; 26 | 27 | const response = await fetchWithToken(`${API_BASE_URL}is_authenticated/`, requestOptions); 28 | if (!response) { 29 | return false; 30 | } 31 | 32 | const data: AuthResponse = await response.json(); 33 | return data; 34 | } catch (error) { 35 | return false; 36 | } 37 | } 38 | 39 | export async function fetchWithToken(url: string, options: RequestInit = {}): Promise { 40 | try { 41 | const accessToken = localStorage.getItem("accessToken"); 42 | 43 | if (!options.headers) { 44 | options.headers = {}; 45 | } 46 | 47 | if (accessToken) { 48 | (options.headers as Record)["Authorization"] = `Bearer ${accessToken}`; 49 | } else { 50 | return; 51 | } 52 | 53 | let response = await fetch(url, options); 54 | 55 | if (response.status === 401) { 56 | const refreshResponse = await fetch(`${API_BASE_URL}token/refresh/`, { 57 | method: "POST", 58 | headers: { 59 | "Content-Type": "application/json", 60 | }, 61 | body: JSON.stringify({ accessToken: accessToken }), 62 | }); 63 | 64 | if (refreshResponse.ok) { 65 | const refreshData: RefreshTokenResponse = await refreshResponse.json(); 66 | const newAccessToken = refreshData.accessToken; 67 | localStorage.setItem("accessToken", newAccessToken); 68 | (options.headers as Record)["Authorization"] = `Bearer ${newAccessToken}`; 69 | response = await fetch(url, options); 70 | } else { 71 | router.push({ name: "not-authorized" }); 72 | } 73 | } 74 | 75 | return response; 76 | } catch (error) { 77 | return; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /frontend/src/global/types.ts: -------------------------------------------------------------------------------- 1 | import { Component as VueComponent } from "vue"; 2 | import { 3 | ANSWER_REQUIRED, 4 | GOOGLE, 5 | HIGHLY_RELEVANT, 6 | IMPORTANT, 7 | INFORMATIVE, 8 | MICROSOFT, 9 | MIGHT_REQUIRE_ANSWER, 10 | NO_ANSWER_REQUIRED, 11 | NOT_RELEVANT, 12 | POSSIBLY_RELEVANT, 13 | USELESS, 14 | } from "./const"; 15 | 16 | export type EmailProvider = typeof GOOGLE | typeof MICROSOFT; 17 | 18 | export interface KeyValuePair { 19 | key: string; 20 | value: string; 21 | } 22 | 23 | export interface AttachmentType { 24 | extension: string; 25 | name: string; 26 | } 27 | 28 | export interface EmailSender { 29 | id?: number; 30 | username: string; 31 | email: string; 32 | } 33 | 34 | export interface Category { 35 | name: string; 36 | description: string; 37 | } 38 | 39 | export interface NavigationPage { 40 | name: string; 41 | href: string; 42 | icon: VueComponent; 43 | current?: boolean; 44 | target?: string; 45 | activePaths?: string[]; 46 | } 47 | 48 | interface Sender { 49 | email: string; 50 | name: string; 51 | } 52 | 53 | interface EmailAttachment { 54 | attachmentName: string; 55 | attachmentId: number; 56 | } 57 | 58 | interface EmailFlags { 59 | spam: boolean; 60 | scam: boolean; 61 | newsletter: boolean; 62 | notification: boolean; 63 | meeting: boolean; 64 | } 65 | 66 | export interface FetchDataResult { 67 | success: boolean; 68 | data?: any; 69 | blob?: any; 70 | error?: string; 71 | } 72 | 73 | export interface Recipient { 74 | username?: string; 75 | email: string; 76 | } 77 | 78 | export interface Email { 79 | id?: number; 80 | subject: string; 81 | sender: Sender; 82 | providerId: string; 83 | shortSummary?: string; 84 | oneLineSummary?: string; 85 | cc: Sender[]; 86 | bcc: Sender[]; 87 | read?: boolean; 88 | answerLater?: boolean; 89 | hasAttachments: boolean; 90 | attachments: EmailAttachment[]; 91 | sentDate: string; 92 | sentTime: string; 93 | answer?: boolean; 94 | archive?: boolean; 95 | relevance?: string; 96 | priority?: string; 97 | flags?: EmailFlags; 98 | category?: string; 99 | htmlContent?: string; 100 | } 101 | 102 | export interface EmailDetails { 103 | data: { 104 | [category: string]: { 105 | [priority: string]: Email[]; 106 | }; 107 | }; 108 | } 109 | 110 | export interface EmailLinked { 111 | email: string; 112 | typeApi: string; 113 | isServerConfig: boolean; 114 | } 115 | 116 | export interface UploadedFile { 117 | name: string; 118 | size: number; 119 | } 120 | 121 | export interface AiRecipient { 122 | username: string; 123 | email: string | { username: string; email: string }[]; 124 | } 125 | 126 | export interface Agent { 127 | id: string; 128 | agent_name: string; 129 | ai_template: string; 130 | email_example?: string; 131 | picture: string; 132 | length: string; 133 | formality: string; 134 | icon_name: string; 135 | } 136 | 137 | export interface Frequency { 138 | key: string; 139 | label: string; 140 | priceSuffix?: string; 141 | } 142 | 143 | export interface AomailSearchFilter { 144 | advanced?: boolean; 145 | emailProvider?: string[]; 146 | subject?: string; 147 | senderEmail?: string; 148 | senderName?: string; 149 | CCEmails?: string[]; 150 | CCNames?: string[]; 151 | category?: string; 152 | emailAddresses?: string[]; 153 | archive?: boolean; 154 | replyLater?: boolean; 155 | read?: boolean; 156 | sentDate?: Date; 157 | readDate?: Date; 158 | answer?: (typeof ANSWER_REQUIRED | typeof MIGHT_REQUIRE_ANSWER | typeof NO_ANSWER_REQUIRED)[]; 159 | relevance?: (typeof HIGHLY_RELEVANT | typeof POSSIBLY_RELEVANT | typeof NOT_RELEVANT)[]; 160 | priority?: (typeof IMPORTANT | typeof INFORMATIVE | typeof USELESS)[]; 161 | hasAttachments?: boolean; 162 | spam?: boolean; 163 | scam?: boolean; 164 | newsletter?: boolean; 165 | notification?: boolean; 166 | meeting?: boolean; 167 | } 168 | 169 | export interface ApiSearchFilter { 170 | advanced?: boolean; 171 | emailProvider?: string[]; 172 | fileExtensions?: string[]; 173 | filenames?: string[]; 174 | searchIn?: Record; 175 | fromAddresses?: string[]; 176 | toAddresses?: string[]; 177 | subject?: string; 178 | body?: string; 179 | dateFrom?: string; // Format allowed: YYYY-MM-DD 180 | } 181 | 182 | export type Message = { 183 | textHtml: string; 184 | isUser: boolean; 185 | buttonOptions?: KeyValuePair[]; 186 | }; 187 | 188 | export interface Filter { 189 | id?: number; 190 | name: string; 191 | important: boolean; 192 | informative: boolean; 193 | useless: boolean; 194 | read: boolean; 195 | notification: boolean; 196 | newsletter: boolean; 197 | spam: boolean; 198 | scam: boolean; 199 | meeting: boolean; 200 | relevance?: string; 201 | answer?: string; 202 | category?: string; 203 | } 204 | -------------------------------------------------------------------------------- /frontend/src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import en from "./en.json"; 2 | import fr from "./fr.json"; 3 | 4 | const messages = { 5 | french: fr, 6 | american: en, 7 | german: en, 8 | russian: en, 9 | spanish: en, 10 | chinese: en, 11 | indian: en, 12 | }; 13 | 14 | export default messages; 15 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import "./assets/css/tailwind.css"; 4 | import "./assets/css/fonts.css"; 5 | import "./assets/css/quill.css"; 6 | import { I18n } from "vue-i18n"; 7 | import { i18n, initializePreferences } from "./global/preferences"; 8 | import router from "./router/router"; 9 | import FlagIcon from "vue-flag-icon"; 10 | 11 | const app = createApp(App); 12 | app.use(router); 13 | app.use(FlagIcon); 14 | app.use(i18n); 15 | 16 | initializePreferences(i18n as I18n).then(() => { 17 | app.mount("#app"); 18 | }); 19 | -------------------------------------------------------------------------------- /frontend/src/pages/Analytics/components/CharTitle.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /frontend/src/pages/CustomCategorization/components/ChatInput.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 59 | -------------------------------------------------------------------------------- /frontend/src/pages/Errors/401NotAuthorized.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 51 | -------------------------------------------------------------------------------- /frontend/src/pages/Errors/404NotFound.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 36 | -------------------------------------------------------------------------------- /frontend/src/pages/Inbox/components/AssistantChat.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 70 | 71 | 76 | -------------------------------------------------------------------------------- /frontend/src/pages/Inbox/utils/types.ts: -------------------------------------------------------------------------------- 1 | export interface Tab { 2 | id: string; 3 | name: string; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/pages/Labels/components/Filters.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 65 | -------------------------------------------------------------------------------- /frontend/src/pages/Labels/components/Label.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 83 | -------------------------------------------------------------------------------- /frontend/src/pages/Labels/utils/types.ts: -------------------------------------------------------------------------------- 1 | export interface LabelData { 2 | id: number; 3 | itemName: string; 4 | platform: string; 5 | postageDeadlineDate: string; 6 | postageDeadlineTime: string; 7 | carrier: string; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/pages/Logout/Logout.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 30 | -------------------------------------------------------------------------------- /frontend/src/pages/ResetPassword/PasswordResetLink.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 129 | -------------------------------------------------------------------------------- /frontend/src/pages/Rules/components/TagInput.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 92 | -------------------------------------------------------------------------------- /frontend/src/pages/Rules/utils/types.ts: -------------------------------------------------------------------------------- 1 | export interface RuleData { 2 | id?: number; 3 | logicalOperator: "AND" | "OR"; 4 | // --- email triggers --- // 5 | domains: string[]; 6 | senderEmails: string[]; 7 | hasAttachements: boolean; 8 | // --- after AI processing triggers --- // 9 | categories: string[]; 10 | priorities: string[]; 11 | answers: string[]; 12 | relevances: string[]; 13 | flags: string[]; 14 | // --- AI triggers --- // 15 | emailDealWith: string; 16 | 17 | // --- static actions --- // 18 | actionTransferRecipients: string[]; 19 | actionSetFlags: string[]; 20 | actionMarkAs: string[]; 21 | actionDelete: boolean; 22 | actionSetCategory: string; 23 | actionSetPriority: string; 24 | actionSetRelevance: string; 25 | actionSetAnswer: string; 26 | // --- AI actions --- // 27 | actionReplyPrompt: string; 28 | } 29 | 30 | export interface FilterPayload { 31 | advanced?: boolean; 32 | search?: string; 33 | sort?: string; 34 | order?: string; 35 | block?: boolean; 36 | categoryName?: string; 37 | priority?: string; 38 | senderName?: string; 39 | senderEmail?: string; 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/pages/Search/components/ApiEmailModal.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 101 | -------------------------------------------------------------------------------- /frontend/src/pages/Search/utils/types.ts: -------------------------------------------------------------------------------- 1 | import { Email, EmailProvider } from "@/global/types"; 2 | 3 | export type EmailApiIds = { 4 | [key in EmailProvider]?: Record; // string represents the email address 5 | }; 6 | 7 | export type EmailApiListType = { 8 | [key in EmailProvider]?: Record; 9 | }; 10 | -------------------------------------------------------------------------------- /frontend/src/pages/Settings/components/ThemeSelection.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 101 | -------------------------------------------------------------------------------- /frontend/src/pages/Settings/components/UpdateUserDescriptionModal.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 111 | -------------------------------------------------------------------------------- /frontend/src/pages/Settings/components/UserEmailLinked.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 83 | -------------------------------------------------------------------------------- /frontend/src/pages/Settings/utils/types.ts: -------------------------------------------------------------------------------- 1 | export interface Plan { 2 | value: any; 3 | plan: string; 4 | isActive: boolean; 5 | isTrial: boolean; 6 | expiresThe: Date; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/pages/Signup/components/NewCategoryModal.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 94 | -------------------------------------------------------------------------------- /frontend/src/pages/Signup/components/StepMarker.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 57 | -------------------------------------------------------------------------------- /frontend/src/pages/Signup/components/StepsTracker.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 38 | -------------------------------------------------------------------------------- /frontend/src/pages/Subscription/utils/types.ts: -------------------------------------------------------------------------------- 1 | export interface Plan { 2 | value: any; 3 | plan: string; 4 | isActive: boolean; 5 | isTrial: boolean; 6 | expiresThe: Date; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/router/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory, RouteLocationNormalized, NavigationGuardNext } from "vue-router"; 2 | import routes from "./routes"; 3 | import { isUserAuthenticated } from "@/global/security"; 4 | 5 | const router = createRouter({ 6 | history: createWebHistory(), 7 | routes: routes, 8 | }); 9 | 10 | router.beforeEach(async (to: RouteLocationNormalized, _from: RouteLocationNormalized, next: NavigationGuardNext) => { 11 | try { 12 | const requiresAuth = (to.meta as { requiresAuth?: boolean }).requiresAuth; 13 | if (requiresAuth) { 14 | const authCheck = await isUserAuthenticated(); 15 | 16 | if (!authCheck || typeof authCheck !== "object" || !authCheck.isAuthenticated) { 17 | next({ name: "not-authorized" }); 18 | return; 19 | } 20 | 21 | if (!authCheck.isActive && to.meta.allowInactive) { 22 | next(); 23 | return; 24 | } 25 | 26 | if (!authCheck.isActive) { 27 | next({ name: "settings" }); 28 | return; 29 | } 30 | 31 | next(); 32 | } else { 33 | next(); 34 | } 35 | } catch (error) { 36 | next({ name: "login" }); 37 | } 38 | }); 39 | 40 | export default router; 41 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | import forms from "@tailwindcss/forms"; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | darkMode: "class", 6 | content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], 7 | theme: { 8 | extend: { 9 | keyframes: { 10 | slideIn: { 11 | "0%": { transform: "translateX(100%)" }, 12 | "100%": { transform: "translateX(0)" }, 13 | }, 14 | slideOut: { 15 | "0%": { transform: "translateX(0)" }, 16 | "100%": { transform: "translateX(100%)" }, 17 | }, 18 | marquee: { 19 | "0%": { transform: "translateX(100%)" }, 20 | "100%": { transform: "translateX(-100%)" }, 21 | }, 22 | }, 23 | animation: { 24 | slideIn: "slideIn 0.5s ease-out forwards", 25 | slideOut: "slideOut 0.5s ease-out forwards", 26 | marquee: "marquee 30s linear infinite", 27 | }, 28 | }, 29 | }, 30 | plugins: [forms], 31 | safelist: [ 32 | { 33 | pattern: 34 | /(bg|text|stroke|ring|hover:bg|focus:bg|group-hover:text|group-active:text|group-focus:text)-(orange|blue|gray|red|green|yellow|stone)(-\d+)?$/, 35 | variants: ["hover", "focus", "active"], 36 | }, 37 | ], 38 | }; 39 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "useDefineForClassFields": true, 14 | "sourceMap": true, 15 | "baseUrl": ".", 16 | "resolveJsonModule": true, 17 | "types": ["webpack-env"], 18 | "paths": { 19 | "@/*": ["src/*"] 20 | }, 21 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 22 | }, 23 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx"], 24 | "exclude": ["node_modules"] 25 | } -------------------------------------------------------------------------------- /frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require("@vue/cli-service"); 2 | const TerserPlugin = require("terser-webpack-plugin"); 3 | const WebpackObfuscator = require("webpack-obfuscator"); 4 | 5 | module.exports = defineConfig({ 6 | transpileDependencies: true, 7 | devServer: { 8 | ...(process.env.NODE_ENV === "development" && { 9 | server: { 10 | type: process.env.ALLOWED_HOSTS.includes("localhost") ? "http" : "https", 11 | }, 12 | headers: { 13 | "Cross-Origin-Opener-Policy": "same-origin-allow-popups", 14 | "Cross-Origin-Embedder-Policy": "unsafe-none", 15 | }, 16 | liveReload: true, 17 | watchFiles: { 18 | options: { 19 | usePolling: true, 20 | }, 21 | }, 22 | allowedHosts: process.env.ALLOWED_HOSTS.split(","), 23 | }), 24 | ...(process.env.NODE_ENV === "production" && { 25 | productionSourceMap: true, 26 | headers: { 27 | // Handled by Nginx 28 | }, 29 | }), 30 | }, 31 | configureWebpack: (config) => { 32 | if (process.env.NODE_ENV === "production") { 33 | config.optimization = { 34 | minimize: true, 35 | minimizer: [ 36 | new TerserPlugin({ 37 | terserOptions: { 38 | output: { 39 | comments: false, 40 | }, 41 | compress: { 42 | drop_console: true, 43 | drop_debugger: true, 44 | }, 45 | }, 46 | extractComments: false, 47 | }), 48 | ], 49 | }; 50 | config.plugins.push( 51 | new WebpackObfuscator({ 52 | rotateStringArray: true, 53 | stringArray: true, 54 | stringArrayThreshold: 0.5, 55 | deadCodeInjection: false, 56 | deadCodeInjectionThreshold: 0.1, 57 | debugProtection: false, 58 | compact: true, 59 | controlFlowFlattening: false, 60 | controlFlowFlatteningThreshold: 0.75, 61 | }) 62 | ); 63 | } 64 | }, 65 | }); 66 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | # -- FILE: pytest.ini (or tox.ini) 2 | [tool:pytest] 3 | DJANGO_SETTINGS_MODULE = config.settings 4 | # -- recommended but optional: 5 | python_files = tests.py test_*.py *_tests.py 6 | addopts = --reuse-db --nomigrations -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | export BACKEND_PORT=8010 2 | export FRONTEND_PORT=8090 3 | export SERVER_FRONTEND_PORT=8080 # 8080 for dev, 80 for prod 4 | export DB_PORT=5490 5 | export ENV="aomail" 6 | export NODE_ENV="development" # "development" or "production" 7 | export POSTGRES_USER="test" 8 | export POSTGRES_PASSWORD="test" 9 | export POSTGRES_DB="aomaildb" 10 | export ALLOWED_HOSTS="localhost:8010" 11 | export BASE_URL="http://localhost:8090/"; 12 | export API_BASE_URL="http://localhost:8010/aomail/"; 13 | 14 | if [ $NODE_ENV = "development" ]; then 15 | echo "Starting in development mode" 16 | docker compose -p ${ENV}_project up --build -d frontend_dev backend_dev 17 | container_name="${ENV}_project-backend_dev-1" 18 | elif [ $NODE_ENV = "production" ]; then 19 | echo "Starting in production mode" 20 | docker compose -p ${ENV}_prod up --build -d frontend_prod backend_prod 21 | container_name="${ENV}_prod-backend_prod-1" 22 | fi 23 | 24 | # Extract the ID of the Google renew subscription 25 | ID="" 26 | while [ -z "$ID" ]; do 27 | ID=$(docker exec -i $container_name crontab -l 2>/dev/null | grep 'crontab run' | awk -F 'run ' '{print $2}' | awk '{print $1}' | head -n 1) 28 | if [ -z "$ID" ]; then 29 | echo "No ID found. Retrying in 5 seconds..." 30 | sleep 5 31 | fi 32 | done 33 | echo "ID found: $ID" 34 | 35 | # Get the absolute path of the script's directory 36 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 37 | 38 | # Define the cron job with the log file located in the same directory as the script 39 | CRON_JOB="0 3 * * * docker exec -i ${container_name} /usr/local/bin/python /app/manage.py crontab run $ID >> $SCRIPT_DIR/aomail-cron.log 2>&1" 40 | 41 | # Add cron job if it doesn’t already exist 42 | (crontab -l | grep -F "$CRON_JOB") && echo "Cron job already exists" || (crontab -l; echo "$CRON_JOB") | crontab - 43 | --------------------------------------------------------------------------------