XYZ-35: Added Support for GlobalRelay Compliance Export Format

* Added username to ChannelMemberHistory struct in anticipation of supporting GlobalRelay in Compliance Export
* Removed translation from debug output - this makes it complicated to use utils functions from tests in the enterprise repo
* Added an advanced email function that allows for greater control over message details. Updated MessageExport config to support GlobalRelay. Added attachment support to InBucket unit tests
* Moving templates in from enterprise to solve test issues
* Added export format to diagnostics
* Changed email attachment code to use FileBackend so that S3 storage is properly supported
Этот коммит содержится в:
Jonathan
2018-02-07 09:02:46 -05:00
коммит произвёл GitHub
родитель b2ee507793
Коммит d3e934d07a
17 изменённых файлов: 413 добавлений и 51 удалений

Просмотреть файл

@@ -501,6 +501,7 @@ func (a *App) trackConfig() {
a.SendDiagnostic(TRACK_CONFIG_MESSAGE_EXPORT, map[string]interface{}{ a.SendDiagnostic(TRACK_CONFIG_MESSAGE_EXPORT, map[string]interface{}{
"enable_message_export": *cfg.MessageExportSettings.EnableExport, "enable_message_export": *cfg.MessageExportSettings.EnableExport,
"export_format": *cfg.MessageExportSettings.ExportFormat,
"daily_run_time": *cfg.MessageExportSettings.DailyRunTime, "daily_run_time": *cfg.MessageExportSettings.DailyRunTime,
"default_export_from_timestamp": *cfg.MessageExportSettings.ExportFromTimestamp, "default_export_from_timestamp": *cfg.MessageExportSettings.ExportFromTimestamp,
"batch_size": *cfg.MessageExportSettings.BatchSize, "batch_size": *cfg.MessageExportSettings.BatchSize,

Просмотреть файл

@@ -131,10 +131,6 @@
"id": "api.admin.upload_brand_image.too_large.app_error", "id": "api.admin.upload_brand_image.too_large.app_error",
"translation": "Unable to upload file. File is too large." "translation": "Unable to upload file. File is too large."
}, },
{
"id": "api.api.init.parsing_templates.debug",
"translation": "Parsing server templates at %v"
},
{ {
"id": "api.api.init.parsing_templates.error", "id": "api.api.init.parsing_templates.error",
"translation": "Failed to parse server templates %v" "translation": "Failed to parse server templates %v"
@@ -4858,6 +4854,14 @@
"id": "model.config.is_valid.message_export.batch_size.app_error", "id": "model.config.is_valid.message_export.batch_size.app_error",
"translation": "Message export job BatchSize must be a positive integer" "translation": "Message export job BatchSize must be a positive integer"
}, },
{
"id": "model.config.is_valid.message_export.export_type.app_error",
"translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
},
{
"id": "model.config.is_valid.message_export.global_relay_email_address.app_error",
"translation": "Message export job GlobalRelayEmailAddress must be set to a valid email address"
},
{ {
"id": "model.config.is_valid.message_export.daily_runtime.app_error", "id": "model.config.is_valid.message_export.daily_runtime.app_error",
"translation": "Message export job DailyRuntime must be a 24-hour time stamp in the form HH:MM." "translation": "Message export job DailyRuntime must be a 24-hour time stamp in the form HH:MM."
@@ -7082,6 +7086,10 @@
"id": "utils.mail.new_client.auth.app_error", "id": "utils.mail.new_client.auth.app_error",
"translation": "Failed to authenticate on SMTP server" "translation": "Failed to authenticate on SMTP server"
}, },
{
"id": "utils.mail.sendMail.attachments.write_error",
"translation": "Failed to write attachment to email"
},
{ {
"id": "utils.mail.new_client.helo.error", "id": "utils.mail.new_client.helo.error",
"translation": "Failed to to set the HELO to SMTP server %v" "translation": "Failed to to set the HELO to SMTP server %v"
@@ -7158,10 +7166,6 @@
"id": "web.create_dir.error", "id": "web.create_dir.error",
"translation": "Failed to create directory watcher %v" "translation": "Failed to create directory watcher %v"
}, },
{
"id": "web.dir_fail.error",
"translation": "Failed in directory watcher %v"
},
{ {
"id": "web.do_load_channel.error", "id": "web.do_load_channel.error",
"translation": "Error in getting users profile for id=%v forcing logout" "translation": "Error in getting users profile for id=%v forcing logout"
@@ -7266,18 +7270,10 @@
"id": "web.parsing_templates.debug", "id": "web.parsing_templates.debug",
"translation": "Parsing templates at %v" "translation": "Parsing templates at %v"
}, },
{
"id": "web.parsing_templates.error",
"translation": "Failed to parse templates %v"
},
{ {
"id": "web.post_permalink.app_error", "id": "web.post_permalink.app_error",
"translation": "Invalid Post ID" "translation": "Invalid Post ID"
}, },
{
"id": "web.reparse_templates.info",
"translation": "Re-parsing templates because of modified file %v"
},
{ {
"id": "web.reset_password.expired_link.app_error", "id": "web.reset_password.expired_link.app_error",
"translation": "The password reset link has expired" "translation": "The password reset link has expired"

Просмотреть файл

@@ -6,7 +6,10 @@ package model
type ChannelMemberHistory struct { type ChannelMemberHistory struct {
ChannelId string ChannelId string
UserId string UserId string
UserEmail string `db:"Email"`
JoinTime int64 JoinTime int64
LeaveTime *int64 LeaveTime *int64
// these two fields are never set in the database - when we SELECT, we join on Users to get them
UserEmail string `db:"Email"`
Username string
} }

Просмотреть файл

@@ -158,6 +158,9 @@ const (
PLUGIN_SETTINGS_DEFAULT_DIRECTORY = "./plugins" PLUGIN_SETTINGS_DEFAULT_DIRECTORY = "./plugins"
PLUGIN_SETTINGS_DEFAULT_CLIENT_DIRECTORY = "./client/plugins" PLUGIN_SETTINGS_DEFAULT_CLIENT_DIRECTORY = "./client/plugins"
COMPLIANCE_EXPORT_TYPE_ACTIANCE = "actiance"
COMPLIANCE_EXPORT_TYPE_GLOBALRELAY = "globalrelay"
) )
type ServiceSettings struct { type ServiceSettings struct {
@@ -1623,9 +1626,13 @@ func (s *PluginSettings) SetDefaults() {
type MessageExportSettings struct { type MessageExportSettings struct {
EnableExport *bool EnableExport *bool
ExportFormat *string
DailyRunTime *string DailyRunTime *string
ExportFromTimestamp *int64 ExportFromTimestamp *int64
BatchSize *int BatchSize *int
// formatter-specific settings - these are only expected to be non-nil if ExportFormat is set to the associated format
GlobalRelayEmailAddress *string
} }
func (s *MessageExportSettings) SetDefaults() { func (s *MessageExportSettings) SetDefaults() {
@@ -1633,6 +1640,10 @@ func (s *MessageExportSettings) SetDefaults() {
s.EnableExport = NewBool(false) s.EnableExport = NewBool(false)
} }
if s.ExportFormat == nil {
s.ExportFormat = NewString(COMPLIANCE_EXPORT_TYPE_ACTIANCE)
}
if s.DailyRunTime == nil { if s.DailyRunTime == nil {
s.DailyRunTime = NewString("01:00") s.DailyRunTime = NewString("01:00")
} }
@@ -2170,6 +2181,16 @@ func (mes *MessageExportSettings) isValid(fs FileSettings) *AppError {
return NewAppError("Config.IsValid", "model.config.is_valid.message_export.daily_runtime.app_error", nil, err.Error(), http.StatusBadRequest) return NewAppError("Config.IsValid", "model.config.is_valid.message_export.daily_runtime.app_error", nil, err.Error(), http.StatusBadRequest)
} else if mes.BatchSize == nil || *mes.BatchSize < 0 { } else if mes.BatchSize == nil || *mes.BatchSize < 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.message_export.batch_size.app_error", nil, "", http.StatusBadRequest) return NewAppError("Config.IsValid", "model.config.is_valid.message_export.batch_size.app_error", nil, "", http.StatusBadRequest)
} else if mes.ExportFormat == nil || (*mes.ExportFormat != COMPLIANCE_EXPORT_TYPE_ACTIANCE && *mes.ExportFormat != COMPLIANCE_EXPORT_TYPE_GLOBALRELAY) {
return NewAppError("Config.IsValid", "model.config.is_valid.message_export.export_type.app_error", nil, "", http.StatusBadRequest)
}
if *mes.ExportFormat == COMPLIANCE_EXPORT_TYPE_GLOBALRELAY {
// validating email addresses is hard - just make sure it contains an '@' sign
// see https://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address
if mes.GlobalRelayEmailAddress == nil || !strings.Contains(*mes.GlobalRelayEmailAddress, "@") {
return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay_email_address.app_error", nil, "", http.StatusBadRequest)
}
} }
} }
return nil return nil

Просмотреть файл

@@ -136,7 +136,7 @@ func TestMessageExportSettingsIsValidBatchSizeInvalid(t *testing.T) {
require.Error(t, mes.isValid(*fs)) require.Error(t, mes.isValid(*fs))
} }
func TestMessageExportSettingsIsValid(t *testing.T) { func TestMessageExportSettingsIsValidExportFormatInvalid(t *testing.T) {
fs := &FileSettings{ fs := &FileSettings{
DriverName: NewString("foo"), // bypass file location check DriverName: NewString("foo"), // bypass file location check
} }
@@ -147,6 +147,55 @@ func TestMessageExportSettingsIsValid(t *testing.T) {
BatchSize: NewInt(100), BatchSize: NewInt(100),
} }
// should fail fast because export format isn't set
require.Error(t, mes.isValid(*fs))
}
func TestMessageExportSettingsIsValidGlobalRelayEmailAddressInvalid(t *testing.T) {
fs := &FileSettings{
DriverName: NewString("foo"), // bypass file location check
}
mes := &MessageExportSettings{
EnableExport: NewBool(true),
ExportFormat: NewString(COMPLIANCE_EXPORT_TYPE_GLOBALRELAY),
ExportFromTimestamp: NewInt64(0),
DailyRunTime: NewString("15:04"),
BatchSize: NewInt(100),
}
// should fail fast because global relay email address isn't set
require.Error(t, mes.isValid(*fs))
}
func TestMessageExportSettingsIsValidActiance(t *testing.T) {
fs := &FileSettings{
DriverName: NewString("foo"), // bypass file location check
}
mes := &MessageExportSettings{
EnableExport: NewBool(true),
ExportFormat: NewString(COMPLIANCE_EXPORT_TYPE_ACTIANCE),
ExportFromTimestamp: NewInt64(0),
DailyRunTime: NewString("15:04"),
BatchSize: NewInt(100),
}
// should pass because everything is valid
require.Nil(t, mes.isValid(*fs))
}
func TestMessageExportSettingsIsValidGlobalRelay(t *testing.T) {
fs := &FileSettings{
DriverName: NewString("foo"), // bypass file location check
}
mes := &MessageExportSettings{
EnableExport: NewBool(true),
ExportFormat: NewString(COMPLIANCE_EXPORT_TYPE_GLOBALRELAY),
ExportFromTimestamp: NewInt64(0),
DailyRunTime: NewString("15:04"),
BatchSize: NewInt(100),
GlobalRelayEmailAddress: NewString("test@mattermost.com"),
}
// should pass because everything is valid // should pass because everything is valid
require.Nil(t, mes.isValid(*fs)) require.Nil(t, mes.isValid(*fs))
} }
@@ -159,6 +208,7 @@ func TestMessageExportSetDefaults(t *testing.T) {
require.Equal(t, "01:00", *mes.DailyRunTime) require.Equal(t, "01:00", *mes.DailyRunTime)
require.Equal(t, int64(0), *mes.ExportFromTimestamp) require.Equal(t, int64(0), *mes.ExportFromTimestamp)
require.Equal(t, 10000, *mes.BatchSize) require.Equal(t, 10000, *mes.BatchSize)
require.Equal(t, COMPLIANCE_EXPORT_TYPE_ACTIANCE, *mes.ExportFormat)
} }
func TestMessageExportSetDefaultsExportEnabledExportFromTimestampNil(t *testing.T) { func TestMessageExportSetDefaultsExportEnabledExportFromTimestampNil(t *testing.T) {

Просмотреть файл

@@ -9,6 +9,7 @@ type MessageExport struct {
UserId *string UserId *string
UserEmail *string UserEmail *string
Username *string
PostId *string PostId *string
PostCreateAt *int64 PostCreateAt *int64

Просмотреть файл

@@ -110,7 +110,8 @@ func (s SqlChannelMemberHistoryStore) getFromChannelMemberHistoryTable(startTime
query := ` query := `
SELECT SELECT
cmh.*, cmh.*,
u.Email u.Email,
u.Username
FROM ChannelMemberHistory cmh FROM ChannelMemberHistory cmh
INNER JOIN Users u ON cmh.UserId = u.Id INNER JOIN Users u ON cmh.UserId = u.Id
WHERE cmh.ChannelId = :ChannelId WHERE cmh.ChannelId = :ChannelId
@@ -130,9 +131,10 @@ func (s SqlChannelMemberHistoryStore) getFromChannelMemberHistoryTable(startTime
func (s SqlChannelMemberHistoryStore) getFromChannelMembersTable(startTime int64, endTime int64, channelId string) ([]*model.ChannelMemberHistory, error) { func (s SqlChannelMemberHistoryStore) getFromChannelMembersTable(startTime int64, endTime int64, channelId string) ([]*model.ChannelMemberHistory, error) {
query := ` query := `
SELECT DISTINCT SELECT DISTINCT
ch.ChannelId, ch.ChannelId,
ch.UserId, ch.UserId,
u.email u.Email,
u.Username
FROM ChannelMembers AS ch FROM ChannelMembers AS ch
INNER JOIN Users AS u ON ch.UserId = u.id INNER JOIN Users AS u ON ch.UserId = u.id
WHERE ch.ChannelId = :ChannelId` WHERE ch.ChannelId = :ChannelId`
@@ -158,7 +160,7 @@ func (s SqlChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64, limit
query = query =
`DELETE FROM ChannelMemberHistory `DELETE FROM ChannelMemberHistory
WHERE ctid IN ( WHERE ctid IN (
SELECT ctid FROM ChannelMemberHistory SELECT ctid FROM ChannelMemberHistory
WHERE LeaveTime IS NOT NULL WHERE LeaveTime IS NOT NULL
AND LeaveTime <= :EndTime AND LeaveTime <= :EndTime
LIMIT :Limit LIMIT :Limit

Просмотреть файл

@@ -225,7 +225,8 @@ func (s SqlComplianceStore) MessageExport(after int64, limit int) store.StoreCha
Channels.Id AS ChannelId, Channels.Id AS ChannelId,
Channels.DisplayName AS ChannelDisplayName, Channels.DisplayName AS ChannelDisplayName,
Users.Id AS UserId, Users.Id AS UserId,
Users.Email AS UserEmail Users.Email AS UserEmail,
Users.Username
FROM FROM
Posts Posts
LEFT OUTER JOIN Channels ON Posts.ChannelId = Channels.Id LEFT OUTER JOIN Channels ON Posts.ChannelId = Channels.Id

Просмотреть файл

@@ -35,6 +35,7 @@ func testLogJoinEvent(t *testing.T, ss store.Store) {
user := model.User{ user := model.User{
Email: model.NewId() + "@mattermost.com", Email: model.NewId() + "@mattermost.com",
Nickname: model.NewId(), Nickname: model.NewId(),
Username: model.NewId(),
} }
user = *store.Must(ss.User().Save(&user)).(*model.User) user = *store.Must(ss.User().Save(&user)).(*model.User)
@@ -57,6 +58,7 @@ func testLogLeaveEvent(t *testing.T, ss store.Store) {
user := model.User{ user := model.User{
Email: model.NewId() + "@mattermost.com", Email: model.NewId() + "@mattermost.com",
Nickname: model.NewId(), Nickname: model.NewId(),
Username: model.NewId(),
} }
user = *store.Must(ss.User().Save(&user)).(*model.User) user = *store.Must(ss.User().Save(&user)).(*model.User)
@@ -82,6 +84,7 @@ func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, ss store.Store) {
user := model.User{ user := model.User{
Email: model.NewId() + "@mattermost.com", Email: model.NewId() + "@mattermost.com",
Nickname: model.NewId(), Nickname: model.NewId(),
Username: model.NewId(),
} }
user = *store.Must(ss.User().Save(&user)).(*model.User) user = *store.Must(ss.User().Save(&user)).(*model.User)
@@ -108,6 +111,7 @@ func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId) assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId) assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail) assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime, channelMembers[0].JoinTime) assert.Equal(t, joinTime, channelMembers[0].JoinTime)
assert.Nil(t, channelMembers[0].LeaveTime) assert.Nil(t, channelMembers[0].LeaveTime)
@@ -117,6 +121,7 @@ func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId) assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId) assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail) assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime, channelMembers[0].JoinTime) assert.Equal(t, joinTime, channelMembers[0].JoinTime)
assert.Nil(t, channelMembers[0].LeaveTime) assert.Nil(t, channelMembers[0].LeaveTime)
@@ -129,6 +134,7 @@ func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId) assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId) assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail) assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime, channelMembers[0].JoinTime) assert.Equal(t, joinTime, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime, *channelMembers[0].LeaveTime) assert.Equal(t, leaveTime, *channelMembers[0].LeaveTime)
@@ -138,6 +144,7 @@ func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId) assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId) assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail) assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime, channelMembers[0].JoinTime) assert.Equal(t, joinTime, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime, *channelMembers[0].LeaveTime) assert.Equal(t, leaveTime, *channelMembers[0].LeaveTime)
@@ -160,6 +167,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, ss store.Store) {
user := model.User{ user := model.User{
Email: model.NewId() + "@mattermost.com", Email: model.NewId() + "@mattermost.com",
Nickname: model.NewId(), Nickname: model.NewId(),
Username: model.NewId(),
} }
user = *store.Must(ss.User().Save(&user)).(*model.User) user = *store.Must(ss.User().Save(&user)).(*model.User)
@@ -192,6 +200,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId) assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId) assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail) assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime-500, channelMembers[0].JoinTime) assert.Equal(t, joinTime-500, channelMembers[0].JoinTime)
assert.Equal(t, joinTime-100, *channelMembers[0].LeaveTime) assert.Equal(t, joinTime-100, *channelMembers[0].LeaveTime)
@@ -201,6 +210,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId) assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId) assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail) assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime-100, channelMembers[0].JoinTime) assert.Equal(t, joinTime-100, channelMembers[0].JoinTime)
assert.Equal(t, joinTime+500, *channelMembers[0].LeaveTime) assert.Equal(t, joinTime+500, *channelMembers[0].LeaveTime)
@@ -210,6 +220,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId) assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId) assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail) assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime+100, channelMembers[0].JoinTime) assert.Equal(t, joinTime+100, channelMembers[0].JoinTime)
assert.Equal(t, joinTime+500, *channelMembers[0].LeaveTime) assert.Equal(t, joinTime+500, *channelMembers[0].LeaveTime)
@@ -219,6 +230,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId) assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId) assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail) assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime+100, channelMembers[0].JoinTime) assert.Equal(t, joinTime+100, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime-100, *channelMembers[0].LeaveTime) assert.Equal(t, leaveTime-100, *channelMembers[0].LeaveTime)
@@ -228,6 +240,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId) assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId) assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail) assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime-100, channelMembers[0].JoinTime) assert.Equal(t, joinTime-100, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime+100, *channelMembers[0].LeaveTime) assert.Equal(t, leaveTime+100, *channelMembers[0].LeaveTime)
@@ -237,6 +250,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId) assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId) assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail) assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, leaveTime+100, channelMembers[0].JoinTime) assert.Equal(t, leaveTime+100, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime+200, *channelMembers[0].LeaveTime) assert.Equal(t, leaveTime+200, *channelMembers[0].LeaveTime)
} }
@@ -255,12 +269,14 @@ func testPermanentDeleteBatch(t *testing.T, ss store.Store) {
user := model.User{ user := model.User{
Email: model.NewId() + "@mattermost.com", Email: model.NewId() + "@mattermost.com",
Nickname: model.NewId(), Nickname: model.NewId(),
Username: model.NewId(),
} }
user = *store.Must(ss.User().Save(&user)).(*model.User) user = *store.Must(ss.User().Save(&user)).(*model.User)
user2 := model.User{ user2 := model.User{
Email: model.NewId() + "@mattermost.com", Email: model.NewId() + "@mattermost.com",
Nickname: model.NewId(), Nickname: model.NewId(),
Username: model.NewId(),
} }
user2 = *store.Must(ss.User().Save(&user2)).(*model.User) user2 = *store.Must(ss.User().Save(&user2)).(*model.User)

Просмотреть файл

@@ -341,7 +341,8 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) {
// and two users that are a part of that team // and two users that are a part of that team
user1 := &model.User{ user1 := &model.User{
Email: model.NewId(), Email: model.NewId(),
Username: model.NewId(),
} }
user1 = store.Must(ss.User().Save(user1)).(*model.User) user1 = store.Must(ss.User().Save(user1)).(*model.User)
store.Must(ss.Team().SaveMember(&model.TeamMember{ store.Must(ss.Team().SaveMember(&model.TeamMember{
@@ -350,7 +351,8 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) {
}, -1)) }, -1))
user2 := &model.User{ user2 := &model.User{
Email: model.NewId(), Email: model.NewId(),
Username: model.NewId(),
} }
user2 = store.Must(ss.User().Save(user2)).(*model.User) user2 = store.Must(ss.User().Save(user2)).(*model.User)
store.Must(ss.Team().SaveMember(&model.TeamMember{ store.Must(ss.Team().SaveMember(&model.TeamMember{
@@ -415,6 +417,7 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) {
assert.Equal(t, channel.DisplayName, *messageExportMap[post1.Id].ChannelDisplayName) assert.Equal(t, channel.DisplayName, *messageExportMap[post1.Id].ChannelDisplayName)
assert.Equal(t, user1.Id, *messageExportMap[post1.Id].UserId) assert.Equal(t, user1.Id, *messageExportMap[post1.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post1.Id].UserEmail) assert.Equal(t, user1.Email, *messageExportMap[post1.Id].UserEmail)
assert.Equal(t, user1.Username, *messageExportMap[post1.Id].Username)
// post2 was made by user1 in channel1 and team1 // post2 was made by user1 in channel1 and team1
assert.Equal(t, post2.Id, *messageExportMap[post2.Id].PostId) assert.Equal(t, post2.Id, *messageExportMap[post2.Id].PostId)
@@ -424,6 +427,7 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) {
assert.Equal(t, channel.DisplayName, *messageExportMap[post2.Id].ChannelDisplayName) assert.Equal(t, channel.DisplayName, *messageExportMap[post2.Id].ChannelDisplayName)
assert.Equal(t, user1.Id, *messageExportMap[post2.Id].UserId) assert.Equal(t, user1.Id, *messageExportMap[post2.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post2.Id].UserEmail) assert.Equal(t, user1.Email, *messageExportMap[post2.Id].UserEmail)
assert.Equal(t, user1.Username, *messageExportMap[post2.Id].Username)
// post3 is a DM between user1 and user2 // post3 is a DM between user1 and user2
assert.Equal(t, post3.Id, *messageExportMap[post3.Id].PostId) assert.Equal(t, post3.Id, *messageExportMap[post3.Id].PostId)
@@ -432,4 +436,5 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) {
assert.Equal(t, directMessageChannel.Id, *messageExportMap[post3.Id].ChannelId) assert.Equal(t, directMessageChannel.Id, *messageExportMap[post3.Id].ChannelId)
assert.Equal(t, user1.Id, *messageExportMap[post3.Id].UserId) assert.Equal(t, user1.Id, *messageExportMap[post3.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post3.Id].UserEmail) assert.Equal(t, user1.Email, *messageExportMap[post3.Id].UserEmail)
assert.Equal(t, user1.Username, *messageExportMap[post3.Id].Username)
} }

Просмотреть файл

@@ -0,0 +1,91 @@
{{define "globalrelay_compliance_export"}}
<style type="text/css">
body {
font-family:Arial, sans-serif;
font-size:14px;
font-weight:normal;
}
.summary-list ul {
padding: 0px;
list-style:none;
}
.summary-list li {
display: inline;
padding: 0 1em 0 0;
}
.summary-list .bold {
font-weight: bold;
}
.participants {
border-collapse:collapse;
border-spacing:0;
}
.participants td {
padding:10px 5px;
border:1px solid black;
overflow:hidden;
text-align: center;
word-break:normal;
}
.participants th {
padding:10px 5px;
border:1px solid black;
overflow:hidden;
word-break:normal;
}
.participants th,td {
vertical-align:top
}
.message-list ul {
list-style:none;
padding: 0;
}
.message-list li {
padding: 0 0 1em 0;
}
.message .sent_time {
font-weight:bold;
}
.message .username {
font-weight:bold;
}
.message .email {
font-weight: bold;
}
</style>
<h1>Mattermost Compliance Export</h1>
<h2>Conversation Summary</h2>
<div class="summary-list">
<ul>
<li><span class="bold">Channel:&nbsp;</span>{{.Props.ChannelName}}</li>
<li><span class="bold">Started:&nbsp;</span>{{.Props.Started}}</li>
<li><span class="bold">Ended:&nbsp;</span>{{.Props.Ended}}</li>
<li><span class="bold">Duration:&nbsp;</span>{{.Props.Duration}}&nbsp;Minutes</li>
</ul>
</div>
<table class="participants">
<tr>
<th class="username">Username<br></th>
<th class="email">Email</th>
<th class="joined">Joined</th>
<th class="left">Left</th>
<th class="duration">Duration</th>
<th class="messages">Messages</th>
</tr>
{{.Props.ParticipantRows}}
</table>
<h2>Messages</h2>
<div class="message-list">
<ul>
{{.Props.Messages}}
</ul>
</div>
<p>Exported on {{.Props.ExportDate}}</p>
{{end}}

Просмотреть файл

@@ -0,0 +1,8 @@
{{define "globalrelay_compliance_export_message"}}
<li class="message">
<span class="sent_time">{{.Props.SentTime}}</span>
<span class="username">@{{.Props.Username}}</span>
<span class="email">({{.Props.Email}}):</span>
<span class="message">{{.Props.Message}}</span>
</li>
{{end}}

Просмотреть файл

@@ -0,0 +1,10 @@
{{define "globalrelay_compliance_export_participant_row"}}
<tr>
<td class="username">@{{.Props.Username}}</td>
<td class="email">{{.Props.Email}}</td>
<td class="joined">{{.Props.Joined}}</td>
<td class="left">{{.Props.Left}}</td>
<td class="duration">{{.Props.DurationMinutes}} Minutes</td>
<td class="messages">{{.Props.NumMessages}}</td>
</tr>
{{end}}

Просмотреть файл

@@ -23,7 +23,7 @@ type HTMLTemplateWatcher struct {
func NewHTMLTemplateWatcher(directory string) (*HTMLTemplateWatcher, error) { func NewHTMLTemplateWatcher(directory string) (*HTMLTemplateWatcher, error) {
templatesDir, _ := FindDir(directory) templatesDir, _ := FindDir(directory)
l4g.Debug(T("api.api.init.parsing_templates.debug"), templatesDir) l4g.Debug("Parsing server templates at %v", templatesDir)
ret := &HTMLTemplateWatcher{ ret := &HTMLTemplateWatcher{
stop: make(chan struct{}), stop: make(chan struct{}),
@@ -55,15 +55,15 @@ func NewHTMLTemplateWatcher(directory string) (*HTMLTemplateWatcher, error) {
return return
case event := <-watcher.Events: case event := <-watcher.Events:
if event.Op&fsnotify.Write == fsnotify.Write { if event.Op&fsnotify.Write == fsnotify.Write {
l4g.Info(T("web.reparse_templates.info"), event.Name) l4g.Info("Re-parsing templates because of modified file %v", event.Name)
if htmlTemplates, err := template.ParseGlob(templatesDir + "*.html"); err != nil { if htmlTemplates, err := template.ParseGlob(templatesDir + "*.html"); err != nil {
l4g.Error(T("web.parsing_templates.error"), err) l4g.Error("Failed to parse templates %v", err)
} else { } else {
ret.templates.Store(htmlTemplates) ret.templates.Store(htmlTemplates)
} }
} }
case err := <-watcher.Errors: case err := <-watcher.Errors:
l4g.Error(T("web.dir_fail.error"), err) l4g.Error("Failed in directory watcher %s", err)
} }
} }
}() }()

Просмотреть файл

@@ -4,6 +4,7 @@
package utils package utils
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@@ -37,6 +38,12 @@ type JSONMessageInbucket struct {
Text string Text string
HTML string `json:"Html"` HTML string `json:"Html"`
} }
Attachments []struct {
Filename string
ContentType string `json:"content-type"`
DownloadLink string `json:"download-link"`
Bytes []byte `json:"-"`
}
} }
func ParseEmail(email string) string { func ParseEmail(email string) string {
@@ -89,21 +96,54 @@ func GetMessageFromMailbox(email, id string) (results JSONMessageInbucket, err e
var record JSONMessageInbucket var record JSONMessageInbucket
url := fmt.Sprintf("%s%s%s/%s", getInbucketHost(), INBUCKET_API, parsedEmail, id) url := fmt.Sprintf("%s%s%s/%s", getInbucketHost(), INBUCKET_API, parsedEmail, id)
req, err := http.NewRequest("GET", url, nil) emailResponse, err := get(url)
if err != nil { if err != nil {
return record, err return record, err
} }
defer emailResponse.Body.Close()
err = json.NewDecoder(emailResponse.Body).Decode(&record)
// download attachments
if record.Attachments != nil && len(record.Attachments) > 0 {
for i := range record.Attachments {
if bytes, err := downloadAttachment(record.Attachments[i].DownloadLink); err != nil {
return record, err
} else {
record.Attachments[i].Bytes = make([]byte, len(bytes))
copy(record.Attachments[i].Bytes, bytes)
}
}
}
return record, err
}
func downloadAttachment(url string) ([]byte, error) {
attachmentResponse, err := get(url)
if err != nil {
return nil, err
}
defer attachmentResponse.Body.Close()
buf := new(bytes.Buffer)
io.Copy(buf, attachmentResponse.Body)
return buf.Bytes(), nil
}
func get(url string) (*http.Response, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
client := &http.Client{} client := &http.Client{}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return record, err return nil, err
} }
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&record) return resp, nil
return record, err
} }
func DeleteMailBox(email string) (err error) { func DeleteMailBox(email string) (err error) {

Просмотреть файл

@@ -15,6 +15,8 @@ import (
"net/http" "net/http"
"io"
l4g "github.com/alecthomas/log4go" l4g "github.com/alecthomas/log4go"
"github.com/mattermost/html2text" "github.com/mattermost/html2text"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
@@ -104,36 +106,72 @@ func TestConnection(config *model.Config) {
} }
func SendMailUsingConfig(to, subject, htmlBody string, config *model.Config) *model.AppError { func SendMailUsingConfig(to, subject, htmlBody string, config *model.Config) *model.AppError {
fromMail := mail.Address{Name: config.EmailSettings.FeedbackName, Address: config.EmailSettings.FeedbackEmail}
return sendMail(to, to, fromMail, subject, htmlBody, nil, nil, config)
}
// allows for sending an email with attachments and differing MIME/SMTP recipients
func SendMailUsingConfigAdvanced(mimeTo, smtpTo string, from mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, config *model.Config) *model.AppError {
return sendMail(mimeTo, smtpTo, from, subject, htmlBody, attachments, mimeHeaders, config)
}
func sendMail(mimeTo, smtpTo string, from mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, config *model.Config) *model.AppError {
if !config.EmailSettings.SendEmailNotifications || len(config.EmailSettings.SMTPServer) == 0 { if !config.EmailSettings.SendEmailNotifications || len(config.EmailSettings.SMTPServer) == 0 {
return nil return nil
} }
l4g.Debug(T("utils.mail.send_mail.sending.debug"), to, subject) l4g.Debug(T("utils.mail.send_mail.sending.debug"), mimeTo, subject)
htmlMessage := "\r\n<html><body>" + htmlBody + "</body></html>" htmlMessage := "\r\n<html><body>" + htmlBody + "</body></html>"
fromMail := mail.Address{Name: config.EmailSettings.FeedbackName, Address: config.EmailSettings.FeedbackEmail}
txtBody, err := html2text.FromString(htmlBody) txtBody, err := html2text.FromString(htmlBody)
if err != nil { if err != nil {
l4g.Warn(err) l4g.Warn(err)
txtBody = "" txtBody = ""
} }
m := gomail.NewMessage(gomail.SetCharset("UTF-8")) headers := map[string][]string{
m.SetHeaders(map[string][]string{ "From": {from.String()},
"From": {fromMail.String()}, "To": {mimeTo},
"To": {to},
"Subject": {encodeRFC2047Word(subject)}, "Subject": {encodeRFC2047Word(subject)},
"Content-Transfer-Encoding": {"8bit"}, "Content-Transfer-Encoding": {"8bit"},
"Auto-Submitted": {"auto-generated"}, "Auto-Submitted": {"auto-generated"},
"Precedence": {"bulk"}, "Precedence": {"bulk"},
}) }
m.SetDateHeader("Date", time.Now()) if mimeHeaders != nil {
for k, v := range mimeHeaders {
headers[k] = []string{encodeRFC2047Word(v)}
}
}
m := gomail.NewMessage(gomail.SetCharset("UTF-8"))
m.SetHeaders(headers)
m.SetDateHeader("Date", time.Now())
m.SetBody("text/plain", txtBody) m.SetBody("text/plain", txtBody)
m.AddAlternative("text/html", htmlMessage) m.AddAlternative("text/html", htmlMessage)
if attachments != nil {
fileBackend, err := NewFileBackend(&config.FileSettings)
if err != nil {
return err
}
for _, fileInfo := range attachments {
m.Attach(fileInfo.Name, gomail.SetCopyFunc(func(writer io.Writer) error {
bytes, err := fileBackend.ReadFile(fileInfo.Path)
if err != nil {
return err
}
if _, err := writer.Write(bytes); err != nil {
return model.NewAppError("SendMail", "utils.mail.sendMail.attachments.write_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}))
}
}
conn, err1 := connectToSMTPServer(config) conn, err1 := connectToSMTPServer(config)
if err1 != nil { if err1 != nil {
return err1 return err1
@@ -147,11 +185,11 @@ func SendMailUsingConfig(to, subject, htmlBody string, config *model.Config) *mo
defer c.Quit() defer c.Quit()
defer c.Close() defer c.Close()
if err := c.Mail(fromMail.Address); err != nil { if err := c.Mail(from.Address); err != nil {
return model.NewAppError("SendMail", "utils.mail.send_mail.from_address.app_error", nil, err.Error(), http.StatusInternalServerError) return model.NewAppError("SendMail", "utils.mail.send_mail.from_address.app_error", nil, err.Error(), http.StatusInternalServerError)
} }
if err := c.Rcpt(to); err != nil { if err := c.Rcpt(smtpTo); err != nil {
return model.NewAppError("SendMail", "utils.mail.send_mail.to_address.app_error", nil, err.Error(), http.StatusInternalServerError) return model.NewAppError("SendMail", "utils.mail.send_mail.to_address.app_error", nil, err.Error(), http.StatusInternalServerError)
} }

Просмотреть файл

@@ -7,6 +7,10 @@ import (
"strings" "strings"
"testing" "testing"
"net/mail"
"github.com/mattermost/mattermost-server/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -39,9 +43,9 @@ func TestSendMailUsingConfig(t *testing.T) {
require.Nil(t, err) require.Nil(t, err)
T = GetUserTranslations("en") T = GetUserTranslations("en")
var emailTo string = "test@example.com" var emailTo = "test@example.com"
var emailSubject string = "Testing this email" var emailSubject = "Testing this email"
var emailBody string = "This is a test from autobot" var emailBody = "This is a test from autobot"
//Delete all the messages before check the sample email //Delete all the messages before check the sample email
DeleteMailBox(emailTo) DeleteMailBox(emailTo)
@@ -50,7 +54,7 @@ func TestSendMailUsingConfig(t *testing.T) {
t.Log(err) t.Log(err)
t.Fatal("Should connect to the STMP Server") t.Fatal("Should connect to the STMP Server")
} else { } else {
//Check if the email was send to the rigth email address //Check if the email was send to the right email address
var resultsMailbox JSONMessageHeaderInbucket var resultsMailbox JSONMessageHeaderInbucket
err := RetryInbucket(5, func() error { err := RetryInbucket(5, func() error {
var err error var err error
@@ -75,3 +79,78 @@ func TestSendMailUsingConfig(t *testing.T) {
} }
} }
} }
func TestSendMailUsingConfigAdvanced(t *testing.T) {
cfg, _, err := LoadConfig("config.json")
require.Nil(t, err)
T = GetUserTranslations("en")
var mimeTo = "test@example.com"
var smtpTo = "test2@example.com"
var from = mail.Address{Name: "Nobody", Address: "nobody@mattermost.com"}
var emailSubject = "Testing this email"
var emailBody = "This is a test from autobot"
//Delete all the messages before check the sample email
DeleteMailBox(smtpTo)
// create a file that will be attached to the email
fileBackend, err := NewFileBackend(&cfg.FileSettings)
assert.Nil(t, err)
fileContents := []byte("hello world")
fileName := "file.txt"
assert.Nil(t, fileBackend.WriteFile(fileContents, fileName))
defer fileBackend.RemoveFile(fileName)
attachments := make([]*model.FileInfo, 1)
attachments[0] = &model.FileInfo{
Name: fileName,
Path: fileName,
}
headers := make(map[string]string)
headers["TestHeader"] = "TestValue"
if err := SendMailUsingConfigAdvanced(mimeTo, smtpTo, from, emailSubject, emailBody, attachments, headers, cfg); err != nil {
t.Log(err)
t.Fatal("Should connect to the STMP Server")
} else {
//Check if the email was send to the right email address
var resultsMailbox JSONMessageHeaderInbucket
err := RetryInbucket(5, func() error {
var err error
resultsMailbox, err = GetMailBox(smtpTo)
return err
})
if err != nil {
t.Log(err)
t.Fatal("No emails found for address " + smtpTo)
}
if err == nil && len(resultsMailbox) > 0 {
if !strings.ContainsAny(resultsMailbox[0].To[0], smtpTo) {
t.Fatal("Wrong To recipient")
} else {
if resultsEmail, err := GetMessageFromMailbox(smtpTo, resultsMailbox[0].ID); err == nil {
if !strings.Contains(resultsEmail.Body.Text, emailBody) {
t.Log(resultsEmail.Body.Text)
t.Fatal("Received message")
}
// verify that the To header of the email message is set to the MIME recipient, even though we got it out of the SMTP recipient's email inbox
assert.Equal(t, mimeTo, resultsEmail.Header["To"][0])
// verify that the MIME from address is correct - unfortunately, we can't verify the SMTP from address
assert.Equal(t, from.String(), resultsEmail.Header["From"][0])
// check that the custom mime headers came through - header case seems to get mutated
assert.Equal(t, "TestValue", resultsEmail.Header["Testheader"][0])
// ensure that the attachment was successfully sent
assert.Len(t, resultsEmail.Attachments, 1)
assert.Equal(t, fileName, resultsEmail.Attachments[0].Filename)
assert.Equal(t, fileContents, resultsEmail.Attachments[0].Bytes)
}
}
}
}
}