Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: html emails #1987

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitmodules

This file was deleted.

2 changes: 1 addition & 1 deletion .java-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
14.0
17.0
1 change: 1 addition & 0 deletions backend/api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ apply plugin: 'io.spring.dependency-management'

repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}

kotlin {
Expand Down
1 change: 1 addition & 0 deletions backend/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ apply plugin: "org.gradle.test-retry"

repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}

dependencyManagement {
Expand Down
3 changes: 2 additions & 1 deletion backend/app/src/main/kotlin/io/tolgee/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.domain.EntityScan
import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.data.jpa.repository.config.EnableJpaAuditing
import org.springframework.data.jpa.repository.config.EnableJpaRepositories

@SpringBootApplication(
scanBasePackages = ["io.tolgee"],
exclude = [LdapAutoConfiguration::class],
exclude = [LdapAutoConfiguration::class, ThymeleafAutoConfiguration::class],
)
@EnableJpaAuditing
@EntityScan("io.tolgee.model")
Expand Down
19 changes: 11 additions & 8 deletions backend/data/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ apply plugin: 'org.hibernate.orm'

repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}

idea {
Expand All @@ -70,6 +71,7 @@ allOpen {
annotation("org.springframework.beans.factory.annotation.Configurable")
}

apply from: "$rootDir/gradle/email.gradle"
apply from: "$rootDir/gradle/liquibase.gradle"

configureLiquibase("public", "hibernate:spring:io.tolgee", 'src/main/resources/db/changelog/schema.xml')
Expand Down Expand Up @@ -97,6 +99,10 @@ dependencies {
implementation "org.springframework.boot:spring-boot-configuration-processor"
implementation "org.springframework.boot:spring-boot-starter-batch"
implementation "org.springframework.boot:spring-boot-starter-websocket"
implementation "org.springframework.boot:spring-boot-starter-thymeleaf"
implementation "org.springframework.boot:spring-boot-starter-thymeleaf"

implementation 'com.github.transferwise:spring-icu:0.3.0'

/**
* DB
Expand Down Expand Up @@ -169,6 +175,7 @@ dependencies {
implementation("org.apache.commons:commons-configuration2:2.10.1")
implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jacksonVersion"
implementation("com.opencsv:opencsv:5.9")
implementation 'ognl:ognl:3.3.4'

/**
* Google translation API
Expand Down Expand Up @@ -230,18 +237,14 @@ tasks.named('compileJava') {
inputs.files(tasks.named('processResources'))
}

tasks.named('compileKotlin') {
dependsOn 'buildEmails'
}

ktlint {
debug = true
verbose = true
filter {
exclude("**/PluralData.kt")
}
}

hibernate {
enhancement {
enableDirtyTracking = false
enableAssociationManagement = false
enableExtendedEnhancement = false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Copyright (C) 2024 Tolgee s.r.o. and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.tolgee.configuration

import io.hypersistence.utils.hibernate.query.QueryStackTraceLogger
import org.hibernate.cfg.AvailableSettings
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Component

@Component
@Profile("hibernate-trace")
class HibernateConfig : HibernatePropertiesCustomizer {
override fun customize(hibernateProperties: MutableMap<String, Any>) {
hibernateProperties[AvailableSettings.STATEMENT_INSPECTOR] = QueryStackTraceLogger("io.tolgee")
}
}
85 changes: 85 additions & 0 deletions backend/data/src/main/kotlin/io/tolgee/email/EmailService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Copyright (C) 2024 Tolgee s.r.o. and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.tolgee.email

import io.tolgee.configuration.tolgee.SmtpProperties
import io.tolgee.dtos.misc.EmailAttachment
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.mail.javamail.JavaMailSender
import org.springframework.mail.javamail.MimeMessageHelper
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Service
import org.thymeleaf.TemplateEngine
import org.thymeleaf.context.Context
import java.util.*

@Service
class EmailService(
private val smtpProperties: SmtpProperties,
private val mailSender: JavaMailSender,
@Qualifier("emailTemplateEngine") private val templateEngine: TemplateEngine,
) {
private val smtpFrom
get() =
smtpProperties.from
?: throw IllegalStateException(
"SMTP sender is not configured. " +
"See https://docs.tolgee.io/platform/self_hosting/configuration#smtp",
)

@Async
fun sendEmailTemplate(
recipient: String,
template: String,
locale: Locale,
properties: Map<String, Any> = mapOf(),
attachments: List<EmailAttachment> = listOf(),
) {
val context = Context(locale, properties)
val html = templateEngine.process(template, context)
val subject = extractEmailTitle(html)

sendEmail(recipient, subject, html, attachments)
}

@Async
fun sendEmail(
recipient: String,
subject: String,
contents: String,
attachments: List<EmailAttachment> = listOf(),
) {
val message = mailSender.createMimeMessage()
val helper = MimeMessageHelper(message, MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED, "UTF8")

helper.setFrom(smtpFrom)
helper.setTo(recipient)
helper.setSubject(subject)
helper.setText(contents, true)
attachments.forEach { helper.addAttachment(it.name, it.inputStreamSource) }

mailSender.send(message)
}

private fun extractEmailTitle(html: String): String {
return REGEX_TITLE.find(html)!!.groupValues[1]
}

companion object {
private val REGEX_TITLE = Regex("<title>(.+?)</title>")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Copyright (C) 2024 Tolgee s.r.o. and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.tolgee.email

import com.transferwise.icu.ICUMessageSource
import com.transferwise.icu.ICUReloadableResourceBundleMessageSource
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.context.MessageSource
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.thymeleaf.TemplateEngine
import org.thymeleaf.spring6.SpringTemplateEngine
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver
import org.thymeleaf.templateresolver.ITemplateResolver
import java.util.*

@Configuration
class EmailTemplateConfig {
@Bean("emailTemplateResolver")
fun templateResolver(): ClassLoaderTemplateResolver {
val templateResolver = ClassLoaderTemplateResolver()
templateResolver.characterEncoding = "UTF-8"
templateResolver.prefix = "/email-templates/"
templateResolver.suffix = ".html"
return templateResolver
}

@Bean("emailMessageSource")
fun messageSource(): ICUMessageSource {
val messageSource = ICUReloadableResourceBundleMessageSource()
messageSource.setBasenames("email-i18n/messages", "email-i18n-test/messages")
messageSource.setDefaultEncoding("UTF-8")
messageSource.setDefaultLocale(Locale.ENGLISH)
return messageSource
}

@Bean("emailTemplateEngine")
fun templateEngine(
@Qualifier("emailTemplateResolver") templateResolver: ITemplateResolver,
@Qualifier("emailMessageSource") messageSource: MessageSource,
): TemplateEngine {
val templateEngine = SpringTemplateEngine()
templateEngine.enableSpringELCompiler = true
templateEngine.templateResolvers = setOf(templateResolver)
templateEngine.setTemplateEngineMessageSource(messageSource)
return templateEngine
}
}
1 change: 1 addition & 0 deletions backend/data/src/main/resources/email-i18n
1 change: 1 addition & 0 deletions backend/data/src/main/resources/email-templates
Loading
Loading