// Copyright (c) 2024 Mattermost Community Enterprise // Message Export Implementation package message_export import ( "net/http" "time" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" ) // MessageExportConfig holds configuration for the message export interface type MessageExportConfig struct { Store store.Store Config func() *model.Config Logger mlog.LoggerIFace } // MessageExportImpl implements the MessageExportInterface type MessageExportImpl struct { store store.Store config func() *model.Config logger mlog.LoggerIFace } // NewMessageExportInterface creates a new message export interface func NewMessageExportInterface(cfg *MessageExportConfig) *MessageExportImpl { return &MessageExportImpl{ store: cfg.Store, config: cfg.Config, logger: cfg.Logger, } } // StartSynchronizeJob starts a new message export synchronization job func (me *MessageExportImpl) StartSynchronizeJob(rctx request.CTX, exportFromTimestamp int64) (*model.Job, *model.AppError) { cfg := me.config() // Check if message export is enabled if cfg.MessageExportSettings.EnableExport == nil || !*cfg.MessageExportSettings.EnableExport { return nil, model.NewAppError("StartSynchronizeJob", "message_export.not_enabled", nil, "Message export is not enabled", http.StatusNotImplemented) } // Create job data jobData := map[string]string{ "export_from_timestamp": time.Unix(0, exportFromTimestamp*int64(time.Millisecond)).Format(time.RFC3339), } // Add export format to job data if cfg.MessageExportSettings.ExportFormat != nil { jobData["export_format"] = *cfg.MessageExportSettings.ExportFormat } else { jobData["export_format"] = model.ComplianceExportTypeActiance } // Create the job job := &model.Job{ Id: model.NewId(), Type: model.JobTypeMessageExport, Status: model.JobStatusPending, Data: jobData, CreateAt: model.GetMillis(), } // In a real implementation, we would save the job to the store // and let the job scheduler pick it up if me.store != nil { savedJob, err := me.store.Job().Save(job) if err != nil { return nil, model.NewAppError("StartSynchronizeJob", "message_export.save_job_failed", nil, err.Error(), http.StatusInternalServerError) } job = savedJob } me.logger.Info("Started message export synchronization job", mlog.String("job_id", job.Id), mlog.String("export_from_timestamp", jobData["export_from_timestamp"]), mlog.String("export_format", jobData["export_format"]), ) return job, nil } // Export formats supported const ( ExportFormatActiance = "actiance" ExportFormatGlobalrelay = "globalrelay" ExportFormatGlobalrelayZip = "globalrelay-zip" ExportFormatCSV = "csv" ) // MessageExportRecord represents an exported message type MessageExportRecord struct { PostId string TeamId string TeamName string TeamDisplayName string ChannelId string ChannelName string ChannelType string UserId string UserEmail string Username string PostCreateAt int64 PostMessage string PostType string PostFileIds []string } // FormatMessage formats a message for export based on the export format func (me *MessageExportImpl) FormatMessage(record *MessageExportRecord, format string) ([]byte, error) { switch format { case ExportFormatActiance: return me.formatActiance(record) case ExportFormatGlobalrelay, ExportFormatGlobalrelayZip: return me.formatGlobalrelay(record) case ExportFormatCSV: return me.formatCSV(record) default: return me.formatActiance(record) } } func (me *MessageExportImpl) formatActiance(record *MessageExportRecord) ([]byte, error) { // Actiance XML format xml := ` ` + record.PostId + ` ` + record.ChannelId + ` ` + record.UserId + ` ` + record.UserEmail + ` ` + time.Unix(0, record.PostCreateAt*int64(time.Millisecond)).Format(time.RFC3339) + ` ` + escapeXML(record.PostMessage) + ` ` return []byte(xml), nil } func (me *MessageExportImpl) formatGlobalrelay(record *MessageExportRecord) ([]byte, error) { // GlobalRelay EML format (simplified) eml := `From: ` + record.UserEmail + ` To: ` + record.ChannelName + `@mattermost.local Date: ` + time.Unix(0, record.PostCreateAt*int64(time.Millisecond)).Format(time.RFC1123Z) + ` Subject: Message in ` + record.ChannelName + ` Message-ID: <` + record.PostId + `@mattermost.local> Content-Type: text/plain; charset="UTF-8" ` + record.PostMessage return []byte(eml), nil } func (me *MessageExportImpl) formatCSV(record *MessageExportRecord) ([]byte, error) { // CSV row createTime := time.Unix(0, record.PostCreateAt*int64(time.Millisecond)).Format(time.RFC3339) csv := escapeCSV(record.PostId) + "," + escapeCSV(record.TeamName) + "," + escapeCSV(record.ChannelName) + "," + escapeCSV(record.Username) + "," + escapeCSV(record.UserEmail) + "," + escapeCSV(createTime) + "," + escapeCSV(record.PostMessage) + "\n" return []byte(csv), nil } func escapeXML(s string) string { result := "" for _, c := range s { switch c { case '<': result += "<" case '>': result += ">" case '&': result += "&" case '"': result += """ case '\'': result += "'" default: result += string(c) } } return result } func escapeCSV(s string) string { needsQuotes := false for _, c := range s { if c == '"' || c == ',' || c == '\n' || c == '\r' { needsQuotes = true break } } if !needsQuotes { return s } result := "\"" for _, c := range s { if c == '"' { result += "\"\"" } else { result += string(c) } } result += "\"" return result }