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 <ticket_id>
!reopen <ticket_id>
!assign <ticket_id> <agent_email>
"""
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