Duplicate Document creation from same mail

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)
)

helpdesk_ticket.py :

import frappe
from frappe.model.document import Document
from frappe.utils import now

class Helpdesk_Ticket(Document):
def before_insert(self):
self.created_datetime = now()
if not self.status:
self.status = frappe.db.get_value(
“Status_Master”, {“status”: “Open”}, “name”
)
if not self.ticket_source:
self.ticket_source = frappe.db.get_value(
“Source_Master”, {“source”: “Phone”}, “name”
)

def save_attachments(self, attachments):
    for attachment in attachments:
        file_name = attachment.get("filename")
        file_content = attachment.get("content")

        if file_name and file_content:
            _file = frappe.get_doc(
                {
                    "doctype": "File",
                    "file_name": file_name,
                    "content": file_content,
                    "attached_to_doctype": "Helpdesk_Ticket",
                    "attached_to_name": self.name,
                    "is_public": 1,
                }
            )
            _file.insert()
            self.add_comment("Attachment", f"File {file_name} attached.")

def before_save(self):
    self.last_updated_time = now()

    # Track previous status
    previous_status = (
        self.get_doc_before_save().status if not self.is_new() else None
    )
    resolved_status = frappe.db.get_value(
        "Status_Master", {"status": "Resolved"}, "name"
    )
    closed_status = frappe.db.get_value(
        "Status_Master", {"status": "Closed"}, "name"
    )
    reopened_statuses = [
        frappe.db.get_value("Status_Master", {"status": "Open"}, "name"),
        frappe.db.get_value("Status_Master", {"status": "Pending"}, "name"),
    ]

    # Set resolved_datetime if status is set to 'Resolved'
    if self.status == resolved_status and previous_status != resolved_status:
        if not self.resolved_datetime:
            self.resolved_datetime = now()
            self.db_set("resolved_datetime", self.resolved_datetime)

    # Handle is_reopened flag
    if (
        previous_status in [resolved_status, closed_status]
        and self.status in reopened_statuses
    ):
        self.is_reopened = 1
        self.db_set("is_reopened", 1)

        # Increment times_reopened
        self.times_reopened = (self.times_reopened or 0) + 1
        self.db_set("times_reopened", self.times_reopened)

        # Clear resolved_datetime if reopened
        self.resolved_datetime = None
        self.db_set("resolved_datetime", None)

def validate(self):
    closed_status = frappe.db.get_value(
        "Status_Master", {"status": "Closed"}, "name"
    )
    resolved_status = frappe.db.get_value(
        "Status_Master", {"status": "Resolved"}, "name"
    )

    if self.status == closed_status:
        mandatory_fields = [
            "subject",
            "status",
            "priority",
            "ticket_type",
            "company",
            "branch",
            "division",
            "department",
            "agent",
            "description",
        ]
        for field in mandatory_fields:
            if not self.get(field):
                frappe.throw((f"{field} is mandatory to close the ticket"))

    # Validation for manual ticket creation
    phone_source = frappe.db.get_value("Source_Master", {"source": "Phone"}, "name")
    email_source = frappe.db.get_value("Source_Master", {"source": "Email"}, "name")

    if self.ticket_source == phone_source:
        mandatory_fields = [
            "subject",
            "raised_by",
            "status",
            "priority",
            "ticket_type",
            "company",
            "branch",
            "division",
            "department",
            "agent",
            "description",
        ]
        for field in mandatory_fields:
            if not self.get(field):
                frappe.throw((f"{field} is mandatory for ticket creation"))
    elif self.ticket_source == email_source and not self.is_new():
        # Check if the ticket is being edited
        mandatory_fields = [
            "subject",
            "raised_by",
            "status",
            "priority",
            "ticket_type",
            "company",
            "branch",
            "division",
            "department",
            "agent",
            "description",
            "contact_email",  # Add email-specific mandatory fields
        ]
        for field in mandatory_fields:
            if not self.get(field):
                frappe.throw(("All Fields are mandatory for saving a Ticket"))

def after_insert(self):
    # Log the ticket creation in the activity stream
    self.log_ticket_activity(
        f"Ticket {self.name} created with subject: {self.subject} and description: {self.description}"
    )

    # Only add a creation comment if the ticket is created via email
    email_source = frappe.db.get_value("Source_Master", {"source": "Email"}, "name")
    if self.ticket_source == email_source:
        self.add_creation_comment()

def add_creation_comment(self):
    comment_content = f"**{self.subject}**\n\n{self.description}"
    comment = frappe.get_doc(
        {
            "doctype": "Communication",
            "communication_type": "Comment",
            "reference_doctype": "Helpdesk_Ticket",
            "reference_name": self.name,
            "content": comment_content,
            "communication_medium": "Other",
            "sender": self.contact_email or "admin@example.com",
        }
    )
    comment.insert(ignore_permissions=True)

def log_ticket_activity(self, message):
    activity = frappe.get_doc(
        {
            "doctype": "Communication",
            "communication_type": "Comment",
            "reference_doctype": "Helpdesk_Ticket",
            "reference_name": self.name,
            "content": message,
            "communication_medium": "Other",
            "sender": self.contact_email or "admin@example.com",
        }
    )
    activity.insert(ignore_permissions=True)
    frappe.db.commit()

can anyone help me in this issue

regarding help in this

  • Ticket not creating for some mails

  • Attachments are not storing in tickets

  • Reply creating new tickets

  • Some mails creating tickets 2 or more times

  • Reply all not sending to cc and bcc

Solved now

refined the code and changed the method to use frappe default method email_pull