mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Merge branch 'master' into advanced-permissions-phase-1
This commit is contained in:
@@ -395,6 +395,10 @@ func testS3(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.FileSettings.AmazonS3SecretAccessKey == model.FAKE_SETTING {
|
||||
cfg.FileSettings.AmazonS3SecretAccessKey = c.App.Config().FileSettings.AmazonS3SecretAccessKey
|
||||
}
|
||||
|
||||
license := c.App.License()
|
||||
backend, appErr := utils.NewFileBackend(&cfg.FileSettings, license != nil && *license.Features.Compliance)
|
||||
if appErr == nil {
|
||||
|
||||
@@ -262,28 +262,34 @@ func TestEmailTest(t *testing.T) {
|
||||
defer th.TearDown()
|
||||
Client := th.Client
|
||||
|
||||
SendEmailNotifications := th.App.Config().EmailSettings.SendEmailNotifications
|
||||
SMTPServer := th.App.Config().EmailSettings.SMTPServer
|
||||
SMTPPort := th.App.Config().EmailSettings.SMTPPort
|
||||
FeedbackEmail := th.App.Config().EmailSettings.FeedbackEmail
|
||||
defer func() {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.EmailSettings.SendEmailNotifications = SendEmailNotifications })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.EmailSettings.SMTPServer = SMTPServer })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.EmailSettings.SMTPPort = SMTPPort })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.EmailSettings.FeedbackEmail = FeedbackEmail })
|
||||
}()
|
||||
config := model.Config{
|
||||
EmailSettings: model.EmailSettings{
|
||||
SMTPServer: "",
|
||||
SMTPPort: "",
|
||||
},
|
||||
}
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.EmailSettings.SendEmailNotifications = false })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.EmailSettings.SMTPServer = "" })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.EmailSettings.SMTPPort = "" })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.EmailSettings.FeedbackEmail = "" })
|
||||
|
||||
_, resp := Client.TestEmail()
|
||||
_, resp := Client.TestEmail(&config)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
_, resp = th.SystemAdminClient.TestEmail()
|
||||
_, resp = th.SystemAdminClient.TestEmail(&config)
|
||||
CheckErrorMessage(t, resp, "api.admin.test_email.missing_server")
|
||||
CheckBadRequestStatus(t, resp)
|
||||
|
||||
inbucket_host := os.Getenv("CI_HOST")
|
||||
if inbucket_host == "" {
|
||||
inbucket_host = "dockerhost"
|
||||
}
|
||||
|
||||
inbucket_port := os.Getenv("CI_INBUCKET_PORT")
|
||||
if inbucket_port == "" {
|
||||
inbucket_port = "9000"
|
||||
}
|
||||
|
||||
config.EmailSettings.SMTPServer = inbucket_host
|
||||
config.EmailSettings.SMTPPort = inbucket_port
|
||||
_, resp = th.SystemAdminClient.TestEmail(&config)
|
||||
CheckOKStatus(t, resp)
|
||||
}
|
||||
|
||||
func TestDatabaseRecycle(t *testing.T) {
|
||||
@@ -491,7 +497,7 @@ func TestS3TestConnection(t *testing.T) {
|
||||
AmazonS3AccessKeyId: model.MINIO_ACCESS_KEY,
|
||||
AmazonS3SecretAccessKey: model.MINIO_SECRET_KEY,
|
||||
AmazonS3Bucket: "",
|
||||
AmazonS3Endpoint: "",
|
||||
AmazonS3Endpoint: s3Endpoint,
|
||||
AmazonS3SSL: model.NewBool(false),
|
||||
},
|
||||
}
|
||||
@@ -506,23 +512,14 @@ func TestS3TestConnection(t *testing.T) {
|
||||
}
|
||||
|
||||
config.FileSettings.AmazonS3Bucket = model.MINIO_BUCKET
|
||||
_, resp = th.SystemAdminClient.TestS3Connection(&config)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
if resp.Error.Message != "S3 Endpoint is required" {
|
||||
t.Fatal("should return error - missing s3 endpoint")
|
||||
}
|
||||
|
||||
config.FileSettings.AmazonS3Endpoint = s3Endpoint
|
||||
_, resp = th.SystemAdminClient.TestS3Connection(&config)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
if resp.Error.Message != "S3 Region is required" {
|
||||
t.Fatal("should return error - missing s3 region")
|
||||
}
|
||||
|
||||
config.FileSettings.AmazonS3Region = "us-east-1"
|
||||
_, resp = th.SystemAdminClient.TestS3Connection(&config)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
config.FileSettings.AmazonS3Region = ""
|
||||
_, resp = th.SystemAdminClient.TestS3Connection(&config)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
config.FileSettings.AmazonS3Bucket = "Wrong_bucket"
|
||||
_, resp = th.SystemAdminClient.TestS3Connection(&config)
|
||||
CheckInternalErrorStatus(t, resp)
|
||||
|
||||
@@ -55,10 +55,15 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
|
||||
|
||||
if channel.Type == model.CHANNEL_DIRECT {
|
||||
var otherUserId string
|
||||
if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId {
|
||||
otherUserId = userIds[1]
|
||||
} else {
|
||||
otherUserId = userIds[0]
|
||||
|
||||
userIds := strings.Split(channel.Name, "__")
|
||||
|
||||
if userIds[0] != userIds[1] {
|
||||
if userIds[0] == post.UserId {
|
||||
otherUserId = userIds[1]
|
||||
} else {
|
||||
otherUserId = userIds[0]
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := profileMap[otherUserId]; ok {
|
||||
@@ -89,7 +94,7 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
|
||||
delete(mentionedUserIds, post.UserId)
|
||||
}
|
||||
|
||||
if len(m.OtherPotentialMentions) > 0 {
|
||||
if len(m.OtherPotentialMentions) > 0 && !post.IsSystemMessage() {
|
||||
if result := <-a.Srv.Store.User().GetProfilesByUsernames(m.OtherPotentialMentions, team.Id); result.Err == nil {
|
||||
outOfChannelMentions := result.Data.([]*model.User)
|
||||
if channel.Type != model.CHANNEL_GROUP {
|
||||
|
||||
@@ -91,18 +91,11 @@ func (a *App) ActivatePlugins() {
|
||||
active := a.PluginEnv.IsPluginActive(id)
|
||||
|
||||
if pluginState.Enable && !active {
|
||||
if err := a.PluginEnv.ActivatePlugin(id); err != nil {
|
||||
l4g.Error(err.Error())
|
||||
if err := a.activatePlugin(plugin.Manifest); err != nil {
|
||||
l4g.Error("%v plugin enabled in config.json but failing to activate err=%v", plugin.Manifest.Id, err.DetailedError)
|
||||
continue
|
||||
}
|
||||
|
||||
if plugin.Manifest.HasClient() {
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_ACTIVATED, "", "", "", nil)
|
||||
message.Add("manifest", plugin.Manifest.ClientManifest())
|
||||
a.Publish(message)
|
||||
}
|
||||
|
||||
l4g.Info("Activated %v plugin", id)
|
||||
} else if !pluginState.Enable && active {
|
||||
if err := a.deactivatePlugin(plugin.Manifest); err != nil {
|
||||
l4g.Error(err.Error())
|
||||
@@ -111,6 +104,21 @@ func (a *App) ActivatePlugins() {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) activatePlugin(manifest *model.Manifest) *model.AppError {
|
||||
if err := a.PluginEnv.ActivatePlugin(manifest.Id); err != nil {
|
||||
return model.NewAppError("activatePlugin", "app.plugin.activate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if manifest.HasClient() {
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_ACTIVATED, "", "", "", nil)
|
||||
message.Add("manifest", manifest.ClientManifest())
|
||||
a.Publish(message)
|
||||
}
|
||||
|
||||
l4g.Info("Activated %v plugin", manifest.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) deactivatePlugin(manifest *model.Manifest) *model.AppError {
|
||||
if err := a.PluginEnv.DeactivatePlugin(manifest.Id); err != nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
@@ -301,11 +309,18 @@ func (a *App) EnablePlugin(id string) *model.AppError {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := a.activatePlugin(manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: true}
|
||||
})
|
||||
|
||||
if err := a.SaveConfig(a.Config(), true); err != nil {
|
||||
if err.Id == "ent.cluster.save_config.error" {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.cluster.save_config.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -195,3 +197,26 @@ func TestPluginCommands(t *testing.T) {
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
||||
}
|
||||
|
||||
type pluginBadActivation struct {
|
||||
testPlugin
|
||||
}
|
||||
|
||||
func (p *pluginBadActivation) OnActivate(api plugin.API) error {
|
||||
return errors.New("won't activate for some reason")
|
||||
}
|
||||
|
||||
func TestPluginBadActivation(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.InstallPlugin(&model.Manifest{
|
||||
Id: "foo",
|
||||
}, &pluginBadActivation{})
|
||||
|
||||
t.Run("EnablePlugin bad activation", func(t *testing.T) {
|
||||
err := th.App.EnablePlugin("foo")
|
||||
assert.NotNil(t, err)
|
||||
assert.True(t, strings.Contains(err.DetailedError, "won't activate for some reason"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -553,7 +553,7 @@ func migrateAuthToLdapCmdF(command *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
fromAuth := args[0]
|
||||
matchField := args[1]
|
||||
matchField := args[2]
|
||||
|
||||
if len(fromAuth) == 0 || (fromAuth != "email" && fromAuth != "gitlab" && fromAuth != "saml") {
|
||||
return errors.New("Invalid from_auth argument")
|
||||
@@ -594,7 +594,7 @@ func migrateAuthToSamlCmdF(command *cobra.Command, args []string) error {
|
||||
matchesFile := ""
|
||||
matches := map[string]string{}
|
||||
if !autoFlag {
|
||||
matchesFile = args[1]
|
||||
matchesFile = args[2]
|
||||
|
||||
file, e := ioutil.ReadFile(matchesFile)
|
||||
if e != nil {
|
||||
|
||||
34
i18n/de.json
34
i18n/de.json
@@ -1370,7 +1370,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.file.upload_file.incorrect_number_of_files.app_error",
|
||||
"translation": "Unable to upload files. Incorrect number of files specified."
|
||||
"translation": "Konnte Dateien nicht hochladen. Falsche Anzahl von Dateien spezifiziert."
|
||||
},
|
||||
{
|
||||
"id": "api.file.upload_file.large_image.app_error",
|
||||
@@ -1618,7 +1618,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.singup_with_oauth.expired_link.app_error",
|
||||
"translation": "Der Registrierungs-Link ist abgelaufen"
|
||||
"translation": "Der Registrierungslink ist abgelaufen"
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.singup_with_oauth.invalid_link.app_error",
|
||||
@@ -1812,15 +1812,15 @@
|
||||
},
|
||||
{
|
||||
"id": "api.post.send_notifications_and_forget.push_image_only",
|
||||
"translation": " Eine oder mehrere Dateien hochgeladen in "
|
||||
"translation": " hat eine oder mehrere Dateien hochgeladen in "
|
||||
},
|
||||
{
|
||||
"id": "api.post.send_notifications_and_forget.push_image_only_dm",
|
||||
"translation": " Eine oder mehrere Dateien in einer Direktnachricht hochgeladen"
|
||||
"translation": " hat eine oder mehrere Dateien in einer Direktnachricht hochgeladen"
|
||||
},
|
||||
{
|
||||
"id": "api.post.send_notifications_and_forget.push_image_only_no_channel",
|
||||
"translation": " Eine oder mehrere Dateien hochgeladen in "
|
||||
"translation": " hat eine oder mehrere Dateien hochgeladen"
|
||||
},
|
||||
{
|
||||
"id": "api.post.send_notifications_and_forget.push_in",
|
||||
@@ -2308,11 +2308,11 @@
|
||||
},
|
||||
{
|
||||
"id": "api.team.move_channel.post.error",
|
||||
"translation": "Fehler beim Senden des Kanalzwecks"
|
||||
"translation": "Fehler beim Senden der Nachricht zur Verschiebung des Kanals."
|
||||
},
|
||||
{
|
||||
"id": "api.team.move_channel.success",
|
||||
"translation": "This channel has been moved to this team from %v."
|
||||
"translation": "Dieser Kanal wurde von %v in dieses Team verschoben."
|
||||
},
|
||||
{
|
||||
"id": "api.team.permanent_delete_team.attempting.warn",
|
||||
@@ -2720,7 +2720,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.user.create_user.signup_link_mismatched_invite_id.app_error",
|
||||
"translation": "Der Einladungslink scheint nicht gültig zu sein"
|
||||
"translation": "Der Registrierungslink scheint nicht gültig zu sein"
|
||||
},
|
||||
{
|
||||
"id": "api.user.create_user.team_name.app_error",
|
||||
@@ -3080,7 +3080,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.webhook.incoming.error",
|
||||
"translation": "Could not decode the multipart payload of incoming webhook."
|
||||
"translation": "Konnte die Multipart-Daten des eingehenden Webhooks nicht entschlüsseln."
|
||||
},
|
||||
{
|
||||
"id": "api.webhook.init.debug",
|
||||
@@ -3656,7 +3656,7 @@
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.disabled.app_error",
|
||||
"translation": "Plugins have been disabled. Please check your logs for details."
|
||||
"translation": "Plugins wurden deaktiviert. Bitte prüfen Sie Ihre Logs für Details."
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.extract.app_error",
|
||||
@@ -4192,15 +4192,15 @@
|
||||
},
|
||||
{
|
||||
"id": "ent.migration.migratetosaml.email_already_used_by_other_user",
|
||||
"translation": "Email already used by another SAML user."
|
||||
"translation": "E-Mail-Adresse wird bereits von einem anderen SAML-Benutzer verwendet."
|
||||
},
|
||||
{
|
||||
"id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file",
|
||||
"translation": "User not found in the users file."
|
||||
"translation": "Benutzer nicht in der Benutzerdatei gefunden."
|
||||
},
|
||||
{
|
||||
"id": "ent.migration.migratetosaml.username_already_used_by_other_user",
|
||||
"translation": "Username already used by another Mattermost user."
|
||||
"translation": "Benutzername wird bereits von einem anderen Mattermost-Benutzer verwendet."
|
||||
},
|
||||
{
|
||||
"id": "ent.saml.attribute.app_error",
|
||||
@@ -4904,7 +4904,7 @@
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.message_export.export_type.app_error",
|
||||
"translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
|
||||
"translation": "'ExportFormat' des Nachrichten-Export-Jobs muss 'actiance' oder 'globalrelay' sein."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.message_export.file_location.app_error",
|
||||
@@ -4916,7 +4916,7 @@
|
||||
},
|
||||
{
|
||||
"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"
|
||||
"translation": "Nachrichten-Export-Job GlobalRelayEmailAddress muss eine gültige E-Mail-Adresse sein."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.password_length.app_error",
|
||||
@@ -5048,7 +5048,7 @@
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.websocket_url.app_error",
|
||||
"translation": "Die WebRTC-Gateway-Websocket-URL muss gesetzt und eine gültige URL sein sowie mit ws:// oder wss:// beginnen."
|
||||
"translation": "Websocket-URL muss eine gültige URL sein sowie mit ws:// oder wss:// beginnen."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.write_timeout.app_error",
|
||||
@@ -7136,7 +7136,7 @@
|
||||
},
|
||||
{
|
||||
"id": "utils.mail.sendMail.attachments.write_error",
|
||||
"translation": "Failed to write attachment to email"
|
||||
"translation": "Fehler beim Hinzufügen des Mailanhanges"
|
||||
},
|
||||
{
|
||||
"id": "utils.mail.send_mail.close.app_error",
|
||||
|
||||
@@ -3718,6 +3718,10 @@
|
||||
"id": "app.plugin.config.app_error",
|
||||
"translation": "Error saving plugin state in config"
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.cluster.save_config.app_error",
|
||||
"translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled."
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.deactivate.app_error",
|
||||
"translation": "Unable to deactivate plugin"
|
||||
|
||||
@@ -3636,7 +3636,7 @@
|
||||
},
|
||||
{
|
||||
"id": "app.notification.subject.direct.full",
|
||||
"translation": "[{{.SubjectText}}] Nuovo messaggio diretto da {{.SenderDisplayName}} il {{.Day}}/{{.Month}}/{{.Year}}"
|
||||
"translation": "[{{.SiteName}}] Nuovo messaggio diretto da {{.SenderDisplayName}} il {{.Day}}/{{.Month}}/{{.Year}}"
|
||||
},
|
||||
{
|
||||
"id": "app.notification.subject.notification.full",
|
||||
|
||||
56
i18n/ko.json
56
i18n/ko.json
@@ -101,7 +101,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.admin.test_email.reenter_password",
|
||||
"translation": "SMTP 서버, 포트, 사용자 정보가 변경되었습니다. SMTP 비밀번호를 다시 입력해주세요."
|
||||
"translation": "SMTP 서버, 포트 또는 사용자이름이 변경되었습니다. SMTP 비밀번호를 다시 입력해주세요."
|
||||
},
|
||||
{
|
||||
"id": "api.admin.test_email.subject",
|
||||
@@ -141,7 +141,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.auth.unable_to_get_user.app_error",
|
||||
"translation": "권한을 확인하기 위한 유저 정보를 가져올 수 없습니다."
|
||||
"translation": "권한을 확인하기 위한 사용자 정보를 가져올 수 없습니다."
|
||||
},
|
||||
{
|
||||
"id": "api.brand.init.debug",
|
||||
@@ -197,11 +197,11 @@
|
||||
},
|
||||
{
|
||||
"id": "api.channel.change_channel_privacy.private_to_public",
|
||||
"translation": "This channel has been converted to a Public Channel and can be joined by any team member."
|
||||
"translation": "이 채널은 공개 채널로 전환되어 모든 팀원이 들어올 수 있습니다."
|
||||
},
|
||||
{
|
||||
"id": "api.channel.change_channel_privacy.public_to_private",
|
||||
"translation": "This channel has been converted to a Private Channel."
|
||||
"translation": "이 채널은 비공개 채널로 변경되었습니다."
|
||||
},
|
||||
{
|
||||
"id": "api.channel.create_channel.direct_channel.app_error",
|
||||
@@ -433,7 +433,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command.execute_command.not_found.app_error",
|
||||
"translation": "Command with a trigger of '{{.Trigger}}' not found. To send a message beginning with \"/\", try adding an empty space at the beginning of the message."
|
||||
"translation": "'{{.Trigger}}' 트리거에 실행되는 커맨드를 찾지 못했습니다. \"/\"로 시작하는 메시지를 보내려면 메시지 시작 부분에 빈 공간을 추가하십시오."
|
||||
},
|
||||
{
|
||||
"id": "api.command.execute_command.save.app_error",
|
||||
@@ -521,7 +521,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_header.name",
|
||||
"translation": "header"
|
||||
"translation": "머리글"
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_header.permission.app_error",
|
||||
@@ -541,11 +541,11 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_purpose.desc",
|
||||
"translation": "Edit the channel purpose"
|
||||
"translation": "체널 설명 수정하기"
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_purpose.direct_group.app_error",
|
||||
"translation": "Cannot set purpose for direct message channels. Use /header to set the header instead."
|
||||
"translation": "개인 메시지의 설명은 설정할 수 없습니다. 머리글을 설정하려면 /header 를 사용하세요."
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_purpose.hint",
|
||||
@@ -557,11 +557,11 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_purpose.name",
|
||||
"translation": "purpose"
|
||||
"translation": "설명"
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_purpose.permission.app_error",
|
||||
"translation": "당신은 채널 머릿말을 수정할 권한을 가지고 있지 않습니다."
|
||||
"translation": "당신은 채널 설명을 수정할 권한을 가지고 있지 않습니다."
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_purpose.update_channel.app_error",
|
||||
@@ -573,11 +573,11 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_rename.desc",
|
||||
"translation": "Rename the channel"
|
||||
"translation": "채널 이름 바꾸기"
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_rename.direct_group.app_error",
|
||||
"translation": "개인 메시지 채널은 나갈 수 없습니다"
|
||||
"translation": "개인 메시지 채널의 이름은 변경 할 수 없습니다."
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_rename.hint",
|
||||
@@ -585,23 +585,23 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_rename.message.app_error",
|
||||
"translation": "메시지는 /echo 명령어와 함께 제공되어야 합니다."
|
||||
"translation": "메시지는 /rename 명령어와 함께 제공되어야 합니다."
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_rename.name",
|
||||
"translation": "rename"
|
||||
"translation": "이름변경"
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_rename.permission.app_error",
|
||||
"translation": "당신은 채널 머릿말을 수정할 권한을 가지고 있지 않습니다."
|
||||
"translation": "당신은 채널 이름을 수정할 권한을 가지고 있지 않습니다."
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_rename.too_long.app_error",
|
||||
"translation": "Channel name must be {{.Length}} or fewer characters"
|
||||
"translation": "채널 이름은 {{.Length}} 자 이하여야 합니다."
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_rename.too_short.app_error",
|
||||
"translation": "Channel name must be {{.Length}} or more characters"
|
||||
"translation": "채널 이름은 {{.Length}} 자 이상이여야 합니다."
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_rename.update_channel.app_error",
|
||||
@@ -609,11 +609,11 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_rename.update_channel.success",
|
||||
"translation": "채널 머릿말이 성공적으로 업데이트되었습니다."
|
||||
"translation": "채널이름이 성공적으로 업데이트되었습니다."
|
||||
},
|
||||
{
|
||||
"id": "api.command_code.desc",
|
||||
"translation": "Display text as a code block"
|
||||
"translation": "텍스트를 코드 블록으로 표시합니다."
|
||||
},
|
||||
{
|
||||
"id": "api.command_code.hint",
|
||||
@@ -621,11 +621,11 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_code.message.app_error",
|
||||
"translation": "메시지는 /echo 명령어와 함께 제공되어야 합니다."
|
||||
"translation": "메시지는 /code 명령어와 함께 제공되어야 합니다."
|
||||
},
|
||||
{
|
||||
"id": "api.command_code.name",
|
||||
"translation": "code"
|
||||
"translation": "코드"
|
||||
},
|
||||
{
|
||||
"id": "api.command_collapse.desc",
|
||||
@@ -645,7 +645,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_dnd.disabled",
|
||||
"translation": "Do Not Disturb is disabled."
|
||||
"translation": "방해 금지 모드가 해제되었습니다."
|
||||
},
|
||||
{
|
||||
"id": "api.command_dnd.error",
|
||||
@@ -705,7 +705,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_groupmsg.desc",
|
||||
"translation": "Sends a Group Message to the specified users"
|
||||
"translation": "지정된 사용자에게 그룹 메시지를 보냅니다."
|
||||
},
|
||||
{
|
||||
"id": "api.command_groupmsg.fail.app_error",
|
||||
@@ -760,7 +760,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_help.name",
|
||||
"translation": "help"
|
||||
"translation": "도움말"
|
||||
},
|
||||
{
|
||||
"id": "api.command_join.desc",
|
||||
@@ -816,7 +816,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_leave.success",
|
||||
"translation": "%v 가 채널을 떠났습니다."
|
||||
"translation": "채널을 떠났습니다."
|
||||
},
|
||||
{
|
||||
"id": "api.command_logout.desc",
|
||||
@@ -1772,7 +1772,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.post.make_direct_channel_visible.get_2_members.error",
|
||||
"translation": "다이렉트 채널에 2명의 구성원을 가져올 수 없습니다. channel_id={{.ChannelId}}"
|
||||
"translation": "개인 메시지 채널에 2명의 사용자를 가져오는데 실패했습니다. 채널 아이디={{.ChannelId}}"
|
||||
},
|
||||
{
|
||||
"id": "api.post.make_direct_channel_visible.get_members.error",
|
||||
@@ -3636,7 +3636,7 @@
|
||||
},
|
||||
{
|
||||
"id": "app.notification.subject.direct.full",
|
||||
"translation": "{{.SubjectText}} on {{.TeamDisplayName}} at {{.Month}} {{.Day}}, {{.Year}}"
|
||||
"translation": "[{{.SiteName}}] {{.SenderDisplayName}} (으)로부터 {{.Month}} {{.Day}}, {{.Year}} 에 새로운 개인 메시지가 왔습니다."
|
||||
},
|
||||
{
|
||||
"id": "app.notification.subject.notification.full",
|
||||
@@ -3712,7 +3712,7 @@
|
||||
},
|
||||
{
|
||||
"id": "app.user_access_token.disabled",
|
||||
"translation": "Personal access tokens are disabled on this server. Please contact your system administrator for details."
|
||||
"translation": "개인 액세스 토큰이 현재 서버에서 활성화 되어있지 않습니다. 시스템 관리자에게 연락하여 자세한 사항을 확인하시길 바랍니다."
|
||||
},
|
||||
{
|
||||
"id": "app.user_access_token.invalid_or_missing",
|
||||
|
||||
@@ -3636,7 +3636,7 @@
|
||||
},
|
||||
{
|
||||
"id": "app.notification.subject.direct.full",
|
||||
"translation": "{{.SubjectText}} in {{.TeamDisplayName}} van {{.SenderDisplayName}} op {{.Month}} {{.Day}} {{.Year}}"
|
||||
"translation": "[{{.SiteName}}] Nieuw direct bericht van {{.SenderDisplayName}} op {{.Month}} {{.Day}} {{.Year}}"
|
||||
},
|
||||
{
|
||||
"id": "app.notification.subject.notification.full",
|
||||
|
||||
@@ -1370,7 +1370,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.file.upload_file.incorrect_number_of_files.app_error",
|
||||
"translation": "Unable to upload files. Incorrect number of files specified."
|
||||
"translation": "Não é possível enviar arquivos. Número incorreto de arquivos especificado."
|
||||
},
|
||||
{
|
||||
"id": "api.file.upload_file.large_image.app_error",
|
||||
@@ -1820,7 +1820,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.post.send_notifications_and_forget.push_image_only_no_channel",
|
||||
"translation": " enviado um ou mais arquivos em "
|
||||
"translation": " enviado um ou mais arquivos"
|
||||
},
|
||||
{
|
||||
"id": "api.post.send_notifications_and_forget.push_in",
|
||||
@@ -3080,7 +3080,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.webhook.incoming.error",
|
||||
"translation": "Could not decode the multipart payload of incoming webhook."
|
||||
"translation": "Não foi possível decodificar a carga multiparte do webhook de entrada."
|
||||
},
|
||||
{
|
||||
"id": "api.webhook.init.debug",
|
||||
@@ -3656,7 +3656,7 @@
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.disabled.app_error",
|
||||
"translation": "Plugins have been disabled. Please check your logs for details."
|
||||
"translation": "Os plugins foram desativados. Verifique os seus logs para obter detalhes."
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.extract.app_error",
|
||||
@@ -4192,15 +4192,15 @@
|
||||
},
|
||||
{
|
||||
"id": "ent.migration.migratetosaml.email_already_used_by_other_user",
|
||||
"translation": "Email already used by another SAML user."
|
||||
"translation": "Email já usado por outro usuário SAML."
|
||||
},
|
||||
{
|
||||
"id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file",
|
||||
"translation": "User not found in the users file."
|
||||
"translation": "Usuário não encontrado no arquivo de usuários."
|
||||
},
|
||||
{
|
||||
"id": "ent.migration.migratetosaml.username_already_used_by_other_user",
|
||||
"translation": "Username already used by another Mattermost user."
|
||||
"translation": "Nome de usuário já usado por outro usuário Mattermost."
|
||||
},
|
||||
{
|
||||
"id": "ent.saml.attribute.app_error",
|
||||
@@ -4904,7 +4904,7 @@
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.message_export.export_type.app_error",
|
||||
"translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
|
||||
"translation": "Na tarefa de exportação de mensagens o ExportFormat deve ser 'actiance' ou 'globalrelay'"
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.message_export.file_location.app_error",
|
||||
@@ -4916,7 +4916,7 @@
|
||||
},
|
||||
{
|
||||
"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"
|
||||
"translation": "Na tarefa de exportação de mensagens o GlobalRelayEmailAddress deve ser um endereço de email válido"
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.password_length.app_error",
|
||||
@@ -5048,7 +5048,7 @@
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.websocket_url.app_error",
|
||||
"translation": "A URL Websocket do WebRTC Gateway deve ser uma URL válida e começar com ws:// ou wss://."
|
||||
"translation": "A URL Websocket deve ser uma URL válida e começar com ws:// ou wss://."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.write_timeout.app_error",
|
||||
@@ -7136,7 +7136,7 @@
|
||||
},
|
||||
{
|
||||
"id": "utils.mail.sendMail.attachments.write_error",
|
||||
"translation": "Failed to write attachment to email"
|
||||
"translation": "Falha ao escrever o anexo para o e-mail"
|
||||
},
|
||||
{
|
||||
"id": "utils.mail.send_mail.close.app_error",
|
||||
|
||||
@@ -2308,11 +2308,11 @@
|
||||
},
|
||||
{
|
||||
"id": "api.team.move_channel.post.error",
|
||||
"translation": "发送频道作用消息失败"
|
||||
"translation": "发送频道移动消息失败。"
|
||||
},
|
||||
{
|
||||
"id": "api.team.move_channel.success",
|
||||
"translation": "This channel has been moved to this team from %v."
|
||||
"translation": "此频道已从 %v 移至此团队。"
|
||||
},
|
||||
{
|
||||
"id": "api.team.permanent_delete_team.attempting.warn",
|
||||
@@ -3080,7 +3080,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.webhook.incoming.error",
|
||||
"translation": "Could not decode the multipart payload of incoming webhook."
|
||||
"translation": "无法解码传入的 webhook 混合数据。"
|
||||
},
|
||||
{
|
||||
"id": "api.webhook.init.debug",
|
||||
@@ -4904,7 +4904,7 @@
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.message_export.export_type.app_error",
|
||||
"translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
|
||||
"translation": "消息导出任务 ExportFormat 必须为 'actiance' 或 'globalrelay'"
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.message_export.file_location.app_error",
|
||||
@@ -4916,7 +4916,7 @@
|
||||
},
|
||||
{
|
||||
"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"
|
||||
"translation": "消息导出任务 GlobalRelayEmailAddress 必须为有效的电子邮箱地址"
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.password_length.app_error",
|
||||
|
||||
@@ -1370,7 +1370,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.file.upload_file.incorrect_number_of_files.app_error",
|
||||
"translation": "Unable to upload files. Incorrect number of files specified."
|
||||
"translation": "無法上傳檔案。檔案數量不對。"
|
||||
},
|
||||
{
|
||||
"id": "api.file.upload_file.large_image.app_error",
|
||||
@@ -1812,7 +1812,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.post.send_notifications_and_forget.push_image_only",
|
||||
"translation": "已上傳一個或更多檔案"
|
||||
"translation": "已上傳一個或更多檔案至"
|
||||
},
|
||||
{
|
||||
"id": "api.post.send_notifications_and_forget.push_image_only_dm",
|
||||
@@ -2308,11 +2308,11 @@
|
||||
},
|
||||
{
|
||||
"id": "api.team.move_channel.post.error",
|
||||
"translation": "發送頻道用途訊息失敗"
|
||||
"translation": "發送頻道移動訊息失敗"
|
||||
},
|
||||
{
|
||||
"id": "api.team.move_channel.success",
|
||||
"translation": "This channel has been moved to this team from %v."
|
||||
"translation": "此頻道已從 %v 移動至此團隊。"
|
||||
},
|
||||
{
|
||||
"id": "api.team.permanent_delete_team.attempting.warn",
|
||||
@@ -3080,7 +3080,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.webhook.incoming.error",
|
||||
"translation": "Could not decode the multipart payload of incoming webhook."
|
||||
"translation": "無法解碼 Incoming Webhook 的 multipart 內容。"
|
||||
},
|
||||
{
|
||||
"id": "api.webhook.init.debug",
|
||||
@@ -3656,7 +3656,7 @@
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.disabled.app_error",
|
||||
"translation": "Plugins have been disabled. Please check your logs for details."
|
||||
"translation": "模組已被停用。詳情請看系統紀錄。"
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.extract.app_error",
|
||||
@@ -4192,15 +4192,15 @@
|
||||
},
|
||||
{
|
||||
"id": "ent.migration.migratetosaml.email_already_used_by_other_user",
|
||||
"translation": "Email already used by another SAML user."
|
||||
"translation": "電子郵件已被其他 SAML 使用者使用。"
|
||||
},
|
||||
{
|
||||
"id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file",
|
||||
"translation": "User not found in the users file."
|
||||
"translation": "在使用者檔案中找不到使用者。"
|
||||
},
|
||||
{
|
||||
"id": "ent.migration.migratetosaml.username_already_used_by_other_user",
|
||||
"translation": "Username already used by another Mattermost user."
|
||||
"translation": "使用者名稱已被其他 Mattermost 使用者使用。 "
|
||||
},
|
||||
{
|
||||
"id": "ent.saml.attribute.app_error",
|
||||
@@ -4904,7 +4904,7 @@
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.message_export.export_type.app_error",
|
||||
"translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
|
||||
"translation": "訊息匯出工作的 ExportFormat 必須為 'actiance' 或 'globalrelay'"
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.message_export.file_location.app_error",
|
||||
@@ -4916,7 +4916,7 @@
|
||||
},
|
||||
{
|
||||
"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"
|
||||
"translation": "訊息匯出工作的 GlobalRelayEmailAddress 必須為 'actiance' 或 'globalrelay'"
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.password_length.app_error",
|
||||
@@ -5048,7 +5048,7 @@
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.websocket_url.app_error",
|
||||
"translation": "WebRTC 閘道 Websocket 網址必須是以 ws:// 或 wss:// 起始的有效網址。"
|
||||
"translation": "Websocket 網址必須是以 ws:// 或 wss:// 起始的有效網址。"
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.write_timeout.app_error",
|
||||
@@ -7136,7 +7136,7 @@
|
||||
},
|
||||
{
|
||||
"id": "utils.mail.sendMail.attachments.write_error",
|
||||
"translation": "Failed to write attachment to email"
|
||||
"translation": "無法寫入附加檔案到電子郵件"
|
||||
},
|
||||
{
|
||||
"id": "utils.mail.send_mail.close.app_error",
|
||||
|
||||
11
mkdocs.yml
11
mkdocs.yml
@@ -1,11 +0,0 @@
|
||||
site_name: Mattermost Documentation
|
||||
site_url: http://docs.mattermost.org
|
||||
repo_url: https://github.com/mattermost/platform
|
||||
repo_name: GitHub
|
||||
site_favicon: favicon.ico
|
||||
copyright: "Copyright (c) 2015-2017 Mattermost, Inc. All Rights Reserved."
|
||||
strict: true
|
||||
docs_dir: doc
|
||||
site_dir: documentation-html
|
||||
use_directory_urls: false
|
||||
theme: mkdocs
|
||||
@@ -2102,8 +2102,8 @@ func (c *Client4) GetPing() (string, *Response) {
|
||||
}
|
||||
|
||||
// TestEmail will attempt to connect to the configured SMTP server.
|
||||
func (c *Client4) TestEmail() (bool, *Response) {
|
||||
if r, err := c.DoApiPost(c.GetTestEmailRoute(), ""); err != nil {
|
||||
func (c *Client4) TestEmail(config *Config) (bool, *Response) {
|
||||
if r, err := c.DoApiPost(c.GetTestEmailRoute(), config.ToJson()); err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
if len(os.Args) < 3 || os.Args[0] != "sandbox.runProcess" {
|
||||
if len(os.Args) < 4 || os.Args[0] != "sandbox.runProcess" {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func init() {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := runProcess(&config, os.Args[2]); err != nil {
|
||||
if err := runProcess(&config, os.Args[2], os.Args[3]); err != nil {
|
||||
if eerr, ok := err.(*exec.ExitError); ok {
|
||||
if status, ok := eerr.Sys().(syscall.WaitStatus); ok {
|
||||
os.Exit(status.ExitStatus())
|
||||
@@ -98,13 +98,7 @@ func systemMountPoints() (points []*MountPoint) {
|
||||
return
|
||||
}
|
||||
|
||||
func runProcess(config *Configuration, path string) error {
|
||||
root, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
func runProcess(config *Configuration, path, root string) error {
|
||||
if err := syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, ""); err != nil {
|
||||
return errors.Wrapf(err, "unable to make root private")
|
||||
}
|
||||
@@ -330,9 +324,10 @@ func runExecutable(path string) error {
|
||||
|
||||
type process struct {
|
||||
command *exec.Cmd
|
||||
root string
|
||||
}
|
||||
|
||||
func newProcess(ctx context.Context, config *Configuration, path string) (rpcplugin.Process, io.ReadWriteCloser, error) {
|
||||
func newProcess(ctx context.Context, config *Configuration, path string) (pOut rpcplugin.Process, rwcOut io.ReadWriteCloser, errOut error) {
|
||||
configJSON, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -345,8 +340,18 @@ func newProcess(ctx context.Context, config *Configuration, path string) (rpcplu
|
||||
defer childFiles[0].Close()
|
||||
defer childFiles[1].Close()
|
||||
|
||||
root, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer func() {
|
||||
if errOut != nil {
|
||||
os.RemoveAll(root)
|
||||
}
|
||||
}()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "/proc/self/exe")
|
||||
cmd.Args = []string{"sandbox.runProcess", string(configJSON), path}
|
||||
cmd.Args = []string{"sandbox.runProcess", string(configJSON), path, root}
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.ExtraFiles = childFiles
|
||||
@@ -378,19 +383,21 @@ func newProcess(ctx context.Context, config *Configuration, path string) (rpcplu
|
||||
|
||||
return &process{
|
||||
command: cmd,
|
||||
root: root,
|
||||
}, ipc, nil
|
||||
}
|
||||
|
||||
func (p *process) Wait() error {
|
||||
defer os.RemoveAll(p.root)
|
||||
return p.command.Wait()
|
||||
}
|
||||
|
||||
func init() {
|
||||
if len(os.Args) < 1 || os.Args[0] != "sandbox.checkSupportInNamespace" {
|
||||
if len(os.Args) < 2 || os.Args[0] != "sandbox.checkSupportInNamespace" {
|
||||
return
|
||||
}
|
||||
|
||||
if err := checkSupportInNamespace(); err != nil {
|
||||
if err := checkSupportInNamespace(os.Args[1]); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -398,13 +405,7 @@ func init() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func checkSupportInNamespace() error {
|
||||
root, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
func checkSupportInNamespace(root string) error {
|
||||
if err := syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, ""); err != nil {
|
||||
return errors.Wrapf(err, "unable to make root private")
|
||||
}
|
||||
@@ -444,8 +445,14 @@ func checkSupport() error {
|
||||
|
||||
stderr := &bytes.Buffer{}
|
||||
|
||||
root, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
cmd := exec.Command("/proc/self/exe")
|
||||
cmd.Args = []string{"sandbox.checkSupportInNamespace"}
|
||||
cmd.Args = []string{"sandbox.checkSupportInNamespace", root}
|
||||
cmd.Stderr = stderr
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWUSER,
|
||||
|
||||
@@ -687,31 +687,70 @@ func (s SqlPostStore) getRootPosts(channelId string, offset int, limit int) stor
|
||||
func (s SqlPostStore) getParentsPosts(channelId string, offset int, limit int) store.StoreChannel {
|
||||
return store.Do(func(result *store.StoreResult) {
|
||||
var posts []*model.Post
|
||||
_, err := s.GetReplica().Select(&posts,
|
||||
`SELECT
|
||||
q2.*
|
||||
_, err := s.GetReplica().Select(&posts, `
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
Posts q2
|
||||
INNER JOIN
|
||||
(SELECT DISTINCT
|
||||
q3.RootId
|
||||
FROM
|
||||
(SELECT
|
||||
RootId
|
||||
FROM
|
||||
Posts
|
||||
WHERE
|
||||
ChannelId = :ChannelId1
|
||||
AND DeleteAt = 0
|
||||
ORDER BY CreateAt DESC
|
||||
LIMIT :Limit OFFSET :Offset) q3
|
||||
WHERE q3.RootId != '') q1
|
||||
ON q1.RootId = q2.Id OR q1.RootId = q2.RootId
|
||||
Posts
|
||||
WHERE
|
||||
ChannelId = :ChannelId2
|
||||
AND DeleteAt = 0
|
||||
ORDER BY CreateAt`,
|
||||
map[string]interface{}{"ChannelId1": channelId, "Offset": offset, "Limit": limit, "ChannelId2": channelId})
|
||||
Id IN (SELECT * FROM (
|
||||
-- The root post of any replies in the window
|
||||
(SELECT * FROM (
|
||||
SELECT
|
||||
CASE RootId
|
||||
WHEN '' THEN NULL
|
||||
ELSE RootId
|
||||
END
|
||||
FROM
|
||||
Posts
|
||||
WHERE
|
||||
ChannelId = :ChannelId1
|
||||
AND DeleteAt = 0
|
||||
ORDER BY
|
||||
CreateAt DESC
|
||||
LIMIT :Limit1 OFFSET :Offset1
|
||||
) x )
|
||||
|
||||
UNION
|
||||
|
||||
-- The reply posts to all threads intersecting with the window, including replies
|
||||
-- to root posts in the window itself.
|
||||
(
|
||||
SELECT
|
||||
Id
|
||||
FROM
|
||||
Posts
|
||||
WHERE RootId IN (SELECT * FROM (
|
||||
SELECT
|
||||
CASE RootId
|
||||
-- If there is no RootId, return the post id itself to be considered
|
||||
-- as a root post.
|
||||
WHEN '' THEN Id
|
||||
-- If there is a RootId, this post isn't a root post and return its
|
||||
-- root to be considered as a root post.
|
||||
ELSE RootId
|
||||
END
|
||||
FROM
|
||||
Posts
|
||||
WHERE
|
||||
ChannelId = :ChannelId2
|
||||
AND DeleteAt = 0
|
||||
ORDER BY
|
||||
CreateAt DESC
|
||||
LIMIT :Limit2 OFFSET :Offset2
|
||||
) x )
|
||||
)
|
||||
) x )
|
||||
AND
|
||||
DeleteAt = 0
|
||||
`, map[string]interface{}{
|
||||
"ChannelId1": channelId,
|
||||
"ChannelId2": channelId,
|
||||
"Offset1": offset,
|
||||
"Offset2": offset,
|
||||
"Limit1": limit,
|
||||
"Limit2": limit,
|
||||
})
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlPostStore.GetLinearPosts", "store.sql_post.get_parents_posts.app_error", nil, "channelId="+channelId+" err="+err.Error(), http.StatusInternalServerError)
|
||||
} else {
|
||||
|
||||
@@ -5,6 +5,7 @@ package storetest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -491,125 +492,182 @@ func testPostStoreGetWithChildren(t *testing.T, ss store.Store) {
|
||||
}
|
||||
|
||||
func testPostStoreGetPostsWithDetails(t *testing.T, ss store.Store) {
|
||||
o1 := &model.Post{}
|
||||
o1.ChannelId = model.NewId()
|
||||
o1.UserId = model.NewId()
|
||||
o1.Message = "zz" + model.NewId() + "b"
|
||||
o1 = (<-ss.Post().Save(o1)).Data.(*model.Post)
|
||||
assertPosts := func(expected []*model.Post, actual map[string]*model.Post) {
|
||||
expectedIds := make([]string, 0, len(expected))
|
||||
expectedMessages := make([]string, 0, len(expected))
|
||||
for _, post := range expected {
|
||||
expectedIds = append(expectedIds, post.Id)
|
||||
expectedMessages = append(expectedMessages, post.Message)
|
||||
}
|
||||
sort.Strings(expectedIds)
|
||||
sort.Strings(expectedMessages)
|
||||
|
||||
actualIds := make([]string, 0, len(actual))
|
||||
actualMessages := make([]string, 0, len(actual))
|
||||
for _, post := range actual {
|
||||
actualIds = append(actualIds, post.Id)
|
||||
actualMessages = append(actualMessages, post.Message)
|
||||
}
|
||||
sort.Strings(actualIds)
|
||||
sort.Strings(actualMessages)
|
||||
|
||||
if assert.Equal(t, expectedIds, actualIds) {
|
||||
assert.Equal(t, expectedMessages, actualMessages)
|
||||
}
|
||||
}
|
||||
|
||||
root1 := &model.Post{}
|
||||
root1.ChannelId = model.NewId()
|
||||
root1.UserId = model.NewId()
|
||||
root1.Message = "zz" + model.NewId() + "b"
|
||||
root1 = (<-ss.Post().Save(root1)).Data.(*model.Post)
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
o2 := &model.Post{}
|
||||
o2.ChannelId = o1.ChannelId
|
||||
o2.UserId = model.NewId()
|
||||
o2.Message = "zz" + model.NewId() + "b"
|
||||
o2.ParentId = o1.Id
|
||||
o2.RootId = o1.Id
|
||||
o2 = (<-ss.Post().Save(o2)).Data.(*model.Post)
|
||||
root1Reply1 := &model.Post{}
|
||||
root1Reply1.ChannelId = root1.ChannelId
|
||||
root1Reply1.UserId = model.NewId()
|
||||
root1Reply1.Message = "zz" + model.NewId() + "b"
|
||||
root1Reply1.ParentId = root1.Id
|
||||
root1Reply1.RootId = root1.Id
|
||||
root1Reply1 = (<-ss.Post().Save(root1Reply1)).Data.(*model.Post)
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
o2a := &model.Post{}
|
||||
o2a.ChannelId = o1.ChannelId
|
||||
o2a.UserId = model.NewId()
|
||||
o2a.Message = "zz" + model.NewId() + "b"
|
||||
o2a.ParentId = o1.Id
|
||||
o2a.RootId = o1.Id
|
||||
o2a = (<-ss.Post().Save(o2a)).Data.(*model.Post)
|
||||
root1Reply2 := &model.Post{}
|
||||
root1Reply2.ChannelId = root1.ChannelId
|
||||
root1Reply2.UserId = model.NewId()
|
||||
root1Reply2.Message = "zz" + model.NewId() + "b"
|
||||
root1Reply2.ParentId = root1.Id
|
||||
root1Reply2.RootId = root1.Id
|
||||
root1Reply2 = (<-ss.Post().Save(root1Reply2)).Data.(*model.Post)
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
o3 := &model.Post{}
|
||||
o3.ChannelId = o1.ChannelId
|
||||
o3.UserId = model.NewId()
|
||||
o3.Message = "zz" + model.NewId() + "b"
|
||||
o3.ParentId = o1.Id
|
||||
o3.RootId = o1.Id
|
||||
o3 = (<-ss.Post().Save(o3)).Data.(*model.Post)
|
||||
root1Reply3 := &model.Post{}
|
||||
root1Reply3.ChannelId = root1.ChannelId
|
||||
root1Reply3.UserId = model.NewId()
|
||||
root1Reply3.Message = "zz" + model.NewId() + "b"
|
||||
root1Reply3.ParentId = root1.Id
|
||||
root1Reply3.RootId = root1.Id
|
||||
root1Reply3 = (<-ss.Post().Save(root1Reply3)).Data.(*model.Post)
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
o4 := &model.Post{}
|
||||
o4.ChannelId = o1.ChannelId
|
||||
o4.UserId = model.NewId()
|
||||
o4.Message = "zz" + model.NewId() + "b"
|
||||
o4 = (<-ss.Post().Save(o4)).Data.(*model.Post)
|
||||
root2 := &model.Post{}
|
||||
root2.ChannelId = root1.ChannelId
|
||||
root2.UserId = model.NewId()
|
||||
root2.Message = "zz" + model.NewId() + "b"
|
||||
root2 = (<-ss.Post().Save(root2)).Data.(*model.Post)
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
o5 := &model.Post{}
|
||||
o5.ChannelId = o1.ChannelId
|
||||
o5.UserId = model.NewId()
|
||||
o5.Message = "zz" + model.NewId() + "b"
|
||||
o5.ParentId = o4.Id
|
||||
o5.RootId = o4.Id
|
||||
o5 = (<-ss.Post().Save(o5)).Data.(*model.Post)
|
||||
root2Reply1 := &model.Post{}
|
||||
root2Reply1.ChannelId = root1.ChannelId
|
||||
root2Reply1.UserId = model.NewId()
|
||||
root2Reply1.Message = "zz" + model.NewId() + "b"
|
||||
root2Reply1.ParentId = root2.Id
|
||||
root2Reply1.RootId = root2.Id
|
||||
root2Reply1 = (<-ss.Post().Save(root2Reply1)).Data.(*model.Post)
|
||||
|
||||
r1 := (<-ss.Post().GetPosts(o1.ChannelId, 0, 4, false)).Data.(*model.PostList)
|
||||
r1 := (<-ss.Post().GetPosts(root1.ChannelId, 0, 4, false)).Data.(*model.PostList)
|
||||
|
||||
if r1.Order[0] != o5.Id {
|
||||
t.Fatal("invalid order")
|
||||
expectedOrder := []string{
|
||||
root2Reply1.Id,
|
||||
root2.Id,
|
||||
root1Reply3.Id,
|
||||
root1Reply2.Id,
|
||||
}
|
||||
|
||||
if r1.Order[1] != o4.Id {
|
||||
t.Fatal("invalid order")
|
||||
expectedPosts := []*model.Post{
|
||||
root1,
|
||||
root1Reply1,
|
||||
root1Reply2,
|
||||
root1Reply3,
|
||||
root2,
|
||||
root2Reply1,
|
||||
}
|
||||
|
||||
if r1.Order[2] != o3.Id {
|
||||
t.Fatal("invalid order")
|
||||
}
|
||||
assert.Equal(t, expectedOrder, r1.Order)
|
||||
assertPosts(expectedPosts, r1.Posts)
|
||||
|
||||
if r1.Order[3] != o2a.Id {
|
||||
t.Fatal("invalid order")
|
||||
}
|
||||
|
||||
if len(r1.Posts) != 6 { //the last 4, + o1 (o2a and o3's parent) + o2 (in same thread as o2a and o3)
|
||||
t.Fatal("wrong size")
|
||||
}
|
||||
|
||||
if r1.Posts[o1.Id].Message != o1.Message {
|
||||
t.Fatal("Missing parent")
|
||||
}
|
||||
|
||||
r2 := (<-ss.Post().GetPosts(o1.ChannelId, 0, 4, true)).Data.(*model.PostList)
|
||||
|
||||
if r2.Order[0] != o5.Id {
|
||||
t.Fatal("invalid order")
|
||||
}
|
||||
|
||||
if r2.Order[1] != o4.Id {
|
||||
t.Fatal("invalid order")
|
||||
}
|
||||
|
||||
if r2.Order[2] != o3.Id {
|
||||
t.Fatal("invalid order")
|
||||
}
|
||||
|
||||
if r2.Order[3] != o2a.Id {
|
||||
t.Fatal("invalid order")
|
||||
}
|
||||
|
||||
if len(r2.Posts) != 6 { //the last 4, + o1 (o2a and o3's parent) + o2 (in same thread as o2a and o3)
|
||||
t.Fatal("wrong size")
|
||||
}
|
||||
|
||||
if r2.Posts[o1.Id].Message != o1.Message {
|
||||
t.Fatal("Missing parent")
|
||||
}
|
||||
r2 := (<-ss.Post().GetPosts(root1.ChannelId, 0, 4, true)).Data.(*model.PostList)
|
||||
assert.Equal(t, expectedOrder, r2.Order)
|
||||
assertPosts(expectedPosts, r2.Posts)
|
||||
|
||||
// Run once to fill cache
|
||||
<-ss.Post().GetPosts(o1.ChannelId, 0, 30, true)
|
||||
<-ss.Post().GetPosts(root1.ChannelId, 0, 30, true)
|
||||
expectedOrder = []string{
|
||||
root2Reply1.Id,
|
||||
root2.Id,
|
||||
root1Reply3.Id,
|
||||
root1Reply2.Id,
|
||||
root1Reply1.Id,
|
||||
root1.Id,
|
||||
}
|
||||
|
||||
o6 := &model.Post{}
|
||||
o6.ChannelId = o1.ChannelId
|
||||
o6.UserId = model.NewId()
|
||||
o6.Message = "zz" + model.NewId() + "b"
|
||||
o6 = (<-ss.Post().Save(o6)).Data.(*model.Post)
|
||||
root3 := &model.Post{}
|
||||
root3.ChannelId = root1.ChannelId
|
||||
root3.UserId = model.NewId()
|
||||
root3.Message = "zz" + model.NewId() + "b"
|
||||
root3 = (<-ss.Post().Save(root3)).Data.(*model.Post)
|
||||
|
||||
// Should only be 6 since we hit the cache
|
||||
r3 := (<-ss.Post().GetPosts(o1.ChannelId, 0, 30, true)).Data.(*model.PostList)
|
||||
assert.Equal(t, 6, len(r3.Order))
|
||||
// Response should be the same despite the new post since we hit the cache
|
||||
r3 := (<-ss.Post().GetPosts(root1.ChannelId, 0, 30, true)).Data.(*model.PostList)
|
||||
assert.Equal(t, expectedOrder, r3.Order)
|
||||
assertPosts(expectedPosts, r3.Posts)
|
||||
|
||||
ss.Post().InvalidateLastPostTimeCache(o1.ChannelId)
|
||||
ss.Post().InvalidateLastPostTimeCache(root1.ChannelId)
|
||||
|
||||
// Cache was invalidated, we should get all the posts
|
||||
r4 := (<-ss.Post().GetPosts(o1.ChannelId, 0, 30, true)).Data.(*model.PostList)
|
||||
assert.Equal(t, 7, len(r4.Order))
|
||||
r4 := (<-ss.Post().GetPosts(root1.ChannelId, 0, 30, true)).Data.(*model.PostList)
|
||||
expectedOrder = []string{
|
||||
root3.Id,
|
||||
root2Reply1.Id,
|
||||
root2.Id,
|
||||
root1Reply3.Id,
|
||||
root1Reply2.Id,
|
||||
root1Reply1.Id,
|
||||
root1.Id,
|
||||
}
|
||||
expectedPosts = []*model.Post{
|
||||
root1,
|
||||
root1Reply1,
|
||||
root1Reply2,
|
||||
root1Reply3,
|
||||
root2,
|
||||
root2Reply1,
|
||||
root3,
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedOrder, r4.Order)
|
||||
assertPosts(expectedPosts, r4.Posts)
|
||||
|
||||
// Replies past the window should be included if the root post itself is in the window
|
||||
root3Reply1 := &model.Post{}
|
||||
root3Reply1.ChannelId = root1.ChannelId
|
||||
root3Reply1.UserId = model.NewId()
|
||||
root3Reply1.Message = "zz" + model.NewId() + "b"
|
||||
root3Reply1.ParentId = root3.Id
|
||||
root3Reply1.RootId = root3.Id
|
||||
root3Reply1 = (<-ss.Post().Save(root3Reply1)).Data.(*model.Post)
|
||||
|
||||
r5 := (<-ss.Post().GetPosts(root1.ChannelId, 1, 5, false)).Data.(*model.PostList)
|
||||
expectedOrder = []string{
|
||||
root3.Id,
|
||||
root2Reply1.Id,
|
||||
root2.Id,
|
||||
root1Reply3.Id,
|
||||
root1Reply2.Id,
|
||||
}
|
||||
expectedPosts = []*model.Post{
|
||||
root1,
|
||||
root1Reply1,
|
||||
root1Reply2,
|
||||
root1Reply3,
|
||||
root2,
|
||||
root2Reply1,
|
||||
root3,
|
||||
root3Reply1,
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedOrder, r5.Order)
|
||||
assertPosts(expectedPosts, r5.Posts)
|
||||
}
|
||||
|
||||
func testPostStoreGetPostsBeforeAfter(t *testing.T, ss store.Store) {
|
||||
|
||||
@@ -59,6 +59,7 @@ func RenderWebError(w http.ResponseWriter, r *http.Request, status int, params u
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(status)
|
||||
fmt.Fprintln(w, `<!DOCTYPE html><html><head></head>`)
|
||||
fmt.Fprintln(w, `<body onload="window.location = '`+template.HTMLEscapeString(template.JSEscapeString(destination))+`'">`)
|
||||
|
||||
@@ -449,6 +449,9 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L
|
||||
hasImageProxy := c.ServiceSettings.ImageProxyType != nil && *c.ServiceSettings.ImageProxyType != "" && c.ServiceSettings.ImageProxyURL != nil && *c.ServiceSettings.ImageProxyURL != ""
|
||||
props["HasImageProxy"] = strconv.FormatBool(hasImageProxy)
|
||||
|
||||
props["EnableThemeSelection"] = "true"
|
||||
props["AllowCustomThemes"] = "true"
|
||||
|
||||
if license != nil {
|
||||
props["ExperimentalTownSquareIsReadOnly"] = strconv.FormatBool(*c.TeamSettings.ExperimentalTownSquareIsReadOnly)
|
||||
props["ExperimentalEnableAuthenticationTransfer"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableAuthenticationTransfer)
|
||||
|
||||
@@ -253,12 +253,9 @@ func CheckMandatoryS3Fields(settings *model.FileSettings) *model.AppError {
|
||||
return model.NewAppError("S3File", "api.admin.test_s3.missing_s3_bucket", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// if S3 endpoint is not set call the set defaults to set that
|
||||
if len(settings.AmazonS3Endpoint) == 0 {
|
||||
return model.NewAppError("S3File", "api.admin.test_s3.missing_s3_endpoint", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if len(settings.AmazonS3Region) == 0 {
|
||||
return model.NewAppError("S3File", "api.admin.test_s3.missing_s3_region", nil, "", http.StatusBadRequest)
|
||||
settings.SetDefaults()
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -19,14 +19,14 @@ func TestCheckMandatoryS3Fields(t *testing.T) {
|
||||
|
||||
cfg.AmazonS3Bucket = "test-mm"
|
||||
err = CheckMandatoryS3Fields(&cfg)
|
||||
if err == nil || err.Message != "api.admin.test_s3.missing_s3_endpoint" {
|
||||
t.Fatal("should've failed with missing s3 endpoint")
|
||||
if err != nil {
|
||||
t.Fatal("should've not failed")
|
||||
}
|
||||
|
||||
cfg.AmazonS3Endpoint = "s3.newendpoint.com"
|
||||
cfg.AmazonS3Endpoint = ""
|
||||
err = CheckMandatoryS3Fields(&cfg)
|
||||
if err == nil || err.Message != "api.admin.test_s3.missing_s3_region" {
|
||||
t.Fatal("should've failed with missing s3 region")
|
||||
if err != nil || cfg.AmazonS3Endpoint != "s3.amazonaws.com" {
|
||||
t.Fatal("should've not failed because it should set the endpoint to the default")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
123
utils/mail.go
123
utils/mail.go
@@ -26,16 +26,27 @@ func encodeRFC2047Word(s string) string {
|
||||
return mime.BEncoding.Encode("utf-8", s)
|
||||
}
|
||||
|
||||
type SmtpConnectionInfo struct {
|
||||
SmtpUsername string
|
||||
SmtpPassword string
|
||||
SmtpServer string
|
||||
SmtpPort string
|
||||
SkipCertVerification bool
|
||||
ConnectionSecurity string
|
||||
Auth bool
|
||||
}
|
||||
|
||||
type authChooser struct {
|
||||
smtp.Auth
|
||||
Config *model.Config
|
||||
connectionInfo *SmtpConnectionInfo
|
||||
}
|
||||
|
||||
func (a *authChooser) Start(server *smtp.ServerInfo) (string, []byte, error) {
|
||||
a.Auth = LoginAuth(a.Config.EmailSettings.SMTPUsername, a.Config.EmailSettings.SMTPPassword, a.Config.EmailSettings.SMTPServer+":"+a.Config.EmailSettings.SMTPPort)
|
||||
smtpAddress := a.connectionInfo.SmtpServer + ":" + a.connectionInfo.SmtpPort
|
||||
a.Auth = LoginAuth(a.connectionInfo.SmtpUsername, a.connectionInfo.SmtpPassword, smtpAddress)
|
||||
for _, method := range server.Auth {
|
||||
if method == "PLAIN" {
|
||||
a.Auth = smtp.PlainAuth("", a.Config.EmailSettings.SMTPUsername, a.Config.EmailSettings.SMTPPassword, a.Config.EmailSettings.SMTPServer+":"+a.Config.EmailSettings.SMTPPort)
|
||||
a.Auth = smtp.PlainAuth("", a.connectionInfo.SmtpUsername, a.connectionInfo.SmtpPassword, a.connectionInfo.SmtpServer+":"+a.connectionInfo.SmtpPort)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -76,22 +87,23 @@ func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func connectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) {
|
||||
func ConnectToSMTPServerAdvanced(connectionInfo *SmtpConnectionInfo) (net.Conn, *model.AppError) {
|
||||
var conn net.Conn
|
||||
var err error
|
||||
|
||||
if config.EmailSettings.ConnectionSecurity == model.CONN_SECURITY_TLS {
|
||||
smtpAddress := connectionInfo.SmtpServer + ":" + connectionInfo.SmtpPort
|
||||
if connectionInfo.ConnectionSecurity == model.CONN_SECURITY_TLS {
|
||||
tlsconfig := &tls.Config{
|
||||
InsecureSkipVerify: *config.EmailSettings.SkipServerCertificateVerification,
|
||||
ServerName: config.EmailSettings.SMTPServer,
|
||||
InsecureSkipVerify: connectionInfo.SkipCertVerification,
|
||||
ServerName: connectionInfo.SmtpServer,
|
||||
}
|
||||
|
||||
conn, err = tls.Dial("tcp", config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort, tlsconfig)
|
||||
conn, err = tls.Dial("tcp", smtpAddress, tlsconfig)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("SendMail", "utils.mail.connect_smtp.open_tls.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
} else {
|
||||
conn, err = net.Dial("tcp", config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort)
|
||||
conn, err = net.Dial("tcp", smtpAddress)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("SendMail", "utils.mail.connect_smtp.open.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -100,14 +112,24 @@ func connectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func newSMTPClient(conn net.Conn, config *model.Config) (*smtp.Client, *model.AppError) {
|
||||
c, err := smtp.NewClient(conn, config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort)
|
||||
func ConnectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) {
|
||||
return ConnectToSMTPServerAdvanced(
|
||||
&SmtpConnectionInfo{
|
||||
ConnectionSecurity: config.EmailSettings.ConnectionSecurity,
|
||||
SkipCertVerification: *config.EmailSettings.SkipServerCertificateVerification,
|
||||
SmtpServer: config.EmailSettings.SMTPServer,
|
||||
SmtpPort: config.EmailSettings.SMTPPort,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func NewSMTPClientAdvanced(conn net.Conn, hostname string, connectionInfo *SmtpConnectionInfo) (*smtp.Client, *model.AppError) {
|
||||
c, err := smtp.NewClient(conn, connectionInfo.SmtpServer+":"+connectionInfo.SmtpPort)
|
||||
if err != nil {
|
||||
l4g.Error(T("utils.mail.new_client.open.error"), err)
|
||||
return nil, model.NewAppError("SendMail", "utils.mail.connect_smtp.open_tls.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
hostname := GetHostnameFromSiteURL(*config.ServiceSettings.SiteURL)
|
||||
if hostname != "" {
|
||||
err := c.Hello(hostname)
|
||||
if err != nil {
|
||||
@@ -116,35 +138,51 @@ func newSMTPClient(conn net.Conn, config *model.Config) (*smtp.Client, *model.Ap
|
||||
}
|
||||
}
|
||||
|
||||
if config.EmailSettings.ConnectionSecurity == model.CONN_SECURITY_STARTTLS {
|
||||
if connectionInfo.ConnectionSecurity == model.CONN_SECURITY_STARTTLS {
|
||||
tlsconfig := &tls.Config{
|
||||
InsecureSkipVerify: *config.EmailSettings.SkipServerCertificateVerification,
|
||||
ServerName: config.EmailSettings.SMTPServer,
|
||||
InsecureSkipVerify: connectionInfo.SkipCertVerification,
|
||||
ServerName: connectionInfo.SmtpServer,
|
||||
}
|
||||
c.StartTLS(tlsconfig)
|
||||
}
|
||||
|
||||
if *config.EmailSettings.EnableSMTPAuth {
|
||||
if err = c.Auth(&authChooser{Config: config}); err != nil {
|
||||
if connectionInfo.Auth {
|
||||
if err = c.Auth(&authChooser{connectionInfo: connectionInfo}); err != nil {
|
||||
return nil, model.NewAppError("SendMail", "utils.mail.new_client.auth.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func NewSMTPClient(conn net.Conn, config *model.Config) (*smtp.Client, *model.AppError) {
|
||||
return NewSMTPClientAdvanced(
|
||||
conn,
|
||||
GetHostnameFromSiteURL(*config.ServiceSettings.SiteURL),
|
||||
&SmtpConnectionInfo{
|
||||
ConnectionSecurity: config.EmailSettings.ConnectionSecurity,
|
||||
SkipCertVerification: *config.EmailSettings.SkipServerCertificateVerification,
|
||||
SmtpServer: config.EmailSettings.SMTPServer,
|
||||
SmtpPort: config.EmailSettings.SMTPPort,
|
||||
Auth: *config.EmailSettings.EnableSMTPAuth,
|
||||
SmtpUsername: config.EmailSettings.SMTPUsername,
|
||||
SmtpPassword: config.EmailSettings.SMTPPassword,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestConnection(config *model.Config) {
|
||||
if !config.EmailSettings.SendEmailNotifications {
|
||||
return
|
||||
}
|
||||
|
||||
conn, err1 := connectToSMTPServer(config)
|
||||
conn, err1 := ConnectToSMTPServer(config)
|
||||
if err1 != nil {
|
||||
l4g.Error(T("utils.mail.test.configured.error"), T(err1.Message), err1.DetailedError)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
c, err2 := newSMTPClient(conn, config)
|
||||
c, err2 := NewSMTPClient(conn, config)
|
||||
if err2 != nil {
|
||||
l4g.Error(T("utils.mail.test.configured.error"), T(err2.Message), err2.DetailedError)
|
||||
return
|
||||
@@ -155,19 +193,38 @@ func TestConnection(config *model.Config) {
|
||||
|
||||
func SendMailUsingConfig(to, subject, htmlBody string, config *model.Config, enableComplianceFeatures bool) *model.AppError {
|
||||
fromMail := mail.Address{Name: config.EmailSettings.FeedbackName, Address: config.EmailSettings.FeedbackEmail}
|
||||
return sendMail(to, to, fromMail, subject, htmlBody, nil, nil, config, enableComplianceFeatures)
|
||||
|
||||
return SendMailUsingConfigAdvanced(to, to, fromMail, subject, htmlBody, nil, nil, config, enableComplianceFeatures)
|
||||
}
|
||||
|
||||
// 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, enableComplianceFeatures bool) *model.AppError {
|
||||
return sendMail(mimeTo, smtpTo, from, subject, htmlBody, attachments, mimeHeaders, config, enableComplianceFeatures)
|
||||
}
|
||||
|
||||
func sendMail(mimeTo, smtpTo string, from mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, config *model.Config, enableComplianceFeatures bool) *model.AppError {
|
||||
if !config.EmailSettings.SendEmailNotifications || len(config.EmailSettings.SMTPServer) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
conn, err := ConnectToSMTPServer(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
c, err := NewSMTPClient(conn, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Quit()
|
||||
defer c.Close()
|
||||
|
||||
fileBackend, err := NewFileBackend(&config.FileSettings, enableComplianceFeatures)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return SendMail(c, mimeTo, smtpTo, from, subject, htmlBody, attachments, mimeHeaders, fileBackend)
|
||||
}
|
||||
|
||||
func SendMail(c *smtp.Client, mimeTo, smtpTo string, from mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, fileBackend FileBackend) *model.AppError {
|
||||
l4g.Debug(T("utils.mail.send_mail.sending.debug"), mimeTo, subject)
|
||||
|
||||
htmlMessage := "\r\n<html><body>" + htmlBody + "</body></html>"
|
||||
@@ -197,11 +254,6 @@ func sendMail(mimeTo, smtpTo string, from mail.Address, subject, htmlBody string
|
||||
m.AddAlternative("text/html", htmlMessage)
|
||||
|
||||
if attachments != nil {
|
||||
fileBackend, err := NewFileBackend(&config.FileSettings, enableComplianceFeatures)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, fileInfo := range attachments {
|
||||
bytes, err := fileBackend.ReadFile(fileInfo.Path)
|
||||
if err != nil {
|
||||
@@ -217,19 +269,6 @@ func sendMail(mimeTo, smtpTo string, from mail.Address, subject, htmlBody string
|
||||
}
|
||||
}
|
||||
|
||||
conn, err1 := connectToSMTPServer(config)
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
c, err2 := newSMTPClient(conn, config)
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
defer c.Quit()
|
||||
defer c.Close()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -4,24 +4,27 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMailConnection(t *testing.T) {
|
||||
func TestMailConnectionFromConfig(t *testing.T) {
|
||||
cfg, _, err := LoadConfig("config.json")
|
||||
require.Nil(t, err)
|
||||
|
||||
if conn, err := connectToSMTPServer(cfg); err != nil {
|
||||
if conn, err := ConnectToSMTPServer(cfg); err != nil {
|
||||
t.Log(err)
|
||||
t.Fatal("Should connect to the STMP Server")
|
||||
} else {
|
||||
if _, err1 := newSMTPClient(conn, cfg); err1 != nil {
|
||||
if _, err1 := NewSMTPClient(conn, cfg); err1 != nil {
|
||||
t.Log(err)
|
||||
t.Fatal("Should get new smtp client")
|
||||
}
|
||||
@@ -30,7 +33,53 @@ func TestMailConnection(t *testing.T) {
|
||||
cfg.EmailSettings.SMTPServer = "wrongServer"
|
||||
cfg.EmailSettings.SMTPPort = "553"
|
||||
|
||||
if _, err := connectToSMTPServer(cfg); err == nil {
|
||||
if _, err := ConnectToSMTPServer(cfg); err == nil {
|
||||
t.Log(err)
|
||||
t.Fatal("Should not to the STMP Server")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMailConnectionAdvanced(t *testing.T) {
|
||||
cfg, _, err := LoadConfig("config.json")
|
||||
require.Nil(t, err)
|
||||
|
||||
if conn, err := ConnectToSMTPServerAdvanced(
|
||||
&SmtpConnectionInfo{
|
||||
ConnectionSecurity: cfg.EmailSettings.ConnectionSecurity,
|
||||
SkipCertVerification: *cfg.EmailSettings.SkipServerCertificateVerification,
|
||||
SmtpServer: cfg.EmailSettings.SMTPServer,
|
||||
SmtpPort: cfg.EmailSettings.SMTPPort,
|
||||
},
|
||||
); err != nil {
|
||||
t.Log(err)
|
||||
t.Fatal("Should connect to the STMP Server")
|
||||
} else {
|
||||
if _, err1 := NewSMTPClientAdvanced(
|
||||
conn,
|
||||
GetHostnameFromSiteURL(*cfg.ServiceSettings.SiteURL),
|
||||
&SmtpConnectionInfo{
|
||||
ConnectionSecurity: cfg.EmailSettings.ConnectionSecurity,
|
||||
SkipCertVerification: *cfg.EmailSettings.SkipServerCertificateVerification,
|
||||
SmtpServer: cfg.EmailSettings.SMTPServer,
|
||||
SmtpPort: cfg.EmailSettings.SMTPPort,
|
||||
Auth: *cfg.EmailSettings.EnableSMTPAuth,
|
||||
SmtpUsername: cfg.EmailSettings.SMTPUsername,
|
||||
SmtpPassword: cfg.EmailSettings.SMTPPassword,
|
||||
},
|
||||
); err1 != nil {
|
||||
t.Log(err)
|
||||
t.Fatal("Should get new smtp client")
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := ConnectToSMTPServerAdvanced(
|
||||
&SmtpConnectionInfo{
|
||||
ConnectionSecurity: cfg.EmailSettings.ConnectionSecurity,
|
||||
SkipCertVerification: *cfg.EmailSettings.SkipServerCertificateVerification,
|
||||
SmtpServer: "wrongServer",
|
||||
SmtpPort: "553",
|
||||
},
|
||||
); err == nil {
|
||||
t.Log(err)
|
||||
t.Fatal("Should not to the STMP Server")
|
||||
}
|
||||
@@ -79,7 +128,7 @@ func TestSendMailUsingConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
/*func TestSendMailUsingConfigAdvanced(t *testing.T) {
|
||||
func TestSendMailUsingConfigAdvanced(t *testing.T) {
|
||||
cfg, _, err := LoadConfig("config.json")
|
||||
require.Nil(t, err)
|
||||
T = GetUserTranslations("en")
|
||||
@@ -171,20 +220,17 @@ func TestSendMailUsingConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
func TestAuthMethods(t *testing.T) {
|
||||
config := model.Config{
|
||||
EmailSettings: model.EmailSettings{
|
||||
EnableSMTPAuth: model.NewBool(false),
|
||||
SMTPUsername: "test",
|
||||
SMTPPassword: "fakepass",
|
||||
SMTPServer: "fakeserver",
|
||||
SMTPPort: "25",
|
||||
auth := &authChooser{
|
||||
connectionInfo: &SmtpConnectionInfo{
|
||||
SmtpUsername: "test",
|
||||
SmtpPassword: "fakepass",
|
||||
SmtpServer: "fakeserver",
|
||||
SmtpPort: "25",
|
||||
},
|
||||
}
|
||||
|
||||
auth := &authChooser{Config: &config}
|
||||
tests := []struct {
|
||||
desc string
|
||||
server *smtp.ServerInfo
|
||||
|
||||
Reference in New Issue
Block a user