mirror of
https://github.com/draga79/NotiMail.git
synced 2024-09-19 19:14:19 +02:00
Added SQLite database support and fixes
This commit is contained in:
parent
02c2935b67
commit
4373fb86c4
2 changed files with 85 additions and 17 deletions
96
NotiMail.py
96
NotiMail.py
|
@ -4,11 +4,16 @@ Version: 0.8
|
||||||
Author: Stefano Marinelli
|
Author: Stefano Marinelli
|
||||||
License: BSD 3-Clause License
|
License: BSD 3-Clause License
|
||||||
|
|
||||||
NotiMail is a script designed to monitor an email inbox using the IMAP IDLE feature,
|
NotiMail is a script designed to monitor an email inbox using the IMAP IDLE feature
|
||||||
and send notifications via HTTP POST requests when a new email arrives.
|
and send notifications via HTTP POST requests when a new email arrives. This version includes
|
||||||
|
additional features to store processed email UIDs in a SQLite3 database and ensure they are not
|
||||||
|
processed repeatedly.
|
||||||
|
|
||||||
The script uses IMAP to connect to an email server, enters IDLE mode to wait for new emails,
|
The script uses:
|
||||||
and sends a notification containing the sender and subject of the new email upon receipt.
|
- IMAP to connect to an email server
|
||||||
|
- IDLE mode to wait for new emails
|
||||||
|
- Sends a notification containing the sender and subject of the new email upon receipt
|
||||||
|
- Maintains a SQLite database to keep track of processed emails
|
||||||
|
|
||||||
Python Dependencies:
|
Python Dependencies:
|
||||||
- imaplib: For handling IMAP connections.
|
- imaplib: For handling IMAP connections.
|
||||||
|
@ -16,6 +21,10 @@ Python Dependencies:
|
||||||
- requests: For sending HTTP POST notifications.
|
- requests: For sending HTTP POST notifications.
|
||||||
- configparser: For reading the configuration from a file.
|
- configparser: For reading the configuration from a file.
|
||||||
- time, socket: For handling timeouts and delays.
|
- time, socket: For handling timeouts and delays.
|
||||||
|
- sqlite3: For database operations.
|
||||||
|
- datetime: For date and time operations.
|
||||||
|
- signal, sys: For handling script shutdown and signals.
|
||||||
|
- BytesParser from email.parser: For parsing raw email data.
|
||||||
|
|
||||||
Configuration:
|
Configuration:
|
||||||
The script reads configuration data from a file named config.ini. Ensure it is properly
|
The script reads configuration data from a file named config.ini. Ensure it is properly
|
||||||
|
@ -50,21 +59,57 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSI
|
||||||
OF SUCH DAMAGE.
|
OF SUCH DAMAGE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
import imaplib
|
import imaplib
|
||||||
import email
|
import email
|
||||||
import requests
|
import requests
|
||||||
import configparser
|
import configparser
|
||||||
import time
|
import time
|
||||||
import socket
|
import socket
|
||||||
|
import sqlite3
|
||||||
|
import datetime
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
from email import policy
|
from email import policy
|
||||||
from email.parser import BytesParser
|
from email.parser import BytesParser
|
||||||
|
|
||||||
|
class DatabaseHandler:
|
||||||
|
def __init__(self, db_name="processed_emails.db"):
|
||||||
|
self.connection = sqlite3.connect(db_name)
|
||||||
|
self.cursor = self.connection.cursor()
|
||||||
|
self.create_table()
|
||||||
|
|
||||||
|
def create_table(self):
|
||||||
|
self.cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS processed_emails (
|
||||||
|
uid TEXT PRIMARY KEY,
|
||||||
|
notified INTEGER,
|
||||||
|
processed_date TEXT
|
||||||
|
)''')
|
||||||
|
self.connection.commit()
|
||||||
|
|
||||||
|
def add_email(self, uid, notified):
|
||||||
|
date_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
self.cursor.execute("INSERT INTO processed_emails (uid, notified, processed_date) VALUES (?, ?, ?)",
|
||||||
|
(uid, notified, date_str))
|
||||||
|
self.connection.commit()
|
||||||
|
|
||||||
|
def is_email_notified(self, uid):
|
||||||
|
self.cursor.execute("SELECT * FROM processed_emails WHERE uid = ? AND notified = 1", (uid,))
|
||||||
|
return bool(self.cursor.fetchone())
|
||||||
|
|
||||||
|
def delete_old_emails(self, days=7):
|
||||||
|
date_limit_str = (datetime.datetime.now() - datetime.timedelta(days=days)).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
self.cursor.execute("DELETE FROM processed_emails WHERE processed_date < ?", (date_limit_str,))
|
||||||
|
self.connection.commit()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.connection.close()
|
||||||
|
|
||||||
|
|
||||||
class EmailProcessor:
|
class EmailProcessor:
|
||||||
def __init__(self, mail):
|
def __init__(self, mail):
|
||||||
self.mail = mail
|
self.mail = mail
|
||||||
self.processed_emails = set()
|
self.db_handler = DatabaseHandler()
|
||||||
|
|
||||||
def fetch_unseen_emails(self):
|
def fetch_unseen_emails(self):
|
||||||
status, messages = self.mail.uid('search', None, "UNSEEN")
|
status, messages = self.mail.uid('search', None, "UNSEEN")
|
||||||
|
@ -76,8 +121,9 @@ class EmailProcessor:
|
||||||
def process(self):
|
def process(self):
|
||||||
print("Fetching the latest email...")
|
print("Fetching the latest email...")
|
||||||
for message in self.fetch_unseen_emails():
|
for message in self.fetch_unseen_emails():
|
||||||
if message in self.processed_emails:
|
uid = message.decode('utf-8')
|
||||||
print(f"Email UID {message} already processed, skipping...")
|
if self.db_handler.is_email_notified(uid):
|
||||||
|
print(f"Email UID {uid} already processed and notified, skipping...")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
_, msg = self.mail.uid('fetch', message, '(BODY.PEEK[])')
|
_, msg = self.mail.uid('fetch', message, '(BODY.PEEK[])')
|
||||||
|
@ -89,18 +135,24 @@ class EmailProcessor:
|
||||||
print('Body:', email_message.get_payload())
|
print('Body:', email_message.get_payload())
|
||||||
print('------')
|
print('------')
|
||||||
Notifier.send_notification(email_message.get('From'), email_message.get('Subject'))
|
Notifier.send_notification(email_message.get('From'), email_message.get('Subject'))
|
||||||
self.processed_emails.add(message)
|
# Add UID to database to ensure it is not processed in future runs
|
||||||
|
self.db_handler.add_email(uid, 1)
|
||||||
|
|
||||||
|
# Delete entries older than 7 days
|
||||||
|
self.db_handler.delete_old_emails()
|
||||||
|
|
||||||
class Notifier:
|
class Notifier:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def send_notification(mail_from, mail_subject):
|
def send_notification(mail_from, mail_subject):
|
||||||
try:
|
try:
|
||||||
ntfy_url = config['NTFY']['NtfyURL']
|
ntfy_url = config['NTFY']['NtfyURL']
|
||||||
|
# Sanitize mail_subject and mail_from to ensure they only contain characters that can be encoded in 'latin-1'
|
||||||
|
sanitized_subject = mail_subject.encode('latin-1', errors='replace').decode('latin-1')
|
||||||
|
sanitized_from = mail_from.encode('latin-1', errors='replace').decode('latin-1')
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
ntfy_url,
|
ntfy_url,
|
||||||
data=mail_from.encode(encoding='utf-8'),
|
data=sanitized_from.encode(encoding='utf-8'),
|
||||||
headers={"Title": mail_subject}
|
headers={"Title": sanitized_subject}
|
||||||
)
|
)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
print("Notification sent successfully!")
|
print("Notification sent successfully!")
|
||||||
|
@ -110,7 +162,6 @@ class Notifier:
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
print(f"An error occurred: {str(e)}")
|
print(f"An error occurred: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
class IMAPHandler:
|
class IMAPHandler:
|
||||||
def __init__(self, host, email_user, email_pass):
|
def __init__(self, host, email_user, email_pass):
|
||||||
self.host = host
|
self.host = host
|
||||||
|
@ -172,9 +223,25 @@ host = config['EMAIL']['Host']
|
||||||
# Set a global timeout for all socket operations
|
# Set a global timeout for all socket operations
|
||||||
socket.setdefaulttimeout(600) # e.g., 600 seconds or 10 minutes
|
socket.setdefaulttimeout(600) # e.g., 600 seconds or 10 minutes
|
||||||
|
|
||||||
print("Script started. Press Ctrl+C to stop it anytime.")
|
def shutdown_handler(signum, frame):
|
||||||
|
print("Shutdown signal received. Cleaning up...")
|
||||||
try:
|
try:
|
||||||
|
handler.mail.logout()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
processor.db_handler.close()
|
||||||
|
print("Cleanup complete. Exiting.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Register the signal handlers
|
||||||
|
signal.signal(signal.SIGTERM, shutdown_handler)
|
||||||
|
signal.signal(signal.SIGINT, shutdown_handler)
|
||||||
|
|
||||||
|
print("Script started. Press Ctrl+C to stop it anytime.")
|
||||||
handler = IMAPHandler(host, email_user, email_pass)
|
handler = IMAPHandler(host, email_user, email_pass)
|
||||||
|
processor = EmailProcessor(None) # Creating an instance for graceful shutdown handling
|
||||||
|
|
||||||
|
try:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
handler.connect()
|
handler.connect()
|
||||||
|
@ -187,12 +254,11 @@ try:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"An unexpected error occurred: {str(e)}")
|
print(f"An unexpected error occurred: {str(e)}")
|
||||||
Notifier.send_notification("Script Error", f"An unexpected error occurred: {str(e)}")
|
Notifier.send_notification("Script Error", f"An unexpected error occurred: {str(e)}")
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("User pressed Ctrl+C, exiting...")
|
|
||||||
finally:
|
finally:
|
||||||
print("Logging out and closing the connection...")
|
print("Logging out and closing the connection...")
|
||||||
try:
|
try:
|
||||||
handler.mail.logout()
|
handler.mail.logout()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
processor.db_handler.close()
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,8 @@ Mobile devices often use IMAP IDLE, maintaining a persistent connection to ensur
|
||||||
|
|
||||||
- **Leverages 'ntfy' for Alerts**: Rather than having your device always on alert, NotiMail sends notifications via the `ntfy` service, ensuring you're promptly informed.
|
- **Leverages 'ntfy' for Alerts**: Rather than having your device always on alert, NotiMail sends notifications via the `ntfy` service, ensuring you're promptly informed.
|
||||||
|
|
||||||
|
- **Database Integration**: NotiMail uses an SQLite3 database to store and manage processed email UIDs, preventing repeated processing.
|
||||||
|
|
||||||
- **Built for Resilience**: With connectivity hiccups in mind, NotiMail ensures you're always the first to know.
|
- **Built for Resilience**: With connectivity hiccups in mind, NotiMail ensures you're always the first to know.
|
||||||
|
|
||||||
|
|
||||||
|
@ -97,7 +99,7 @@ Activate the virtual environment:
|
||||||
|
|
||||||
**4. Install the Required Libraries:**
|
**4. Install the Required Libraries:**
|
||||||
|
|
||||||
Install the necessary Python libraries using `pip`.
|
Install the necessary Python libraries using `pip`, for example:
|
||||||
|
|
||||||
bash
|
bash
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue