870 lines
23 KiB
Go
870 lines
23 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db"
|
|
"github.com/gorilla/mux"
|
|
"github.com/jhillyerd/enmime"
|
|
"golang.org/x/oauth2"
|
|
"golang.org/x/oauth2/google"
|
|
"google.golang.org/api/gmail/v1"
|
|
"google.golang.org/api/option"
|
|
)
|
|
|
|
type EmailHandler struct {
|
|
queries *db.Queries
|
|
db *sql.DB
|
|
gmailService *gmail.Service
|
|
}
|
|
|
|
type EmailResponse struct {
|
|
ID int32 `json:"id"`
|
|
Subject string `json:"subject"`
|
|
UserID int32 `json:"user_id"`
|
|
Created time.Time `json:"created"`
|
|
GmailMessageID *string `json:"gmail_message_id,omitempty"`
|
|
AttachmentCount int32 `json:"attachment_count"`
|
|
IsDownloaded *bool `json:"is_downloaded,omitempty"`
|
|
}
|
|
|
|
type EmailDetailResponse struct {
|
|
ID int32 `json:"id"`
|
|
Subject string `json:"subject"`
|
|
UserID int32 `json:"user_id"`
|
|
Created time.Time `json:"created"`
|
|
GmailMessageID *string `json:"gmail_message_id,omitempty"`
|
|
GmailThreadID *string `json:"gmail_thread_id,omitempty"`
|
|
RawHeaders *string `json:"raw_headers,omitempty"`
|
|
IsDownloaded *bool `json:"is_downloaded,omitempty"`
|
|
Enquiries []int32 `json:"enquiries,omitempty"`
|
|
Invoices []int32 `json:"invoices,omitempty"`
|
|
PurchaseOrders []int32 `json:"purchase_orders,omitempty"`
|
|
Jobs []int32 `json:"jobs,omitempty"`
|
|
}
|
|
|
|
type EmailAttachmentResponse struct {
|
|
ID int32 `json:"id"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Size int32 `json:"size"`
|
|
Filename string `json:"filename"`
|
|
IsMessageBody bool `json:"is_message_body"`
|
|
GmailAttachmentID *string `json:"gmail_attachment_id,omitempty"`
|
|
Created time.Time `json:"created"`
|
|
}
|
|
|
|
func NewEmailHandler(queries *db.Queries, database *sql.DB) *EmailHandler {
|
|
// Try to initialize Gmail service
|
|
gmailService, err := getGmailService("credentials.json", "token.json")
|
|
if err != nil {
|
|
// Log the error but continue without Gmail service
|
|
fmt.Printf("Warning: Gmail service not available: %v\n", err)
|
|
}
|
|
|
|
return &EmailHandler{
|
|
queries: queries,
|
|
db: database,
|
|
gmailService: gmailService,
|
|
}
|
|
}
|
|
|
|
// List emails with pagination and filtering
|
|
func (h *EmailHandler) List(w http.ResponseWriter, r *http.Request) {
|
|
// Parse query parameters
|
|
limitStr := r.URL.Query().Get("limit")
|
|
offsetStr := r.URL.Query().Get("offset")
|
|
search := r.URL.Query().Get("search")
|
|
userID := r.URL.Query().Get("user_id")
|
|
|
|
// Set defaults
|
|
limit := 50
|
|
offset := 0
|
|
|
|
if limitStr != "" {
|
|
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
|
|
limit = l
|
|
}
|
|
}
|
|
|
|
if offsetStr != "" {
|
|
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
|
|
offset = o
|
|
}
|
|
}
|
|
|
|
// Build query
|
|
query := `
|
|
SELECT e.id, e.subject, e.user_id, e.created, e.gmail_message_id, e.email_attachment_count, e.is_downloaded
|
|
FROM emails e`
|
|
|
|
var args []interface{}
|
|
var conditions []string
|
|
|
|
if search != "" {
|
|
conditions = append(conditions, "e.subject LIKE ?")
|
|
args = append(args, "%"+search+"%")
|
|
}
|
|
|
|
if userID != "" {
|
|
conditions = append(conditions, "e.user_id = ?")
|
|
args = append(args, userID)
|
|
}
|
|
|
|
if len(conditions) > 0 {
|
|
query += " WHERE " + joinConditions(conditions, " AND ")
|
|
}
|
|
|
|
query += " ORDER BY e.id DESC LIMIT ? OFFSET ?"
|
|
args = append(args, limit, offset)
|
|
|
|
rows, err := h.db.Query(query, args...)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var emails []EmailResponse
|
|
for rows.Next() {
|
|
var email EmailResponse
|
|
var gmailMessageID sql.NullString
|
|
var isDownloaded sql.NullBool
|
|
|
|
err := rows.Scan(
|
|
&email.ID,
|
|
&email.Subject,
|
|
&email.UserID,
|
|
&email.Created,
|
|
&gmailMessageID,
|
|
&email.AttachmentCount,
|
|
&isDownloaded,
|
|
)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if gmailMessageID.Valid {
|
|
email.GmailMessageID = &gmailMessageID.String
|
|
}
|
|
if isDownloaded.Valid {
|
|
email.IsDownloaded = &isDownloaded.Bool
|
|
}
|
|
|
|
emails = append(emails, email)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(emails)
|
|
}
|
|
|
|
// Get a specific email with details
|
|
func (h *EmailHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
emailID, err := strconv.Atoi(vars["id"])
|
|
if err != nil {
|
|
http.Error(w, "Invalid email ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get email details
|
|
query := `
|
|
SELECT e.id, e.subject, e.user_id, e.created, e.gmail_message_id,
|
|
e.gmail_thread_id, e.raw_headers, e.is_downloaded
|
|
FROM emails e
|
|
WHERE e.id = ?`
|
|
|
|
var email EmailDetailResponse
|
|
var gmailMessageID, gmailThreadID, rawHeaders sql.NullString
|
|
var isDownloaded sql.NullBool
|
|
|
|
err = h.db.QueryRow(query, emailID).Scan(
|
|
&email.ID,
|
|
&email.Subject,
|
|
&email.UserID,
|
|
&email.Created,
|
|
&gmailMessageID,
|
|
&gmailThreadID,
|
|
&rawHeaders,
|
|
&isDownloaded,
|
|
)
|
|
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
http.Error(w, "Email not found", http.StatusNotFound)
|
|
} else {
|
|
http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
if gmailMessageID.Valid {
|
|
email.GmailMessageID = &gmailMessageID.String
|
|
}
|
|
if gmailThreadID.Valid {
|
|
email.GmailThreadID = &gmailThreadID.String
|
|
}
|
|
if rawHeaders.Valid {
|
|
email.RawHeaders = &rawHeaders.String
|
|
}
|
|
if isDownloaded.Valid {
|
|
email.IsDownloaded = &isDownloaded.Bool
|
|
}
|
|
|
|
// Get associated enquiries
|
|
enquiryRows, err := h.db.Query("SELECT enquiry_id FROM emails_enquiries WHERE email_id = ?", emailID)
|
|
if err == nil {
|
|
defer enquiryRows.Close()
|
|
for enquiryRows.Next() {
|
|
var enquiryID int32
|
|
if enquiryRows.Scan(&enquiryID) == nil {
|
|
email.Enquiries = append(email.Enquiries, enquiryID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get associated invoices
|
|
invoiceRows, err := h.db.Query("SELECT invoice_id FROM emails_invoices WHERE email_id = ?", emailID)
|
|
if err == nil {
|
|
defer invoiceRows.Close()
|
|
for invoiceRows.Next() {
|
|
var invoiceID int32
|
|
if invoiceRows.Scan(&invoiceID) == nil {
|
|
email.Invoices = append(email.Invoices, invoiceID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get associated purchase orders
|
|
poRows, err := h.db.Query("SELECT purchase_order_id FROM emails_purchase_orders WHERE email_id = ?", emailID)
|
|
if err == nil {
|
|
defer poRows.Close()
|
|
for poRows.Next() {
|
|
var poID int32
|
|
if poRows.Scan(&poID) == nil {
|
|
email.PurchaseOrders = append(email.PurchaseOrders, poID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get associated jobs
|
|
jobRows, err := h.db.Query("SELECT job_id FROM emails_jobs WHERE email_id = ?", emailID)
|
|
if err == nil {
|
|
defer jobRows.Close()
|
|
for jobRows.Next() {
|
|
var jobID int32
|
|
if jobRows.Scan(&jobID) == nil {
|
|
email.Jobs = append(email.Jobs, jobID)
|
|
}
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(email)
|
|
}
|
|
|
|
// List attachments for an email
|
|
func (h *EmailHandler) ListAttachments(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
emailID, err := strconv.Atoi(vars["id"])
|
|
if err != nil {
|
|
http.Error(w, "Invalid email ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// First check if attachments are already in database
|
|
query := `
|
|
SELECT id, name, type, size, filename, is_message_body, gmail_attachment_id, created
|
|
FROM email_attachments
|
|
WHERE email_id = ?
|
|
ORDER BY is_message_body DESC, created ASC`
|
|
|
|
rows, err := h.db.Query(query, emailID)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var attachments []EmailAttachmentResponse
|
|
hasStoredAttachments := false
|
|
|
|
for rows.Next() {
|
|
hasStoredAttachments = true
|
|
var attachment EmailAttachmentResponse
|
|
var gmailAttachmentID sql.NullString
|
|
|
|
err := rows.Scan(
|
|
&attachment.ID,
|
|
&attachment.Name,
|
|
&attachment.Type,
|
|
&attachment.Size,
|
|
&attachment.Filename,
|
|
&attachment.IsMessageBody,
|
|
&gmailAttachmentID,
|
|
&attachment.Created,
|
|
)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if gmailAttachmentID.Valid {
|
|
attachment.GmailAttachmentID = &gmailAttachmentID.String
|
|
}
|
|
|
|
attachments = append(attachments, attachment)
|
|
}
|
|
|
|
// If no stored attachments and this is a Gmail email, try to fetch from Gmail
|
|
if !hasStoredAttachments && h.gmailService != nil {
|
|
// Get Gmail message ID
|
|
var gmailMessageID sql.NullString
|
|
err := h.db.QueryRow("SELECT gmail_message_id FROM emails WHERE id = ?", emailID).Scan(&gmailMessageID)
|
|
|
|
if err == nil && gmailMessageID.Valid {
|
|
// Fetch message metadata from Gmail
|
|
message, err := h.gmailService.Users.Messages.Get("me", gmailMessageID.String).
|
|
Format("FULL").Do()
|
|
|
|
if err == nil && message.Payload != nil {
|
|
// Extract attachment info from Gmail message
|
|
attachmentIndex := int32(1)
|
|
h.extractGmailAttachments(message.Payload, &attachments, &attachmentIndex)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if this is an HTMX request
|
|
if r.Header.Get("HX-Request") == "true" {
|
|
// Return HTML for HTMX
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
|
|
if len(attachments) == 0 {
|
|
// No attachments found
|
|
html := `<div class="notification is-light">
|
|
<p class="has-text-grey">No attachments found for this email.</p>
|
|
</div>`
|
|
w.Write([]byte(html))
|
|
return
|
|
}
|
|
|
|
// Build HTML table for attachments
|
|
var htmlBuilder strings.Builder
|
|
htmlBuilder.WriteString(`<div class="box">
|
|
<h3 class="title is-5">Attachments</h3>
|
|
<div class="table-container">
|
|
<table class="table is-fullwidth">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Type</th>
|
|
<th>Size</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>`)
|
|
|
|
for _, att := range attachments {
|
|
icon := `<i class="fas fa-paperclip"></i>`
|
|
if att.IsMessageBody {
|
|
icon = `<i class="fas fa-envelope"></i>`
|
|
}
|
|
|
|
downloadURL := fmt.Sprintf("/api/v1/emails/%d/attachments/%d", emailID, att.ID)
|
|
if att.GmailAttachmentID != nil {
|
|
downloadURL = fmt.Sprintf("/api/v1/emails/%d/attachments/%d/stream", emailID, att.ID)
|
|
}
|
|
|
|
htmlBuilder.WriteString(fmt.Sprintf(`
|
|
<tr>
|
|
<td>
|
|
<span class="icon has-text-grey">%s</span>
|
|
%s
|
|
</td>
|
|
<td><span class="tag is-light">%s</span></td>
|
|
<td>%d bytes</td>
|
|
<td>
|
|
<a href="%s" target="_blank" class="button is-small is-info">
|
|
<span class="icon"><i class="fas fa-download"></i></span>
|
|
<span>Download</span>
|
|
</a>
|
|
</td>
|
|
</tr>`, icon, att.Name, att.Type, att.Size, downloadURL))
|
|
}
|
|
|
|
htmlBuilder.WriteString(`
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>`)
|
|
|
|
w.Write([]byte(htmlBuilder.String()))
|
|
return
|
|
}
|
|
|
|
// Return JSON for API requests
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(attachments)
|
|
}
|
|
|
|
// Helper function to extract attachment info from Gmail message parts
|
|
func (h *EmailHandler) extractGmailAttachments(part *gmail.MessagePart, attachments *[]EmailAttachmentResponse, index *int32) {
|
|
// Check if this part is an attachment
|
|
// Some attachments may not have filenames or may be inline
|
|
if part.Body != nil && part.Body.AttachmentId != "" {
|
|
filename := part.Filename
|
|
if filename == "" {
|
|
// Try to generate a filename from content type
|
|
switch part.MimeType {
|
|
case "application/pdf":
|
|
filename = "attachment.pdf"
|
|
case "image/png":
|
|
filename = "image.png"
|
|
case "image/jpeg":
|
|
filename = "image.jpg"
|
|
case "text/plain":
|
|
filename = "text.txt"
|
|
default:
|
|
filename = "attachment"
|
|
}
|
|
}
|
|
|
|
attachment := EmailAttachmentResponse{
|
|
ID: *index,
|
|
Name: filename,
|
|
Type: part.MimeType,
|
|
Size: int32(part.Body.Size),
|
|
Filename: filename,
|
|
IsMessageBody: false,
|
|
GmailAttachmentID: &part.Body.AttachmentId,
|
|
Created: time.Now(), // Use current time as placeholder
|
|
}
|
|
*attachments = append(*attachments, attachment)
|
|
*index++
|
|
}
|
|
|
|
// Process sub-parts
|
|
for _, subPart := range part.Parts {
|
|
h.extractGmailAttachments(subPart, attachments, index)
|
|
}
|
|
}
|
|
|
|
// Search emails
|
|
func (h *EmailHandler) Search(w http.ResponseWriter, r *http.Request) {
|
|
query := r.URL.Query().Get("q")
|
|
if query == "" {
|
|
http.Error(w, "Search query is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Parse optional parameters
|
|
limitStr := r.URL.Query().Get("limit")
|
|
limit := 20
|
|
|
|
if limitStr != "" {
|
|
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
|
|
limit = l
|
|
}
|
|
}
|
|
|
|
// Search in subjects and headers
|
|
sqlQuery := `
|
|
SELECT e.id, e.subject, e.user_id, e.created, e.gmail_message_id, e.email_attachment_count, e.is_downloaded
|
|
FROM emails e
|
|
WHERE e.subject LIKE ? OR e.raw_headers LIKE ?
|
|
ORDER BY e.id DESC
|
|
LIMIT ?`
|
|
|
|
searchTerm := "%" + query + "%"
|
|
rows, err := h.db.Query(sqlQuery, searchTerm, searchTerm, limit)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var emails []EmailResponse
|
|
for rows.Next() {
|
|
var email EmailResponse
|
|
var gmailMessageID sql.NullString
|
|
var isDownloaded sql.NullBool
|
|
|
|
err := rows.Scan(
|
|
&email.ID,
|
|
&email.Subject,
|
|
&email.UserID,
|
|
&email.Created,
|
|
&gmailMessageID,
|
|
&email.AttachmentCount,
|
|
&isDownloaded,
|
|
)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if gmailMessageID.Valid {
|
|
email.GmailMessageID = &gmailMessageID.String
|
|
}
|
|
if isDownloaded.Valid {
|
|
email.IsDownloaded = &isDownloaded.Bool
|
|
}
|
|
|
|
emails = append(emails, email)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(emails)
|
|
}
|
|
|
|
// Stream email content from Gmail
|
|
func (h *EmailHandler) StreamContent(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
emailID, err := strconv.Atoi(vars["id"])
|
|
if err != nil {
|
|
http.Error(w, "Invalid email ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get email details to check if it's a Gmail email
|
|
query := `
|
|
SELECT e.gmail_message_id, e.subject, e.created, e.user_id
|
|
FROM emails e
|
|
WHERE e.id = ?`
|
|
|
|
var gmailMessageID sql.NullString
|
|
var subject string
|
|
var created time.Time
|
|
var userID int32
|
|
|
|
err = h.db.QueryRow(query, emailID).Scan(&gmailMessageID, &subject, &created, &userID)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
http.Error(w, "Email not found", http.StatusNotFound)
|
|
} else {
|
|
http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
if !gmailMessageID.Valid {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
html := `
|
|
<div class="notification is-warning">
|
|
<strong>Local Email</strong><br>
|
|
This email is not from Gmail and does not have stored content available for display.
|
|
</div>`
|
|
w.Write([]byte(html))
|
|
return
|
|
}
|
|
|
|
// Check for stored message body content in attachments
|
|
attachmentQuery := `
|
|
SELECT id, name, type, size
|
|
FROM email_attachments
|
|
WHERE email_id = ? AND is_message_body = 1
|
|
ORDER BY created ASC`
|
|
|
|
attachmentRows, err := h.db.Query(attachmentQuery, emailID)
|
|
if err == nil {
|
|
defer attachmentRows.Close()
|
|
if attachmentRows.Next() {
|
|
var attachmentID int32
|
|
var name, attachmentType string
|
|
var size int32
|
|
|
|
if attachmentRows.Scan(&attachmentID, &name, &attachmentType, &size) == nil {
|
|
// Found stored message body - would normally read the content from file storage
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
html := fmt.Sprintf(`
|
|
<div class="content">
|
|
<div class="notification is-success is-light">
|
|
<strong>Stored Email Content</strong><br>
|
|
Message body is stored locally as attachment: %s (%s, %d bytes)
|
|
</div>
|
|
<div class="box">
|
|
<p><em>Content would be loaded from local storage here.</em></p>
|
|
<p>Attachment ID: %d</p>
|
|
</div>
|
|
</div>`, name, attachmentType, size, attachmentID)
|
|
w.Write([]byte(html))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try to fetch from Gmail if service is available
|
|
if h.gmailService != nil {
|
|
// Fetch from Gmail
|
|
message, err := h.gmailService.Users.Messages.Get("me", gmailMessageID.String).
|
|
Format("RAW").Do()
|
|
if err != nil {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
html := fmt.Sprintf(`
|
|
<div class="notification is-danger">
|
|
<strong>Gmail API Error</strong><br>
|
|
Failed to fetch email from Gmail: %v
|
|
</div>`, err)
|
|
w.Write([]byte(html))
|
|
return
|
|
}
|
|
|
|
// Decode message
|
|
rawEmail, err := base64.URLEncoding.DecodeString(message.Raw)
|
|
if err != nil {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
html := fmt.Sprintf(`
|
|
<div class="notification is-danger">
|
|
<strong>Decode Error</strong><br>
|
|
Failed to decode email: %v
|
|
</div>`, err)
|
|
w.Write([]byte(html))
|
|
return
|
|
}
|
|
|
|
// Parse with enmime
|
|
env, err := enmime.ReadEnvelope(bytes.NewReader(rawEmail))
|
|
if err != nil {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
html := fmt.Sprintf(`
|
|
<div class="notification is-danger">
|
|
<strong>Parse Error</strong><br>
|
|
Failed to parse email: %v
|
|
</div>`, err)
|
|
w.Write([]byte(html))
|
|
return
|
|
}
|
|
|
|
// Stream HTML or Text directly to client
|
|
if env.HTML != "" {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
w.Write([]byte(env.HTML))
|
|
} else if env.Text != "" {
|
|
// Convert plain text to HTML for better display
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
html := fmt.Sprintf(`
|
|
<div class="content">
|
|
<div class="notification is-info is-light">
|
|
<strong>Plain Text Email</strong><br>
|
|
This email contains only plain text content.
|
|
</div>
|
|
<div class="box">
|
|
<pre style="white-space: pre-wrap; font-family: inherit;">%s</pre>
|
|
</div>
|
|
</div>`, env.Text)
|
|
w.Write([]byte(html))
|
|
} else {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
html := `
|
|
<div class="notification is-warning">
|
|
<strong>No Content</strong><br>
|
|
No HTML or text content found in this email.
|
|
</div>`
|
|
w.Write([]byte(html))
|
|
}
|
|
return
|
|
}
|
|
|
|
// No Gmail service available - show error
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
html := fmt.Sprintf(`
|
|
<div class="content">
|
|
<div class="notification is-warning is-light">
|
|
<strong>Gmail Service Unavailable</strong><br>
|
|
<small>Subject: %s</small><br>
|
|
<small>Date: %s</small><br>
|
|
<small>Gmail Message ID: <code>%s</code></small>
|
|
</div>
|
|
|
|
<div class="box">
|
|
<div class="content">
|
|
<h4>Integration Status</h4>
|
|
<p>Gmail service is not available. To enable email content display:</p>
|
|
<ol>
|
|
<li>Ensure <code>credentials.json</code> and <code>token.json</code> files are present</li>
|
|
<li>Configure Gmail API OAuth2 authentication</li>
|
|
<li>Restart the application</li>
|
|
</ol>
|
|
<p class="has-text-grey-light is-size-7">
|
|
<strong>Gmail Message ID:</strong> <code>%s</code>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>`,
|
|
subject,
|
|
created.Format("2006-01-02 15:04:05"),
|
|
gmailMessageID.String,
|
|
gmailMessageID.String)
|
|
|
|
w.Write([]byte(html))
|
|
}
|
|
|
|
// Stream attachment from Gmail
|
|
func (h *EmailHandler) StreamAttachment(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
emailID, err := strconv.Atoi(vars["id"])
|
|
if err != nil {
|
|
http.Error(w, "Invalid email ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
attachmentID := vars["attachmentId"]
|
|
|
|
// Get email's Gmail message ID
|
|
var gmailMessageID sql.NullString
|
|
err = h.db.QueryRow("SELECT gmail_message_id FROM emails WHERE id = ?", emailID).Scan(&gmailMessageID)
|
|
if err != nil || !gmailMessageID.Valid {
|
|
http.Error(w, "Email not found or not a Gmail email", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
if h.gmailService == nil {
|
|
http.Error(w, "Gmail service not available", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
// For dynamic attachments, we need to fetch the message and find the attachment
|
|
message, err := h.gmailService.Users.Messages.Get("me", gmailMessageID.String).
|
|
Format("FULL").Do()
|
|
if err != nil {
|
|
http.Error(w, "Failed to fetch email from Gmail", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Find the attachment by index
|
|
var targetAttachment *gmail.MessagePart
|
|
attachmentIndex := 1
|
|
findAttachment(message.Payload, attachmentID, &attachmentIndex, &targetAttachment)
|
|
|
|
if targetAttachment == nil || targetAttachment.Body == nil || targetAttachment.Body.AttachmentId == "" {
|
|
http.Error(w, "Attachment not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Fetch attachment data from Gmail
|
|
attachment, err := h.gmailService.Users.Messages.Attachments.
|
|
Get("me", gmailMessageID.String, targetAttachment.Body.AttachmentId).Do()
|
|
if err != nil {
|
|
http.Error(w, "Failed to fetch attachment from Gmail", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Decode base64
|
|
data, err := base64.URLEncoding.DecodeString(attachment.Data)
|
|
if err != nil {
|
|
http.Error(w, "Failed to decode attachment", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Set headers and stream
|
|
filename := targetAttachment.Filename
|
|
if filename == "" {
|
|
// Generate filename from content type (same logic as extractGmailAttachments)
|
|
switch targetAttachment.MimeType {
|
|
case "application/pdf":
|
|
filename = "attachment.pdf"
|
|
case "image/png":
|
|
filename = "image.png"
|
|
case "image/jpeg":
|
|
filename = "image.jpg"
|
|
case "text/plain":
|
|
filename = "text.txt"
|
|
default:
|
|
filename = "attachment"
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", targetAttachment.MimeType)
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(data)))
|
|
w.Write(data)
|
|
}
|
|
|
|
// Helper function to find attachment by index
|
|
func findAttachment(part *gmail.MessagePart, targetID string, currentIndex *int, result **gmail.MessagePart) {
|
|
// Check if this part is an attachment (same logic as extractGmailAttachments)
|
|
if part.Body != nil && part.Body.AttachmentId != "" {
|
|
fmt.Printf("Checking attachment %d (looking for %s): %s\n", *currentIndex, targetID, part.Filename)
|
|
if strconv.Itoa(*currentIndex) == targetID {
|
|
fmt.Printf("Found matching attachment!\n")
|
|
*result = part
|
|
return
|
|
}
|
|
*currentIndex++
|
|
}
|
|
|
|
for _, subPart := range part.Parts {
|
|
findAttachment(subPart, targetID, currentIndex, result)
|
|
if *result != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper function to join conditions
|
|
func joinConditions(conditions []string, separator string) string {
|
|
if len(conditions) == 0 {
|
|
return ""
|
|
}
|
|
if len(conditions) == 1 {
|
|
return conditions[0]
|
|
}
|
|
|
|
result := conditions[0]
|
|
for i := 1; i < len(conditions); i++ {
|
|
result += separator + conditions[i]
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Gmail OAuth2 functions
|
|
func getGmailService(credentialsFile, tokenFile string) (*gmail.Service, error) {
|
|
ctx := context.Background()
|
|
|
|
b, err := ioutil.ReadFile(credentialsFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to read client secret file: %v", err)
|
|
}
|
|
|
|
config, err := google.ConfigFromJSON(b, gmail.GmailReadonlyScope)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse client secret file to config: %v", err)
|
|
}
|
|
|
|
client := getClient(config, tokenFile)
|
|
srv, err := gmail.NewService(ctx, option.WithHTTPClient(client))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to retrieve Gmail client: %v", err)
|
|
}
|
|
|
|
return srv, nil
|
|
}
|
|
|
|
func getClient(config *oauth2.Config, tokFile string) *http.Client {
|
|
tok, err := tokenFromFile(tokFile)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return config.Client(context.Background(), tok)
|
|
}
|
|
|
|
func tokenFromFile(file string) (*oauth2.Token, error) {
|
|
f, err := os.Open(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
tok := &oauth2.Token{}
|
|
err = json.NewDecoder(f).Decode(tok)
|
|
return tok, err
|
|
} |