Samfredoly commited on
Commit
04dab42
·
verified ·
1 Parent(s): acba745

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +164 -65
app.py CHANGED
@@ -1,6 +1,7 @@
1
  import logging
2
  import ssl
3
  import smtplib
 
4
  from email.mime.text import MIMEText
5
  from flask import Flask, request, jsonify
6
  from aiosmtpd.controller import Controller
@@ -8,7 +9,8 @@ from aiosmtpd.handlers import Sink
8
  from aiosmtpd.smtp import SMTP, AuthResult, LoginPassword
9
  from dotenv import load_dotenv
10
  import os
11
- from typing import Optional # Add this import
 
12
 
13
  # Load environment variables
14
  load_dotenv()
@@ -18,15 +20,19 @@ logging.basicConfig(level=logging.INFO)
18
  logger = logging.getLogger("app")
19
 
20
  # SMTP Server Configuration
21
- SMTP_HOST = "0.0.0.0"
22
- SMTP_PORT = 587
23
- SMTP_USER = os.getenv("SMTP_USER", "noreply@yourdomain.com") # Use a valid domain
 
 
24
  SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "password")
25
 
26
- # WhatsApp webhook verification token
27
  WHATSAPP_WEBHOOK_TOKEN = os.getenv("WHATSAPP_WEBHOOK_TOKEN", "your_webhook_token")
 
 
28
 
29
- # Authentication Credentials
30
  VALID_USERS = {
31
  SMTP_USER: SMTP_PASSWORD,
32
  }
