Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
213 lines
5.9 KiB
Go
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 += "<"
|
|
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
|
|
}
|