Files changed (1) hide show
  1. app.py +68 -196
app.py CHANGED
@@ -4,123 +4,36 @@ 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
8
- from aiosmtpd.handlers import Sink
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
14
 
15
  # Load environment variables
16
  load_dotenv()
17
 
18
  # Configure logging
19
  logging.basicConfig(level=logging.INFO)
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", "my_secure_accesstoken123")
32
- WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID", "default_phone_number_id")
33
- META_ACCESS_TOKEN = os.getenv("META_ACCESS_TOKEN", "default_meta_access_token")
34
-
35
- # Global conversation state mapping sender's WhatsApp number to their conversation details.
36
  conversations = {}
37
 
38
- # Authentication Credentials for SMTP server
39
- VALID_USERS = {
40
- SMTP_USER: SMTP_PASSWORD,
41
- }
42
-
43
- # Flask app for WhatsApp bot
44
- app = Flask(__name__)
45
-
46
- # Regular expression for validating email addresses
47
  EMAIL_REGEX = re.compile(r"^[^@]+@[^@]+\.[^@]+$")
48
 
49
  def validate_email(email: str) -> bool:
50
- """Simple email validation using a regex."""
51
  return re.match(EMAIL_REGEX, email) is not None
52
 
53
- class AuthenticatedSMTPHandler(Sink):
54
- """
55
- Custom SMTP handler that logs incoming emails and enforces simple LOGIN authentication.
56
- """
57
- async def handle_AUTH(self, server: SMTP, args: str, mechanism: str) -> Optional[AuthResult]:
58
- if mechanism.upper() != "LOGIN":
59
- return AuthResult(success=False, handled=False)
60
- return None
61
-
62
- async def handle_AUTH_LOGIN(self, server: SMTP, args: str) -> Optional[AuthResult]:
63
- try:
64
- parts = args.split()
65
- if len(parts) != 2:
66
- return AuthResult(success=False, message="Invalid authentication format")
67
- username, password = parts
68
- if username in VALID_USERS and VALID_USERS[username] == password:
69
- logger.info(f"Authenticated user: {username}")
70
- return AuthResult(success=True, auth_data=LoginPassword(username, password))
71
- else:
72
- logger.warning(f"Authentication failed for user: {username}")
73
- return AuthResult(success=False, message="Invalid credentials")
74
- except Exception as e:
75
- logger.error(f"Authentication error: {e}")
76
- return AuthResult(success=False, message="Authentication failed")
77
-
78
- async def handle_DATA(self, server: SMTP, session, envelope):
79
- try:
80
- message_content = envelope.content.decode('utf-8', errors='replace')
81
- logger.info(f"Received email from: {envelope.mail_from}")
82
- logger.info(f"Recipients: {envelope.rcpt_tos}")
83
- logger.info(f"Message data:\n{message_content}")
84
- return "250 Message accepted for delivery"
85
- except Exception as e:
86
- logger.error(f"Error processing email data: {e}")
87
- return "451 Internal server error"
88
-
89
- def start_smtp_server() -> Controller:
90
- """
91
- Starts the SMTP server with SSL/TLS and authentication.
92
- """
93
- try:
94
- ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
95
- ssl_context.load_cert_chain("cert.pem", "key.pem")
96
- logger.info("SSL context created successfully.")
97
- except Exception as e:
98
- logger.error(f"Failed to load SSL certificate and key: {e}")
99
- raise
100
-
101
- handler = AuthenticatedSMTPHandler()
102
- controller = Controller(
103
- handler,
104
- hostname=SMTP_HOST,
105
- port=SMTP_PORT,
106
- enable_SMTPUTF8=True,
107
- ssl_context=ssl_context,
108
- auth_required=True,
109
- )
110
-
111
- try:
112
- logger.info(f"Starting SMTP server on {SMTP_HOST}:{SMTP_PORT} with SSL/TLS...")
113
- controller.start()
114
- except Exception as e:
115
- logger.error(f"Failed to start SMTP server: {e}")
116
- raise
117
-
118
- return controller
119
-
120
  def send_email(from_addr: str, to_addr: str, subject: str, body: str) -> bool:
121
  """
122
- Sends an email using the SMTP client. The 'from_addr' is provided by the user.
123
- Note: Some SMTP servers restrict the 'From' address to the authenticated user.
124
  """
125
  msg = MIMEText(body)
126
  msg['Subject'] = subject
@@ -128,7 +41,7 @@ def send_email(from_addr: str, to_addr: str, subject: str, body: str) -> bool:
128
  msg['To'] = to_addr
129
 
130
  try:
131
- with smtplib.SMTP(CLIENT_SMTP_HOST, CLIENT_SMTP_PORT) as server:
132
  server.starttls() # Upgrade to TLS