@@ -34,8 +40,45 @@ VALID_USERS = {
34
  # Flask app for WhatsApp bot
35
  app = Flask(__name__)
36
 
37
- # SMTP Server Handler
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  class AuthenticatedSMTPHandler(Sink):
 
 
 
39
  async def handle_AUTH(self, server: SMTP, args: str, mechanism: str) -> Optional[AuthResult]:
40
  if mechanism.upper() != "LOGIN":
41
  return AuthResult(success=False, handled=False)
@@ -43,26 +86,44 @@ class AuthenticatedSMTPHandler(Sink):
43
 
44
  async def handle_AUTH_LOGIN(self, server: SMTP, args: str) -> Optional[AuthResult]:
45
  try:
46
- username, password = args.split()
 
 
 
 
47
  if username in VALID_USERS and VALID_USERS[username] == password:
 
48
  return AuthResult(success=True, auth_data=LoginPassword(username, password))
49
  else:
 
50
  return AuthResult(success=False, message="Invalid credentials")
51
  except Exception as e:
52
  logger.error(f"Authentication error: {e}")
53
  return AuthResult(success=False, message="Authentication failed")
54
 
55
  async def handle_DATA(self, server: SMTP, session, envelope):
56
- logger.info(f"Received email from: {envelope.mail_from}")
57
- logger.info(f"Recipients: {envelope.rcpt_tos}")
58
- logger.info(f"Message data:\n{envelope.content.decode('utf-8')}")
59
- return "250 Message accepted for delivery"
60
-
61
- # Start SMTP Server
62
- def start_smtp_server():
63
- # Enable SSL/TLS
64
- ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
65
- ssl_context.load_cert_chain("cert.pem", "key.pem") # Load your certificate and key
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
  handler = AuthenticatedSMTPHandler()
68
  controller = Controller(
@@ -70,82 +131,120 @@ def start_smtp_server():
70
  hostname=SMTP_HOST,
71
  port=SMTP_PORT,
72
  enable_SMTPUTF8=True,
73
- ssl_context=ssl_context, # Enable SSL/TLS
74
  auth_required=True,
75
  )
76
 
77
- logger.info(f"Starting SMTP server on {SMTP_HOST}:{SMTP_PORT} with SSL/TLS...")
78
- controller.start()
 
 
 
 
 
79
  return controller
80
 
81
- # WhatsApp Webhook
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  @app.route("/webhook", methods=["GET", "POST"])
83
  def webhook():
 
 
 
 
 
84
  if request.method == "GET":
85
- # Verify webhook
86
  mode = request.args.get("hub.mode")
87
  token = request.args.get("hub.verify_token")
88
  challenge = request.args.get("hub.challenge")
89
 
90
  if mode == "subscribe" and token == WHATSAPP_WEBHOOK_TOKEN:
91
- logger.info("Webhook verified successfully")
92
  return challenge, 200
93
  else:
94
- logger.error("Webhook verification failed")
95
  return "Verification failed", 403
96
 
97
  elif request.method == "POST":
98
- # Handle incoming messages
99
  data = request.json
100
  logger.info(f"Incoming webhook data: {data}")
101
 
102
  try:
103
- # Extract message details
104
- message = data["entry"][0]["changes"][0]["value"]["messages"][0]
105
- sender_number = message["from"]
106
- message_body = message["text"]["body"]
107
-
108
- # Parse the message (format: "send email to <recipient> subject <subject> body <body>")
109
- parts = message_body.split("subject")
110
- recipient_part = parts[0].replace("send email to", "").strip()
111
- subject_part = parts[1].split("body")[0].strip()
112
- body_part = parts[1].split("body")[1].strip()
113
-
114
- recipient = recipient_part
115
- subject = subject_part
116
- body = body_part
117
-
118
- # Send email
119
- send_email(recipient, subject, body)
120
- response_msg = f"Email sent to {recipient} with subject '{subject}'."
 
121
  except Exception as e:
122
  logger.error(f"Error processing message: {e}")
123
- response_msg = "Invalid format. Use: 'send email to <recipient> subject <subject> body <body>'."
 
124
 
125
- # Send response back to WhatsApp
126
  return jsonify({"status": "success", "message": response_msg}), 200
127
 
128
- # Send Email Function
129
- def send_email(recipient, subject, body):
130
- msg = MIMEText(body)
131
- msg['Subject'] = subject
132
- msg['From'] = SMTP_USER # Use a valid "From" address
133
- msg['To'] = recipient
 
 
134
 
135
- with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
136
- server.starttls() # Enable TLS
137
- server.login(SMTP_USER, SMTP_PASSWORD)
138
- server.sendmail(SMTP_USER, [recipient], msg.as_string())
139
- logger.info(f"Email sent to {recipient}")
 
 
 
 
140
 
141
- # Main Function
142
- if __name__ == "__main__":
143
- # Start SMTP server
144
- smtp_controller = start_smtp_server()
145
 
146
- # Start Flask app for WhatsApp bot
147
  try:
148
- app.run(host="0.0.0.0", port=5000)
149
  except KeyboardInterrupt:
150
- logger.info("Shutting down SMTP server...")
151
- smtp_controller.stop()
 
 
 
 
 
 
1
  import logging
2
  import ssl
3
  import smtplib
4
+ import re
5
  from email.mime.text import MIMEText
6
  from flask import Flask, request, jsonify
7
  from aiosmtpd.controller import Controller
 
9
  from aiosmtpd.smtp import SMTP, AuthResult, LoginPassword
10
  from dotenv import load_dotenv
11
  import os
12
+ import threading
13
+ from typing import Optional, Tuple
14
 
15
  # Load environment variables
16
  load_dotenv()
 
20
  logger = logging.getLogger("app")
21
 
22
  # SMTP Server Configuration
23
+ SMTP_HOST = os.getenv("SMTP_HOST", "0.0.0.0")
24
+ SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
25
+ CLIENT_SMTP_HOST = os.getenv("CLIENT_SMTP_HOST", "localhost")
26
+ CLIENT_SMTP_PORT = SMTP_PORT
27
+ SMTP_USER = os.getenv("SMTP_USER", "noreply@yourdomain.com")
28
  SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "password")
29
 
30
+ # WhatsApp webhook configuration
31
  WHATSAPP_WEBHOOK_TOKEN = os.getenv("WHATSAPP_WEBHOOK_TOKEN", "your_webhook_token")
32
+ # Add a default phone number ID from the environment
33
+ WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID", "default_phone_number_id")
34
 
35
+ # Authentication Credentials for SMTP server
36
  VALID_USERS = {
37
  SMTP_USER: SMTP_PASSWORD,
38
  }
 
40
  # Flask app for WhatsApp bot
41
  app = Flask(__name__)
42
 
