aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--commands/msg/invite.go190
-rw-r--r--doc/aerc.1.scd9
-rw-r--r--go.mod4
-rw-r--r--go.sum13
-rw-r--r--lib/calendar/calendar.go201
-rw-r--r--lib/structure_helpers.go16
6 files changed, 432 insertions, 1 deletions
diff --git a/commands/msg/invite.go b/commands/msg/invite.go
new file mode 100644
index 00000000..c15e2653
--- /dev/null
+++ b/commands/msg/invite.go
@@ -0,0 +1,190 @@
+package msg
+
+import (
+ "errors"
+ "fmt"
+ "io"
+
+ "git.sr.ht/~rjarry/aerc/lib"
+ "git.sr.ht/~rjarry/aerc/lib/calendar"
+ "git.sr.ht/~rjarry/aerc/lib/format"
+ "git.sr.ht/~rjarry/aerc/models"
+ "git.sr.ht/~rjarry/aerc/widgets"
+ "github.com/emersion/go-message/mail"
+)
+
+type invite struct{}
+
+func init() {
+ register(invite{})
+}
+
+func (invite) Aliases() []string {
+ return []string{"accept", "accept-tentative", "decline"}
+}
+
+func (invite) Complete(aerc *widgets.Aerc, args []string) []string {
+ return nil
+}
+
+func (invite) Execute(aerc *widgets.Aerc, args []string) error {
+
+ acct := aerc.SelectedAccount()
+ if acct == nil {
+ return errors.New("no account selected")
+ }
+ store := acct.Store()
+ if store == nil {
+ return errors.New("cannot perform action: messages still loading")
+ }
+ msg, err := acct.SelectedMessage()
+ if err != nil {
+ return err
+ }
+
+ part := lib.FindCalendartext(msg.BodyStructure, nil)
+ if part == nil {
+ return fmt.Errorf("no invitation found (missing text/calendar)")
+ }
+
+ subject := trimLocalizedRe(msg.Envelope.Subject)
+ switch args[0] {
+ case "accept":
+ subject = "Accepted: " + subject
+ case "accept-tentative":
+ subject = "Tentatively Accepted: " + subject
+ case "decline":
+ subject = "Declined: " + subject
+ default:
+ return fmt.Errorf("no participation status defined")
+ }
+
+ conf := acct.AccountConfig()
+ from, err := mail.ParseAddress(conf.From)
+ if err != nil {
+ return err
+ }
+ var aliases []*mail.Address
+ if conf.Aliases != "" {
+ aliases, err = mail.ParseAddressList(conf.Aliases)
+ if err != nil {
+ return err
+ }
+ }
+
+ // figure out the sending from address if we have aliases
+ if len(aliases) != 0 {
+ rec := newAddrSet()
+ rec.AddList(msg.Envelope.To)
+ rec.AddList(msg.Envelope.Cc)
+ // test the from first, it has priority over any present alias
+ if rec.Contains(from) {
+ // do nothing
+ } else {
+ for _, a := range aliases {
+ if rec.Contains(a) {
+ from = a
+ break
+ }
+ }
+ }
+ }
+
+ var (
+ to []*mail.Address
+ )
+
+ if len(msg.Envelope.ReplyTo) != 0 {
+ to = msg.Envelope.ReplyTo
+ } else {
+ to = msg.Envelope.From
+ }
+
+ if !aerc.Config().Compose.ReplyToSelf {
+ for i, v := range to {
+ if v.Address == from.Address {
+ to = append(to[:i], to[i+1:]...)
+ break
+ }
+ }
+ if len(to) == 0 {
+ to = msg.Envelope.To
+ }
+ }
+
+ recSet := newAddrSet() // used for de-duping
+ recSet.AddList(to)
+
+ h := &mail.Header{}
+ h.SetAddressList("from", []*mail.Address{from})
+ h.SetSubject(subject)
+ h.SetMsgIDList("in-reply-to", []string{msg.Envelope.MessageId})
+ err = setReferencesHeader(h, msg.RFC822Headers)
+ if err != nil {
+ aerc.PushError(fmt.Sprintf("could not set references: %v", err))
+ }
+ original := models.OriginalMail{
+ From: format.FormatAddresses(msg.Envelope.From),
+ Date: msg.Envelope.Date,
+ RFC822Headers: msg.RFC822Headers,
+ }
+
+ handleInvite := func(reader io.Reader) (*calendar.Reply, error) {
+ cr, err := calendar.CreateReply(reader, from, args[0])
+ if err != nil {
+ return nil, err
+ }
+ for _, org := range cr.Organizers {
+ organizer, err := mail.ParseAddress(org)
+ if err != nil {
+ continue
+ }
+ if !recSet.Contains(organizer) {
+ to = append(to, organizer)
+ }
+ }
+ h.SetAddressList("to", to)
+ return cr, nil
+ }
+
+ addTab := func(cr *calendar.Reply) error {
+ composer, err := widgets.NewComposer(aerc, acct, aerc.Config(),
+ acct.AccountConfig(), acct.Worker(), "", h, original)
+ if err != nil {
+ aerc.PushError("Error: " + err.Error())
+ return err
+ }
+
+ composer.SetContents(cr.PlainText)
+ composer.AppendPart(cr.MimeType, cr.Params, cr.CalendarText)
+ composer.FocusTerminal()
+
+ tab := aerc.NewTab(composer, subject)
+ composer.OnHeaderChange("Subject", func(subject string) {
+ if subject == "" {
+ tab.Name = "New email"
+ } else {
+ tab.Name = subject
+ }
+ tab.Content.Invalidate()
+ })
+
+ composer.OnClose(func(c *widgets.Composer) {
+ if c.Sent() {
+ store.Answered([]uint32{msg.Uid}, true, nil)
+ }
+ })
+
+ return nil
+ }
+
+ store.FetchBodyPart(msg.Uid, part, func(reader io.Reader) {
+ if cr, err := handleInvite(reader); err != nil {
+ aerc.PushError(err.Error())
+ return
+ } else {
+ addTab(cr)
+ }
+ })
+ return nil
+}
diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd
index 0b33cee3..0c071ea3 100644
--- a/doc/aerc.1.scd
+++ b/doc/aerc.1.scd
@@ -112,9 +112,18 @@ message list, the message in the message viewer, etc).
*month*: Messages are stored in folders per year and subfolders per month
+*accept*
+ Accepts an iCalendar meeting invitation.
+
+*accept-tentative*
+ Accepts an iCalendar meeting invitation tentatively.
+
*copy* <target>
Copies the selected message to the target folder.
+*decline*
+ Declines an iCalendar meeting invitation.
+
*delete*
Deletes the selected message.
diff --git a/go.mod b/go.mod
index fac7bb39..923ec074 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.13
require (
git.sr.ht/~sircmpwn/getopt v1.0.0
github.com/ProtonMail/go-crypto v0.0.0-20211221144345-a4f6767435ab
+ github.com/arran4/golang-ical v0.0.0-20220517104411-fd89fefb0182 // indirect
github.com/creack/pty v1.1.17
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964
github.com/ddevault/go-libvterm v0.0.0-20190526194226-b7d861da3810
@@ -32,7 +33,7 @@ require (
github.com/mitchellh/go-homedir v1.1.0
github.com/pkg/errors v0.9.1
github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab
- github.com/stretchr/testify v1.4.0
+ github.com/stretchr/testify v1.7.1
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/zenhack/go.notmuch v0.0.0-20211022191430-4d57e8ad2a8b
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
@@ -42,6 +43,7 @@ require (
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect
+ gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 // indirect
)
replace golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20200420072808-71bec3603bf3
diff --git a/go.sum b/go.sum
index 48315d1c..92d32467 100644
--- a/go.sum
+++ b/go.sum
@@ -40,6 +40,8 @@ github.com/ProtonMail/crypto v0.0.0-20200420072808-71bec3603bf3/go.mod h1:Pxr7w4
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
github.com/ProtonMail/go-crypto v0.0.0-20211221144345-a4f6767435ab h1:5FiL/TCaiKCss/BLMIACDxxadYrx767l9kh0qYX+sLQ=
github.com/ProtonMail/go-crypto v0.0.0-20211221144345-a4f6767435ab/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
+github.com/arran4/golang-ical v0.0.0-20220517104411-fd89fefb0182 h1:mUsKridvWp4dgfkO/QWtgGwuLtZYpjKgsm15JRRik3o=
+github.com/arran4/golang-ical v0.0.0-20220517104411-fd89fefb0182/go.mod h1:BSTTrYHuM12oAL8jDdcmPdw02SBThKYWNFHQlvEG6b0=
github.com/brunnre8/go.notmuch v0.0.0-20201126061756-caa2daf7093c h1:dh58QrW3/S/aCnQPFoeRRE9zMauKooDFd5zh1dLtxXs=
github.com/brunnre8/go.notmuch v0.0.0-20201126061756-caa2daf7093c/go.mod h1:zJtFvR3NinVdmBiLyB4MyXKmqyVfZEb2cK97ISfTgV8=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -49,6 +51,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
@@ -166,6 +169,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kyoh86/xdg v1.2.0 h1:CERuT/ShdTDj+A2UaX3hQ3mOV369+Sj+wyn2nIRIIkI=
github.com/kyoh86/xdg v1.2.0/go.mod h1:/mg8zwu1+qe76oTFUBnyS7rJzk7LLC0VGEzJyJ19DHs=
github.com/lithammer/fuzzysearch v1.1.3 h1:+t5SevHLfi3IHcTx7LT3S+od4OcUmjzxD1xmnvtgG38=
@@ -188,6 +192,7 @@ github.com/miolini/datacounter v1.0.2 h1:mGTL0vqEAtH7mwNJS1JIpd6jwTAP6cBQQ2P8apa
github.com/miolini/datacounter v1.0.2/go.mod h1:C45dc2hBumHjDpEU64IqPwR6TDyPVpzOqqRTN7zmBUA=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -206,6 +211,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -467,10 +475,15 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 h1:dbuHpmKjkDzSOMKAWl10QNlgaZUd3V1q99xc81tt2Kc=
+gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/lib/calendar/calendar.go b/lib/calendar/calendar.go
new file mode 100644
index 00000000..d53aec5b
--- /dev/null
+++ b/lib/calendar/calendar.go
@@ -0,0 +1,201 @@
+package calendar
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net/mail"
+ "regexp"
+ "strings"
+ "time"
+
+ ics "github.com/arran4/golang-ical"
+)
+
+type Reply struct {
+ MimeType string
+ Params map[string]string
+ CalendarText io.ReadWriter
+ PlainText io.ReadWriter
+ Organizers []string
+}
+
+func (cr *Reply) AddOrganizer(o string) {
+ cr.Organizers = append(cr.Organizers, o)
+}
+
+// CreateReply parses a ics request and return a ics reply (RFC 2446, Section 3.2.3)
+func CreateReply(reader io.Reader, from *mail.Address, partstat string) (*Reply, error) {
+
+ cr := Reply{
+ MimeType: "text/calendar",
+ Params: map[string]string{
+ "charset": "UTF-8",
+ "method": "REPLY",
+ },
+ CalendarText: &bytes.Buffer{},
+ PlainText: &bytes.Buffer{},
+ }
+
+ var (
+ status ics.ParticipationStatus
+ action string
+ )
+
+ switch partstat {
+ case "accept":
+ status = ics.ParticipationStatusAccepted
+ action = "accepted"
+ case "accept-tentative":
+ status = ics.ParticipationStatusTentative
+ action = "tentatively accepted"
+ case "decline":
+ status = ics.ParticipationStatusDeclined
+ action = "declined"
+ default:
+ return nil, fmt.Errorf("participation status %s is not implemented", partstat)
+ }
+
+ name := from.Name
+ if name == "" {
+ name = from.Address
+ }
+ fmt.Fprintf(cr.PlainText, "%s has %s this invitation.", name, action)
+
+ invite, err := parse(reader)
+ if err != nil {
+ return nil, err
+ }
+
+ if ok := invite.request(); !ok {
+ return nil, fmt.Errorf("no reply is requested")
+ }
+
+ // update invite as a reply
+ reply := invite
+ reply.SetMethod(ics.MethodReply)
+ reply.SetProductId("aerc")
+
+ // check all events
+ for _, vevent := range reply.Events() {
+ e := event{vevent}
+
+ // check if we should answer
+ if err := e.isReplyRequested(from.Address); err != nil {
+ return nil, err
+ }
+
+ // make sure we send our reply to the meeting organizer
+ if organizer := e.GetProperty(ics.ComponentPropertyOrganizer); organizer != nil {
+ cr.AddOrganizer(organizer.Value)
+ }
+
+ // update attendee participation status
+ e.updateAttendees(status, from.Address)
+
+ // update timestamp
+ e.SetDtStampTime(time.Now())
+
+ // remove any subcomponents of event
+ e.Components = nil
+ }
+
+ // keep only timezone and event components
+ reply.clean()
+
+ if len(reply.Events()) == 0 {
+ return nil, fmt.Errorf("no events to respond to")
+ }
+
+ if err := reply.SerializeTo(cr.CalendarText); err != nil {
+ return nil, err
+ }
+ return &cr, nil
+}
+
+type calendar struct {
+ *ics.Calendar
+}
+
+func parse(reader io.Reader) (*calendar, error) {
+ // fix capitalized mailto for parsing of ics file
+ var sb strings.Builder
+ io.Copy(&sb, reader)
+ re := regexp.MustCompile("MAILTO:(.+@)")
+ str := re.ReplaceAllString(sb.String(), "mailto:${1}")
+
+ // parse calendar
+ invite, err := ics.ParseCalendar(strings.NewReader(str))
+ if err != nil {
+ return nil, err
+ }
+ return &calendar{invite}, nil
+}
+
+func (cal *calendar) request() (ok bool) {
+ ok = false
+ for i := range cal.CalendarProperties {
+ if cal.CalendarProperties[i].IANAToken == string(ics.PropertyMethod) {
+ if cal.CalendarProperties[i].Value == string(ics.MethodRequest) {
+ ok = true
+ return
+ }
+ }
+ }
+ return
+}
+
+func (cal *calendar) clean() {
+ var clean []ics.Component
+ for _, comp := range cal.Components {
+ switch comp.(type) {
+ case *ics.VTimezone, *ics.VEvent:
+ clean = append(clean, comp)
+ default:
+ continue
+ }
+ }
+ cal.Components = clean
+}
+
+type event struct {
+ *ics.VEvent
+}
+
+func (e *event) isReplyRequested(from string) error {
+ var present bool = false
+ var rsvp bool = false
+ for _, a := range e.Attendees() {
+ if a.Email() == from {
+ present = true
+ if r, ok := a.ICalParameters[string(ics.ParameterRsvp)]; ok {
+ if len(r) > 0 && strings.ToLower(r[0]) == "true" {
+ rsvp = true
+ }
+ }
+ }
+ }
+ if !present {
+ return fmt.Errorf("we are not invited")
+ }
+ if !rsvp {
+ return fmt.Errorf("we don't have to rsvp")
+ }
+ return nil
+}
+
+func (e *event) updateAttendees(status ics.ParticipationStatus, from string) {
+ var clean []ics.IANAProperty
+ for _, prop := range e.Properties {
+ if prop.IANAToken == string(ics.ComponentPropertyAttendee) {
+ att := ics.Attendee{prop}
+ if att.Email() != from {
+ continue
+ }
+ prop.ICalParameters[string(ics.ParameterParticipationStatus)] = []string{string(status)}
+ delete(prop.ICalParameters, string(ics.ParameterRsvp))
+ }
+ clean = append(clean, prop)
+ }
+ e.Properties = clean
+}
diff --git a/lib/structure_helpers.go b/lib/structure_helpers.go
index 95719dd7..ac6950af 100644
--- a/lib/structure_helpers.go
+++ b/lib/structure_helpers.go
@@ -22,6 +22,22 @@ func FindPlaintext(bs *models.BodyStructure, path []int) []int {
return nil
}
+func FindCalendartext(bs *models.BodyStructure, path []int) []int {
+ for i, part := range bs.Parts {
+ cur := append(path, i+1)
+ if strings.ToLower(part.MIMEType) == "text" &&
+ strings.ToLower(part.MIMESubType) == "calendar" {
+ return cur
+ }
+ if strings.ToLower(part.MIMEType) == "multipart" {
+ if path := FindCalendartext(part, cur); path != nil {
+ return path
+ }
+ }
+ }
+ return nil
+}
+
func FindFirstNonMultipart(bs *models.BodyStructure, path []int) []int {
for i, part := range bs.Parts {
cur := append(path, i+1)