diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..9cc86b6 --- /dev/null +++ b/.env.sample @@ -0,0 +1 @@ +DESKTOP_ASSISTANT_SMTP_PWD=YOUR_EMAIL_PASSWORD \ No newline at end of file diff --git a/.gitignore b/.gitignore index fe76169..c0896d0 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,7 @@ $RECYCLE.BIN/ .vsix # IntelliJ Idea -.idea \ No newline at end of file +.idea + +# Environment Variables +.env \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f07de3a..91c04dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,6 +28,7 @@ speechrecognition~=3.10.0 urllib3~=2.2.2 wheel~=0.41.2 wikipedia~=1.4.0 +python-dotenv~=1.0.1 wmi~=1.5.1 pycaw~=20240210 feedparser~=6.0.11 diff --git a/src/assistant.py b/src/assistant.py index f172319..d36922c 100755 --- a/src/assistant.py +++ b/src/assistant.py @@ -8,12 +8,14 @@ """ +import json import re import subprocess from datetime import datetime import commands from infra import clear_screen +from utils import load_email_config from voice_interface import VoiceInterface LISTENING_ERROR = "Say that again please..." @@ -133,6 +135,65 @@ def execute_query(self, query: str) -> None: r"\b(?:of|in|at)\s+(\w+)", query ) # Extract the city name just after the word 'of' commands.weather_reporter(self.__voice_interface, cities[0]) + elif "email" in query: + query = query.lower() + + data = load_email_config() + + if data.get("server") == "smtp.example.com": + self.__voice_interface.speak( + "Please setup email config file before sending mail." + ) + else: + self.__voice_interface.speak("who do you want to send email to?") + receiver = None + validEmail = False + while not validEmail: + receiver = self.listen_for_query() + + if receiver in data.get("contacts").keys(): + print( + f"Receiver selected from contacts: {data.get("contacts").get(receiver)}" + ) + receiver = data.get("contacts").get(receiver) + validEmail = True + elif re.match( + r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", receiver + ): + validEmail = True + else: + self.__voice_interface.speak( + "Valid Email not provided or contact does not exists" + ) + + self.__voice_interface.speak( + "What would be the subject of the message? " + ) + subject = None + while subject is None: + subject = self.listen_for_query() + + self.__voice_interface.speak("What would be the body of the email?") + body = None + while body is None: + body = self.listen_for_query() + + print( + f"Sender Address: {data.get("username")}\n" + f"Receiver address: {receiver}\n" + f"Subject: {subject}\n" + f"Body: {body}\n" + ) + + self.__voice_interface.speak("Do You Want to send this email?") + response = None + while response is None: + response = self.listen_for_query() + if "yes" in response.lower() or "sure" in response.lower(): + self.__voice_interface.speak("Sending the email") + commands.send_email(self.__voice_interface, receiver, subject, body) + else: + self.__voice_interface.speak("Request aborted by user") elif "brightness" in query: query = query.lower() diff --git a/src/commands.py b/src/commands.py index 6b27f55..96b7321 100644 --- a/src/commands.py +++ b/src/commands.py @@ -9,10 +9,13 @@ """ +import smtplib +import ssl import subprocess import threading import time from datetime import datetime +from email.message import EmailMessage from subprocess import CalledProcessError, TimeoutExpired import feedparser @@ -23,10 +26,12 @@ import wikipedia import wmi from comtypes import CLSCTX_ALL +from dotenv import dotenv_values from PIL import ImageGrab from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume from infra import __is_darwin, __is_posix, __is_windows, __system_os +from utils import load_email_config from voice_interface import VoiceInterface SUPPORTED_FEATURES = { @@ -42,6 +47,8 @@ from AppOpener import open as open_app ########## Conditional Imports ########## +ENVIRONMENT_VARIABLES = dotenv_values(".env") + def explain_features(vi: VoiceInterface) -> None: """Explains the features available @@ -451,3 +458,33 @@ def weather_reporter(vi: VoiceInterface, city_name: str) -> None: f"The wind speed is expected to be {weather_data.get('wind_speed_10m')}{weather_units.get('wind_speed_10m')}, " "so plan accordingly." ) + + +def send_email(vi: VoiceInterface, toEmail: str, subject: str, body: str): + """ + Send an email to the specified recipient. + + Args: + vi (VoiceInterface): VoiceInterface instance used to speak. + toEmail (str): The recipient's email address. + subject (str): The subject of the email. + body (str): The body content of the email. + + Raises: + ValueError: If any required parameters are missing or invalid. + """ + + data = load_email_config() + CONTEXT = ssl.create_default_context() + msg = EmailMessage() + msg["Subject"] = subject + msg["From"] = data.get("username") + msg["To"] = [toEmail] + msg.set_content(body) + server = smtplib.SMTP_SSL(data.get("server"), data.get("port"), context=CONTEXT) + server.login( + data.get("username"), ENVIRONMENT_VARIABLES.get("DESKTOP_ASSISTANT_SMTP_PWD") + ) + server.send_message(msg) + server.quit() + vi.speak(f"Email sent to {toEmail}") diff --git a/src/config/email_config.json b/src/config/email_config.json new file mode 100644 index 0000000..add5924 --- /dev/null +++ b/src/config/email_config.json @@ -0,0 +1,9 @@ +{ + "server": "smtp.example.com", + "port": 465, + "username":"no-reply@gmail.com", + "contacts": { + "ajay": "ajay@example.com", + "raj": "raj@example.com" + } +} \ No newline at end of file diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..09cbd27 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,11 @@ +import json +import os + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +CONFIG_PATH = os.path.join(BASE_DIR, "config", "email_config.json") + + +def load_email_config(): + with open(CONFIG_PATH, "r") as f: + return json.load(f)