mattermost-community-enterp.../message_export/message_export.go
Claude fad2fe9d3c Initial commit: Mattermost Community Enterprise
Open source implementation of Mattermost Enterprise features:

Authentication & SSO:
- LDAP authentication and sync
- LDAP diagnostics
- SAML 2.0 SSO
- OAuth providers (Google, Office365, OpenID Connect)

Infrastructure:
- Redis-based cluster implementation
- Prometheus metrics
- IP filtering
- Push proxy authentication

Search:
- Bleve search engine (lightweight Elasticsearch alternative)

Compliance & Security:
- Compliance reporting
- Data retention policies
- Message export (Actiance, GlobalRelay, CSV)
- Access control (PAP/PDP)

User Management:
- Account migration (LDAP/SAML)
- ID-loaded push notifications
- Outgoing OAuth connections

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 23:49:14 +09:00

213 lines
5.9 KiB
Go

// 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 := `<?xml version="1.0" encoding="UTF-8"?>
<Message>
<MessageId>` + record.PostId + `</MessageId>
<ConversationId>` + record.ChannelId + `</ConversationId>
<SenderId>` + record.UserId + `</SenderId>
<SenderEmail>` + record.UserEmail + `</SenderEmail>
<DateTime>` + time.Unix(0, record.PostCreateAt*int64(time.Millisecond)).Format(time.RFC3339) + `</DateTime>
<Body>` + escapeXML(record.PostMessage) + `</Body>
</Message>`
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 += "&lt;"
case '>':
result += "&gt;"
case '&':
result += "&amp;"
case '"':
result += "&quot;"
case '\'':
result += "&apos;"
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
}