Untitled
unknown
golang
10 months ago
31 kB
11
Indexable
package main
import (
"bytes"
"encoding/base64"
"fmt"
"html/template"
"mime/multipart"
"net/smtp"
"os"
"path/filepath"
"regexp"
"strings"
pd "github.com/PipedreamHQ/pipedream-go"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
)
// Input types from previous steps
type AnalysisResult struct {
Action string `json:"action"`
Reason string `json:"reason"`
Consecutivo string `json:"consecutivo"`
BoletinRuedaNegocios string `json:"boletin_rueda_negocios"`
Filename string `json:"filename"`
FoundTerms []string `json:"found_terms"`
UsedWord string `json:"used_word"`
PDFText *string `json:"pdf_text,omitempty"`
PDFPreview *string `json:"pdf_text_preview,omitempty"`
}
type DownloadResult struct {
Action string `json:"action"`
Reason string `json:"reason"`
Consecutivo string `json:"consecutivo"`
Downloads []string `json:"downloads"`
BulletinNum int `json:"bulletin_num"`
SuccessCount int `json:"success_count"`
}
type ZipResult struct {
Action string `json:"action"`
Reason string `json:"reason"`
Consecutivo string `json:"consecutivo"`
ZipPath string `json:"zip_path"`
FileCount int `json:"file_count"`
}
type SummarizeResult struct {
Action string `json:"action"`
Reason string `json:"reason"`
FilteredFiles []string `json:"filtered_files"`
DocumentCount int `json:"document_count"`
SummaryResponse string `json:"summary_response"`
BulletinDate string `json:"bulletin_date"`
ProcessedTypes []string `json:"processed_types"`
LatestFTN string `json:"latest_ftn"`
LatestFTP string `json:"latest_ftp"`
}
// Simplified YAML structures
type FinalSummary struct {
ProjectOverview struct {
ProjectType string `yaml:"project_type"`
ContractingEntity string `yaml:"contracting_entity"`
ProjectDescription string `yaml:"project_description"`
EstimatedValue string `yaml:"estimated_value"`
} `yaml:"project_overview"`
ImportantDates struct {
DeliveryDate string `yaml:"delivery_date"`
ContractStartDate string `yaml:"contract_start_date"`
ContractEndDate string `yaml:"contract_end_date"`
} `yaml:"important_dates"`
DetailedDescription struct {
MainObjectives []string `yaml:"main_objectives"`
ScopeOfWork []string `yaml:"scope_of_work"`
Deliverables []string `yaml:"deliverables"`
} `yaml:"detailed_description"`
TechnicalAspects struct {
TechnicalRequirements []string `yaml:"technical_requirements"`
EquipmentRequirements []string `yaml:"equipment_requirements"`
} `yaml:"technical_aspects"`
FinancialAspects struct {
BudgetDetails string `yaml:"budget_details"`
PaymentTerms string `yaml:"payment_terms"`
GuaranteesRequired []string `yaml:"guarantees_required"`
} `yaml:"financial_aspects"`
TeamAndResources struct {
RequiredProfiles []string `yaml:"required_profiles"`
TeamSize string `yaml:"team_size"`
} `yaml:"team_and_resources"`
RequirementsSummary struct {
EssentialRequirements []string `yaml:"essential_requirements"`
} `yaml:"requirements_summary"`
ExecutiveSummary string `yaml:"executive_summary"`
}
type FinalSummaryWrapper struct {
FinalSummary FinalSummary `yaml:"final_summary"`
}
// Simplified email template data
type EmailTemplateData struct {
ContactName string `json:"contact_name"`
BusinessOpportunityTitle string `json:"business_opportunity_title"`
GreetingMessage string `json:"greeting_message"`
BidDate string `json:"bid_date"`
BulletinNumber string `json:"bulletin_number"`
ClientCompany string `json:"client_company"`
BrokerCompany string `json:"broker_company"`
BiddingModality string `json:"bidding_modality"`
ConsecutiveNumber string `json:"consecutive_number"`
ProjectType string `json:"project_type"`
DocumentCount int `json:"document_count"`
// LLM data
ContractingEntity string `json:"contracting_entity"`
ProjectDescription string `json:"project_description"`
EstimatedValue string `json:"estimated_value"`
MainObjectives []string `json:"main_objectives"`
ScopeOfWork []string `json:"scope_of_work"`
Deliverables []string `json:"deliverables"`
TechnicalRequirements []string `json:"technical_requirements"`
EquipmentRequirements []string `json:"equipment_requirements"`
BudgetDetails string `json:"budget_details"`
PaymentTerms string `json:"payment_terms"`
GuaranteesRequired []string `json:"guarantees_required"`
RequiredProfiles []string `json:"required_profiles"`
TeamSize string `json:"team_size"`
EssentialRequirements []string `json:"essential_requirements"`
DeliveryDate string `json:"delivery_date"`
ContractStartDate string `json:"contract_start_date"`
ContractEndDate string `json:"contract_end_date"`
LLMExecutiveSummary string `json:"llm_executive_summary"`
// Contact info
SenderName string `json:"sender_name"`
SenderTitle string `json:"sender_title"`
SenderCompany string `json:"sender_company"`
ContactEmail string `json:"contact_email"`
ContactPhone string `json:"contact_phone"`
CompanyAddress string `json:"company_address"`
CompanyWebsite string `json:"company_website"`
}
type EmailResult struct {
Action string `json:"action"`
Reason string `json:"reason"`
MessageID string `json:"message_id,omitempty"`
ToEmail string `json:"to_email,omitempty"`
FromEmail string `json:"from_email,omitempty"`
}
// Simplified HTML template
const emailTemplate = `<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Oportunidad de Negocio</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 20px; background-color: #f5f5f5; }
.email-container { max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; }
.header h1 { margin: 0; font-size: 28px; font-weight: bold; }
.header p { margin: 10px 0 0 0; font-size: 16px; opacity: 0.9; }
.urgent-banner { background: #ff6b6b; color: white; padding: 15px; text-align: center; font-weight: bold; font-size: 18px; }
.main-content { padding: 30px; }
.section { margin-bottom: 25px; padding: 20px; background: #f8f9fa; border-radius: 8px; border-left: 4px solid #667eea; }
.section h3 { margin: 0 0 15px 0; color: #667eea; font-size: 18px; }
.info-row { display: flex; justify-content: space-between; margin-bottom: 8px; padding: 5px 0; border-bottom: 1px solid #e9ecef; }
.info-row:last-child { border-bottom: none; }
.info-label { font-weight: bold; color: #555; }
.info-value { color: #333; text-align: right; }
.list-item { margin-bottom: 8px; padding-left: 15px; position: relative; }
.list-item::before { content: '•'; position: absolute; left: 0; color: #667eea; font-weight: bold; }
.greeting { font-size: 16px; margin-bottom: 20px; color: #555; }
.footer { background: #2c3e50; color: white; padding: 20px; text-align: center; }
.footer p { margin: 5px 0; font-size: 14px; }
.footer a { color: #3498db; text-decoration: none; }
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<h1>💼 Oportunidad de Negocio</h1>
<p>{{.BusinessOpportunityTitle}}</p>
</div>
<div class="urgent-banner">
📅 FECHA DE PUJA: {{.BidDate}}
</div>
<div class="main-content">
<div class="greeting">
Estimado/a {{.ContactName}},<br><br>
{{.GreetingMessage}}
</div>
<div class="section">
<h3>📋 Información General</h3>
<div class="info-row">
<span class="info-label">Boletín Número:</span>
<span class="info-value">{{.BulletinNumber}}</span>
</div>
<div class="info-row">
<span class="info-label">Empresa:</span>
<span class="info-value">{{.ClientCompany}}</span>
</div>
<div class="info-row">
<span class="info-label">Comisionista:</span>
<span class="info-value">{{.BrokerCompany}}</span>
</div>
<div class="info-row">
<span class="info-label">Modalidad:</span>
<span class="info-value">{{.BiddingModality}}</span>
</div>
<div class="info-row">
<span class="info-label">Consecutivo:</span>
<span class="info-value">{{.ConsecutiveNumber}}</span>
</div>
{{if .ProjectType}}
<div class="info-row">
<span class="info-label">Tipo de Proyecto:</span>
<span class="info-value">{{.ProjectType}}</span>
</div>
{{end}}
{{if .DocumentCount}}
<div class="info-row">
<span class="info-label">Documentos Procesados:</span>
<span class="info-value">{{.DocumentCount}}</span>
</div>
{{end}}
</div>
{{if or .ContractingEntity .ProjectDescription .EstimatedValue}}
<div class="section">
<h3>📋 Resumen del Proyecto</h3>
{{if .ContractingEntity}}
<div class="info-row">
<span class="info-label">Entidad Contratante:</span>
<span class="info-value">{{.ContractingEntity}}</span>
</div>
{{end}}
{{if .ProjectDescription}}
<div class="info-row">
<span class="info-label">Descripción:</span>
<span class="info-value">{{.ProjectDescription}}</span>
</div>
{{end}}
{{if .EstimatedValue}}
<div class="info-row">
<span class="info-label">Valor Estimado:</span>
<span class="info-value">{{.EstimatedValue}}</span>
</div>
{{end}}
</div>
{{end}}
{{if or .MainObjectives .ScopeOfWork .Deliverables}}
<div class="section">
<h3>🎯 Objetivos y Alcance</h3>
{{if .MainObjectives}}
<h4 style="color: #667eea; margin-bottom: 10px;">Objetivos Principales:</h4>
{{range .MainObjectives}}
<div class="list-item">{{.}}</div>
{{end}}
{{end}}
{{if .ScopeOfWork}}
<h4 style="color: #667eea; margin: 15px 0 10px 0;">Alcance del Trabajo:</h4>
{{range .ScopeOfWork}}
<div class="list-item">{{.}}</div>
{{end}}
{{end}}
{{if .Deliverables}}
<h4 style="color: #667eea; margin: 15px 0 10px 0;">Entregables:</h4>
{{range .Deliverables}}
<div class="list-item">{{.}}</div>
{{end}}
{{end}}
</div>
{{end}}
{{if or .TechnicalRequirements .EquipmentRequirements}}
<div class="section">
<h3>🔧 Aspectos Técnicos</h3>
{{if .TechnicalRequirements}}
<h4 style="color: #667eea; margin-bottom: 10px;">Requisitos Técnicos:</h4>
{{range .TechnicalRequirements}}
<div class="list-item">{{.}}</div>
{{end}}
{{end}}
{{if .EquipmentRequirements}}
<h4 style="color: #667eea; margin: 15px 0 10px 0;">Equipamiento Requerido:</h4>
{{range .EquipmentRequirements}}
<div class="list-item">{{.}}</div>
{{end}}
{{end}}
</div>
{{end}}
{{if or .BudgetDetails .PaymentTerms .GuaranteesRequired}}
<div class="section">
<h3>💰 Aspectos Financieros</h3>
{{if .BudgetDetails}}
<div class="info-row">
<span class="info-label">Detalles del Presupuesto:</span>
<span class="info-value">{{.BudgetDetails}}</span>
</div>
{{end}}
{{if .PaymentTerms}}
<div class="info-row">
<span class="info-label">Términos de Pago:</span>
<span class="info-value">{{.PaymentTerms}}</span>
</div>
{{end}}
{{if .GuaranteesRequired}}
<div class="info-row">
<span class="info-label">Garantías Requeridas:</span>
<span class="info-value">{{range $index, $guarantee := .GuaranteesRequired}}{{if $index}}, {{end}}{{$guarantee}}{{end}}</span>
</div>
{{end}}
</div>
{{end}}
{{if or .RequiredProfiles .TeamSize}}
<div class="section">
<h3>👥 Equipo y Recursos</h3>
{{if .TeamSize}}
<div class="info-row">
<span class="info-label">Tamaño del Equipo:</span>
<span class="info-value">{{.TeamSize}}</span>
</div>
{{end}}
{{if .RequiredProfiles}}
<h4 style="color: #667eea; margin: 15px 0 10px 0;">Perfiles Requeridos:</h4>
{{range .RequiredProfiles}}
<div class="list-item">{{.}}</div>
{{end}}
{{end}}
</div>
{{end}}
{{if .EssentialRequirements}}
<div class="section">
<h3>🔒 Requisitos Esenciales</h3>
{{range .EssentialRequirements}}
<div class="list-item">{{.}}</div>
{{end}}
</div>
{{end}}
{{if .LLMExecutiveSummary}}
<div class="section">
<h3>🤖 Resumen Ejecutivo Completo (IA)</h3>
<div style="font-size: 16px; line-height: 1.6; color: #2c3e50;">
{{.LLMExecutiveSummary}}
</div>
</div>
{{end}}
<div style="margin-top: 30px; padding-top: 20px; border-top: 2px solid #ecf0f1;">
<p>Las fichas técnicas completas están adjuntas a este correo.</p>
<div style="margin-top: 20px; font-size: 16px;">
Atentamente,<br>
<strong>{{.SenderName}}</strong><br>
{{.SenderTitle}}<br>
{{.SenderCompany}}
</div>
</div>
</div>
<div class="footer">
<p>📧 {{.ContactEmail}} | 📞 {{.ContactPhone}}</p>
<p>🏢 {{.CompanyAddress}}</p>
<p><a href="{{.CompanyWebsite}}">{{.CompanyWebsite}}</a></p>
</div>
</div>
</body>
</html>`
func init() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
}
func main() {
log.Info().Msg("📧 Starting SMTP email sending process")
// Get environment variables for SMTP
smtpHost := os.Getenv("SMTP_HOST")
smtpPort := os.Getenv("SMTP_PORT")
smtpUsername := os.Getenv("SMTP_USERNAME")
smtpPassword := os.Getenv("SMTP_PASSWORD")
toEmail := os.Getenv("TO_EMAIL")
fromEmail := os.Getenv("FROM_EMAIL")
if smtpHost == "" || smtpPort == "" || smtpUsername == "" || smtpPassword == "" || toEmail == "" || fromEmail == "" {
panic("Missing required environment variables: SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, TO_EMAIL, or FROM_EMAIL")
}
// Extract data from previous steps
analysisData := extractAnalysisResult()
downloadData := extractDownloadResult()
zipData := extractZipResult()
summaryData := extractSummaryResult()
// Check if we should continue
if zipData.Action != "continue" {
exportResult(&EmailResult{
Action: "skip",
Reason: "Previous steps did not complete successfully",
})
return
}
// Build email template data
templateData := buildTemplateData(analysisData, downloadData, summaryData)
// Send email with attachment
result := sendSMTPEmail(smtpHost, smtpPort, smtpUsername, smtpPassword, fromEmail, toEmail, templateData, zipData.ZipPath)
exportResult(result)
log.Info().
Str("action", result.Action).
Str("to", result.ToEmail).
Msg("✅ Email process complete")
}
func extractAnalysisResult() *AnalysisResult {
data := pd.Steps["pdf_analysis"].(map[string]interface{})["analysis_result"].(map[string]interface{})
result := &AnalysisResult{}
if v, ok := data["action"].(string); ok {
result.Action = v
}
if v, ok := data["reason"].(string); ok {
result.Reason = v
}
if v, ok := data["consecutivo"].(string); ok {
result.Consecutivo = v
}
if v, ok := data["boletin_rueda_negocios"].(string); ok {
result.BoletinRuedaNegocios = v
}
if v, ok := data["filename"].(string); ok {
result.Filename = v
}
if v, ok := data["used_word"].(string); ok {
result.UsedWord = v
}
if v, ok := data["pdf_text"].(string); ok {
result.PDFText = &v
}
if v, ok := data["pdf_text_preview"].(string); ok {
result.PDFPreview = &v
}
if arr, ok := data["found_terms"].([]interface{}); ok {
for _, term := range arr {
if s, ok := term.(string); ok {
result.FoundTerms = append(result.FoundTerms, s)
}
}
}
return result
}
func extractDownloadResult() *DownloadResult {
data := pd.Steps["pdf_bulletin_downloader"].(map[string]interface{})["download_result"].(map[string]interface{})
result := &DownloadResult{}
if v, ok := data["action"].(string); ok {
result.Action = v
}
if v, ok := data["reason"].(string); ok {
result.Reason = v
}
if v, ok := data["consecutivo"].(string); ok {
result.Consecutivo = v
}
if v, ok := data["bulletin_num"].(float64); ok {
result.BulletinNum = int(v)
}
if v, ok := data["success_count"].(float64); ok {
result.SuccessCount = int(v)
}
if arr, ok := data["downloads"].([]interface{}); ok {
for _, download := range arr {
if s, ok := download.(string); ok {
result.Downloads = append(result.Downloads, s)
}
}
}
return result
}
func extractZipResult() *ZipResult {
data := pd.Steps["zip_all"].(map[string]interface{})["zip_result"].(map[string]interface{})
result := &ZipResult{}
if v, ok := data["action"].(string); ok {
result.Action = v
}
if v, ok := data["reason"].(string); ok {
result.Reason = v
}
if v, ok := data["consecutivo"].(string); ok {
result.Consecutivo = v
}
if v, ok := data["zip_path"].(string); ok {
result.ZipPath = v
}
if v, ok := data["file_count"].(float64); ok {
result.FileCount = int(v)
}
return result
}
func extractSummaryResult() *SummarizeResult {
data := pd.Steps["summary_pdfs"].(map[string]interface{})["summarize_result"].(map[string]interface{})
result := &SummarizeResult{}
if v, ok := data["action"].(string); ok {
result.Action = v
}
if v, ok := data["reason"].(string); ok {
result.Reason = v
}
if v, ok := data["document_count"].(float64); ok {
result.DocumentCount = int(v)
}
if v, ok := data["summary_response"].(string); ok {
result.SummaryResponse = v
}
if v, ok := data["bulletin_date"].(string); ok {
result.BulletinDate = v
}
if arr, ok := data["filtered_files"].([]interface{}); ok {
for _, file := range arr {
if s, ok := file.(string); ok {
result.FilteredFiles = append(result.FilteredFiles, s)
}
}
}
if arr, ok := data["processed_types"].([]interface{}); ok {
for _, t := range arr {
if s, ok := t.(string); ok {
result.ProcessedTypes = append(result.ProcessedTypes, s)
}
}
}
if v, ok := data["latest_ftn"].(string); ok {
result.LatestFTN = v
}
if v, ok := data["latest_ftp"].(string); ok {
result.LatestFTP = v
}
return result
}
func buildTemplateData(analysis *AnalysisResult, download *DownloadResult, summary *SummarizeResult) *EmailTemplateData {
// Parse YAML summary
var finalSummary FinalSummary
var finalSummaryWrapper FinalSummaryWrapper
if summary.SummaryResponse != "" {
cleanedYAML := strings.TrimPrefix(summary.SummaryResponse, "```yaml\n")
cleanedYAML = strings.TrimSuffix(cleanedYAML, "\n```")
if err := yaml.Unmarshal([]byte(cleanedYAML), &finalSummaryWrapper); err != nil {
if err := yaml.Unmarshal([]byte(cleanedYAML), &finalSummary); err != nil {
log.Warn().Err(err).Msg("⚠️ Failed to parse YAML summary")
} else {
log.Info().Msg("✅ Successfully parsed final_summary YAML format")
}
} else {
log.Info().Msg("✅ Successfully parsed final_summary wrapper YAML format")
finalSummary = finalSummaryWrapper.FinalSummary
}
}
// Extract bid date and bidding modality from PDF text
bidDate := ""
biddingModality := ""
if analysis.PDFText != nil {
bidDate = extractBidDateFromPDF(*analysis.PDFText)
biddingModality = extractBiddingModalityFromPDF(*analysis.PDFText)
} else if analysis.PDFPreview != nil {
bidDate = extractBidDateFromPDF(*analysis.PDFPreview)
biddingModality = extractBiddingModalityFromPDF(*analysis.PDFPreview)
}
// Use LLM-extracted bulletin date if available
if summary.BulletinDate != "" {
bidDate = summary.BulletinDate
}
// Extract company names from filename
clientCompany, brokerCompany := extractCompaniesFromFilename(analysis.Filename)
// Build business opportunity title
businessOpportunityTitle := ""
if finalSummary.ProjectOverview.ProjectDescription != "" {
words := strings.Fields(finalSummary.ProjectOverview.ProjectDescription)
if len(words) > 0 {
maxWords := 8
if len(words) < maxWords {
maxWords = len(words)
}
businessOpportunityTitle = strings.Join(words[:maxWords], " ")
if len(words) > 8 {
businessOpportunityTitle += "..."
}
}
} else if analysis.UsedWord != "" {
businessOpportunityTitle = analysis.UsedWord
}
// Determine project type
projectType := ""
if len(summary.ProcessedTypes) > 0 {
if len(summary.ProcessedTypes) == 1 {
projectType = summary.ProcessedTypes[0]
} else {
projectType = strings.Join(summary.ProcessedTypes, " + ")
}
}
// Get environment variables
contactName := getEnvOrDefault("CONTACT_NAME", "")
senderName := getEnvOrDefault("SENDER_NAME", "")
senderTitle := getEnvOrDefault("SENDER_TITLE", "")
senderCompany := getEnvOrDefault("SENDER_COMPANY", "")
contactPhone := getEnvOrDefault("CONTACT_PHONE", "")
companyAddress := getEnvOrDefault("COMPANY_ADDRESS", "")
companyWebsite := getEnvOrDefault("COMPANY_WEBSITE", "")
return &EmailTemplateData{
ContactName: contactName,
BusinessOpportunityTitle: businessOpportunityTitle,
GreetingMessage: finalSummary.ExecutiveSummary,
BidDate: bidDate,
BulletinNumber: fmt.Sprintf("RN-ALCANCE-%s.1", analysis.BoletinRuedaNegocios),
ClientCompany: clientCompany,
BrokerCompany: brokerCompany,
BiddingModality: biddingModality,
ConsecutiveNumber: analysis.Consecutivo,
ProjectType: projectType,
DocumentCount: summary.DocumentCount,
ContractingEntity: finalSummary.ProjectOverview.ContractingEntity,
ProjectDescription: finalSummary.ProjectOverview.ProjectDescription,
EstimatedValue: finalSummary.ProjectOverview.EstimatedValue,
MainObjectives: finalSummary.DetailedDescription.MainObjectives,
ScopeOfWork: finalSummary.DetailedDescription.ScopeOfWork,
Deliverables: finalSummary.DetailedDescription.Deliverables,
TechnicalRequirements: finalSummary.TechnicalAspects.TechnicalRequirements,
EquipmentRequirements: finalSummary.TechnicalAspects.EquipmentRequirements,
BudgetDetails: finalSummary.FinancialAspects.BudgetDetails,
PaymentTerms: finalSummary.FinancialAspects.PaymentTerms,
GuaranteesRequired: finalSummary.FinancialAspects.GuaranteesRequired,
RequiredProfiles: finalSummary.TeamAndResources.RequiredProfiles,
TeamSize: finalSummary.TeamAndResources.TeamSize,
EssentialRequirements: finalSummary.RequirementsSummary.EssentialRequirements,
DeliveryDate: finalSummary.ImportantDates.DeliveryDate,
ContractStartDate: finalSummary.ImportantDates.ContractStartDate,
ContractEndDate: finalSummary.ImportantDates.ContractEndDate,
LLMExecutiveSummary: finalSummary.ExecutiveSummary,
SenderName: senderName,
SenderTitle: senderTitle,
SenderCompany: senderCompany,
ContactEmail: os.Getenv("FROM_EMAIL"),
ContactPhone: contactPhone,
CompanyAddress: companyAddress,
CompanyWebsite: companyWebsite,
}
}
func extractBidDateFromPDF(pdfText string) string {
patterns := []string{
`comprar[áa]\s+el\s+(\w+),\s+(\d{1,2}\s+de\s+\w+\s+de\s+\d{4})`,
`FECHA\s+DE\s+PUJA:\s*(\d{1,2}\s+de\s+\w+\s+de\s+\d{4})`,
`(?:fecha|puja|compra).*?(\d{1,2}\s+de\s+\w+\s+de\s+\d{4})`,
}
for _, pattern := range patterns {
re := regexp.MustCompile(pattern)
if matches := re.FindStringSubmatch(strings.ToLower(pdfText)); len(matches) > 1 {
return matches[len(matches)-1]
}
}
return ""
}
func extractBiddingModalityFromPDF(pdfText string) string {
re := regexp.MustCompile(`MODALIDAD\s+DE\s+PUJA\s*[:]?\s*([^\n]+)`)
if matches := re.FindStringSubmatch(pdfText); len(matches) > 1 {
return strings.TrimSpace(matches[1])
}
return ""
}
func extractCompaniesFromFilename(filename string) (client, broker string) {
client = "N/A"
broker = "N/A"
name := strings.TrimSuffix(filename, filepath.Ext(filename))
re := regexp.MustCompile(`B\.R\.N\.\s+\d+\s+-\s+(.+)`)
matches := re.FindStringSubmatch(name)
if len(matches) < 2 {
return
}
companiesPart := matches[1]
lastUnderscore := strings.LastIndex(companiesPart, "_")
if lastUnderscore == -1 {
return
}
client = strings.TrimSpace(companiesPart[:lastUnderscore])
broker = strings.TrimSpace(companiesPart[lastUnderscore+1:])
spaceNormalizer := regexp.MustCompile(`\s+`)
client = spaceNormalizer.ReplaceAllString(client, " ")
broker = spaceNormalizer.ReplaceAllString(broker, " ")
return
}
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func sendSMTPEmail(smtpHost, smtpPort, username, password, fromEmail, toEmail string, templateData *EmailTemplateData, attachmentPath string) *EmailResult {
log.Info().
Str("to", toEmail).
Str("from", fromEmail).
Msg("📤 Preparing to send SMTP email")
// Parse and execute template
tmpl, err := template.New("email").Parse(emailTemplate)
if err != nil {
log.Error().Err(err).Msg("❌ Failed to parse email template")
return &EmailResult{Action: "error", Reason: fmt.Sprintf("Failed to parse email template: %v", err)}
}
var htmlBody bytes.Buffer
if err := tmpl.Execute(&htmlBody, templateData); err != nil {
log.Error().Err(err).Msg("❌ Failed to execute email template")
return &EmailResult{Action: "error", Reason: fmt.Sprintf("Failed to execute email template: %v", err)}
}
// Create email message
var emailBuffer bytes.Buffer
writer := multipart.NewWriter(&emailBuffer)
boundary := writer.Boundary()
subject := fmt.Sprintf("Oportunidad de Negocio - %s", templateData.BusinessOpportunityTitle)
headers := fmt.Sprintf("From: BMC Oportunidades <%s>\r\n", fromEmail)
headers += fmt.Sprintf("To: %s\r\n", toEmail)
headers += fmt.Sprintf("Subject: =?UTF-8?B?%s?=\r\n", base64.StdEncoding.EncodeToString([]byte(subject)))
headers += "MIME-Version: 1.0\r\n"
headers += fmt.Sprintf("Content-Type: multipart/mixed; boundary=%s\r\n", boundary)
headers += "\r\n"
emailBuffer.WriteString(headers)
// Add HTML body
htmlPart, err := writer.CreatePart(map[string][]string{
"Content-Type": {"text/html; charset=UTF-8"},
"Content-Transfer-Encoding": {"base64"},
})
if err != nil {
log.Error().Err(err).Msg("❌ Failed to create HTML part")
return &EmailResult{Action: "error", Reason: fmt.Sprintf("Failed to create HTML part: %v", err)}
}
encoded := base64.StdEncoding.EncodeToString(htmlBody.Bytes())
for i := 0; i < len(encoded); i += 76 {
end := i + 76
if end > len(encoded) {
end = len(encoded)
}
htmlPart.Write([]byte(encoded[i:end] + "\r\n"))
}
// Add attachment if exists
if attachmentPath != "" && fileExists(attachmentPath) {
attachmentData, err := os.ReadFile(attachmentPath)
if err != nil {
log.Error().Err(err).Str("path", attachmentPath).Msg("❌ Failed to read attachment")
} else {
attachmentPart, err := writer.CreatePart(map[string][]string{
"Content-Type": {"application/zip"},
"Content-Transfer-Encoding": {"base64"},
"Content-Disposition": {fmt.Sprintf("attachment; filename=\"%s\"", filepath.Base(attachmentPath))},
})
if err != nil {
log.Error().Err(err).Msg("❌ Failed to create attachment part")
} else {
encoded := base64.StdEncoding.EncodeToString(attachmentData)
for i := 0; i < len(encoded); i += 76 {
end := i + 76
if end > len(encoded) {
end = len(encoded)
}
attachmentPart.Write([]byte(encoded[i:end] + "\r\n"))
}
log.Info().Str("filename", filepath.Base(attachmentPath)).Int("size", len(attachmentData)).Msg("📎 Attachment added")
}
}
}
writer.Close()
// Send email
auth := smtp.PlainAuth("", username, password, smtpHost)
addr := fmt.Sprintf("%s:%s", smtpHost, smtpPort)
err = smtp.SendMail(addr, auth, fromEmail, []string{toEmail}, emailBuffer.Bytes())
if err != nil {
log.Error().Err(err).Msg("❌ Failed to send SMTP email")
return &EmailResult{Action: "error", Reason: fmt.Sprintf("Failed to send SMTP email: %v", err)}
}
log.Info().Str("smtp_server", addr).Msg("✅ Email sent successfully via SMTP")
return &EmailResult{
Action: "continue",
Reason: "Email sent successfully via SMTP",
MessageID: fmt.Sprintf("smtp-%d", len(subject)),
ToEmail: toEmail,
FromEmail: fromEmail,
}
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func exportResult(result *EmailResult) {
pd.Export("email_result", result)
log.Info().
Str("action", result.Action).
Str("reason", result.Reason).
Msg("📤 Email result exported")
}
Editor is loading...
Leave a Comment