Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions app/api/v1/mail/ApiMailMail.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from app.interface.mail.InterfaceApiMailMail import InterfaceApiMailMail
from app.utils.logger.logger import logger_api
from app.utils.api.paginate_sort_filter import collection_paginate, CustomPaginateResponse
from app.utils.api.paginate_sort_filter import collection_paginate, CustomPaginateResponse, DeletedFilterQueryArgsSchema
from .schemas.mail import (
MailDetailResponseSchema,
MailListResponseSchema,
Expand Down Expand Up @@ -60,8 +60,9 @@ class ApiMailFolderIdMail(MethodView):
"""

@blp.response(200, MailListResponseSchema, example=MailListResponseSchema.example())
@blp.arguments(DeletedFilterQueryArgsSchema, location="query", arg_name="deleted_query")
@collection_paginate(blp, sort_value_set=MailListResponseSchema.sort_by_values(), filter_value_set=MailListResponseSchema.filter_by_values())
def get(self, collection_param: CollectionPaginateArgs, account_id: str, folder_name: str) -> CustomPaginateResponse:
def get(self, collection_param: CollectionPaginateArgs, deleted_query: dict, account_id: str, folder_name: str) -> CustomPaginateResponse:
"""Fetch the list of mails in a specific folder.

The filtering for this endpoint is special:\r\n
Expand Down Expand Up @@ -100,10 +101,17 @@ def get(self, collection_param: CollectionPaginateArgs, account_id: str, folder_
If you want just to list the mails while not needing the actual content,
set `fields="contents"` and `fields_action="exclude"`.

The `deleted` query parameter (boolean, default `false`) controls whether mails
flagged `\\Deleted` are included: `false` (default) excludes them, `true` includes
them alongside non-deleted mails. It is applied as an IMAP search criterion, not
a post-fetch filter, so pagination is unaffected by it.

---

:param collection_param: pagination, sorting and filtering args
:type collection_param: CollectionPaginateArgs
:param deleted_query: parsed "deleted" query param
:type deleted_query: dict
:param account_id: The account identifier
:type account_id: str
:param folder_name: The folder identifier
Expand All @@ -114,7 +122,7 @@ def get(self, collection_param: CollectionPaginateArgs, account_id: str, folder_
logger_api.debug("Calling ApiMailFolderIdMail: Fetching mail list for account_id: %s, folder_name: %s, params: %s", account_id, folder_name, collection_param)
interface: InterfaceApiMailMail = g.inter

item_count, response, status_code = interface.get_mail_list(account_id, folder_name, collection_param)
item_count, response, status_code = interface.get_mail_list(account_id, folder_name, collection_param, deleted_query["deleted"])

return item_count, response, status_code

Expand All @@ -140,6 +148,8 @@ def post(self, data: dict, account_id: str, folder_name: str) -> ResponseReturnV
* **ham**: Mark the selected mails as not spam.
* **copy**: Copy the selected mails to another folder. The destination folder name must be provided in the ``data`` field as a string.
* **delete**: Delete the selected mails, following the user's mail delete behavior preference.
* **illegal**: Report the selected mails as illegal content and move them to the Junk folder.
* **phishing**: Report the selected mails as phishing and move them to the Junk folder.

:param data: The batch action data containing 'uids', 'action' and optional 'data' field
:type data: dict
Expand Down Expand Up @@ -229,6 +239,8 @@ def post(self, data: dict, account_id: str, folder_name: str, mail_uid: str) ->
* **ham**: Mark the mail as not spam.
* **copy**: Copy the mail to another folder. The destination folder name must be provided in the ``data`` field as a string.
* **delete**: Delete the mail, following the user's mail delete behavior preference.
* **illegal**: Report the mail as illegal content and move it to the Junk folder.
* **phishing**: Report the mail as phishing and move it to the Junk folder.

:param data: The action data containing 'action' and optional 'data' field
:type data: dict
Expand Down
95 changes: 95 additions & 0 deletions app/api/v1/mail/ApiMailMailbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from app.interface.mail.InterfaceApiMailMailbox import InterfaceApiMailMailbox
from app.utils.logger.logger import logger_api
from app.utils.api.ApiBaseResponse import ApiBaseResponse
from app.utils.api.paginate_sort_filter import collection_paginate, CustomPaginateResponse, DeletedFilterQueryArgsSchema
from app.api.v1.mail.schemas.mailbox import (
MailboxCreateSchema,
MailboxUpdateSchema,
Expand All @@ -18,11 +19,16 @@
DelegationResponseSchema,
MailboxPurgeSchema,
MailboxPurgeResponseSchema,
MailboxBatchActionSchema,
MailboxBatchActionResponseSchema,
MailboxSearchSchema,
MailboxSearchResponseSchema,
)

if TYPE_CHECKING:
from app.config.settings.ProcessSetting import ProcessSetting
from app.auth.User import User
from app.utils.api.paginate_sort_filter import CollectionPaginateArgs

blp = Blueprint("Mail Account", __name__, url_prefix="/mailboxes")

Expand Down Expand Up @@ -140,6 +146,51 @@ def post(self, data: dict, account_id: str) -> ResponseReturnValue:
return interface.create_mailbox_delegate(account_id, data)


@blp.route("/<string:account_id>/batch-action")
class ApiMailBoxesAccountBatchAction(MethodView):
"""
Resource: Batch actions across the whole mailbox
"""
@blp.arguments(MailboxBatchActionSchema, example=MailboxBatchActionSchema.example(), error_status_code=400)
@blp.response(200, MailboxBatchActionResponseSchema, example=MailboxBatchActionResponseSchema.example())
def post(self, data: dict, account_id: str) -> ResponseReturnValue:
"""Perform an action (tag, untag, move, spam, ham, copy) on mails from several folders of the account at once.

Behaves like the per-folder batch action endpoint, except that ``uids`` maps folder names
to their list of mail UIDs, so mails from multiple folders can be processed in a single call.
Each folder is processed independently: a failure on one folder does not prevent the others
from being processed, and the per-folder outcome is reported in the response's ``results``
and ``errors`` fields.

**Supported actions:**

* **tag**: Add one or more tags to the selected mails. Tags are provided in the ``data`` field as a list of strings.
* **untag**: Remove one or more tags from the selected mails. Tags to remove are provided in the ``data`` field as a list of strings.
* **move**: Move the selected mails to another folder. The destination folder name must be provided in the ``data`` field as a string.
* **spam**: Mark the selected mails as spam.
* **ham**: Mark the selected mails as not spam.
* **copy**: Copy the selected mails to another folder. The destination folder name must be provided in the ``data`` field as a string.
* **delete**: Delete the selected mails, following the user's mail delete behavior preference.
* **illegal**: Report the selected mails as illegal content and move them to the Junk folder.
* **phishing**: Report the selected mails as phishing and move them to the Junk folder.

:param data: The batch action data containing 'uids' (folder name -> list of uids), 'action' and optional 'data' field
:type data: dict
:param account_id: The account identifier
:type account_id: str
:return: A response indicating the per-folder result of the action
:rtype: ResponseReturnValue
"""
logger_api.debug(
"Calling ApiMailBoxesAccountBatchAction.post for account_id: %s, uids: %s with action: %s",
account_id,
data["uids"],
data["action"]
)
interface: InterfaceApiMailMailbox = g.inter
return interface.mailbox_batch_action(account_id, data)


@blp.route("/<string:account_id>/purge")
class ApiMailBoxesAccountPurge(MethodView):
"""
Expand All @@ -155,3 +206,47 @@ def post(self, purge_data: dict, account_id: str) -> ResponseReturnValue:
interface: InterfaceApiMailMailbox = g.inter
return interface.purge_mailbox(account_id, purge_data)


@blp.route("/<string:account_id>/search")
class ApiMailBoxesAccountSearch(MethodView):
"""
Resource: Advanced Mail Search
"""
@blp.arguments(MailboxSearchSchema, example=MailboxSearchSchema.example(), error_status_code=400)
@blp.response(200, MailboxSearchResponseSchema)
@blp.arguments(DeletedFilterQueryArgsSchema, location="query", arg_name="deleted_query")
@collection_paginate(blp, can_sort=True, sort_value_set={"date", "relevance", "sender", "subject", "size"},
can_filter=True, filter_value_set={"contents"})
def post(self, search_params: dict, collection_param: "CollectionPaginateArgs", deleted_query: dict, account_id: str) -> CustomPaginateResponse:
"""
Advanced mail search across one or multiple folders.

* **operator**: str, 'AND' (default) or 'OR' - how the criteria below are combined.
With 'AND' every provided criterion must match, with 'OR' at least one must match.
* **text**: str, full text search in subject/sender/recipients/body
* **folders**: list[str], list of folder paths to search in (e.g. ["INBOX", "Sent"] or ["all"] for all folders)
* **include_subfolders**: bool, default True - when True, also search the subfolders of each folder listed in "folders"; when False, search only the exact folders listed. Ignored when "folders" is empty or ["all"].
* **date_range**: dict, date range for the search (e.g. {"from": "2023-01-01", "to": "2023-01-31"})
* **has_attachments**: bool, whether to search for emails with attachments
* **to**: str, email address to search for in either the recipient (To) or copy (Cc) headers
* **bcc**: str, blind copy (Bcc) email address to search for
* **from**: list[str], list of sender email addresses to search for
* **subject** : str, keywords to search for in the email subject
* **attachment_type**: list[str], list of attachment types to search for (e.g. ["pdf", "jpg"])
* **is_read**: bool, whether to search for read or unread emails
* **labels**: list[str], list of labels/tags to search for
* **size**: dict, filter by mail size (e.g. {"value": 15, "operator": ">", "unit": "kb"}).
``operator`` is ">" (larger than) or "<" (smaller than), ``unit`` is "kb", "mb" or "gb"
(default "kb"). Uses the native IMAP LARGER/SMALLER search keys.

All search criteria are optional and combined using the "operator" field (AND by default, OR to match any criterion).
Pagination, sorting and field filtering are controlled via query parameters (page, page_size, sort_by, sort_order, fields, fields_action).

The `deleted` query parameter (boolean, default `false`) controls whether mails
flagged `\\Deleted` are included: `false` (default) excludes them, `true` includes
them alongside non-deleted mails. It is applied as part of the IMAP search
criteria, not a post-fetch filter, so pagination is unaffected by it.
"""
logger_api.debug("Calling ApiMailBoxesAccountSearch.post for account_id: %s with params: %s", account_id, search_params)
interface: InterfaceApiMailMailbox = g.inter
return interface.search_mailbox(account_id, search_params, collection_param, deleted_query["deleted"])
4 changes: 2 additions & 2 deletions app/api/v1/mail/schemas/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class MailActionSchema(Schema):
"""
action = fields.String(
required=True,
validate=validate.OneOf(['tag', 'untag', 'move', 'spam', 'ham', 'copy', 'delete'])
validate=validate.OneOf(['tag', 'untag', 'move', 'spam', 'ham', 'copy', 'delete', 'illegal', 'phishing'])
)
data = fields.Raw(required=False, allow_none=True)

Expand All @@ -76,7 +76,7 @@ class MailBatchActionSchema(Schema):
uids = fields.List(fields.Integer(), required=True, validate=validate.Length(min=1))
action = fields.String(
required=True,
validate=validate.OneOf(['tag', 'untag', 'move', 'spam', 'ham', 'copy', 'delete'])
validate=validate.OneOf(['tag', 'untag', 'move', 'spam', 'ham', 'copy', 'delete', 'illegal', 'phishing'])
)
data = fields.Raw(required=False, allow_none=True)

Expand Down
Loading
Loading