I have a doctype helpdesk_ticket in which it create new tickets for every mail received in mail ID ‘Support’
It is working fine but having a small issue
some mails received are not creating new tickets and also some mails received are creating duplicate tickets
also the attachments received in mail are not displaying in the ticket
i have developed a custom function fetch_mail for that which will fetch all mails in every 3 mins
Here is the code for the same
please can anyone help me regarding this issue
api.py:
import frappe
from frappe.utils import now, getdate
from frappe.utils.caching import redis_cache
@frappe.whitelist()
def fetch_email():
try:
email_server = frappe.get_doc(“Email Account”, “Support”)
emails = email_server.get_inbound_mails()
for email in emails:
subject = email.subject.strip() if email.subject else ""
sender_email = email.from_email
description = email.content or email.html_content
attachments = email.attachments or []
received_time = email.date
in_reply_to = getattr(email, "in_reply_to", "").strip()
references = getattr(email, "references", "").strip()
message_id = getattr(email, "message_id", "").strip()
# Normalize the subject by removing 'Re:' or 'Fwd:'
if subject.lower().startswith(("re:", "fwd:")):
clean_subject = subject[4:].strip()
else:
clean_subject = subject
# Check for existing communication by Message ID
existing_communication = frappe.db.exists(
"Communication", {"message_id": message_id}
)
# Skip if this email has already been processed
if existing_communication:
continue
# Check if the email is a reply or forwarded email using In-Reply-To or References
existing_ticket = None
if in_reply_to or references:
# Match existing ticket by email headers
existing_ticket = frappe.get_all(
"Helpdesk_Ticket",
filters={
"contact_email": sender_email,
"subject": clean_subject,
"status": ["not in", ["Closed", "Resolved"]],
"creation": [">=", getdate(now())]
},
fields=["name"],
limit=1
)
if not existing_ticket:
existing_ticket = frappe.get_all(
"Helpdesk_Ticket",
filters={
"contact_email": sender_email,
"subject": clean_subject,
"description": description,
"status": ["not in", ["Closed", "Resolved"]],
"creation": [">=", getdate(now())]
},
fields=["name"],
)
if existing_ticket:
ticket_name = existing_ticket[0]["name"]
ticket_doc = frappe.get_doc("Helpdesk_Ticket", ticket_name)
# Log the email content as a communication in the existing ticket
log_email_as_communication(
ticket_doc, subject, description, sender_email, attachments
)
continue # Skip creating a new ticket for replies or matched emails
# Create a new Helpdesk Ticket if no existing ticket is found
ticket = frappe.get_doc(
{
"doctype": "Helpdesk_Ticket",
"subject": subject,
"contact_email": sender_email,
"description": description,
"status": frappe.db.get_value(
"Status_Master", {"status": "Open"}, "name"
),
"ticket_source": frappe.db.get_value(
"Source_Master", {"source": "Email"}, "name"
),
"created_datetime": now(),
"last_updated_time": now(),
}
)
ticket.insert()
# Log the ticket creation in the activity stream
ticket.add_comment("Info", f"Ticket created via email from {sender_email}")
# Log the email as a communication in the new ticket
log_email_as_communication(ticket, subject, description, sender_email, attachments)
frappe.db.commit()
except Exception as e:
frappe.log_error(frappe.get_traceback(), "Helpdesk Email Fetch Error")
def log_email_as_communication(
ticket_doc, subject, content, sender_email, attachments=None
):
“”“Logs the email as communication under the specified Helpdesk Ticket and attaches files.”“”
communication = frappe.get_doc(
{
“doctype”: “Communication”,
“communication_type”: “Communication”,
“reference_doctype”: “Helpdesk_Ticket”,
“reference_name”: ticket_doc.name,
“subject”: subject,
“content”: content,
“communication_medium”: “Email”,
“sender”: sender_email,
“recipients”: ticket_doc.contact_email,
“message_id”: frappe.generate_hash(
length=10
), # Generating a unique message ID for this communication
}
)
communication.insert(ignore_permissions=True)
# If there are attachments, save them to the Communication
if attachments:
save_attachments(communication, attachments)
def save_attachments(related_doc, attachments):
“”“Handles and saves email attachments to the specified document.”“”
attachment_links =
for attachment in attachments:
file_name = attachment.get(“filename”) or attachment.get(“name”)
file_url = attachment.get(“file_url”)
content = attachment.get(“content”)
# Debugging: Log the incoming attachment details
frappe.log_error(f"Processing attachment: {attachment}", "Attachment Debug")
# Skip processing if both file_name and file_url are missing
if not file_name and not file_url:
frappe.log_error(
"Attachment is missing file_name and file_url.", "Attachment Error"
)
continue
# Check if content is base64 encoded and decode it if necessary
if content:
try:
content = content.decode("base64")
except Exception as e:
frappe.log_error(
f"Failed to decode attachment content: {str(e)}", "Attachment Error"
)
continue
# Create the File document only if valid data is available
_file = frappe.get_doc(
{
"doctype": "File",
"attached_to_doctype": related_doc.doctype,
"attached_to_name": related_doc.name,
"file_name": file_name,
"file_url": file_url,
"content": content,
"is_private": 0, # Adjust privacy based on your requirements
}
)
_file.insert(ignore_permissions=True)
# Append file URL or file name to the attachment_links list
attachment_links.append(
f'<a href="{_file.file_url}" target="_blank">{_file.file_name}</a>'
)
# Save the attachment links in the custom 'attachment' field if it's a Helpdesk Ticket
if related_doc.doctype == "Helpdesk_Ticket" and attachment_links:
related_doc.attachment = ", ".join(attachment_links)
related_doc.save(ignore_permissions=True)
elif related_doc.doctype == "Communication" and attachment_links:
related_doc.db_set("attachments", ", ".join(attachment_links))
@redis_cache()
def get_attachments(doctype, name):
“”“Fetches attachments related to a particular document.”“”
QBFile = frappe.qb.DocType(“File”)
return (
frappe.qb.from_(QBFile)
.select(QBFile.name, QBFile.file_url, QBFile.file_name)
.where(QBFile.attached_to_doctype == doctype)
.where(QBFile.attached_to_name == name)
.run(as_dict=True)
)