Skip to content

Commit

Permalink
Merge pull request #4 from ryanpeach/feature/email
Browse files Browse the repository at this point in the history
Emails now work
  • Loading branch information
ryanpeach authored Mar 16, 2024
2 parents fa9b55d + 81e24f2 commit 41170d2
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 11 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
outputs
*.mp3
config.toml

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
31 changes: 29 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <postmaster@sandbox*.mailgun.org>" # Just an example
receiver_email = ""
smtp_password = ""
```
65 changes: 58 additions & 7 deletions dogbarking/__main__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
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

app = typer.Typer()


@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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down
71 changes: 71 additions & 0 deletions dogbarking/email.py
Original file line number Diff line number Diff line change
@@ -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)
38 changes: 37 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 41170d2

Please sign in to comment.