[31988f42] Lead-Engine: Produktivsetzung und Anfrage per Teams
Implementiert: * **End-to-End Test-Button pro Lead:** Ein neuer Button "🧪 Test-Versand (an floke.com@gmail.com)" wurde in der Lead-Detailansicht hinzugefügt, um spezifische Leads sicher zu testen. * **Verbesserte E-Mail-Generierung:** * Der LLM-Prompt wurde optimiert, um redundante Termin-Vorschläge und Betreffzeilen im generierten E-Mail-Text zu vermeiden. * Der E-Mail-Body wurde umstrukturiert für eine klarere und leserlichere Integration des LLM-generierten Textes und der dynamischen Terminvorschläge. * **HTML-Signatur mit Inline-Bildern:** * Ein Skript zum Extrahieren von HTML-Signaturen und eingebetteten Bildern aus -Dateien wurde erstellt und ausgeführt. * Die -Funktion wurde überarbeitet, um die neue HTML-Signatur und alle zugehörigen Bilder dynamisch als Inline-Anhänge zu versenden. * **Bugfixes und verbesserte Diagnosefähigkeit:** * Der für wurde durch Verschieben der Funktion in den globalen Bereich behoben. * Die im Kalender-Abruf wurde durch die explizite Übergabe der Zeitzoneninformation an die Graph API korrigiert. * Fehlende Uhrzeit in Teams-Nachrichten behoben. * Umfassendes Logging wurde in kritischen Funktionen (, , ) implementiert, um die Diagnosefähigkeit bei zukünftigen Problemen zu verbessern.
This commit is contained in:
@@ -1 +1 @@
|
|||||||
{"task_id": "30388f42-8544-8088-bc48-e59e9b973e91", "token": "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", "readme_path": null, "session_start_time": "2026-03-08T14:55:15.337017"}
|
{"task_id": "31988f42-8544-80fc-8bc1-dcc57acdfd4f", "token": "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", "readme_path": null, "session_start_time": "2026-03-09T06:47:23.911130"}
|
||||||
@@ -50,6 +50,21 @@ st.title("🚀 Lead Engine: TradingTwins")
|
|||||||
# Sidebar Actions
|
# Sidebar Actions
|
||||||
st.sidebar.header("Actions")
|
st.sidebar.header("Actions")
|
||||||
|
|
||||||
|
if st.sidebar.button("🚀 Trigger Test-Lead (Teams)"):
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
# The feedback server runs on port 8004 inside the same container
|
||||||
|
response = requests.get("http://localhost:8004/test_lead")
|
||||||
|
if response.status_code == 202:
|
||||||
|
st.sidebar.success("Test lead triggered successfully!")
|
||||||
|
st.toast("Check Teams for the notification.")
|
||||||
|
else:
|
||||||
|
st.sidebar.error(f"Error: {response.status_code} - {response.text}")
|
||||||
|
except Exception as e:
|
||||||
|
st.sidebar.error(f"Failed to trigger test: {e}")
|
||||||
|
|
||||||
|
st.sidebar.divider()
|
||||||
|
|
||||||
if st.sidebar.button("1. Ingest Emails (Mock)"):
|
if st.sidebar.button("1. Ingest Emails (Mock)"):
|
||||||
from ingest import ingest_mock_leads
|
from ingest import ingest_mock_leads
|
||||||
init_db()
|
init_db()
|
||||||
@@ -246,6 +261,24 @@ if not df.empty:
|
|||||||
# Always display the draft from the database if it exists
|
# Always display the draft from the database if it exists
|
||||||
if row.get('response_draft'):
|
if row.get('response_draft'):
|
||||||
st.text_area("Email Entwurf", value=row['response_draft'], height=400)
|
st.text_area("Email Entwurf", value=row['response_draft'], height=400)
|
||||||
|
|
||||||
|
if st.button("🧪 Test-Versand (an floke.com@gmail.com)", key=f"test_send_{row['id']}"):
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
payload = {
|
||||||
|
"company_name": row['company_name'],
|
||||||
|
"contact_name": row['contact_name'],
|
||||||
|
"opener": row['response_draft']
|
||||||
|
}
|
||||||
|
response = requests.post("http://localhost:8004/test_specific_lead", json=payload)
|
||||||
|
if response.status_code == 202:
|
||||||
|
st.success("Specific test lead triggered!")
|
||||||
|
st.toast("Check Teams for the notification.")
|
||||||
|
else:
|
||||||
|
st.error(f"Error: {response.status_code} - {response.text}")
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Failed to trigger test: {e}")
|
||||||
|
|
||||||
st.button("📋 Copy to Clipboard", key=f"copy_{row['id']}", on_click=lambda: st.write("Copy functionality simulated"))
|
st.button("📋 Copy to Clipboard", key=f"copy_{row['id']}", on_click=lambda: st.write("Copy functionality simulated"))
|
||||||
else:
|
else:
|
||||||
st.info("Sync with Company Explorer first to generate a response.")
|
st.info("Sync with Company Explorer first to generate a response.")
|
||||||
|
|||||||
@@ -194,14 +194,12 @@ def generate_email_draft(lead_data, company_data, booking_link="[IHR BUCHUNGSLIN
|
|||||||
2. EINSTIEG: Nutze den inhaltlichen Kern von: "{ce_opener}".
|
2. EINSTIEG: Nutze den inhaltlichen Kern von: "{ce_opener}".
|
||||||
3. DER ÜBERGANG: Verknüpfe dies mit der Anfrage zu {purpose}. Erkläre, dass manuelle Prozesse bei {qualitative_area} angesichts der Dokumentationspflichten und des Fachkräftemangels zum Risiko werden.
|
3. DER ÜBERGANG: Verknüpfe dies mit der Anfrage zu {purpose}. Erkläre, dass manuelle Prozesse bei {qualitative_area} angesichts der Dokumentationspflichten und des Fachkräftemangels zum Risiko werden.
|
||||||
4. DIE LÖSUNG: Schlage die Kombination aus {solution['solution_text']} als integriertes Konzept vor, um das Team in Reinigung, Service und Patientenansprache spürbar zu entlasten.
|
4. DIE LÖSUNG: Schlage die Kombination aus {solution['solution_text']} als integriertes Konzept vor, um das Team in Reinigung, Service und Patientenansprache spürbar zu entlasten.
|
||||||
5. ROI: Sprich kurz die Amortisation (18-24 Monate) an – als Argument für den wirtschaftlichen Entscheider.
|
- ROI: Sprich kurz die Amortisation (18-24 Monate) an – als Argument für den wirtschaftlichen Entscheider.
|
||||||
6. CTA: Schlag konkret den {suggested_date} vor. Alternativ: {booking_link}
|
6. CTA: Schließe die E-Mail ab und leite zu den nächsten Schritten über, ohne direkt Termine vorzuschlagen oder nach Links zu fragen.
|
||||||
|
|
||||||
STIL: Senior, lösungsorientiert, direkt. Keine unnötigen Füllwörter.
|
STIL: Senior, lösungsorientiert, direkt. Keine unnötigen Füllwörter.
|
||||||
|
|
||||||
FORMAT:
|
FORMAT:
|
||||||
Betreff: [Prägnant, z.B. Automatisierungskonzept für {company_name}]
|
|
||||||
|
|
||||||
[E-Mail Text]
|
[E-Mail Text]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -214,7 +212,9 @@ def generate_email_draft(lead_data, company_data, booking_link="[IHR BUCHUNGSLIN
|
|||||||
response = requests.post(url, headers=headers, json=payload)
|
response = requests.post(url, headers=headers, json=payload)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
result = response.json()
|
result = response.json()
|
||||||
return result['candidates'][0]['content']['parts'][0]['text']
|
# Remove the placeholder from the LLM-generated text
|
||||||
|
cleaned_text = result['candidates'][0]['content']['parts'][0]['text'].replace(booking_link, '').strip()
|
||||||
|
return cleaned_text
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error generating draft: {str(e)}"
|
return f"Error generating draft: {str(e)}"
|
||||||
|
|
||||||
|
|||||||
BIN
lead-engine/trading_twins/image001.png
Normal file
BIN
lead-engine/trading_twins/image001.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
BIN
lead-engine/trading_twins/image002.png
Normal file
BIN
lead-engine/trading_twins/image002.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
lead-engine/trading_twins/image003.png
Normal file
BIN
lead-engine/trading_twins/image003.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
lead-engine/trading_twins/image004.png
Normal file
BIN
lead-engine/trading_twins/image004.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
BIN
lead-engine/trading_twins/image005.png
Normal file
BIN
lead-engine/trading_twins/image005.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -9,11 +9,31 @@ from datetime import datetime, timedelta
|
|||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
from threading import Thread, Lock
|
from threading import Thread, Lock
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
import logging
|
||||||
from fastapi import FastAPI, Response, BackgroundTasks
|
from fastapi import FastAPI, Response, BackgroundTasks
|
||||||
|
from pydantic import BaseModel
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
# --- Setup Logging ---
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
import msal
|
import msal
|
||||||
from .models import init_db, ProposalJob, ProposedSlot
|
from .models import init_db, ProposalJob, ProposedSlot
|
||||||
|
|
||||||
|
class TestLeadPayload(BaseModel):
|
||||||
|
company_name: str
|
||||||
|
contact_name: str
|
||||||
|
opener: str
|
||||||
|
|
||||||
|
def format_date_for_email(dt: datetime) -> str:
|
||||||
|
"""Formats a datetime object to 'Heute HH:MM', 'Morgen HH:MM', or 'DD.MM. HH:MM'."""
|
||||||
|
now = datetime.now(TZ_BERLIN).date()
|
||||||
|
if dt.date() == now:
|
||||||
|
return dt.strftime("Heute %H:%M Uhr")
|
||||||
|
elif dt.date() == (now + timedelta(days=1)):
|
||||||
|
return dt.strftime("Morgen %H:%M Uhr")
|
||||||
|
else:
|
||||||
|
return dt.strftime("%d.%m. %H:%M Uhr")
|
||||||
|
|
||||||
# --- Setup ---
|
# --- Setup ---
|
||||||
TZ_BERLIN = ZoneInfo("Europe/Berlin")
|
TZ_BERLIN = ZoneInfo("Europe/Berlin")
|
||||||
DB_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "trading_twins.db")
|
DB_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "trading_twins.db")
|
||||||
@@ -48,32 +68,31 @@ def get_access_token(client_id, client_secret, tenant_id):
|
|||||||
return result.get('access_token')
|
return result.get('access_token')
|
||||||
|
|
||||||
def get_availability(target_email, app_creds):
|
def get_availability(target_email, app_creds):
|
||||||
print(f"DEBUG: Requesting availability for {target_email}")
|
logging.info(f"Requesting availability for {target_email}")
|
||||||
token = get_access_token(*app_creds)
|
token = get_access_token(*app_creds)
|
||||||
if not token:
|
if not token:
|
||||||
print("DEBUG: Failed to acquire access token.")
|
logging.error("Failed to acquire access token for calendar.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json", "Prefer": 'outlook.timezone="Europe/Berlin"'}
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json", "Prefer": 'outlook.timezone="Europe/Berlin"'}
|
||||||
start_time = datetime.now(TZ_BERLIN).replace(hour=0, minute=0, second=0, microsecond=0)
|
start_time = datetime.now(TZ_BERLIN).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
end_time = start_time + timedelta(days=3)
|
end_time = start_time + timedelta(days=3)
|
||||||
# Use 15-minute intervals for finer granularity
|
# Use 15-minute intervals for finer granularity
|
||||||
payload = {"schedules": [target_email], "startTime": {"dateTime": start_time.isoformat()}, "endTime": {"dateTime": end_time.isoformat()}, "availabilityViewInterval": 15}
|
payload = {"schedules": [target_email], "startTime": {"dateTime": start_time.isoformat(), "timeZone": str(TZ_BERLIN)}, "endTime": {"dateTime": end_time.isoformat(), "timeZone": str(TZ_BERLIN)}, "availabilityViewInterval": 15}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
url = f"{GRAPH_API_ENDPOINT}/users/{target_email}/calendar/getSchedule"
|
url = f"{GRAPH_API_ENDPOINT}/users/{target_email}/calendar/getSchedule"
|
||||||
r = requests.post(url, headers=headers, json=payload)
|
r = requests.post(url, headers=headers, json=payload)
|
||||||
print(f"DEBUG: API Status Code: {r.status_code}")
|
logging.info(f"Graph API getSchedule status code: {r.status_code}")
|
||||||
|
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
view = r.json()['value'][0].get('availabilityView', '')
|
view = r.json()['value'][0].get('availabilityView', '')
|
||||||
print(f"DEBUG: Availability View received (Length: {len(view)})")
|
logging.info(f"Availability View received (Length: {len(view)})")
|
||||||
return start_time, view, 15
|
return start_time, view, 15
|
||||||
else:
|
else:
|
||||||
print(f"DEBUG: API Error Response: {r.text}")
|
logging.error(f"Graph API Error Response: {r.text}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"DEBUG: Exception during API call: {e}")
|
logging.error(f"Exception during Graph API call: {e}")
|
||||||
pass
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def find_slots(start, view, interval):
|
def find_slots(start, view, interval):
|
||||||
@@ -135,6 +154,22 @@ def trigger_test_lead(background_tasks: BackgroundTasks):
|
|||||||
background_tasks.add_task(process_lead, req_id, "Testfirma GmbH", "Wir haben Ihre Anfrage erhalten.", TEST_RECEIVER_EMAIL, "Max Mustermann")
|
background_tasks.add_task(process_lead, req_id, "Testfirma GmbH", "Wir haben Ihre Anfrage erhalten.", TEST_RECEIVER_EMAIL, "Max Mustermann")
|
||||||
return {"status": "Test lead triggered", "id": req_id}
|
return {"status": "Test lead triggered", "id": req_id}
|
||||||
|
|
||||||
|
@app.post("/test_specific_lead", status_code=202)
|
||||||
|
def trigger_specific_test_lead(payload: TestLeadPayload, background_tasks: BackgroundTasks):
|
||||||
|
"""Triggers a lead process with specific data but sends email to the TEST_RECEIVER_EMAIL."""
|
||||||
|
req_id = f"test_specific_{int(time.time())}"
|
||||||
|
# Key difference: Use data from payload, but force the receiver email
|
||||||
|
background_tasks.add_task(
|
||||||
|
process_lead,
|
||||||
|
request_id=req_id,
|
||||||
|
company=payload.company_name,
|
||||||
|
opener=payload.opener,
|
||||||
|
receiver=TEST_RECEIVER_EMAIL, # <--- FORCED TEST EMAIL
|
||||||
|
name=payload.contact_name
|
||||||
|
)
|
||||||
|
return {"status": "Specific test lead triggered", "id": req_id}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/stop/{job_uuid}")
|
@app.get("/stop/{job_uuid}")
|
||||||
def stop(job_uuid: str):
|
def stop(job_uuid: str):
|
||||||
db = SessionLocal(); job = db.query(ProposalJob).filter(ProposalJob.job_uuid == job_uuid).first()
|
db = SessionLocal(); job = db.query(ProposalJob).filter(ProposalJob.job_uuid == job_uuid).first()
|
||||||
@@ -157,80 +192,144 @@ def book_slot(job_uuid: str, ts: int):
|
|||||||
db.close(); return Response("Fehler bei Kalender.", 500)
|
db.close(); return Response("Fehler bei Kalender.", 500)
|
||||||
|
|
||||||
# --- Workflow Logic ---
|
# --- Workflow Logic ---
|
||||||
def send_email(subject, body, to_email, signature, banner_path=None):
|
def send_email(subject, body, to_email):
|
||||||
|
"""
|
||||||
|
Sends an email using Microsoft Graph API, attaching a dynamically generated
|
||||||
|
HTML signature with multiple inline images.
|
||||||
|
"""
|
||||||
|
logging.info(f"Preparing to send email to {to_email} with subject: '{subject}'")
|
||||||
|
|
||||||
|
# 1. Read the signature file
|
||||||
|
try:
|
||||||
|
with open(SIGNATURE_FILE_PATH, 'r', encoding='utf-8') as f:
|
||||||
|
signature_html = f.read()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Could not read signature file: {e}")
|
||||||
|
signature_html = "" # Fallback to no signature
|
||||||
|
|
||||||
|
# 2. Find and prepare all signature images as attachments
|
||||||
attachments = []
|
attachments = []
|
||||||
if banner_path and os.path.exists(banner_path):
|
image_dir = os.path.dirname(SIGNATURE_FILE_PATH)
|
||||||
with open(banner_path, "rb") as f:
|
image_files = [f for f in os.listdir(image_dir) if f.startswith('image') and f.endswith('.png')]
|
||||||
|
|
||||||
|
for filename in image_files:
|
||||||
|
try:
|
||||||
|
with open(os.path.join(image_dir, filename), "rb") as f:
|
||||||
content_bytes = f.read()
|
content_bytes = f.read()
|
||||||
content_b64 = base64.b64encode(content_bytes).decode("utf-8")
|
content_b64 = base64.b64encode(content_bytes).decode("utf-8")
|
||||||
|
|
||||||
attachments.append({
|
attachments.append({
|
||||||
"@odata.type": "#microsoft.graph.fileAttachment",
|
"@odata.type": "#microsoft.graph.fileAttachment",
|
||||||
"name": "RoboPlanetBannerWebinarEinladung.png",
|
"name": filename,
|
||||||
"contentBytes": content_b64,
|
"contentBytes": content_b64,
|
||||||
"isInline": True,
|
"isInline": True,
|
||||||
"contentId": "banner_image"
|
"contentId": filename
|
||||||
})
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Could not process image {filename}: {e}")
|
||||||
|
|
||||||
|
# 3. Get access token
|
||||||
catchall = os.getenv("EMAIL_CATCHALL"); to_email = catchall if catchall else to_email
|
catchall = os.getenv("EMAIL_CATCHALL"); to_email = catchall if catchall else to_email
|
||||||
token = get_access_token(AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID)
|
token = get_access_token(AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID)
|
||||||
if not token: return
|
if not token:
|
||||||
|
logging.error("Failed to get access token for sending email.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 4. Construct and send the email
|
||||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
payload = {"message": {"subject": subject, "body": {"contentType": "HTML", "content": body + signature}, "toRecipients": [{"emailAddress": {"address": to_email}}]}, "saveToSentItems": "true"}
|
full_body = body + signature_html
|
||||||
if attachments: payload["message"]["attachments"] = attachments
|
payload = {
|
||||||
requests.post(f"{GRAPH_API_ENDPOINT}/users/{SENDER_EMAIL}/sendMail", headers=headers, json=payload)
|
"message": {
|
||||||
|
"subject": subject,
|
||||||
|
"body": {"contentType": "HTML", "content": full_body},
|
||||||
|
"toRecipients": [{"emailAddress": {"address": to_email}}]
|
||||||
|
},
|
||||||
|
"saveToSentItems": "true"
|
||||||
|
}
|
||||||
|
if attachments:
|
||||||
|
payload["message"]["attachments"] = attachments
|
||||||
|
|
||||||
|
response = requests.post(f"{GRAPH_API_ENDPOINT}/users/{SENDER_EMAIL}/sendMail", headers=headers, json=payload)
|
||||||
|
logging.info(f"Send mail API response status: {response.status_code}")
|
||||||
|
if response.status_code not in [200, 202]:
|
||||||
|
logging.error(f"Error sending mail: {response.text}")
|
||||||
|
|
||||||
|
|
||||||
def process_lead(request_id, company, opener, receiver, name):
|
def process_lead(request_id, company, opener, receiver, name):
|
||||||
|
logging.info(f"--- Starting process_lead for request_id: {request_id} ---")
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
job = ProposalJob(job_uuid=request_id, customer_email=receiver, customer_company=company, customer_name=name, status="pending")
|
job = ProposalJob(job_uuid=request_id, customer_email=receiver, customer_company=company, customer_name=name, status="pending")
|
||||||
db.add(job); db.commit()
|
db.add(job)
|
||||||
|
db.commit()
|
||||||
|
logging.info(f"Job {request_id} created and saved to DB.")
|
||||||
|
|
||||||
cal_data = get_availability("e.melcer@robo-planet.de", (CAL_APPID, CAL_SECRET, CAL_TENNANT_ID))
|
cal_data = get_availability("e.melcer@robo-planet.de", (CAL_APPID, CAL_SECRET, CAL_TENNANT_ID))
|
||||||
suggestions = find_slots(*cal_data) if cal_data else []
|
suggestions = find_slots(*cal_data) if cal_data else []
|
||||||
|
|
||||||
# --- FALLBACK LOGIC ---
|
|
||||||
if not suggestions:
|
if not suggestions:
|
||||||
print("WARNING: No slots found via API. Creating fallback slots.")
|
logging.warning(f"No slots found via API for job {request_id}. Creating fallback slots.")
|
||||||
now = datetime.now(TZ_BERLIN)
|
now = datetime.now(TZ_BERLIN)
|
||||||
# Tomorrow 10:00
|
|
||||||
tomorrow = (now + timedelta(days=1)).replace(hour=10, minute=0, second=0, microsecond=0)
|
tomorrow = (now + timedelta(days=1)).replace(hour=10, minute=0, second=0, microsecond=0)
|
||||||
# Day after tomorrow 14:00
|
|
||||||
overmorrow = (now + timedelta(days=2)).replace(hour=14, minute=0, second=0, microsecond=0)
|
overmorrow = (now + timedelta(days=2)).replace(hour=14, minute=0, second=0, microsecond=0)
|
||||||
suggestions = [tomorrow, overmorrow]
|
suggestions = [tomorrow, overmorrow]
|
||||||
# --------------------
|
|
||||||
|
|
||||||
for s in suggestions: db.add(ProposedSlot(job_id=job.id, start_time=s, end_time=s+timedelta(minutes=15)))
|
logging.info(f"Found/created {len(suggestions)} slot suggestions for job {request_id}.")
|
||||||
|
for s in suggestions:
|
||||||
|
db.add(ProposedSlot(job_id=job.id, start_time=s, end_time=s + timedelta(minutes=15)))
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
send_time = datetime.now(TZ_BERLIN) + timedelta(minutes=DEFAULT_WAIT_MINUTES)
|
send_time = datetime.now(TZ_BERLIN) + timedelta(minutes=DEFAULT_WAIT_MINUTES)
|
||||||
# Using the more detailed card from teams_notification.py
|
logging.info(f"Sending Teams approval card for job {request_id}.")
|
||||||
from .teams_notification import send_approval_card
|
from .teams_notification import send_approval_card
|
||||||
send_approval_card(job_uuid=request_id, customer_name=company, time_string=send_time.strftime("%H:%M"), webhook_url=TEAMS_WEBHOOK_URL, api_base_url=FEEDBACK_SERVER_BASE_URL)
|
send_approval_card(job_uuid=request_id, customer_name=company, time_string=send_time.strftime("%H:%M"), webhook_url=TEAMS_WEBHOOK_URL, api_base_url=FEEDBACK_SERVER_BASE_URL)
|
||||||
|
|
||||||
send_time = datetime.now(TZ_BERLIN) + timedelta(minutes=DEFAULT_WAIT_MINUTES)
|
logging.info(f"Waiting for response or timeout until {send_time.strftime('%H:%M:%S')} for job {request_id}")
|
||||||
while datetime.now(TZ_BERLIN) < send_time:
|
wait_until = datetime.now(TZ_BERLIN) + timedelta(minutes=DEFAULT_WAIT_MINUTES)
|
||||||
|
while datetime.now(TZ_BERLIN) < wait_until:
|
||||||
db.refresh(job)
|
db.refresh(job)
|
||||||
if job.status in ["cancelled", "send_now"]: break
|
if job.status in ["cancelled", "send_now"]:
|
||||||
|
logging.info(f"Status for job {request_id} changed to '{job.status}'. Exiting wait loop.")
|
||||||
|
break
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
if job.status == "cancelled": db.close(); return
|
db.refresh(job)
|
||||||
|
if job.status == "cancelled":
|
||||||
|
logging.info(f"Job {request_id} was cancelled. No email will be sent.")
|
||||||
|
return
|
||||||
|
|
||||||
|
logging.info(f"Timeout reached or 'Send Now' clicked for job {request_id}. Proceeding to send email.")
|
||||||
booking_html = "<ul>"
|
booking_html = "<ul>"
|
||||||
for s in suggestions: booking_html += f'<li><a href="{FEEDBACK_SERVER_BASE_URL}/book_slot/{request_id}/{int(s.timestamp())}">{s.strftime("%d.%m %H:%M")}</a></li>'
|
for s in suggestions:
|
||||||
|
booking_html += f'<li><a href="{FEEDBACK_SERVER_BASE_URL}/book_slot/{request_id}/{int(s.timestamp())}">{format_date_for_email(s)}</a></li>'
|
||||||
booking_html += "</ul>"
|
booking_html += "</ul>"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(SIGNATURE_FILE_PATH, 'r') as f: sig = f.read()
|
with open(SIGNATURE_FILE_PATH, 'r') as f:
|
||||||
except: sig = ""
|
sig = f.read()
|
||||||
|
except:
|
||||||
|
sig = ""
|
||||||
|
|
||||||
# THIS IS THE CORRECTED EMAIL BODY
|
|
||||||
email_body = f"""
|
email_body = f"""
|
||||||
<p>Hallo {name},</p>
|
<p>Hallo {name},</p>
|
||||||
<p>{opener}</p>
|
{opener}
|
||||||
<p>Hätten Sie an einem dieser Termine Zeit für ein kurzes Gespräch?</p>
|
<p>Ich freue mich auf den Austausch und schlage Ihnen hierfür konkrete Termine vor:</p>
|
||||||
{booking_html}
|
<ul>
|
||||||
"""
|
{booking_html}
|
||||||
|
</ul>
|
||||||
|
<p>Mit freundlichen Grüßen,</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
send_email(f"Ihr Kontakt mit RoboPlanet - {company}", email_body, receiver)
|
||||||
|
job.status = "sent"
|
||||||
|
db.commit()
|
||||||
|
logging.info(f"--- Finished process_lead for request_id: {request_id} ---")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"FATAL error in process_lead for request_id {request_id}: {e}", exc_info=True)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
send_email(f"Ihr Kontakt mit RoboPlanet - {company}", email_body, receiver, sig, BANNER_FILE_PATH)
|
|
||||||
job.status = "sent"; db.commit(); db.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8004)
|
uvicorn.run(app, host="0.0.0.0", port=8004)
|
||||||
|
|||||||
@@ -1,13 +1,77 @@
|
|||||||
Freundliche Grüße<br>
|
<table class="MsoNormalTable" border="0" cellspacing="0" cellpadding="0" width="460" style="width:345.0pt">
|
||||||
Elizabeta Melcer<br>
|
<tbody>
|
||||||
Inside Sales Managerin<br>
|
<tr>
|
||||||
<img src="https://www.robo-planet.de/wp-content/uploads/2023/07/Wackler_Logo.png" alt="Wackler Logo" width="100"><br>
|
<td width="380" style="width:285.0pt;padding:0cm 0cm .75pt 0cm">
|
||||||
RoboPlanet GmbH<br>
|
<p class="MsoNormal"><span style="font-size:10.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F">Freundliche Grüße</span><span style="font-size:10.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F"><br>
|
||||||
Schatzbogen 39, 81829 München<br>
|
<br>
|
||||||
T: +49 89 420490-402 | M: +49 175 8334071<br>
|
</span><span style="font-size:10.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F">Elizabeta Melcer</span><span style="font-size:10.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F"><br>
|
||||||
e.melcer@robo-planet.de | www.robo-planet.de<br>
|
</span><span style="font-size:10.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F">Inside Sales Managerin</span><span style="font-size:9.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F;mso-ligatures:none"><o:p></o:p></span></p>
|
||||||
<a href="https://www.linkedin.com/company/roboplanet">LinkedIn</a> <a href="https://www.instagram.com/roboplanet.de/">Instagram</a> <a href="https://www.robo-planet.de/newsletter">Newsletteranmeldung</a><br>
|
</td>
|
||||||
Sitz der Gesellschaft München | Geschäftsführung: Axel Banoth<br>
|
</tr>
|
||||||
Registergericht AG München, HRB 296113 | USt.-IdNr. DE400464410<br>
|
</tbody>
|
||||||
<a href="https://www.robo-planet.de/datenschutz">Hinweispflichten zum Datenschutz</a><br>
|
</table>
|
||||||
<img src="cid:banner_image" alt="RoboPlanet Webinar Einladung">
|
<p class="MsoNormal"><span style="display:none"><o:p> </o:p></span></p>
|
||||||
|
<table class="MsoNormalTable" border="0" cellspacing="0" cellpadding="0" width="460" style="width:345.0pt;border-collapse:collapse">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td width="380" colspan="2" style="width:285.0pt;padding:0cm 0cm .75pt 0cm"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="222" style="width:166.5pt;padding:0cm 0cm .75pt 0cm">
|
||||||
|
<p class="MsoNormal"><a href="https://www.robo-planet.de/"><span style="font-size:9.0pt;font-family:"Verdana",sans-serif;color:blue;text-decoration:none"><img border="0" width="203" height="58" style="width:2.1166in;height:.6083in" id="Bild_x0020_1" src="cid:image001.png" alt="Wackler Logo"></span></a><span style="font-size:9.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F"><o:p></o:p></span></p>
|
||||||
|
</td>
|
||||||
|
<td style="padding:0cm 0cm 0cm 0cm"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="320" colspan="2" style="width:240.0pt;padding:3.75pt 0cm 3.75pt 0cm">
|
||||||
|
<p class="MsoNormal"><span style="font-size:9.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F">RoboPlanet GmbH</span><span style="font-size:9.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F"><br>
|
||||||
|
</span><span style="font-size:8.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F">Schatzbogen 39, 81829 München</span><span style="font-size:9.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F"><br>
|
||||||
|
</span><span style="font-size:8.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F">T:</span><span style="font-size:8.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F">
|
||||||
|
<a href="tel:+49%2089%20420490-402"><span style="color:#3D3C3F;text-decoration:none">+49 89 420490-402</span></a>
|
||||||
|
</span><span style="font-size:8.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F">| M:</span><span style="font-size:8.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F">
|
||||||
|
<a href="tel:+49%20175%208334071"><span style="color:#3D3C3F;text-decoration:none">+49 175 8334071</span></a><br>
|
||||||
|
<a href="mailto:e.melcer@robo-planet.de"><span style="color:#3D3C3F;text-decoration:none">e.melcer@robo-planet.de</span></a></span><span style="font-size:8.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F"> |</span><span style="font-size:8.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F">
|
||||||
|
<a href="https://www.robo-planet.de"><span style="color:#3D3C3F;text-decoration:none">www.robo-planet.de</span></a>
|
||||||
|
</span><span style="font-size:9.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F"><o:p></o:p></span></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="MsoNormal"><span style="display:none"><o:p> </o:p></span></p>
|
||||||
|
<table class="MsoNormalTable" border="0" cellspacing="0" cellpadding="0" width="460" style="width:345.0pt;border-collapse:collapse">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td width="24" style="width:18.0pt;padding:2.25pt 1.5pt 4.5pt 0cm">
|
||||||
|
<p class="MsoNormal"><a href="https://eur03.safelinks.protection.outlook.com/?url=https%3A%2F%2Fde.linkedin.com%2Fcompany%2Frobo-planet&data=05%7C02%7Cc.godelmann%40robo-planet.de%7C2fb46eff763446be654708de79274f88%7C6d85a9ef3878420b8f4338d6cb12b665%7C0%7C0%7C639081406901590486%7CUnknown%7CTWFpbGZsb3d8eyJFbXB0eU1hcGkiOnRydWUsIlYiOiIwLjAuMDAwMCIsIlAiOiJXaW4zMiIsIkFOIjoiTWFpbCIsIldUIjoyfQ%3D%3D%7C0%7C%7C%7C&sdata=BbI3CP9VyHoPVpWOr5pnH1rMGr98M0fxtgfuxWqMmW4%3D&reserved=0" originalsrc="https://de.linkedin.com/company/robo-planet"><span style="font-family:"Verdana",sans-serif;color:blue;text-decoration:none"><img border="0" width="20" height="20" style="width:.2083in;height:.2083in" id="Bild_x0020_2" src="cid:image002.png" alt="LinkedIn"></span></a><span style="font-family:"Verdana",sans-serif;color:#3D3C3F"> </span><a href="https://eur03.safelinks.protection.outlook.com/?url=https%3A%2F%2Fwww.instagram.com%2Froboplanet.de%2F&data=05%7C02%7Cc.godelmann%40robo-planet.de%7C2fb46eff763446be654708de79274f88%7C6d85a9ef3878420b8f4338d6cb12b665%7C0%7C0%7C639081406901647062%7CUnknown%7CTWFpbGZsb3d8eyJFbXB0eU1hcGkiOnRydWUsIlYiOiIwLjAuMDAwMCIsIlAiOiJXaW4zMiIsIkFOIjoiTWFpbCIsIldUIjoyfQ%3D%3D%7C0%7C%7C%7C&sdata=VSVFcj7Ceebb9mpIO70Lbsf98Li%2F9rhyqLlZgAW1j1I%3D&reserved=0" originalsrc="https://www.instagram.com/roboplanet.de/"><span style="font-family:"Verdana",sans-serif;color:blue;text-decoration:none"><img border="0" width="20" height="20" style="width:.2083in;height:.2083in" id="Bild_x0020_3" src="cid:image003.png" alt="Instagram"></span></a><span style="font-family:"Verdana",sans-serif;color:#3D3C3F"> </span><a href="https://eur03.safelinks.protection.outlook.com/?url=https%3A%2F%2Frobo-planet.de%2Fnewsletter%2F&data=05%7C02%7Cc.godelmann%40robo-planet.de%7C2fb46eff763446be654708de79274f88%7C6d85a9ef3878420b8f4338d6cb12b665%7C0%7C0%7C639081406901673306%7CUnknown%7CTWFpbGZsb3d8eyJFbXB0eU1hcGkiOnRydWUsIlYiOiIwLjAuMDAwMCIsIlAiOiJXaW4zMiIsIkFOIjoiTWFpbCIsIldUIjoyfQ%3D%3D%7C0%7C%7C%7C&sdata=PaPijR42wD4nP4bDnVO%2F4ldg2IaqD%2Bl6vmx6C9blxIs%3D&reserved=0" originalsrc="https://robo-planet.de/newsletter/" title=""""><span style="font-family:"Verdana",sans-serif;color:blue;text-decoration:none"><img border="0" width="110" height="20" style="width:1.15in;height:.2083in" id="_x0030_.n7mdoupwgbb" src="cid:image004.png" alt="Newsletteranmeldung"></span></a><span style="font-family:"Verdana",sans-serif;color:#3D3C3F"><o:p></o:p></span></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="MsoNormal"><span style="display:none"><o:p> </o:p></span></p>
|
||||||
|
<table class="MsoNormalTable" border="0" cellspacing="0" cellpadding="0" width="460" style="width:345.0pt;border-collapse:collapse">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0cm 0cm 0cm 0cm">
|
||||||
|
<p class="MsoNormal"><span style="font-size:7.5pt;font-family:"Verdana",sans-serif;color:#9B9B9B">Sitz der Gesellschaft München | Geschäftsführung: Axel Banoth</span><span style="font-size:7.5pt;font-family:"Verdana",sans-serif;color:#9B9B9B"><br>
|
||||||
|
</span><span style="font-size:7.5pt;font-family:"Verdana",sans-serif;color:#9B9B9B">Registergericht AG München, HRB 296113 | USt.-IdNr. DE400464410</span><span style="font-size:7.5pt;font-family:"Verdana",sans-serif;color:#9B9B9B"><br>
|
||||||
|
<a href="https://eur03.safelinks.protection.outlook.com/?url=https%3A%2F%2Frobo-planet.de%2Fhinweispflichten%2F&data=05%7C02%7Cc.godelmann%40robo-planet.de%7C2fb46eff763446be654708de79274f88%7C6d85a9ef3878420b8f4338d6cb12b665%7C0%7C0%7C639081406901692853%7CUnknown%7CTWFpbGZsb3d8eyJFbXB0eU1hcGkiOnRydWUsIlYiOiIwLjAuMDAwMCIsIlAiOiJXaW4zMiIsIkFOIjoiTWFpbCIsIldUIjoyfQ%3D%3D%7C0%7C%7C%7C&sdata=GHfn%2Fdye4Tzfw%2BeCeq%2BQXfrhOloPoA%2FH4RfLVKcbG40%3D&reserved=0" originalsrc="https://robo-planet.de/hinweispflichten/"><span style="color:#0069B4">Hinweispflichten</span></a></span><span style="font-size:7.5pt;font-family:"Verdana",sans-serif;color:#9B9B9B"> zum
|
||||||
|
Datenschutz</span><span style="font-size:7.5pt;font-family:"Verdana",sans-serif;color:#9B9B9B">
|
||||||
|
<o:p></o:p></span></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="MsoNormal"><span style="display:none"><o:p> </o:p></span></p>
|
||||||
|
<table class="MsoNormalTable" border="0" cellspacing="0" cellpadding="0" width="460" style="width:345.0pt;border-collapse:collapse">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:3.75pt 0cm 3.75pt 0cm">
|
||||||
|
<p class="MsoNormal"><a href="https://eur03.safelinks.protection.outlook.com/?url=https%3A%2F%2Frobo-planet.de%2Fpraxis-webinar-einsatz-von-robotikloesungen-im-einzelhandel%2F&data=05%7C02%7Cc.godelmann%40robo-planet.de%7C2fb46eff763446be654708de79274f88%7C6d85a9ef3878420b8f4338d6cb12b665%7C0%7C0%7C639081406901714427%7CUnknown%7CTWFpbGZsb3d8eyJFbXB0eU1hcGkiOnRydWUsIlYiOiIwLjAuMDAwMCIsIlAiOiJXaW4zMiIsIkFOIjoiTWFpbCIsIldUIjoyfQ%3D%3D%7C0%7C%7C%7C&sdata=rIFrWwFgYwjSZ0pPvMyePr7oTejMHYiGG1SpY%2FGaINk%3D&reserved=0" originalsrc="https://robo-planet.de/praxis-webinar-einsatz-von-robotikloesungen-im-einzelhandel/" title=""Webinar Einzelhandel""><span style="font-size:9.0pt;font-family:"Verdana",sans-serif;color:blue;text-decoration:none"><img border="0" width="460" height="100" style="width:4.7916in;height:1.0416in" id="_x0030_.zdrws9hgwm8" src="cid:image005.png" alt="RoboPlanetBannerWebinarEinladung.png"></span></a><span style="font-size:9.0pt;font-family:"Verdana",sans-serif;color:#3D3C3F"><o:p></o:p></span></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="MsoNormal"><span style="font-size:10.0pt;font-family:"Verdana",sans-serif"><o:p> </o:p></span></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ def send_approval_card(job_uuid, customer_name, time_string, webhook_url=DEFAULT
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "TextBlock",
|
"type": "TextBlock",
|
||||||
"text": "Wenn Du bis {time_string} Uhr NICHT reagierst, wird die generierte E-Mail automatisch ausgesendet.",
|
"text": f"Wenn Du bis {time_string} Uhr NICHT reagierst, wird die generierte E-Mail automatisch ausgesendet.",
|
||||||
"isSubtle": True,
|
"isSubtle": True,
|
||||||
"wrap": True
|
"wrap": True
|
||||||
}
|
}
|
||||||
@@ -60,5 +60,5 @@ def send_approval_card(job_uuid, customer_name, time_string, webhook_url=DEFAULT
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Fehler beim Senden an Teams: {e}")
|
logging.error(f"Fehler beim Senden an Teams: {e}")
|
||||||
return False
|
return False
|
||||||
|
|||||||
85
scripts/extract_signature_assets.py
Normal file
85
scripts/extract_signature_assets.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import email
|
||||||
|
from email.message import Message
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Define paths
|
||||||
|
eml_file_path = '/app/docs/FYI .eml'
|
||||||
|
output_dir = '/app/lead-engine/trading_twins/'
|
||||||
|
signature_file_path = os.path.join(output_dir, 'signature.html')
|
||||||
|
|
||||||
|
def extract_assets():
|
||||||
|
"""
|
||||||
|
Parses an .eml file to extract the HTML signature and its embedded images.
|
||||||
|
The images are saved to disk, and the HTML is cleaned up to use simple
|
||||||
|
Content-ID (cid) references for use with the Microsoft Graph API.
|
||||||
|
"""
|
||||||
|
if not os.path.exists(eml_file_path):
|
||||||
|
print(f"Error: EML file not found at {eml_file_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(eml_file_path, 'r', errors='ignore') as f:
|
||||||
|
msg = email.message_from_file(f)
|
||||||
|
|
||||||
|
html_content = ""
|
||||||
|
images = {}
|
||||||
|
|
||||||
|
for part in msg.walk():
|
||||||
|
content_type = part.get_content_type()
|
||||||
|
content_disposition = str(part.get("Content-Disposition"))
|
||||||
|
|
||||||
|
if content_type == 'text/html' and "attachment" not in content_disposition:
|
||||||
|
payload = part.get_payload(decode=True)
|
||||||
|
charset = part.get_content_charset() or 'Windows-1252'
|
||||||
|
try:
|
||||||
|
html_content = payload.decode(charset)
|
||||||
|
except (UnicodeDecodeError, AttributeError):
|
||||||
|
html_content = payload.decode('latin1')
|
||||||
|
|
||||||
|
|
||||||
|
if content_type.startswith('image/') and "attachment" not in content_disposition:
|
||||||
|
content_id = part.get('Content-ID', '').strip('<>')
|
||||||
|
filename = part.get_filename()
|
||||||
|
if filename and content_id:
|
||||||
|
images[filename] = {
|
||||||
|
"data": part.get_payload(decode=True),
|
||||||
|
"original_cid": content_id
|
||||||
|
}
|
||||||
|
|
||||||
|
if not html_content:
|
||||||
|
print("Error: Could not find HTML part in the EML file.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Isolate the signature part of the HTML
|
||||||
|
signature_start = html_content.find('Freundliche Gr')
|
||||||
|
if signature_start != -1:
|
||||||
|
# Step back to the start of the table containing the greeting
|
||||||
|
table_start = html_content.rfind('<table', 0, signature_start)
|
||||||
|
if table_start != -1:
|
||||||
|
signature_html = html_content[table_start:]
|
||||||
|
else:
|
||||||
|
signature_html = html_content # Fallback
|
||||||
|
else:
|
||||||
|
print("Warning: Could not find a clear starting point for the signature. Using full HTML body.")
|
||||||
|
signature_html = html_content
|
||||||
|
|
||||||
|
# Save images and update HTML content
|
||||||
|
print(f"Found {len(images)} images to process.")
|
||||||
|
for filename, image_info in images.items():
|
||||||
|
image_path = os.path.join(output_dir, filename)
|
||||||
|
with open(image_path, 'wb') as img_file:
|
||||||
|
img_file.write(image_info['data'])
|
||||||
|
print(f"Saved image: {image_path}")
|
||||||
|
|
||||||
|
# Replace the complex cid in the HTML with the simple filename, which will be the new Content-ID
|
||||||
|
signature_html = signature_html.replace(f"cid:{image_info['original_cid']}", f"cid:{filename}")
|
||||||
|
|
||||||
|
# Clean up some quoted-printable artifacts for better readability in the file
|
||||||
|
signature_html = signature_html.replace('=3D"', '="').replace('=\r\n', '')
|
||||||
|
|
||||||
|
with open(signature_file_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(signature_html)
|
||||||
|
print(f"Saved new signature HTML to: {signature_file_path}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
extract_assets()
|
||||||
Reference in New Issue
Block a user