mirror of https://github.com/caronc/apprise
timezone completion
parent
886fd253ee
commit
96ee18298b
|
@ -37,9 +37,9 @@ from ..asset import AppriseAsset
|
|||
from ..manager_config import ConfigurationManager
|
||||
from ..manager_plugins import NotificationManager
|
||||
from ..url import URLBase
|
||||
from ..utils.time import zoneinfo
|
||||
from ..utils.cwe312 import cwe312_url
|
||||
from ..utils.parse import GET_SCHEMA_RE, parse_bool, parse_list, parse_urls
|
||||
from ..utils.time import zoneinfo
|
||||
|
||||
# Test whether token is valid or not
|
||||
VALID_TOKEN = re.compile(r"(?P<token>[a-z0-9][a-z0-9_]+)", re.I)
|
||||
|
@ -900,9 +900,9 @@ class ConfigBase(URLBase):
|
|||
if tokens and isinstance(tokens, dict):
|
||||
|
||||
# Prepare our default timezone (if specified)
|
||||
timezone = str(tokens.get('timezone', tokens.get('tz', '')))
|
||||
timezone = str(tokens.get("timezone", tokens.get("tz", "")))
|
||||
if timezone:
|
||||
default_timezone = zoneinfo(re.sub(r'[^\w/-]+', '', timezone))
|
||||
default_timezone = zoneinfo(re.sub(r"[^\w/-]+", "", timezone))
|
||||
if not default_timezone:
|
||||
ConfigBase.logger.warning(
|
||||
'Ignored invalid timezone "%s"', timezone)
|
||||
|
@ -911,7 +911,7 @@ class ConfigBase(URLBase):
|
|||
default_timezone = asset.tzinfo
|
||||
else:
|
||||
# Set our newly specified timezone
|
||||
setattr(asset, '_tzinfo', default_timezone)
|
||||
asset._tzinfo = default_timezone
|
||||
|
||||
# Iterate over remaining tokens
|
||||
for k, v in tokens.items():
|
||||
|
|
|
@ -27,10 +27,11 @@
|
|||
|
||||
import asyncio
|
||||
from collections.abc import Generator
|
||||
from datetime import tzinfo
|
||||
from functools import partial
|
||||
import re
|
||||
from typing import Any, ClassVar, Optional, TypedDict, Union
|
||||
from zoneinfo import available_timezones
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from ..apprise_attachment import AppriseAttachment
|
||||
from ..common import (
|
||||
|
@ -46,6 +47,7 @@ from ..locale import Translatable, gettext_lazy as _
|
|||
from ..persistent_store import PersistentStore
|
||||
from ..url import URLBase
|
||||
from ..utils.parse import parse_bool
|
||||
from ..utils.time import zoneinfo
|
||||
|
||||
|
||||
class RequirementsSpec(TypedDict, total=False):
|
||||
|
@ -142,8 +144,9 @@ class NotifyBase(URLBase):
|
|||
# Persistent storage default settings
|
||||
persistent_storage = True
|
||||
|
||||
# Timezone Default
|
||||
timezone = 'UTC'
|
||||
# Timezone Default; by setting it to None, the timezone detected
|
||||
# on the server is used
|
||||
timezone = None
|
||||
|
||||
# Default Notify Format
|
||||
notify_format = NotifyFormat.TEXT
|
||||
|
@ -231,9 +234,7 @@ class NotifyBase(URLBase):
|
|||
},
|
||||
"tz": {
|
||||
"name": _("Timezone"),
|
||||
"type": "choice:string",
|
||||
# Supported timezones
|
||||
"values": available_timezones(),
|
||||
"type": "string",
|
||||
# Provide a default
|
||||
"default": timezone,
|
||||
# look up default using the following parent class value at
|
||||
|
@ -291,6 +292,13 @@ class NotifyBase(URLBase):
|
|||
# restrictions and that of body_maxlen
|
||||
overflow_amalgamate_title = False
|
||||
|
||||
# Identifies the timezone to use; if this is not over-ridden, then the
|
||||
# timezone defined in the AppriseAsset() object is used instead. The
|
||||
# Below is expected to be in a ZoneInfo type already. You can have this
|
||||
# automatically initialized by specifying ?tz= on the Apprise URLs
|
||||
__tzinfo = None
|
||||
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize some general configuration that will keep things
|
||||
consistent when working with the notifiers that will inherit this
|
||||
|
@ -340,6 +348,20 @@ class NotifyBase(URLBase):
|
|||
self.logger.warning(err)
|
||||
raise TypeError(err) from None
|
||||
|
||||
if "tz" in kwargs:
|
||||
value = kwargs["tz"]
|
||||
if isinstance(value, ZoneInfo):
|
||||
self.__tzinfo = kwargs["tz"]
|
||||
|
||||
else:
|
||||
self.__tzinfo = zoneinfo(value)
|
||||
if not self.__tzinfo:
|
||||
err = (
|
||||
f"An invalid notification timezone ({value}) was "
|
||||
"specified.")
|
||||
self.logger.warning(err)
|
||||
raise TypeError(err) from None
|
||||
|
||||
if "overflow" in kwargs:
|
||||
value = kwargs["overflow"]
|
||||
try:
|
||||
|
@ -859,6 +881,10 @@ class NotifyBase(URLBase):
|
|||
"overflow": self.overflow_mode.value,
|
||||
}
|
||||
|
||||
# Timezone Information
|
||||
if self.__tzinfo:
|
||||
params["tz"] = self.__tzinfo.key
|
||||
|
||||
# Persistent Storage Setting
|
||||
if self.persistent_storage != NotifyBase.persistent_storage:
|
||||
params["store"] = "yes" if self.persistent_storage else "no"
|
||||
|
@ -924,6 +950,10 @@ class NotifyBase(URLBase):
|
|||
results["emojis"] = parse_bool(results["qsd"].get("emojis"))
|
||||
# Store our persistent storage boolean
|
||||
|
||||
# Allow overriding the default timezone
|
||||
if "tz" in results["qsd"]:
|
||||
results["tz"] = results["qsd"].get("tz", "")
|
||||
|
||||
if "store" in results["qsd"]:
|
||||
results["store"] = results["qsd"]["store"]
|
||||
|
||||
|
@ -970,3 +1000,10 @@ class NotifyBase(URLBase):
|
|||
)
|
||||
|
||||
return self.__store
|
||||
|
||||
@property
|
||||
def tzinfo(self) -> tzinfo:
|
||||
"""Returns our tzinfo file associated with this plugin if set
|
||||
otherwise the default timezone is returned.
|
||||
"""
|
||||
return self.__tzinfo if self.__tzinfo else self.asset.tzinfo
|
||||
|
|
|
@ -25,13 +25,13 @@
|
|||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
from email.header import Header
|
||||
from email.mime.application import MIMEApplication
|
||||
from email.mime.base import MIMEBase
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formataddr, make_msgid
|
||||
from email.utils import format_datetime, formataddr, make_msgid
|
||||
import re
|
||||
import smtplib
|
||||
from typing import Optional
|
||||
|
@ -598,6 +598,7 @@ class NotifyEmail(NotifyBase):
|
|||
headers=headers,
|
||||
names=self.names,
|
||||
pgp=self.pgp if self.use_pgp else None,
|
||||
tzinfo=self.tzinfo,
|
||||
):
|
||||
try:
|
||||
socket.sendmail(
|
||||
|
@ -946,6 +947,9 @@ class NotifyEmail(NotifyBase):
|
|||
# Pretty Good Privacy Support; Pass in an
|
||||
# ApprisePGPController if you wish to use it
|
||||
pgp=None,
|
||||
# Define our timezone; if one isn't provided, then we use
|
||||
# the system time instead
|
||||
tzinfo=None,
|
||||
):
|
||||
"""
|
||||
Generator for emails
|
||||
|
@ -1005,6 +1009,10 @@ class NotifyEmail(NotifyBase):
|
|||
# Generate a host identifier (used for Message-ID Creation)
|
||||
smtp_host = from_addr[1].split("@")[1]
|
||||
|
||||
if not tzinfo:
|
||||
# use server time
|
||||
tzinfo = datetime.now().astimezone().tzinfo
|
||||
|
||||
logger.debug(f"SMTP Host: {smtp_host}")
|
||||
|
||||
# Create a copy of the targets list
|
||||
|
@ -1162,9 +1170,7 @@ class NotifyEmail(NotifyBase):
|
|||
base["From"] = formataddr(from_addr, charset="utf-8")
|
||||
base["To"] = formataddr((to_name, to_addr), charset="utf-8")
|
||||
base["Message-ID"] = make_msgid(domain=smtp_host)
|
||||
base["Date"] = datetime.now(timezone.utc).strftime(
|
||||
"%a, %d %b %Y %H:%M:%S +0000"
|
||||
)
|
||||
base["Date"] = format_datetime(datetime.now(tz=tzinfo))
|
||||
|
||||
if cc:
|
||||
base["Cc"] = ",".join(_cc)
|
||||
|
|
|
@ -25,12 +25,13 @@
|
|||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
import contextlib
|
||||
from typing import Optional
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
from ..logger import logger
|
||||
|
||||
|
||||
def zoneinfo(name: str) -> ZoneInfo:
|
||||
def zoneinfo(name: str) -> Optional[ZoneInfo]:
|
||||
"""
|
||||
More forgiving ZoneInfo instantiation
|
||||
- Accepts lower/upper case
|
||||
|
@ -63,7 +64,12 @@ def zoneinfo(name: str) -> ZoneInfo:
|
|||
return ZoneInfo(zone)
|
||||
|
||||
with contextlib.suppress(IndexError):
|
||||
location = full_zone.split("/")[1:][0]
|
||||
|
||||
# Break our zones and enforce limit
|
||||
zones = full_zone.split("/")[1:3]
|
||||
|
||||
# Possible we'll throw an index error here and that's okay
|
||||
location = zones[-1] if len(zones) == 1 else "/".join(zones)
|
||||
if location and location == lowered:
|
||||
return ZoneInfo(zone)
|
||||
|
||||
|
|
|
@ -1792,6 +1792,7 @@ def test_apprise_details_plugin_verification():
|
|||
"schema",
|
||||
"fullpath",
|
||||
# NotifyBase parameters:
|
||||
"tz",
|
||||
"format",
|
||||
"overflow",
|
||||
"emojis",
|
||||
|
|
|
@ -26,9 +26,10 @@
|
|||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# Disable logging for a cleaner testing output
|
||||
from datetime import tzinfo
|
||||
from datetime import timezone, tzinfo
|
||||
import logging
|
||||
import sys
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -49,6 +50,13 @@ def test_timezone():
|
|||
asset = AppriseAsset(timezone=None)
|
||||
assert isinstance(asset.tzinfo, tzinfo)
|
||||
|
||||
# Timezone can also already be a tzinfo object
|
||||
asset = AppriseAsset(timezone=timezone.utc)
|
||||
assert isinstance(asset.tzinfo, tzinfo)
|
||||
asset = AppriseAsset(timezone=ZoneInfo("America/Toronto"))
|
||||
assert isinstance(asset.tzinfo, tzinfo)
|
||||
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
AppriseAsset(timezone=object)
|
||||
|
||||
|
|
|
@ -3236,7 +3236,7 @@ def test_bytes_to_str():
|
|||
assert utils.disk.bytes_to_str("1024") == "1.00KB"
|
||||
|
||||
|
||||
def test_zoneinfo():
|
||||
def test_time_zoneinfo():
|
||||
"""utils: zoneinfo() testing"""
|
||||
|
||||
# Some valid strings
|
||||
|
@ -3248,6 +3248,17 @@ def test_zoneinfo():
|
|||
assert isinstance(utils.time.zoneinfo("America/Toronto"), tzinfo)
|
||||
assert isinstance(utils.time.zoneinfo("america/toronto"), tzinfo)
|
||||
|
||||
# Edge Case with time also supported:
|
||||
tz = utils.time.zoneinfo("America/Argentina/Cordoba")
|
||||
isinstance(tz, tzinfo)
|
||||
assert isinstance(utils.time.zoneinfo("Argentina/Cordoba"), tzinfo)
|
||||
assert utils.time.zoneinfo("Argentina/Cordoba").key == tz.key
|
||||
assert isinstance(utils.time.zoneinfo("Cordoba"), tzinfo)
|
||||
assert utils.time.zoneinfo("Cordoba").key == "America/Cordoba"
|
||||
|
||||
# Too ambiguous
|
||||
assert utils.time.zoneinfo("Argentina") is None
|
||||
|
||||
# bad data
|
||||
assert utils.time.zoneinfo(object) is None
|
||||
assert utils.time.zoneinfo(None) is None
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from datetime import tzinfo
|
||||
from inspect import cleandoc
|
||||
|
||||
# Disable logging for a cleaner testing output
|
||||
|
@ -33,11 +34,10 @@ import logging
|
|||
import pytest
|
||||
import yaml
|
||||
|
||||
from datetime import tzinfo
|
||||
from apprise.utils.time import zoneinfo
|
||||
from apprise import Apprise, AppriseAsset, ConfigFormat
|
||||
from apprise.config import ConfigBase
|
||||
from apprise.plugins.email import NotifyEmail
|
||||
from apprise.utils.time import zoneinfo
|
||||
|
||||
logging.disable(logging.CRITICAL)
|
||||
|
||||
|
@ -1049,7 +1049,7 @@ urls:
|
|||
|
||||
# Our TimeZone
|
||||
assert isinstance(asset.tzinfo, tzinfo)
|
||||
assert asset.tzinfo.key == zoneinfo('America/Montreal').key
|
||||
assert asset.tzinfo.key == zoneinfo("America/Montreal").key
|
||||
|
||||
# the theme was not updated and remains the same as it was
|
||||
assert asset.theme == AppriseAsset().theme
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from datetime import datetime
|
||||
from email.header import decode_header
|
||||
from inspect import cleandoc
|
||||
import logging
|
||||
|
@ -92,6 +93,14 @@ TEST_URLS = (
|
|||
"instance": TypeError,
|
||||
},
|
||||
),
|
||||
(
|
||||
# invalid Timezone
|
||||
"mailto://user:pass@fastmail.com?tz=invalid",
|
||||
{
|
||||
# An error is thrown for this
|
||||
"instance": TypeError,
|
||||
},
|
||||
),
|
||||
# Pre-Configured Email Services
|
||||
(
|
||||
"mailto://user:pass@gmail.com",
|
||||
|
@ -130,7 +139,7 @@ TEST_URLS = (
|
|||
},
|
||||
),
|
||||
(
|
||||
"mailto://user:pass@fastmail.com",
|
||||
"mailto://user:pass@fastmail.com?tz=UTC",
|
||||
{
|
||||
"instance": email.NotifyEmail,
|
||||
},
|
||||
|
@ -831,6 +840,76 @@ def test_plugin_email_smtplib_send_multiple_recipients(mock_smtplib):
|
|||
assert re.match(r".*cc=baz%40example.org.*", obj.url()) is not None
|
||||
|
||||
|
||||
@mock.patch("smtplib.SMTP")
|
||||
def test_plugin_email_timezone(mock_smtp):
|
||||
"""NotifyEmail() Timezone Handling"""
|
||||
|
||||
response = mock.Mock()
|
||||
mock_smtp.return_value = response
|
||||
|
||||
# Loads America/Toronto
|
||||
results = email.NotifyEmail.parse_url(
|
||||
"mailtos://user:pass123@hotmail.com:123"
|
||||
"?tz=Toronto"
|
||||
)
|
||||
assert isinstance(results, dict)
|
||||
# timezone is detected
|
||||
assert "tz" in results
|
||||
|
||||
# Instantiate the object
|
||||
obj = email.NotifyEmail(**results)
|
||||
assert isinstance(obj, email.NotifyEmail)
|
||||
assert obj.tzinfo.key == "America/Toronto"
|
||||
# Verify our URL has defined our timezone
|
||||
# %2F = escaped '/'
|
||||
assert "tz=America%2FToronto" in obj.url()
|
||||
|
||||
# No Timezone setup/default
|
||||
results = email.NotifyEmail.parse_url(
|
||||
"mailtos://user:pass123@hotmail.com:1235"
|
||||
)
|
||||
assert "tz" not in results
|
||||
|
||||
# Instantiate the object
|
||||
obj = email.NotifyEmail(**results)
|
||||
assert isinstance(obj, email.NotifyEmail)
|
||||
# Defaults to our system
|
||||
assert obj.tzinfo == datetime.now().astimezone().tzinfo
|
||||
assert "tz=" not in obj.url()
|
||||
|
||||
# Now we'll work with an Asset to identify how it can hold
|
||||
# our default global variable (initialization proves case
|
||||
# insensitive initialization is supported)
|
||||
asset = AppriseAsset(timezone="aMErica/vanCOUver")
|
||||
|
||||
# Instatiate our object once again using the same variable set
|
||||
# as above
|
||||
obj = email.NotifyEmail(**results, asset=asset)
|
||||
# Defaults to our system
|
||||
assert obj.tzinfo.key == "America/Vancouver"
|
||||
assert "tz=" not in obj.url()
|
||||
|
||||
# Having ourselves a default variable also does not prevent
|
||||
# anyone from defining their own over-ride is still supported:
|
||||
|
||||
# Loads America/Montreal
|
||||
results = email.NotifyEmail.parse_url(
|
||||
"mailtos://user:pass123@hotmail.com:321"
|
||||
"?tz=Montreal"
|
||||
)
|
||||
assert isinstance(results, dict)
|
||||
# timezone is detected
|
||||
assert "tz" in results
|
||||
|
||||
# Instantiate the object
|
||||
obj = email.NotifyEmail(**results)
|
||||
assert isinstance(obj, email.NotifyEmail)
|
||||
assert obj.tzinfo.key == "America/Montreal"
|
||||
# Verify our URL has defined our timezone
|
||||
# %2F = escaped '/'
|
||||
assert "tz=America%2FMontreal" in obj.url()
|
||||
|
||||
|
||||
@mock.patch("smtplib.SMTP")
|
||||
def test_plugin_email_smtplib_internationalization(mock_smtp):
|
||||
"""NotifyEmail() Internationalization Handling."""
|
||||
|
|
|
@ -217,7 +217,7 @@ apprise_url_tests = (
|
|||
},
|
||||
),
|
||||
(
|
||||
"msteams://{}@{}/{}/{}?tz".format(UUID4, UUID4, "z" * 32, UUID4),
|
||||
"msteams://{}@{}/{}/{}?ta".format(UUID4, UUID4, "z" * 32, UUID4),
|
||||
{
|
||||
"instance": NotifyMSTeams,
|
||||
# Throws a series of i/o exceptions with this flag
|
||||
|
|
Loading…
Reference in New Issue