Source code for core.utils.fetchers.BaseFetcher
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Eonvelope - a open-source self-hostable email archiving server
# Copyright (C) 2024 David Aderbauer & The Eonvelope Contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Module with the :class:`BaseFetcher` class template."""
from __future__ import annotations
import logging
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Self, override
from core.constants import EmailFetchingCriterionChoices
from core.utils import FetchingCriterion
if TYPE_CHECKING:
from collections.abc import Generator
from types import TracebackType
from core.models.Account import Account
from core.models.Email import Email
from core.models.Mailbox import Mailbox
[docs]
class BaseFetcher(ABC):
"""Template class for the mailfetcher classes.
Provides arg-checking for methods.
"""
PROTOCOL = ""
"""Name of the used protocol, should be one of :class:`MailFetchingProtocols`."""
AVAILABLE_FETCHING_CRITERIA: tuple[str, ...] = ("",)
"""Tuple of all criteria available for fetching. Should refer to :class:`MailFetchingCriteria`. Must be immutable!"""
DEFAULT_FETCHING_CRITERION = FetchingCriterion(EmailFetchingCriterionChoices.ALL)
"""Default criterion to use for fetching emails with the fetcher class."""
[docs]
@abstractmethod
def __init__(self, account: Account) -> None:
"""Constructor basis, sets up the instance logger.
Args:
account: The model of the account to fetch from.
"""
self.account = account
self.logger = logging.getLogger(self.__module__)
if account.protocol != self.PROTOCOL:
self.logger.error(
"The protocol of %s is not implemented by fetcher %s!",
account,
self.__class__.__name__,
)
raise ValueError(
f"The protocol of {account} is not implemented by fetcher {self.__class__.__name__}!"
)
[docs]
@abstractmethod
def connect_to_host(self) -> None:
"""Opens the connection to the mailserver."""
[docs]
@abstractmethod
def test(self, mailbox: Mailbox | None = None) -> None:
"""Tests the connection to the mailaccount and, if given, the mailbox.
Args:
mailbox: The mailbox to be tested. Default is `None`.
Raises:
ValueError: If the `mailbox` argument does not belong to :attr:`self.account`.
"""
if mailbox is not None and mailbox.account != self.account:
self.logger.error("%s is not a mailbox of %s!", mailbox, self.account)
raise ValueError(f"{mailbox} is not in {self.account}!")
[docs]
@abstractmethod
def fetch_emails( # type: ignore[return] # this abstractmethod just provides basic arg-checking
self,
mailbox: Mailbox,
criterion: FetchingCriterion = DEFAULT_FETCHING_CRITERION,
) -> Generator[bytes]:
"""Fetches emails based on a criterion from the server.
Args:
mailbox: The model of the mailbox to fetch data from.
criterion: Formatted criterion to filter mails by.
Defaults to :attr:`core.constants.EmailFetchingCriterionChoices.ALL`.
Yields:
Mails in the mailbox matching the criterion as :class:`bytes`.
Raises:
ValueError: If the :attr:`fetching_criterion` is not available for this fetcher.
"""
if criterion not in self.AVAILABLE_FETCHING_CRITERIA:
self.logger.error(
"Fetching by criterion %s is not available via protocol %s!",
criterion,
self.PROTOCOL,
)
raise ValueError(
f"Fetching by criterion {criterion} is not available via protocol {self.PROTOCOL}!"
)
if mailbox.account != self.account:
self.logger.error("%s is not a mailbox of %s!", mailbox, self.account)
raise ValueError(f"{mailbox} is not in {self.account}!")
[docs]
@abstractmethod
def fetch_mailboxes(self) -> list[tuple[str, str]]:
"""Fetches all mailbox names from the server.
Returns:
List of data of all mailboxes in the account. Empty if none are found.
"""
[docs]
@abstractmethod
def restore(self, email: Email) -> None:
"""Restores an email to a mailbox.
Args:
email: The email to restore.
Raises:
ValueError: If the emails mailbox is not in this fetchers account.
FileNotFoundError: If the emails file_path is not set.
"""
if email.mailbox.account != self.account:
self.logger.error("Mailbox of %s is not in %s!", email, self.account)
raise ValueError(f"Mailbox of {email} is not in {self.account}!")
if not email.file_path:
raise FileNotFoundError("Email has no stored eml file.")
[docs]
@abstractmethod
def close(self) -> None:
"""Closes the connection to the mail server."""
[docs]
@override
def __str__(self) -> str:
"""Returns a string representation of the :class:`BaseFetcher` instances.
Returns:
The string representation of the fetcher instance.
"""
return f"{self.__class__.__name__} for {self.account}"
def __enter__(self) -> Self:
"""Framework method for use of class in 'with' statement, creates an instance.
Returns:
The new Fetcher instance.
"""
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
"""Framework method for use of class in 'with' statement, closes an instance.
Args:
exc_type: The exception type that raised close.
exc_value: The exception value that raised close.
traceback: The exception traceback that raised close.
"""
if exc_value or exc_type:
self.logger.error(
"An error %s occurred exiting Fetcher!", exc_type, exc_info=exc_value
)
self.close()