133
  server.login(SMTP_USER, SMTP_PASSWORD)
134
  server.sendmail(from_addr, [to_addr], msg.as_string())
@@ -138,113 +51,72 @@ def send_email(from_addr: str, to_addr: str, subject: str, body: str) -> bool:
138
  logger.error(f"Error sending email from {from_addr} to {to_addr}: {e}")
139
  return False
140
 
141
- @app.route("/webhook", methods=["GET", "POST"])
142
- def webhook():
 
 
 
143
  """
144
- Handles WhatsApp webhook verification and processes incoming messages.
145
- This bot acts as an SMTP assistant guiding the user to provide:
146
- 1. 'From' email address
147
- 2. Recipient ('To') email address
148
- 3. Email subject
149
- 4. Email body
150
- Once all details are provided, the email is sent.
151
  """
152
- if request.method == "GET":
153
- mode = request.args.get("hub.mode")
154
- token = request.args.get("hub.verify_token")
155
- challenge = request.args.get("hub.challenge")
156
-
157
- if mode == "subscribe" and token == WHATSAPP_WEBHOOK_TOKEN:
158
- logger.info("Webhook verified successfully.")
159
- return challenge, 200
160
- else:
161
- logger.error("Webhook verification failed.")
162
- return "Verification failed", 403
163
-
164
- elif request.method == "POST":
165
- try:
166
- data = request.json
167
- logger.info(f"Incoming webhook data: {data}")
168
- value = data["entry"][0]["changes"][0]["value"]
169
- message = value["messages"][0]
170
- sender_number = message.get("from", "unknown")
171
- phone_number_id = value.get("metadata", {}).get("phone_number_id", WHATSAPP_PHONE_NUMBER_ID)
172
- message_body = message.get("text", {}).get("body", "").strip()
173
- except Exception as e:
174
- logger.error(f"Error parsing webhook data: {e}")
175
- return jsonify({"status": "error", "message": "Invalid webhook payload"}), 400
176
-
177
- # Retrieve or initialize the conversation state for this sender.
178
- state = conversations.get(sender_number, {})
179
-
180
- # If no conversation exists, start a new one.
181
- if not state:
182
- conversations[sender_number] = {"step": "from"}
183
- response_msg = "Welcome to SMTP Assistant! Please provide your 'From' email address."
184
- else:
185
- step = state.get("step")
186
- if step == "from":
187
- if validate_email(message_body):
188
- state["from"] = message_body
189
- state["step"] = "to"
190
- response_msg = "Thanks! Now, please provide the recipient's email address."
191
- else:
192
- response_msg = "Invalid email format. Please provide a valid 'From' email address."
193
- elif step == "to":
194
- if validate_email(message_body):
195
- state["to"] = message_body
196
- state["step"] = "subject"
197
- response_msg = "Great! Please provide the email subject."
198
- else:
199
- response_msg = "Invalid email format. Please provide a valid recipient email address."
200
- elif step == "subject":
201
- state["subject"] = message_body
202
- state["step"] = "body"
203
- response_msg = "Almost done! Please provide the email body."
204
- elif step == "body":
205
- state["body"] = message_body
206
- # All details collected; send the email.
207
- from_addr = state.get("from")
208
- to_addr = state.get("to")
209
- subject = state.get("subject")
210
- body = state.get("body")
211
- if send_email(from_addr, to_addr, subject, body):
212
- response_msg = f"Email sent successfully from {from_addr} to {to_addr}."
213
- else:
214
- response_msg = "Failed to send email. Please try again later."
215
- # Clear conversation state.
216
- del conversations[sender_number]
217
- else:
218
- response_msg = "Unrecognized step. Let's start over. Please provide your 'From' email address."
219
- conversations[sender_number] = {"step": "from"}
220
-
221
- # Optionally, if you wish to send a follow-up message via the WhatsApp Business API,
222
- # you can use META_ACCESS_TOKEN in the HTTP request headers.
223
- return jsonify({"status": "success", "message": response_msg}), 200
224
-
225
- def run_flask_app():
226
  try:
227
- app.run(host="0.0.0.0", port=5000)
 
 
 
228
  except Exception as e:
229
- logger.error(f"Error starting Flask app: {e}")
230
-
231
- def main():
232
- try:
233
- smtp_controller = start_smtp_server()
234
- except Exception as e:
235
- logger.critical("Failed to start SMTP server. Exiting.")
236
- return
237
-
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()
 
4
  import re
5
  from email.mime.text import MIMEText
6
  from flask import Flask, request, jsonify
 
 
 
7
  from dotenv import load_dotenv
8
  import os
 
 
9
 