43
+ # Regular expression for parsing the WhatsApp email command.
44
+ # Expected format: "send email to <recipient> subject <subject> body <body>"
45
+ EMAIL_COMMAND_PATTERN = re.compile(
46
+ r"send email to\s+(?P<recipient>\S+)\s+subject\s+(?P<subject>.+?)\s+body\s+(?P<body>.+)$",
47
+ re.IGNORECASE,
48
+ )
49
+
50
+ def validate_email(email: str) -> bool:
51
+ """Basic email validation using a simple regex."""
52
+ pattern = r"^[^@]+@[^@]+\.[^@]+$"
53
+ return re.match(pattern, email) is not None
54
+
55
+ def parse_message(message: str) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]:
56
+ """
57
+ Parse the incoming WhatsApp message.
58
+ Returns a tuple: (recipient, subject, body, error_message)
59
+ If parsing is successful, error_message will be None.
60
+ """
61
+ match = EMAIL_COMMAND_PATTERN.match(message)
62
+ if not match:
63
+ return None, None, None, ("Invalid format. Use: 'send email to <recipient> subject <subject> body <body>'.")
64
+
65
+ recipient = match.group("recipient").strip()
66
+ subject = match.group("subject").strip()
67
+ body = match.group("body").strip()
68
+
69
+ if not validate_email(recipient):
70
+ return None, None, None, f"Invalid email address: {recipient}"
71
+ if not subject:
72
+ return None, None, None, "Subject cannot be empty."
73
+ if not body:
74
+ return None, None, None, "Email body cannot be empty."
75
+
76
+ return recipient, subject, body, None
77
+
78
  class AuthenticatedSMTPHandler(Sink):
79
+ """
80
+ Custom SMTP handler for processing incoming emails with authentication.
81
+ """
82
  async def handle_AUTH(self, server: SMTP, args: str, mechanism: str) -> Optional[AuthResult]:
83
  if mechanism.upper() != "LOGIN":
84
  return AuthResult(success=False, handled=False)
 
86
 
87
  async def handle_AUTH_LOGIN(self, server: SMTP, args: str) -> Optional[AuthResult]:
88
  try:
89
+ # Expecting args in the form "username password"
90
+ parts = args.split()
91
+ if len(parts) != 2:
92
+ return AuthResult(success=False, message="Invalid authentication format")
93
+ username, password = parts
94
  if username in VALID_USERS and VALID_USERS[username] == password:
95
+ logger.info(f"Authenticated user: {username}")
96
  return AuthResult(success=True, auth_data=LoginPassword(username, password))
97
  else:
98
+ logger.warning(f"Authentication failed for user: {username}")
99
  return AuthResult(success=False, message="Invalid credentials")
100
  except Exception as e:
101
  logger.error(f"Authentication error: {e}")
102
  return AuthResult(success=False, message="Authentication failed")
103
 
104
  async def handle_DATA(self, server: SMTP, session, envelope):
105
+ try:
106
+ message_content = envelope.content.decode('utf-8', errors='replace')
107
+ logger.info(f"Received email from: {envelope.mail_from}")
108
+ logger.info(f"Recipients: {envelope.rcpt_tos}")
109
+ logger.info(f"Message data:\n{message_content}")
110
+ return "250 Message accepted for delivery"
111
+ except Exception as e:
112
+ logger.error(f"Error processing email data: {e}")
113
+ return "451 Internal server error"
114
+
115
+ def start_smtp_server() -> Controller:
116
+ """
117
+ Start the SMTP server with SSL/TLS and authentication.
118
+ Returns the controller instance.
119
+ """
120
+ try:
121
+ ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
122
+ ssl_context.load_cert_chain("cert.pem", "key.pem") # Load your certificate and key
123
+ logger.info("SSL context created successfully.")
124
+ except Exception as e:
125
+ logger.error(f"Failed to load SSL certificate and key: {e}")
126
+ raise
127
 
128
  handler = AuthenticatedSMTPHandler()
129
  controller = Controller(
 
131
  hostname=SMTP_HOST,
132
  port=SMTP_PORT,
133
  enable_SMTPUTF8=True,
134
+ ssl_context=ssl_context,
135
  auth_required=True,
136
  )
137
 
138
+ try:
139
+ logger.info(f"Starting SMTP server on {SMTP_HOST}:{SMTP_PORT} with SSL/TLS...")
140
+ controller.start()
141
+ except Exception as e:
142
+ logger.error(f"Failed to start SMTP server: {e}")
143
+ raise
144
+
145
  return controller
