diff --git a/.gitignore b/.gitignore index 4b10a2c..9fbe489 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ outputs *.mp3 +config.toml # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/Readme.md b/Readme.md index d3f472d..543c1a0 100644 --- a/Readme.md +++ b/Readme.md @@ -20,6 +20,33 @@ poetry install pre-commit install ``` -## Usage +# Usage -> python3 -m dog_barking +> python3 -m dogbarking + +# Email + +To send an email to yourself, you need to create a file called `config.toml` in the root of the project. It should look like this: + +```toml +sender_email = "" +receiver_email = "" +smtp_server = "" +smtp_password = "" +smtp_port = 1234 +``` + +Do not share this as it contains your email password. +Optionally, you can exclude your password from this file and set it in your environment variables as `DOGBARKING_SMTP_PASSWORD`. + +Now you need an smtp compatible email provider. I recommend https://www.mailgun.com/ as you can send to 5 authorized emails for free and it was easy to set up. + +For mailgun your `config.toml` would look like this: + +```toml +smtp_server = "smtp.mailgun.org" +smtp_port = 465 +sender_email = "Mailgun Sandbox " # Just an example +receiver_email = "" +smtp_password = "" +``` diff --git a/dogbarking/__main__.py b/dogbarking/__main__.py index 80e2204..dfdc9c8 100644 --- a/dogbarking/__main__.py +++ b/dogbarking/__main__.py @@ -1,12 +1,14 @@ from pathlib import Path -from typing import Annotated +from typing import Annotated, Optional from datetime import datetime import pyaudio +from pydantic import SecretStr import toml import typer from typer_config import use_toml_config from dogbarking.audio import Player, Recorder +from dogbarking.email import Email from dogbarking.math import get_rms from loguru import logger @@ -14,7 +16,7 @@ @app.command() -@use_toml_config() +@use_toml_config(default_value="config.toml") def nogui( volume: Annotated[ float, typer.Argument(help="The volume to play the sound at.", min=0.0, max=1.0) @@ -42,8 +44,43 @@ def nogui( save_path: Annotated[ Path, typer.Argument(help="The path to save the audio file to.") ] = Path("./outputs"), - # email: Annotated[Optional[str], "The email to send the alert to."]=None + sender_email: Annotated[ + Optional[str], typer.Option(help="The email to send the alert from.") + ] = None, + receiver_email: Annotated[ + Optional[str], typer.Option(help="The email to send the alert to.") + ] = None, + smtp_password: Annotated[ + Optional[str], + typer.Option( + help="The password for the email.", envvar="DOGBARKING_SMTP_PASSWORD" + ), + ] = None, + smtp_server: Annotated[ + Optional[str], + typer.Option( + help="The SMTP server to send the email.", envvar="DOGBARKING_SMTP_SERVER" + ), + ] = None, + smtp_port: Annotated[ + Optional[int], + typer.Option( + help="The SMTP port to send the email.", envvar="DOGBARKING_SMTP_PORT" + ), + ] = 465, ): + # Check that the email details are provided if any of them are provided + use_email = any( + [sender_email, receiver_email, smtp_password, smtp_server, smtp_port] + ) + if use_email and not all( + [sender_email, receiver_email, smtp_password, smtp_server, smtp_port] + ): + logger.error( + "If you want to send an email, you need to provide all the details: sender_email, receiver_email, smtp_server, smtp_password, smtp_port" + ) + raise typer.Abort() + logger.warning("Remember to turn your volume all the way up!") # Start Recording @@ -73,13 +110,27 @@ def nogui( # Stop the recording, don't want to record the sound we are playing r.stop() - # Save the recording - filename = save_path / f"{datetime.now().isoformat()}.mp3" - r.save(filename) - # Play the sound p.play_sound() + # Save the recording and send the email + filepath = save_path / f"{datetime.now().isoformat()}.mp3" + r.save(filepath) + if use_email: + assert sender_email is not None + assert receiver_email is not None + assert smtp_password is not None + assert smtp_server is not None + assert smtp_port is not None + Email( + sender_email=sender_email, + receiver_email=receiver_email, + attachment_filepath=filepath, + smtp_password=SecretStr(smtp_password), + smtp_server=smtp_server, + smtp_port=smtp_port, + ).send_email() + # Start recording again r.start() diff --git a/dogbarking/email.py b/dogbarking/email.py new file mode 100644 index 0000000..59029fc --- /dev/null +++ b/dogbarking/email.py @@ -0,0 +1,71 @@ +from pathlib import Path +import textwrap +from pydantic import BaseModel, EmailStr, SecretStr +import smtplib +import ssl +from email import encoders +from email.mime.base import MIMEBase +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from datetime import datetime +from loguru import logger + + +class Email(BaseModel): + sender_email: EmailStr + receiver_email: EmailStr + attachment_filepath: Path + smtp_password: SecretStr + smtp_server: str + smtp_port: int = 465 + + class Config: + arbitrary_types_allowed = True + + def _create_message(self) -> MIMEMultipart: + """Create the email message with attachment.""" + message = MIMEMultipart() + message["From"] = self.sender_email + message["To"] = self.receiver_email + message["Subject"] = f"Dog Barking Alert {datetime.now().isoformat()}" + body = textwrap.dedent( + f"""\ + Your dog was barking at {datetime.now().isoformat()}. + """ + ) + + # Add body to email + message.attach(MIMEText(body, "plain")) + + # Open PDF file in binary mode and attach + with self.attachment_filepath.open("rb") as attachment: + part = MIMEBase("application", "octet-stream") + part.set_payload(attachment.read()) + + # Encode file in ASCII characters to send by email + encoders.encode_base64(part) + part.add_header( + "Content-Disposition", + f"attachment; filename= {str(self.attachment_filepath)}", + ) + message.attach(part) + + return message + + def send_email(self) -> None: + """Send the email using the details provided.""" + logger.info(f"Sending email to {self.receiver_email}...") + message = self._create_message() + text = message.as_string() + + # Log in to server using secure context and send email + context = ssl.create_default_context() + with smtplib.SMTP_SSL( + self.smtp_server, self.smtp_port, context=context + ) as server: + server.ehlo() + if self.smtp_port != 465: + server.starttls(context=context) # Secure the connection + server.ehlo() + server.login(self.sender_email, self.smtp_password.get_secret_value()) + server.sendmail(self.sender_email, self.receiver_email, text) diff --git a/poetry.lock b/poetry.lock index cac3161..db91594 100644 --- a/poetry.lock +++ b/poetry.lock @@ -297,6 +297,41 @@ files = [ {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] +[[package]] +name = "dnspython" +version = "2.6.1" +description = "DNS toolkit" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, + {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, +] + +[package.extras] +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] +dnssec = ["cryptography (>=41)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=0.9.25)"] +idna = ["idna (>=3.6)"] +trio = ["trio (>=0.23)"] +wmi = ["wmi (>=1.5.1)"] + +[[package]] +name = "email-validator" +version = "2.1.1" +description = "A robust email address syntax and deliverability validation library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "email_validator-2.1.1-py3-none-any.whl", hash = "sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05"}, + {file = "email_validator-2.1.1.tar.gz", hash = "sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84"}, +] + +[package.dependencies] +dnspython = ">=2.0.0" +idna = ">=2.0.0" + [[package]] name = "filelock" version = "3.13.1" @@ -977,6 +1012,7 @@ files = [ [package.dependencies] annotated-types = ">=0.4.0" +email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} pydantic-core = "2.16.3" typing-extensions = ">=4.6.1" @@ -1724,4 +1760,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "8a7050b8aac6279dd964c6543255e4f0c902b2335533cb1bc37390f444b4579b" +content-hash = "39459ee4abd448ca12cf8124032b2860e7d51d769a0c13ec94cdc678af3b96e2" diff --git a/pyproject.toml b/pyproject.toml index 386caa4..f8aa9f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ python = "^3.10" streamlit = "^1.32.2" typer = "^0.9.0" toml = "^0.10.2" -pydantic = "^2.6.4" +pydantic = {extras = ["email"], version = "^2.6.4"} numpy = "^1.26.4" pyaudio = "^0.2.14" typer-config = "^1.4.0"