diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala b/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala index 5efc5221..86663752 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala @@ -123,7 +123,8 @@ object OMail { imapUser: Option[String], imapPassword: Option[Password], imapSsl: SSLType, - imapCertCheck: Boolean + imapCertCheck: Boolean, + imapOAuth2: Boolean ) { def toRecord(accId: AccountId) = @@ -135,7 +136,8 @@ object OMail { imapUser, imapPassword, imapSsl, - imapCertCheck + imapCertCheck, + imapOAuth2 ) } diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 9f70659c..d72425d0 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -3873,6 +3873,7 @@ components: - from - sslType - ignoreCertificates + - useOAuth properties: name: type: string @@ -3891,6 +3892,11 @@ components: type: string ignoreCertificates: type: boolean + useOAuth: + type: boolean + description: | + Use the password as an OAuth2 access token with the + authentication scheme XOAUTH2. CalEventCheckResult: description: | The result of checking a calendar event string. diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/MailSettingsRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/MailSettingsRoutes.scala index d9fc09dc..f71dea29 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/MailSettingsRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/MailSettingsRoutes.scala @@ -166,7 +166,8 @@ object MailSettingsRoutes { ru.imapUser, ru.imapPassword, ru.imapSsl.name, - !ru.imapCertCheck + !ru.imapCertCheck, + ru.imapOAuth2 ) def makeSmtpSettings(ems: EmailSettings): Either[String, OMail.SmtpSettings] = { @@ -203,6 +204,7 @@ object MailSettingsRoutes { ims.imapUser, ims.imapPassword, sslt, - !ims.ignoreCertificates + !ims.ignoreCertificates, + ims.useOAuth ) } diff --git a/modules/store/src/main/resources/db/migration/h2/V1.16.0__add_imap_oauth.sql b/modules/store/src/main/resources/db/migration/h2/V1.16.0__add_imap_oauth.sql new file mode 100644 index 00000000..95b46dbd --- /dev/null +++ b/modules/store/src/main/resources/db/migration/h2/V1.16.0__add_imap_oauth.sql @@ -0,0 +1,7 @@ +ALTER TABLE "userimap" +ADD COLUMN "imap_oauth2" boolean NULL; + +UPDATE "userimap" SET "imap_oauth2" = false; + +ALTER TABLE "userimap" +ALTER COLUMN "imap_oauth2" SET NOT NULL; diff --git a/modules/store/src/main/resources/db/migration/mariadb/V1.16.0__add_imap_oauth.sql b/modules/store/src/main/resources/db/migration/mariadb/V1.16.0__add_imap_oauth.sql new file mode 100644 index 00000000..215c22c7 --- /dev/null +++ b/modules/store/src/main/resources/db/migration/mariadb/V1.16.0__add_imap_oauth.sql @@ -0,0 +1,7 @@ +ALTER TABLE `userimap` +ADD COLUMN (`imap_oauth2` boolean); + +UPDATE `userimap` SET `imap_oauth2` = false; + +ALTER TABLE `userimap` +MODIFY `imap_oauth2` boolean NOT NULL; diff --git a/modules/store/src/main/resources/db/migration/postgresql/V1.16.0__add_imap_oauth.sql b/modules/store/src/main/resources/db/migration/postgresql/V1.16.0__add_imap_oauth.sql new file mode 100644 index 00000000..95b46dbd --- /dev/null +++ b/modules/store/src/main/resources/db/migration/postgresql/V1.16.0__add_imap_oauth.sql @@ -0,0 +1,7 @@ +ALTER TABLE "userimap" +ADD COLUMN "imap_oauth2" boolean NULL; + +UPDATE "userimap" SET "imap_oauth2" = false; + +ALTER TABLE "userimap" +ALTER COLUMN "imap_oauth2" SET NOT NULL; diff --git a/modules/store/src/main/scala/docspell/store/records/RUserImap.scala b/modules/store/src/main/scala/docspell/store/records/RUserImap.scala index d7e9edc0..33ce5689 100644 --- a/modules/store/src/main/scala/docspell/store/records/RUserImap.scala +++ b/modules/store/src/main/scala/docspell/store/records/RUserImap.scala @@ -22,6 +22,7 @@ case class RUserImap( imapPassword: Option[Password], imapSsl: SSLType, imapCertCheck: Boolean, + imapOAuth2: Boolean, created: Timestamp ) { @@ -32,6 +33,7 @@ case class RUserImap( imapUser.getOrElse(""), imapPassword.map(_.pass).getOrElse(""), imapSsl, + imapOAuth2, !imapCertCheck ) } @@ -47,7 +49,8 @@ object RUserImap { imapUser: Option[String], imapPassword: Option[Password], imapSsl: SSLType, - imapCertCheck: Boolean + imapCertCheck: Boolean, + imapOAuth2: Boolean ): F[RUserImap] = for { now <- Timestamp.current[F] @@ -62,6 +65,7 @@ object RUserImap { imapPassword, imapSsl, imapCertCheck, + imapOAuth2, now ) @@ -73,7 +77,8 @@ object RUserImap { imapUser: Option[String], imapPassword: Option[Password], imapSsl: SSLType, - imapCertCheck: Boolean + imapCertCheck: Boolean, + imapOAuth2: Boolean ): OptionT[ConnectionIO, RUserImap] = for { now <- OptionT.liftF(Timestamp.current[ConnectionIO]) @@ -89,6 +94,7 @@ object RUserImap { imapPassword, imapSsl, imapCertCheck, + imapOAuth2, now ) @@ -104,6 +110,7 @@ object RUserImap { val imapPass = Column[Password]("imap_password", this) val imapSsl = Column[SSLType]("imap_ssl", this) val imapCertCheck = Column[Boolean]("imap_certcheck", this) + val imapOAuth2 = Column[Boolean]("imap_oauth2", this) val created = Column[Timestamp]("created", this) val all = NonEmptyList.of[Column[_]]( @@ -116,6 +123,7 @@ object RUserImap { imapPass, imapSsl, imapCertCheck, + imapOAuth2, created ) } @@ -129,7 +137,7 @@ object RUserImap { .insert( t, t.all, - sql"${v.id},${v.uid},${v.name},${v.imapHost},${v.imapPort},${v.imapUser},${v.imapPassword},${v.imapSsl},${v.imapCertCheck},${v.created}" + sql"${v.id},${v.uid},${v.name},${v.imapHost},${v.imapPort},${v.imapUser},${v.imapPassword},${v.imapSsl},${v.imapCertCheck},${v.imapOAuth2},${v.created}" ) } @@ -145,7 +153,8 @@ object RUserImap { t.imapUser.setTo(v.imapUser), t.imapPass.setTo(v.imapPassword), t.imapSsl.setTo(v.imapSsl), - t.imapCertCheck.setTo(v.imapCertCheck) + t.imapCertCheck.setTo(v.imapCertCheck), + t.imapOAuth2.setTo(v.imapOAuth2) ) ) } diff --git a/modules/webapp/src/main/elm/Comp/ImapSettingsForm.elm b/modules/webapp/src/main/elm/Comp/ImapSettingsForm.elm index 5a51188b..8f54c7f6 100644 --- a/modules/webapp/src/main/elm/Comp/ImapSettingsForm.elm +++ b/modules/webapp/src/main/elm/Comp/ImapSettingsForm.elm @@ -32,6 +32,7 @@ type alias Model = , password : Maybe String , sslType : Comp.Dropdown.Model SSLType , ignoreCertificates : Bool + , useOAuthToken : Bool } @@ -58,6 +59,7 @@ emptyModel = , selected = Just Data.SSLType.None } , ignoreCertificates = False + , useOAuthToken = False } @@ -87,6 +89,7 @@ init ems = |> Just } , ignoreCertificates = ems.ignoreCertificates + , useOAuthToken = ems.useOAuth } @@ -104,6 +107,7 @@ getSettings model = |> Maybe.withDefault Data.SSLType.None |> Data.SSLType.toString , ignoreCertificates = model.ignoreCertificates + , useOAuth = model.useOAuthToken } ) @@ -116,6 +120,7 @@ type Msg | PassMsg Comp.PasswordInput.Msg | SSLTypeMsg (Comp.Dropdown.Msg SSLType) | ToggleCheckCert + | ToggleUseOAuth isValid : Model -> Bool @@ -159,14 +164,17 @@ update msg model = ToggleCheckCert -> ( { model | ignoreCertificates = not model.ignoreCertificates }, Cmd.none ) + ToggleUseOAuth -> + ( { model | useOAuthToken = not model.useOAuthToken }, Cmd.none ) + view : UiSettings -> Model -> Html Msg view settings model = div [ classList [ ( "ui form", True ) - , ( "error", not (isValid model) ) - , ( "success", isValid model ) + , ( "info error", not (isValid model) ) + , ( "info success", isValid model ) ] ] [ div [ class "required field" ] @@ -227,6 +235,17 @@ view settings model = , label [] [ text "Ignore certificate check" ] ] ] + , div [ class "inline field" ] + [ div [ class "ui checkbox" ] + [ input + [ type_ "checkbox" + , checked model.useOAuthToken + , onCheck (\_ -> ToggleUseOAuth) + ] + [] + , label [] [ text "Enable OAuth2 authentication using the password as access token" ] + ] + ] ] , div [ class "two fields" ] [ div [ class "field" ]