10
  # Load environment variables
11
  load_dotenv()
12
 
13
  # Configure logging
14
  logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger("bot_brain")
16
 
17
  # SMTP Server Configuration
18
  SMTP_HOST = os.getenv("SMTP_HOST", "0.0.0.0")
19
  SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
20
  CLIENT_SMTP_HOST = os.getenv("CLIENT_SMTP_HOST", "localhost")
 
21
  SMTP_USER = os.getenv("SMTP_USER", "noreply@yourdomain.com")
22
  SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "password")
23
 
24
+ # Global conversation state: maps sender's WhatsApp number to conversation data.
 
 
 
 
 
25
  conversations = {}
26
 
27
+ # Regex for validating email addresses
 
 
 
 
 
 
 
 
28
  EMAIL_REGEX = re.compile(r"^[^@]+@[^@]+\.[^@]+$")
29
 
30
  def validate_email(email: str) -> bool:
 
31
  return re.match(EMAIL_REGEX, email) is not None
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  def send_email(from_addr: str, to_addr: str, subject: str, body: str) -> bool:
34
  """
35
+ Sends an email using the SMTP client.
36
+ Note: Some SMTP servers restrict the 'From' address.
37
  """
38
  msg = MIMEText(body)
39
  msg['Subject'] = subject
 
41
  msg['To'] = to_addr
42
 
43
  try:
44
+ with smtplib.SMTP(CLIENT_SMTP_HOST, SMTP_PORT) as server:
45
  server.starttls() # Upgrade to TLS
46
  server.login(SMTP_USER, SMTP_PASSWORD)
47
  server.sendmail(from_addr, [to_addr], msg.as_string())
 
51
  logger.error(f"Error sending email from {from_addr} to {to_addr}: {e}")
52
  return False
53
 
54
+ # Flask app acting as the Bot Brain API
55
+ app = Flask(__name__)
56
+
57
+ @app.route("/bot", methods=["POST"])
58
+ def bot():
59
  """
60
+ Receives a message from the Node.js server.
61
+ The bot implements a conversation flow:
62
+ 1. Ask for the 'From' email address.
63
+ 2. Ask for the recipient ('To') email address.
64
+ 3. Ask for the email subject.
65
+ 4. Ask for the email body.
66
+ When all details are collected, the email is sent.
67
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  try:
69
+ data = request.json
70
+ sender_number = data.get("from", "unknown")
71
+ # Use the message body from WhatsApp (trim any extra spaces)
72
+ message_body = data.get("text", {}).get("body", "").strip()
73
  except Exception as e:
74
+ logger.error("Invalid JSON payload: %s", e)
75
+ return jsonify({"status": "error", "message": "Invalid payload"}), 400
76
+
77
+ state = conversations.get(sender_number, {})
78
+
79
+ if not state:
80
+ # Start a new conversation
81
+ conversations[sender_number] = {"step": "from"}
82
+ response_msg = "Welcome to SMTP Assistant! Please provide your 'From' email address."
83
+ else:
84
+ step = state.get("step")
85
+ if step == "from":
86
+ if validate_email(message_body):
87
+ state["from"] = message_body
88
+ state["step"] = "to"
89
+ response_msg = "Thanks! Now, please provide the recipient's email address."
90
+ else:
91
+ response_msg = "Invalid email format. Please provide a valid 'From' email address."
92
+ elif step == "to":
93
+ if validate_email(message_body):
94
+ state["to"] = message_body
95
+ state["step"] = "subject"
96
+ response_msg = "Great! Please provide the email subject."
97
+ else:
98
+ response_msg = "Invalid email format. Please provide a valid recipient email address."
99
+ elif step == "subject":
100
+ state["subject"] = message_body
101
+ state["step"] = "body"
102
+ response_msg = "Almost done! Please provide the email body."
103
+ elif step == "body":
104
+ state["body"] = message_body
105
+ from_addr = state.get("from")
106
+ to_addr = state.get("to")
107
+ subject = state.get("subject")
108
+ body = state.get("body")
109
+ if send_email(from_addr, to_addr, subject, body):
110
+ response_msg = f"Email sent successfully from {from_addr} to {to_addr}."
111
+ else:
112
+ response_msg = "Failed to send email. Please try again later."
113
+ # Clear the conversation state after sending
114
+ del conversations[sender_number]
115
+ else:
116
+ response_msg = "Unrecognized step. Let's start over. Please provide your 'From' email address."
117
+ conversations[sender_number] = {"step": "from"}
118
 
119
+ return jsonify({"status": "success", "message": response_msg})
 
 
 
 
 
 
120
 
121
  if __name__ == "__main__":
122
+ app.run(host="0.0.0.0", port=5000)