From 1f9f6053e13beb72f95d22e02670fadc771f686f Mon Sep 17 00:00:00 2001 From: Andrei Kurilov <18027129+akurilov@users.noreply.github.com> Date: Sat, 12 Oct 2024 10:40:43 +0300 Subject: [PATCH] feat: handle internal comm --- .gitignore | 1 + README.md | 4 +- api/smtp/backend.go | 18 +++-- api/smtp/session.go | 37 +++++---- config/config.go | 12 ++- config/config_test.go | 6 +- helm/int-email/templates/deployment.yaml | 18 ++++- helm/int-email/values.yaml | 5 ++ main.go | 15 ++-- service/converter/logging.go | 6 +- service/converter/service.go | 55 ++++++++++---- service/converter/service_test.go | 95 ++++++++++++++++++++++-- service/logging.go | 6 +- service/service.go | 8 +- service/service_test.go | 19 +++-- util/html.go | 4 + 16 files changed, 241 insertions(+), 68 deletions(-) diff --git a/.gitignore b/.gitignore index a82563f..1fcf6d8 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ secret*.yaml cover.tmp key.json .idea +service/converter/emaildata.html diff --git a/README.md b/README.md index 251d58a..4de9573 100644 --- a/README.md +++ b/README.md @@ -21,5 +21,7 @@ kubectl create secret generic gcp-dns-secret --from-file=key.json ``` ```shell -kubectl create secret generic int-email --from-literal=rcpts=rcpt1,rcpt2,... +kubectl create secret generic int-email \ + --from-literal=rcptsPublish=rcpt1,rcpt2 \ + --from-literal=rcptsInternal=rcpt3,rcpt4,... ``` diff --git a/api/smtp/backend.go b/api/smtp/backend.go index 55a5590..83cba32 100644 --- a/api/smtp/backend.go +++ b/api/smtp/backend.go @@ -6,20 +6,22 @@ import ( ) type backend struct { - rcpts map[string]bool - dataLimit int64 - svc service.Service + rcptsPublish map[string]bool + rcptsInternal map[string]bool + dataLimit int64 + svc service.Service } -func NewBackend(rcpts map[string]bool, dataLimit int64, svc service.Service) smtp.Backend { +func NewBackend(rcptsPublish, rcptsInternal map[string]bool, dataLimit int64, svc service.Service) smtp.Backend { return backend{ - rcpts: rcpts, - dataLimit: dataLimit, - svc: svc, + rcptsPublish: rcptsPublish, + rcptsInternal: rcptsInternal, + dataLimit: dataLimit, + svc: svc, } } func (b backend) NewSession(c *smtp.Conn) (s smtp.Session, err error) { - s = newSession(b.rcpts, b.dataLimit, b.svc) + s = newSession(b.rcptsPublish, b.rcptsInternal, b.dataLimit, b.svc) return } diff --git a/api/smtp/session.go b/api/smtp/session.go index edb4265..87a1457 100644 --- a/api/smtp/session.go +++ b/api/smtp/session.go @@ -8,24 +8,28 @@ import ( ) type session struct { - rcptsAllowed map[string]bool - dataLimit int64 - svc service.Service + rcptsPublish map[string]bool + rcptsInternal map[string]bool + dataLimit int64 + svc service.Service // - allowed bool - from string + publish bool + internal bool + from string } -func newSession(rcptsAllowed map[string]bool, dataLimit int64, svc service.Service) smtp.Session { +func newSession(rcptsPublish, rcptsInternal map[string]bool, dataLimit int64, svc service.Service) smtp.Session { return &session{ - rcptsAllowed: rcptsAllowed, - dataLimit: dataLimit, - svc: svc, + rcptsPublish: rcptsPublish, + rcptsInternal: rcptsInternal, + dataLimit: dataLimit, + svc: svc, } } func (s *session) Reset() { - s.allowed = false + s.publish = false + s.internal = false s.from = "" return } @@ -40,17 +44,20 @@ func (s *session) Mail(from string, opts *smtp.MailOptions) (err error) { } func (s *session) Rcpt(to string, opts *smtp.RcptOptions) (err error) { - if s.rcptsAllowed[to] { - s.allowed = true + if s.rcptsPublish[to] { + s.publish = true + } + if s.rcptsInternal[to] { + s.internal = true } return } func (s *session) Data(r io.Reader) (err error) { - switch s.allowed { - case true: + switch { + case s.publish, s.internal: r = io.LimitReader(r, s.dataLimit) - err = s.svc.Submit(context.TODO(), s.from, r) + err = s.svc.Submit(context.TODO(), s.from, s.internal, r) if err != nil { err = &smtp.SMTPError{ Code: 554, diff --git a/config/config.go b/config/config.go index c1c8c33..c9486aa 100644 --- a/config/config.go +++ b/config/config.go @@ -21,8 +21,9 @@ type ApiConfig struct { Limit uint32 `envconfig:"API_SMTP_DATA_LIMIT" default:"1048576" required:"true"` } Recipients struct { - Names []string `envconfig:"API_SMTP_RECIPIENTS_NAMES" required:"true"` - Limit uint16 `envconfig:"API_SMTP_RECIPIENTS_LIMIT" default:"100" required:"true"` + Publish []string `envconfig:"API_SMTP_RECIPIENTS_PUBLISH" required:"true"` + Internal []string `envconfig:"API_SMTP_RECIPIENTS_INTERNAL" required:"true"` + Limit uint16 `envconfig:"API_SMTP_RECIPIENTS_LIMIT" default:"100" required:"true"` } Timeout struct { Read time.Duration `envconfig:"API_SMTP_TIMEOUT_READ" default:"1m" required:"true"` @@ -46,6 +47,7 @@ type ApiConfig struct { Backoff time.Duration `envconfig:"API_WRITER_BACKOFF" default:"10s" required:"true"` BatchSize uint32 `envconfig:"API_WRITER_BATCH_SIZE" default:"16" required:"true"` Cache WriterCacheConfig + Internal WriterInternalConfig Uri string `envconfig:"API_WRITER_URI" default:"resolver:50051" required:"true"` } } @@ -55,6 +57,12 @@ type WriterCacheConfig struct { Ttl time.Duration `envconfig:"API_WRITER_CACHE_TTL" default:"24h" required:"true"` } +type WriterInternalConfig struct { + Name string `envconfig:"API_WRITER_INTERNAL_NAME" default:"awkinternal" required:"true"` + Value int32 `envconfig:"API_WRITER_INTERNAL_VALUE" required:"true"` + RateLimitPerMinute int `envconfig:"API_WRITER_INTERNAL_RATE_LIMIT_PER_MINUTE" default:"1" required:"true"` +} + type ReaderConfig struct { UriEventBase string `envconfig:"API_READER_URI_EVT_BASE" default:"https://awakari.com/pub-msg.html?id=" required:"true"` } diff --git a/config/config_test.go b/config/config_test.go index b9b181a..0d79af6 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -14,7 +14,9 @@ func TestConfig(t *testing.T) { os.Setenv("API_WRITER_BACKOFF", "23h") os.Setenv("API_WRITER_URI", "writer:56789") os.Setenv("LOG_LEVEL", "4") - os.Setenv("API_SMTP_RECIPIENTS_NAMES", "rcpt1,rcpt2") + os.Setenv("API_SMTP_RECIPIENTS_PUBLISH", "rcpt1,rcpt2") + os.Setenv("API_SMTP_RECIPIENTS_INTERNAL", "rcpt3,rcpt4") + os.Setenv("API_WRITER_INTERNAL_VALUE", "123") cfg, err := NewConfigFromEnv() assert.Nil(t, err) assert.Equal(t, 23*time.Hour, cfg.Api.Writer.Backoff) @@ -24,5 +26,5 @@ func TestConfig(t *testing.T) { assert.Equal(t, []string{ "rcpt1", "rcpt2", - }, cfg.Api.Smtp.Recipients.Names) + }, cfg.Api.Smtp.Recipients.Publish) } diff --git a/helm/int-email/templates/deployment.yaml b/helm/int-email/templates/deployment.yaml index a6f62a6..d07801c 100644 --- a/helm/int-email/templates/deployment.yaml +++ b/helm/int-email/templates/deployment.yaml @@ -39,11 +39,16 @@ spec: {{- end }} - name: API_SMTP_DATA_LIMIT value: "{{ .Values.api.smtp.data.limit }}" - - name: API_SMTP_RECIPIENTS_NAMES + - name: API_SMTP_RECIPIENTS_PUBLISH valueFrom: secretKeyRef: name: "{{ include "int-email.fullname" . }}" - key: rcpts + key: rcptsPublish + - name: API_SMTP_RECIPIENTS_INTERNAL + valueFrom: + secretKeyRef: + name: "{{ include "int-email.fullname" . }}" + key: rcptsInternal - name: API_SMTP_RECIPIENTS_LIMIT value: "{{ .Values.api.smtp.rcpt.limit }}" - name: API_SMTP_TIMEOUT_READ @@ -74,6 +79,15 @@ spec: value: "{{ .Values.api.writer.cache.ttl }}" - name: API_WRITER_URI value: "{{ .Values.api.writer.uri }}" + - name: API_WRITER_INTERNAL_NAME + value: "{{ .Values.api.writer.internal.name }}" + - name: API_WRITER_INTERNAL_VALUE + valueFrom: + secretKeyRef: + name: "{{ .Values.api.writer.internal.secret }}" + key: "{{ .Values.api.writer.internal.name }}" + - name: API_WRITER_INTERNAL_RATE_LIMIT_PER_MINUTE + value: "{{ .Values.api.writer.internal.rateLimit.minute }}" - name: API_READER_URI_EVT_BASE value: "{{ .Values.api.reader.uriEvtBase }}" - name: LOG_LEVEL diff --git a/helm/int-email/values.yaml b/helm/int-email/values.yaml index 36d9bdc..d29fe4f 100644 --- a/helm/int-email/values.yaml +++ b/helm/int-email/values.yaml @@ -104,6 +104,11 @@ api: reader: uriEvtBase: "https://awakari.com/pub-msg.html?id=" writer: + internal: + name: "awkinternal" + secret: "resolver-internal-attr-val" + rateLimit: + minute: 1 backoff: "10s" batchSize: 16 cache: diff --git a/main.go b/main.go index 7b25f46..85253d6 100644 --- a/main.go +++ b/main.go @@ -45,17 +45,22 @@ func main() { svcWriter := writer.NewService(clientAwk, cfg.Api.Writer.Backoff, cfg.Api.Writer.Cache, log) svcWriter = writer.NewLogging(svcWriter, log) - svcConv := converter.NewConverter(cfg.Api.EventType.Self, util.HtmlPolicy()) + svcConv := converter.NewConverter(cfg.Api.EventType.Self, util.HtmlPolicy(), cfg.Api.Writer.Internal) svcConv = converter.NewLogging(svcConv, log) svc := service.NewService(svcConv, svcWriter, cfg.Api.Group) svc = service.NewLogging(svc, log) - rcpts := map[string]bool{} - for _, name := range cfg.Api.Smtp.Recipients.Names { + rcptsPublish := map[string]bool{} + for _, name := range cfg.Api.Smtp.Recipients.Publish { rcpt := fmt.Sprintf("%s@%s", name, cfg.Api.Smtp.Host) - rcpts[rcpt] = true + rcptsPublish[rcpt] = true } - b := apiSmtp.NewBackend(rcpts, int64(cfg.Api.Smtp.Data.Limit), svc) + rcptsInternal := map[string]bool{} + for _, name := range cfg.Api.Smtp.Recipients.Internal { + rcpt := fmt.Sprintf("%s@%s", name, cfg.Api.Smtp.Host) + rcptsInternal[rcpt] = true + } + b := apiSmtp.NewBackend(rcptsPublish, rcptsInternal, int64(cfg.Api.Smtp.Data.Limit), svc) b = apiSmtp.NewBackendLogging(b, log) srv := smtp.NewServer(b) diff --git a/service/converter/logging.go b/service/converter/logging.go index e014bb7..95fbe57 100644 --- a/service/converter/logging.go +++ b/service/converter/logging.go @@ -21,8 +21,8 @@ func NewLogging(svc Service, log *slog.Logger) Service { } } -func (l logging) Convert(src io.Reader, dst *pb.CloudEvent) (err error) { - err = l.svc.Convert(src, dst) - l.log.Log(context.TODO(), util.LogLevel(err), fmt.Sprintf("converter.Convert(source=%s, objectUrl=%s, evtId=%s): %s", dst.Source, dst.Attributes[ceKeyObjectUrl], dst.Id, err)) +func (l logging) Convert(src io.Reader, dst *pb.CloudEvent, internal bool) (err error) { + err = l.svc.Convert(src, dst, internal) + l.log.Log(context.TODO(), util.LogLevel(err), fmt.Sprintf("converter.Convert(source=%s, objectUrl=%s, evtId=%s, internal=%t): %s", dst.Source, dst.Attributes[ceKeyObjectUrl], dst.Id, internal, err)) return } diff --git a/service/converter/service.go b/service/converter/service.go index 6bad17b..9757f7b 100644 --- a/service/converter/service.go +++ b/service/converter/service.go @@ -4,23 +4,26 @@ import ( "errors" "fmt" "github.com/PuerkitoBio/goquery" + "github.com/awakari/int-email/config" "github.com/cloudevents/sdk-go/binding/format/protobuf/v2/pb" "github.com/jhillyerd/enmime" "github.com/microcosm-cc/bluemonday" "github.com/segmentio/ksuid" "google.golang.org/protobuf/types/known/timestamppb" "io" + "regexp" "strings" "time" ) type Service interface { - Convert(src io.Reader, dst *pb.CloudEvent) (err error) + Convert(src io.Reader, dst *pb.CloudEvent, internal bool) (err error) } type svc struct { - evtType string - htmlPolicy *bluemonday.Policy + evtType string + htmlPolicy *bluemonday.Policy + writerInternalCfg config.WriterInternalConfig } const ceKeyLenMax = 20 @@ -32,7 +35,6 @@ const ceKeyAttFileNames = "attachmentfilenames" const ceSpecVersion = "1.0" var ErrParse = errors.New("failed to parse message") - var headerBlacklist = map[string]bool{ "bcc": true, "cc": true, @@ -53,27 +55,29 @@ var headerBlacklist = map[string]bool{ "xmailgunvariables": true, "xreceived": true, } +var reUrlTail = regexp.MustCompile(`\?[a-zA-Z0-9_\-]+=[a-zA-Z0-9_\-~.%&/#+]*`) -func NewConverter(evtType string, htmlPolicy *bluemonday.Policy) Service { +func NewConverter(evtType string, htmlPolicy *bluemonday.Policy, writerInternalCfg config.WriterInternalConfig) Service { return svc{ - evtType: evtType, - htmlPolicy: htmlPolicy, + evtType: evtType, + htmlPolicy: htmlPolicy, + writerInternalCfg: writerInternalCfg, } } -func (c svc) Convert(src io.Reader, dst *pb.CloudEvent) (err error) { +func (c svc) Convert(src io.Reader, dst *pb.CloudEvent, internal bool) (err error) { var e *enmime.Envelope e, err = enmime.ReadEnvelope(src) switch err { case nil: - err = c.convert(e, dst) + err = c.convert(e, dst, internal) default: err = fmt.Errorf("%w: %s", ErrParse, err) } return } -func (c svc) convert(src *enmime.Envelope, dst *pb.CloudEvent) (err error) { +func (c svc) convert(src *enmime.Envelope, dst *pb.CloudEvent, internal bool) (err error) { for _, k := range src.GetHeaderKeys() { v := src.GetHeader(k) @@ -102,7 +106,7 @@ func (c svc) convert(src *enmime.Envelope, dst *pb.CloudEvent) (err error) { case "listurl": dst.Source = c.convertAddr(v) default: - if !headerBlacklist[ceKey] && v != "" { + if internal || !headerBlacklist[ceKey] && v != "" { v = c.convertAddr(v) dst.Attributes[ceKey] = &pb.CloudEventAttributeValue{ Attr: &pb.CloudEventAttributeValue_CeString{ @@ -125,8 +129,16 @@ func (c svc) convert(src *enmime.Envelope, dst *pb.CloudEvent) (err error) { if src.HTML != "" { err = c.handleHtml(src.HTML, dst) if err == nil { - dst.Data = &pb.CloudEvent_TextData{ - TextData: c.htmlPolicy.Sanitize(src.HTML), + switch internal { + case true: + dst.Data = &pb.CloudEvent_TextData{ + TextData: src.HTML, + } + default: + txt := reUrlTail.ReplaceAllString(src.HTML, "\"") + dst.Data = &pb.CloudEvent_TextData{ + TextData: c.htmlPolicy.Sanitize(txt), + } } } } @@ -181,6 +193,14 @@ func (c svc) convert(src *enmime.Envelope, dst *pb.CloudEvent) (err error) { } } + if internal { + dst.Attributes[c.writerInternalCfg.Name] = &pb.CloudEventAttributeValue{ + Attr: &pb.CloudEventAttributeValue_CeInteger{ + CeInteger: c.writerInternalCfg.Value, + }, + } + } + return } @@ -200,6 +220,10 @@ func (c svc) convertAddr(src string) (dst string) { dst = dst[:len(dst)-1] } } + urlEnd := strings.Index(dst, "?") + if urlEnd > 0 { + dst = dst[:urlEnd] + } return } @@ -220,11 +244,16 @@ func (c svc) handleHtml(src string, evt *pb.CloudEvent) (err error) { } } if urlOrig != "" { + urlEnd := strings.Index(urlOrig, "?") + if urlEnd > 0 { + urlOrig = urlOrig[:urlEnd] + } evt.Attributes[ceKeyObjectUrl] = &pb.CloudEventAttributeValue{ Attr: &pb.CloudEventAttributeValue_CeUri{ CeUri: urlOrig, }, } + break } } } diff --git a/service/converter/service_test.go b/service/converter/service_test.go index 06252ae..1d47b76 100644 --- a/service/converter/service_test.go +++ b/service/converter/service_test.go @@ -1,21 +1,27 @@ package converter import ( + "fmt" + "github.com/awakari/int-email/config" + "github.com/awakari/int-email/util" "github.com/cloudevents/sdk-go/binding/format/protobuf/v2/pb" "github.com/microcosm-cc/bluemonday" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" "io" "log/slog" + "os" "strings" "testing" ) func TestSvc_Convert(t *testing.T) { cases := map[string]struct { - r io.Reader - out *pb.CloudEvent - err error + r io.Reader + internal bool + out *pb.CloudEvent + err error }{ "empty": { r: strings.NewReader(``), @@ -112,6 +118,65 @@ John`), }, }, }, + "internal": { + internal: true, + r: strings.NewReader(`From: John Doe +To: Jane Smith +Subject: Meeting Notes and Attachment +Date: Thu, 10 Oct 2024 12:34:56 +0000 +Message-ID: +MIME-Version: 1.0 +Content-Type: text/plain; charset="UTF-8" +Content-Transfer-Encoding: 7bit + +Hi Jane, + +Please find attached the meeting notes and presentation slides. + +Best regards, +John`), + out: &pb.CloudEvent{ + Attributes: map[string]*pb.CloudEventAttributeValue{ + "awkinternal": { + Attr: &pb.CloudEventAttributeValue_CeInteger{ + CeInteger: 12345, + }, + }, + "contenttype": { + Attr: &pb.CloudEventAttributeValue_CeString{ + CeString: "text/plain; charset=\"UTF-8\"", + }, + }, + "contenttransferencod": { + Attr: &pb.CloudEventAttributeValue_CeString{ + CeString: "7bit", + }, + }, + "objecturl": { + Attr: &pb.CloudEventAttributeValue_CeUri{ + CeUri: "unique-message-id@example.com", + }, + }, + "mimeversion": { + Attr: &pb.CloudEventAttributeValue_CeString{ + CeString: "1.0", + }, + }, + "subject": { + Attr: &pb.CloudEventAttributeValue_CeString{ + CeString: "Meeting Notes and Attachment", + }, + }, + "time": { + Attr: &pb.CloudEventAttributeValue_CeTimestamp{ + CeTimestamp: ×tamppb.Timestamp{ + Seconds: 1728563696, + }, + }, + }, + }, + }, + }, "invalid date format": { r: strings.NewReader(`From: John Doe To: Jane Smith @@ -130,14 +195,17 @@ John`), err: ErrParse, }, } - conv := NewConverter("com_awakari_email_v1", bluemonday.NewPolicy()) + conv := NewConverter("com_awakari_email_v1", bluemonday.NewPolicy(), config.WriterInternalConfig{ + Name: "awkinternal", + Value: 12345, + }) conv = NewLogging(conv, slog.Default()) for k, c := range cases { t.Run(k, func(t *testing.T) { dst := &pb.CloudEvent{ Attributes: make(map[string]*pb.CloudEventAttributeValue), } - err := conv.Convert(c.r, dst) + err := conv.Convert(c.r, dst, c.internal) if c.err == nil { assert.NotZero(t, dst.Id) for attrK, attrV := range c.out.Attributes { @@ -149,3 +217,20 @@ John`), }) } } + +func Test_handleHtml(t *testing.T) { + if os.Getenv("CI") == "true" { + t.Skip("Skipping test in CI environment") + } + d, err := os.ReadFile("emaildata.html") + require.Nil(t, err) + conv := svc{ + htmlPolicy: util.HtmlPolicy(), + } + evt := &pb.CloudEvent{ + Attributes: make(map[string]*pb.CloudEventAttributeValue), + } + err = conv.handleHtml(string(d), evt) + assert.Nil(t, err) + fmt.Printf("%+v\n", evt.Attributes) +} diff --git a/service/logging.go b/service/logging.go index b217f8c..f1bc28a 100644 --- a/service/logging.go +++ b/service/logging.go @@ -20,8 +20,8 @@ func NewLogging(svc Service, log *slog.Logger) Service { } } -func (l logging) Submit(ctx context.Context, from string, r io.Reader) (err error) { - err = l.svc.Submit(ctx, from, r) - l.log.Log(ctx, util.LogLevel(err), fmt.Sprintf("service.Submit(from=%s): %s", from, err)) +func (l logging) Submit(ctx context.Context, from string, internal bool, r io.Reader) (err error) { + err = l.svc.Submit(ctx, from, internal, r) + l.log.Log(ctx, util.LogLevel(err), fmt.Sprintf("service.Submit(from=%s, internal=%t): %s", from, internal, err)) return } diff --git a/service/service.go b/service/service.go index 7639839..482f97e 100644 --- a/service/service.go +++ b/service/service.go @@ -9,7 +9,7 @@ import ( ) type Service interface { - Submit(ctx context.Context, from string, r io.Reader) (err error) + Submit(ctx context.Context, from string, internal bool, r io.Reader) (err error) } type svc struct { @@ -26,14 +26,14 @@ func NewService(conv converter.Service, writer writer.Service, group string) Ser } } -func (s svc) Submit(ctx context.Context, from string, r io.Reader) (err error) { +func (s svc) Submit(ctx context.Context, from string, internal bool, r io.Reader) (err error) { evt := &pb.CloudEvent{ Source: from, Attributes: make(map[string]*pb.CloudEventAttributeValue), } - err = s.conv.Convert(r, evt) + err = s.conv.Convert(r, evt, internal) if err == nil { - err = s.writer.Write(context.TODO(), evt, s.group, from) + err = s.writer.Write(context.TODO(), evt, s.group, evt.Source) } return } diff --git a/service/service_test.go b/service/service_test.go index d9ed352..45336eb 100644 --- a/service/service_test.go +++ b/service/service_test.go @@ -2,6 +2,7 @@ package service import ( "context" + "github.com/awakari/int-email/config" "github.com/awakari/int-email/service/converter" "github.com/awakari/int-email/service/writer" "github.com/microcosm-cc/bluemonday" @@ -14,9 +15,10 @@ import ( func TestSvc_Submit(t *testing.T) { cases := map[string]struct { - from string - in io.Reader - err error + from string + internal bool + in io.Reader + err error }{ "empty": { in: strings.NewReader(""), @@ -62,14 +64,21 @@ John`), } log := slog.Default() s := NewService( - converter.NewLogging(converter.NewConverter("com_awakari_email_v1", bluemonday.NewPolicy()), log), + converter.NewLogging( + converter.NewConverter( + "com_awakari_email_v1", + bluemonday.NewPolicy(), + config.WriterInternalConfig{}, + ), + log, + ), writer.NewLogging(writer.NewMock(), log), "default", ) s = NewLogging(s, log) for k, c := range cases { t.Run(k, func(t *testing.T) { - err := s.Submit(context.TODO(), c.from, c.in) + err := s.Submit(context.TODO(), c.from, c.internal, c.in) assert.ErrorIs(t, err, c.err) }) } diff --git a/util/html.go b/util/html.go index 0e56ca1..57b5f5e 100644 --- a/util/html.go +++ b/util/html.go @@ -190,5 +190,9 @@ func HtmlPolicy() (p *bluemonday.Policy) { p.AllowImages() + p.RequireNoFollowOnLinks(true) + p.RequireCrossOriginAnonymous(true) + p.RequireNoReferrerOnLinks(true) + return }