146
 
147
+ def send_email(recipient: str, subject: str, body: str) -> bool:
148
+ """
149
+ Send an email using the SMTP client.
150
+ Returns True if the email is sent successfully, otherwise False.
151
+ """
152
+ msg = MIMEText(body)
153
+ msg['Subject'] = subject
154
+ msg['From'] = SMTP_USER
155
+ msg['To'] = recipient
156
+
157
+ try:
158
+ with smtplib.SMTP(CLIENT_SMTP_HOST, CLIENT_SMTP_PORT) as server:
159
+ server.starttls() # Upgrade connection to TLS
160
+ server.login(SMTP_USER, SMTP_PASSWORD)
161
+ server.sendmail(SMTP_USER, [recipient], msg.as_string())
162
+ logger.info(f"Email sent to {recipient}")
163
+ return True
164
+ except Exception as e:
165
+ logger.error(f"Error sending email to {recipient}: {e}")
166
+ return False
167
+
168
  @app.route("/webhook", methods=["GET", "POST"])
169
  def webhook():
170
+ """
171
+ Handle WhatsApp webhook verification and incoming messages.
172
+ GET: Verify the webhook.
173
+ POST: Process incoming messages and trigger email sending.
174
+ """
175
  if request.method == "GET":
 
176
  mode = request.args.get("hub.mode")
177
  token = request.args.get("hub.verify_token")
178
  challenge = request.args.get("hub.challenge")
179
 
180
  if mode == "subscribe" and token == WHATSAPP_WEBHOOK_TOKEN:
181
+ logger.info("Webhook verified successfully.")
182
  return challenge, 200
183
  else:
184
+ logger.error("Webhook verification failed.")
185
  return "Verification failed", 403
186
 
187
  elif request.method == "POST":
 
188
  data = request.json
189
  logger.info(f"Incoming webhook data: {data}")
190
 
191
  try:
192
+ value = data["entry"][0]["changes"][0]["value"]
193
+ message = value["messages"][0]
194
+
195
+ # Extract sender's WhatsApp number and phone number ID (fallback to env variable)
196
+ sender_number = message.get("from", "unknown")
197
+ phone_number_id = value.get("metadata", {}).get("phone_number_id", WHATSAPP_PHONE_NUMBER_ID)
198
+ logger.info(f"Received message from {sender_number} using phone number ID: {phone_number_id}")
199
+
200
+ message_body = message.get("text", {}).get("body", "")
201
+ recipient, subject, body, error = parse_message(message_body)
202
+
203
+ if error:
204
+ response_msg = error
205
+ logger.error(f"Message parsing error from {sender_number}: {error}")
206
+ else:
207
+ if send_email(recipient, subject, body):
208
+ response_msg = f"Email sent to {recipient} with subject '{subject}'."
209
+ else:
210
+ response_msg = f"Failed to send email to {recipient}."
211
  except Exception as e:
212
  logger.error(f"Error processing message: {e}")
213
+ response_msg = ("Error processing message. Please ensure the format is: "
214
+ "'send email to <recipient> subject <subject> body <body>'.")
215
 
 
216
  return jsonify({"status": "success", "message": response_msg}), 200
217
 
218
+ def run_flask_app():
219
+ """
220
+ Run the Flask app.
221
+ """
222
+ try:
223
+ app.run(host="0.0.0.0", port=5000)
224
+ except Exception as e:
225
+ logger.error(f"Error starting Flask app: {e}")
226
 
227
+ def main():
228
+ """
229
+ Main function to start the SMTP server and Flask app concurrently.
230
+ """
231
+ try:
232
+ smtp_controller = start_smtp_server()
233
+ except Exception as e:
234
+ logger.critical("Failed to start SMTP server. Exiting.")
235
+ return
236
 
237
+ # Run Flask app in a separate thread
238
+ flask_thread = threading.Thread(target=run_flask_app)
239
+ flask_thread.start()
 
240
 
 
241
  try:
242
+ flask_thread.join()
243
  except KeyboardInterrupt:
244
+ logger.info("Keyboard interrupt received. Shutting down servers...")
245
+ finally:
246
+ smtp_controller.stop()
247
+ logger.info("SMTP server stopped.")
248
+
249
+ if __name__ == "__main__":
250
+ main()