import imaplib import email from email.header import decode_header from tickets.models import Ticket, Message from django.conf import settings from tickets.mail_sender import send_email from tickets.utils import get_message_id, generate_message_id def is_support_agent(email_address): return email_address.lower() in [e.lower() for e in settings.SUPPORT_AGENTS] def strip_command_lines(message_body): """ Removes lines starting with "!" from the message body. """ if not message_body: return "" lines = message_body.splitlines() filtered_lines = [line for line in lines if not line.strip().startswith("!")] return "\n".join(filtered_lines) def parse_commands(ticket, message_body, sender_email, raw_email_bytes): """ Execute inline commands like: !list_open !close !reopen !assign """ if not is_support_agent(sender_email): return # Only agents can modify tickets lines = message_body.splitlines() # Only parse lines that start with "!" to avoid random text command_lines = [line for line in lines if line.strip().startswith("!")] for line in command_lines: line = line.strip() cmd = line.lower() # List open tickets if cmd == "!list_open": open_tickets = Ticket.objects.filter(status="open").order_by("created_at") if not open_tickets.exists(): body = "There are no open tickets." else: body_lines = [ f"#{t.id} | {t.subject} | Reporter: {t.reporter} | Assignee: {t.assignee or 'Unassigned'} | Created: {t.created_at.strftime('%Y-%m-%d %H:%M')}" for t in open_tickets ] body = "Open tickets:\n\n" + "\n".join(body_lines) send_email( to_email=sender_email, subject="Open Tickets List", body=body, ticket=None, sender_email=None ) continue # Commands that target a specific ticket parts = cmd.split() if len(parts) < 2: continue # Skip invalid commands action = parts[0] try: ticket_id = int(parts[1]) ticket = Ticket.objects.get(id=ticket_id) except (ValueError, Ticket.DoesNotExist): send_email( to_email=sender_email, subject=f"Ticket #{parts[1]} Not Found", body=f"No ticket with ID {parts[1]} exists.", ticket=None, sender_email=None ) continue if action == "!close": ticket.status = "closed" ticket.save() send_email( to_email=sender_email, subject=f"[Ticket #{ticket.id}] Closed", body=f"Ticket #{ticket.id} has been closed.", ticket=None, sender_email=None ) message_id = generate_message_id() references = message_id send_email( to_email=ticket.reporter, subject=f"[Ticket #{ticket.id}] Closed", body=f"Ticket #{ticket.id} has been closed. If you still require support, please reply to this email.\n\n– Support", ticket=ticket, sender_email=None, in_reply_to=message_id, references=references ) elif action == "!reopen": ticket.status = "open" ticket.save() send_email( to_email=sender_email, subject=f"[Ticket #{ticket.id}] Reopened", body=f"Ticket #{ticket.id} has been reopened.", ticket=None, sender_email=None ) elif action == "!assign" and len(parts) == 3: ticket.assignee = parts[2] ticket.save() send_email( to_email=sender_email, subject=f"[Ticket #{ticket.id}] Assigned", body=f"Ticket #{ticket.id} has been assigned to {parts[2]}.", ticket=None, sender_email=None ) elif action == ("!reply"): parts = line.split(maxsplit=2) ticket_id = int(parts[1]) ticket = Ticket.objects.get(id=ticket_id) last_message = ticket.messages.order_by("-created_at").first() if last_message: in_reply_to = last_message.message_id references = " ".join(ticket.messages.order_by("created_at").values_list("message_id", flat=True)) else: in_reply_to = generate_message_id() references = in_reply_to message_id = generate_message_id() references = message_id # Create body for message if len(parts) == 3: body=f"{parts[2]}" else: body=strip_command_lines(message_body) send_email( to_email=ticket.reporter, subject=f"RE: [Ticket #{ticket.id}] {ticket.subject}", body=body, ticket=ticket, sender_email=None, in_reply_to=message_id, references=references ) # Store message in the ticket Message.objects.create( ticket=ticket, sender=sender_email, body=body, raw_email=raw_email_bytes.decode("utf-8"), message_id=message_id, in_reply_to=in_reply_to ) def fetch_emails(): created_tickets = [] updated_tickets = [] mail = imaplib.IMAP4_SSL(settings.IMAP_HOST) mail.login(settings.IMAP_USER, settings.IMAP_PASSWORD) mail.select(settings.IMAP_FOLDER) status, messages = mail.search(None, 'UNSEEN') if status != "OK": return created_tickets, updated_tickets for num in messages[0].split(): status, data = mail.fetch(num, '(RFC822)') if status != "OK": continue raw_email_bytes = data[0][1] msg = email.message_from_bytes(raw_email_bytes) subject, encoding = decode_header(msg["Subject"])[0] if isinstance(subject, bytes): subject = subject.decode(encoding or "utf-8") from_email = email.utils.parseaddr(msg.get("From"))[1] in_reply_to = msg.get("In-Reply-To") # Extract plain-text body body = None is_html = False if msg.is_multipart(): for part in msg.walk(): if part.get_content_type() == "text/plain": body = part.get_payload(decode=True).decode(part.get_content_charset() or "utf-8") break elif part.get_content_type() == "text/html": is_html = True if not settings.PLAIN_TEXT_ONLY and body is None: body = part.get_payload(decode=True).decode(part.get_content_charset() or "utf-8") else: if msg.get_content_type() == "text/plain": body = msg.get_payload(decode=True).decode(msg.get_content_charset() or "utf-8") elif msg.get_content_type() == "text/html": is_html = True if not settings.PLAIN_TEXT_ONLY: body = msg.get_payload(decode=True).decode(msg.get_content_charset() or "utf-8") # Reject non-plain-text emails if setting enabled if body is None: send_email( to_email=from_email, subject=f"Re: {subject}", body="Your email was rejected because only plain-text emails are allowed.", ticket=None, sender_email=None ) continue # Check for command-like messages if body.strip().lower().startswith("!"): try: parse_commands(None, body, from_email, raw_email_bytes) except Exception as e: print(e) # Skip creating a ticket if it’s only a command if body.strip().lower() == "!list_open" or body.strip().lower().startswith(("!close", "!reopen", "!assign", "!reply")): continue # Determine if new ticket or reply in_reply_to = msg.get("In-Reply-To") if in_reply_to: in_reply_to = in_reply_to.strip().split()[0] message_id = get_message_id(raw_email_bytes.decode("utf-8")) references = msg.get("References") parent_message_id = None ticket = None if references: refs = [r.strip() for r in references.split()] # split on whitespace parent_message_id = refs[0] # last ID is usually the parent try: if parent_message_id: message = Message.objects.get(message_id=parent_message_id) ticket = message.ticket updated_tickets.append(ticket) send_email(to_email='', body=body, subject=f"RE: [Ticket #{ticket.id}] {ticket.subject}", ticket=ticket, sender_email=from_email, in_reply_to=message_id, references=message_id, ) else: ticket = Ticket.objects.create(subject=subject, reporter=from_email) created_tickets.append(ticket) # Send confirmation to reporter send_email( to_email=from_email, subject=f"[Ticket #{ticket.id}] {subject}", body=f"Hi,\n\nWe have received your ticket #{ticket.id} and are looking into it.\n\n– {settings.SUPPORT_COMPANY} Support", in_reply_to=message_id, references=message_id, ticket=ticket, sender_email=from_email ) # Notify all support agents message_id = get_message_id(raw_email_bytes.decode("utf-8")) print(f"New ticket created with Message-ID: {message_id}") for agent_email in settings.SUPPORT_AGENTS: send_email( to_email=agent_email, subject=f"New Ticket #{ticket.id}: {subject}", body=f"Hello,\n\nA new ticket has been submitted.\n\nTicket #{ticket.id}\nReporter: {from_email}\nSubject: {subject}\n\nMessage:\n{body}\n\n– {settings.SUPPORT_COMPANY} Support", in_reply_to=message_id, references=message_id, ticket=ticket, sender_email=from_email ) except Exception as e: print(f"Error processing email: {e}") # fallback: create new ticket if lookup fails ticket = Ticket.objects.create(subject=subject, reporter=from_email) created_tickets.append(ticket) # Store message in the ticket Message.objects.create( ticket=ticket, sender=from_email, body=body, raw_email=raw_email_bytes.decode("utf-8"), message_id=message_id, in_reply_to=in_reply_to ) # Parse commands parse_commands(ticket, body, from_email, raw_email_bytes) mail.logout() return created_tickets, updated_tickets