新增:发送通道`电子邮箱`支持`S/MIME`或`OpenPGP`加密 #417
This commit is contained in:
parent
8cefd5fded
commit
75b356246c
|
@ -25,6 +25,17 @@ if (isNeedPackage.toBoolean() && isUseBooster.toBoolean()) {
|
|||
}
|
||||
|
||||
android {
|
||||
// 禁用过时 API 警告
|
||||
configure(allprojects) {
|
||||
gradle.projectsEvaluated {
|
||||
tasks.withType(JavaCompile).tap {
|
||||
configureEach {
|
||||
options.compilerArgs << "-Xlint:-removal"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildToolsVersion build_versions.build_tools
|
||||
compileSdkVersion build_versions.target_sdk
|
||||
|
||||
|
@ -162,10 +173,15 @@ android {
|
|||
exclude 'lib/x86/libgojni.so'
|
||||
exclude 'lib/x86_64/libgojni.so'
|
||||
}
|
||||
jniLibs {
|
||||
excludes += ["kotlin/**"]
|
||||
}
|
||||
resources {
|
||||
merge 'META-INF/mailcap'
|
||||
pickFirst 'META-INF/LICENSE.md'
|
||||
pickFirst 'META-INF/NOTICE.md'
|
||||
excludes += ['META-INF/DEPENDENCIES.txt', 'META-INF/LICENSE.txt', 'META-INF/NOTICE.txt', 'META-INF/NOTICE', 'META-INF/LICENSE', 'META-INF/DEPENDENCIES', 'META-INF/notice.txt', 'META-INF/license.txt', 'META-INF/dependencies.txt', 'META-INF/LGPL2.1']
|
||||
excludes += ["META-INF/*.kotlin_module", "META-INF/*.version", "kotlin/**", "DebugProbesKt.bin"]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -208,6 +224,7 @@ android {
|
|||
}
|
||||
namespace 'com.idormy.sms.forwarder'
|
||||
|
||||
if (isNeedClean.toBoolean()) {
|
||||
//编译前清理项目缓存
|
||||
preBuild.dependsOn clean
|
||||
//编译后清理垃圾文件
|
||||
|
@ -225,6 +242,7 @@ android {
|
|||
println "Build failed, cleanTxt not executed."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -305,8 +323,11 @@ dependencies {
|
|||
//implementation 'com.github.tiagohm.MarkdownView:emoji:0.19.0'
|
||||
|
||||
def retrofit2_version = '2.9.0'
|
||||
//noinspection GradleDependency
|
||||
implementation "com.squareup.retrofit2:retrofit:$retrofit2_version"
|
||||
//noinspection GradleDependency
|
||||
implementation "com.squareup.retrofit2:converter-gson:$retrofit2_version"
|
||||
//noinspection GradleDependency
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit2_version"
|
||||
|
||||
def paging_version = "3.1.1"
|
||||
|
@ -325,6 +346,18 @@ dependencies {
|
|||
implementation "com.sun.mail:android-mail:$mail_version"
|
||||
implementation "com.sun.mail:android-activation:$mail_version"
|
||||
|
||||
//国密算法SM4 的JAVA实现(基于BC实现)
|
||||
def bouncycastle_version = '1.77'
|
||||
api "org.bouncycastle:bcprov-jdk18on:$bouncycastle_version"
|
||||
//邮件 S/MIME 加密和签名
|
||||
//implementation "org.spongycastle:bcmail-jdk18on:$bouncycastle_version" //Android下报错
|
||||
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastle_version"
|
||||
//implementation "org.bouncycastle:bctls-jdk18on:$bouncycastle_version"
|
||||
//邮件 PGP 加密和签名
|
||||
//implementation "org.bouncycastle:bcpg-jdk18on:$bouncycastle_version" //Thunderbird无法解密
|
||||
//PGPainless: https://github.com/pgpainless/pgpainless
|
||||
implementation 'org.pgpainless:pgpainless-core:1.6.7'
|
||||
|
||||
//Android Keep Alive(安卓保活),Cactus 集成双进程前台服务,JobScheduler,onePix(一像素),WorkManager,无声音乐
|
||||
//https://github.com/gyf-dev/Cactus
|
||||
implementation 'com.gyf.cactus:cactus:1.1.3-beta13'
|
||||
|
@ -333,9 +366,6 @@ dependencies {
|
|||
implementation 'cn.ppps.andserver:api:2.1.12'
|
||||
kapt 'cn.ppps.andserver:processor:2.1.12'
|
||||
|
||||
//国密算法SM4 的JAVA实现(基于BC实现)
|
||||
api 'org.bouncycastle:bcprov-jdk15on:1.70'
|
||||
|
||||
//Location 是一个通过 Android 自带的 LocationManager 来实现的定位功能:https://github.com/jenly1314/Location
|
||||
implementation 'com.github.pppscn:location:1.0.0'
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.idormy.sms.forwarder.entity.setting
|
||||
|
||||
import com.idormy.sms.forwarder.R
|
||||
import java.io.Serializable
|
||||
|
||||
data class EmailSetting(
|
||||
|
@ -11,6 +12,19 @@ data class EmailSetting(
|
|||
var port: String? = "",
|
||||
var ssl: Boolean? = false,
|
||||
var startTls: Boolean? = false,
|
||||
var toEmail: String? = "",
|
||||
var title: String? = "",
|
||||
) : Serializable
|
||||
var recipients: MutableMap<String, Pair<String, String>> = mutableMapOf(),
|
||||
var toEmail: String? = "",
|
||||
var keystore: String? = "",
|
||||
var password: String? = "",
|
||||
var encryptionProtocol: String = "Plain", //加密协议: S/MIME、OpenPGP、Plain(不传证书)
|
||||
) : Serializable {
|
||||
|
||||
fun getEncryptionProtocolCheckId(): Int {
|
||||
return when (encryptionProtocol) {
|
||||
"S/MIME" -> R.id.rb_encryption_protocol_smime
|
||||
"OpenPGP" -> R.id.rb_encryption_protocol_openpgp
|
||||
else -> R.id.rb_encryption_protocol_plain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
package com.idormy.sms.forwarder.fragment.senders
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Environment
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.gson.Gson
|
||||
import com.hjq.permissions.OnPermissionCallback
|
||||
import com.hjq.permissions.Permission
|
||||
import com.hjq.permissions.XXPermissions
|
||||
import com.idormy.sms.forwarder.R
|
||||
import com.idormy.sms.forwarder.core.BaseFragment
|
||||
import com.idormy.sms.forwarder.core.Core
|
||||
|
@ -41,6 +49,13 @@ import io.reactivex.SingleObserver
|
|||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.pgpainless.PGPainless
|
||||
import org.pgpainless.key.info.KeyRingInfo
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.Date
|
||||
|
||||
@Page(name = "Email")
|
||||
|
@ -52,6 +67,11 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
|
|||
private val viewModel by viewModels<SenderViewModel> { BaseViewModelFactory(context) }
|
||||
private var mCountDownHelper: CountDownButtonHelper? = null
|
||||
private var mailType: String = getString(R.string.other_mail_type) //邮箱类型
|
||||
private var recipientItemMap: MutableMap<Int, LinearLayout> = mutableMapOf()
|
||||
private val downloadPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path
|
||||
|
||||
//加密协议: S/MIME、OpenPGP、Plain(不传证书)
|
||||
private var encryptionProtocol: String = "Plain"
|
||||
|
||||
@JvmField
|
||||
@AutoWired(name = KEY_SENDER_ID)
|
||||
|
@ -98,7 +118,6 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
|
|||
})
|
||||
|
||||
val mailTypeArray = getStringArray(R.array.MailType)
|
||||
Log.d(TAG, mailTypeArray.toString())
|
||||
binding!!.spMailType.setOnItemSelectedListener { _: MaterialSpinner?, position: Int, _: Long, item: Any ->
|
||||
mailType = item.toString()
|
||||
//XToastUtils.warning(mailType)
|
||||
|
@ -112,6 +131,39 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
|
|||
binding!!.spMailType.selectedIndex = mailTypeArray.size - 1
|
||||
binding!!.layoutServiceSetting.visibility = View.VISIBLE
|
||||
|
||||
binding!!.rgEncryptionProtocol.setOnCheckedChangeListener { _, checkedId ->
|
||||
when (checkedId) {
|
||||
R.id.rb_encryption_protocol_smime -> {
|
||||
encryptionProtocol = "S/MIME"
|
||||
binding!!.layoutSenderKeystore.visibility = View.VISIBLE
|
||||
binding!!.tvSenderKeystore.text = getString(R.string.sender_smime_keystore)
|
||||
binding!!.tvEmailTo.text = getString(R.string.email_to_smime)
|
||||
binding!!.tvEmailToTips.text = getString(R.string.email_to_smime_tips)
|
||||
}
|
||||
|
||||
R.id.rb_encryption_protocol_openpgp -> {
|
||||
encryptionProtocol = "OpenPGP"
|
||||
binding!!.layoutSenderKeystore.visibility = View.VISIBLE
|
||||
binding!!.tvSenderKeystore.text = getString(R.string.sender_openpgp_keystore)
|
||||
binding!!.tvEmailTo.text = getString(R.string.email_to_openpgp)
|
||||
binding!!.tvEmailToTips.text = getString(R.string.email_to_openpgp_tips)
|
||||
}
|
||||
|
||||
else -> {
|
||||
encryptionProtocol = "Plain"
|
||||
binding!!.layoutSenderKeystore.visibility = View.GONE
|
||||
binding!!.tvEmailTo.text = getString(R.string.email_to)
|
||||
binding!!.tvEmailToTips.text = getString(R.string.email_to_tips)
|
||||
}
|
||||
}
|
||||
|
||||
//遍历 layout_recipients 子元素,设置 layout_recipient_keystore 可见性
|
||||
for (recipientItem in recipientItemMap.values) {
|
||||
val layoutRecipientKeystore = recipientItem.findViewById<LinearLayout>(R.id.layout_recipient_keystore)
|
||||
layoutRecipientKeystore.visibility = if (encryptionProtocol == "Plain") View.GONE else View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
//新增
|
||||
if (senderId <= 0) {
|
||||
titleBar?.setSubTitle(getString(R.string.add_sender))
|
||||
|
@ -143,6 +195,10 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
|
|||
if (settingVo != null) {
|
||||
if (!TextUtils.isEmpty(settingVo.mailType)) {
|
||||
mailType = settingVo.mailType.toString()
|
||||
//TODO: 替换mailType为当前语言,避免切换语言后失效,历史包袱怎么替换比较优雅?
|
||||
if (mailType == "other" || mailType == "其他邮箱" || mailType == "其他郵箱") {
|
||||
mailType = getString(R.string.other_mail_type)
|
||||
}
|
||||
binding!!.spMailType.setSelectedItem(mailType)
|
||||
if (mailType != getString(R.string.other_mail_type)) {
|
||||
binding!!.layoutServiceSetting.visibility = View.GONE
|
||||
|
@ -155,8 +211,24 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
|
|||
binding!!.etPort.setText(settingVo.port)
|
||||
binding!!.sbSsl.isChecked = settingVo.ssl == true
|
||||
binding!!.sbStartTls.isChecked = settingVo.startTls == true
|
||||
binding!!.etToEmail.setText(settingVo.toEmail)
|
||||
binding!!.etTitleTemplate.setText(settingVo.title)
|
||||
encryptionProtocol = settingVo.encryptionProtocol
|
||||
binding!!.rgEncryptionProtocol.check(settingVo.getEncryptionProtocolCheckId())
|
||||
if (settingVo.recipients.isNotEmpty()) {
|
||||
for ((email, cert) in settingVo.recipients) {
|
||||
addRecipientItem(email, cert)
|
||||
}
|
||||
} else {
|
||||
//兼容旧版本
|
||||
val emails = settingVo.toEmail?.split(",")
|
||||
if (!emails.isNullOrEmpty()) {
|
||||
for (email in emails.toTypedArray()) {
|
||||
addRecipientItem(email)
|
||||
}
|
||||
}
|
||||
}
|
||||
binding!!.etSenderKeystore.setText(settingVo.keystore)
|
||||
binding!!.etSenderPassword.setText(settingVo.password)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -174,6 +246,12 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
|
|||
binding!!.btnTest.setOnClickListener(this)
|
||||
binding!!.btnDel.setOnClickListener(this)
|
||||
binding!!.btnSave.setOnClickListener(this)
|
||||
binding!!.btnAddRecipient.setOnClickListener {
|
||||
addRecipientItem()
|
||||
}
|
||||
binding!!.btnSenderKeystorePicker.setOnClickListener {
|
||||
pickCert(binding!!.etSenderKeystore)
|
||||
}
|
||||
LiveEventBus.get(KEY_SENDER_TEST, String::class.java).observe(this) { mCountDownHelper?.finish() }
|
||||
}
|
||||
|
||||
|
@ -284,21 +362,256 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
|
|||
private fun checkSetting(): EmailSetting {
|
||||
val fromEmail = binding!!.etFromEmail.text.toString().trim()
|
||||
val pwd = binding!!.etPwd.text.toString().trim()
|
||||
val nickname = binding!!.etNickname.text.toString().trim()
|
||||
val host = binding!!.etHost.text.toString().trim()
|
||||
val port = binding!!.etPort.text.toString().trim()
|
||||
val ssl = binding!!.sbSsl.isChecked
|
||||
val startTls = binding!!.sbStartTls.isChecked
|
||||
val toEmail = binding!!.etToEmail.text.toString().trim()
|
||||
val title = binding!!.etTitleTemplate.text.toString().trim()
|
||||
if (TextUtils.isEmpty(fromEmail) || TextUtils.isEmpty(pwd) || TextUtils.isEmpty(toEmail)) {
|
||||
val recipients = getRecipientsFromRecipientItemMap()
|
||||
if (TextUtils.isEmpty(fromEmail) || TextUtils.isEmpty(pwd) || recipients.isEmpty()) {
|
||||
throw Exception(getString(R.string.invalid_email))
|
||||
}
|
||||
for ((email, cert) in recipients) {
|
||||
if (!CommonUtils.checkEmail(email)) {
|
||||
throw Exception(String.format(getString(R.string.invalid_recipient_email), email))
|
||||
}
|
||||
Log.d(TAG, "email: $email, cert: $cert")
|
||||
when (encryptionProtocol) {
|
||||
"S/MIME" -> {
|
||||
when {
|
||||
cert.first.isNotEmpty() && cert.second.isNotEmpty() -> {
|
||||
try {
|
||||
// 判断是否有效的PKCS12私钥证书
|
||||
val keyStore = KeyStore.getInstance("PKCS12")
|
||||
keyStore.load(FileInputStream(cert.first), cert.second.toCharArray())
|
||||
val alias = keyStore.aliases().nextElement()
|
||||
val recipientPublicKey = keyStore.getCertificate(alias) as X509Certificate
|
||||
Log.d(TAG, "PKCS12 Certificate: $recipientPublicKey")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
throw Exception(String.format(getString(R.string.invalid_pkcs12_certificate), email))
|
||||
}
|
||||
}
|
||||
|
||||
cert.first.isNotEmpty() && cert.second.isEmpty() -> {
|
||||
try {
|
||||
// 判断是否有效的X.509公钥证书
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
val fileInputStream = FileInputStream(cert.first)
|
||||
val recipientPublicKey = certFactory.generateCertificate(fileInputStream) as X509Certificate
|
||||
Log.d(TAG, "X.509 Certificate: $recipientPublicKey")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
throw Exception(String.format(getString(R.string.invalid_x509_certificate), email))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"OpenPGP" -> {
|
||||
when {
|
||||
cert.first.isNotEmpty() && cert.second.isNotEmpty() -> {
|
||||
try {
|
||||
//从私钥证书文件提取公钥
|
||||
val recipientPrivateKeyStream = FileInputStream(cert.first)
|
||||
val recipientPGPSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(recipientPrivateKeyStream)
|
||||
val recipientPGPPublicKeyRing = PGPainless.extractCertificate(recipientPGPSecretKeyRing!!)
|
||||
val keyInfo = KeyRingInfo(recipientPGPPublicKeyRing)
|
||||
Log.d(TAG, "recipientPGPPublicKeyRing: $keyInfo")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
throw Exception(String.format(getString(R.string.invalid_x509_certificate), email))
|
||||
}
|
||||
}
|
||||
|
||||
cert.first.isNotEmpty() && cert.second.isEmpty() -> {
|
||||
try {
|
||||
//从证书文件提取公钥
|
||||
val recipientPublicKeyStream = FileInputStream(cert.first)
|
||||
val recipientPGPPublicKeyRing = PGPainless.readKeyRing().publicKeyRing(recipientPublicKeyStream)
|
||||
val keyInfo = KeyRingInfo(recipientPGPPublicKeyRing!!)
|
||||
Log.d(TAG, "recipientPGPPublicKeyRing: $keyInfo")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
throw Exception(String.format(getString(R.string.invalid_x509_certificate), email))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val host = binding!!.etHost.text.toString().trim()
|
||||
val port = binding!!.etPort.text.toString().trim()
|
||||
if (mailType == getString(R.string.other_mail_type) && (TextUtils.isEmpty(host) || TextUtils.isEmpty(port))) {
|
||||
throw Exception(getString(R.string.invalid_email_server))
|
||||
}
|
||||
|
||||
return EmailSetting(mailType, fromEmail, pwd, nickname, host, port, ssl, startTls, toEmail, title)
|
||||
val nickname = binding!!.etNickname.text.toString().trim()
|
||||
val ssl = binding!!.sbSsl.isChecked
|
||||
val startTls = binding!!.sbStartTls.isChecked
|
||||
val title = binding!!.etTitleTemplate.text.toString().trim()
|
||||
val keystore = binding!!.etSenderKeystore.text.toString().trim()
|
||||
val password = binding!!.etSenderPassword.text.toString().trim()
|
||||
if (keystore.isNotEmpty()) {
|
||||
val senderPrivateKeyStream = FileInputStream(keystore)
|
||||
if (senderPrivateKeyStream.available() <= 0) {
|
||||
throw Exception(getString(R.string.invalid_sender_keystore))
|
||||
}
|
||||
when (encryptionProtocol) {
|
||||
"S/MIME" -> {
|
||||
try {
|
||||
// 判断是否有效的PKCS12私钥证书
|
||||
val keyStore = KeyStore.getInstance("PKCS12")
|
||||
keyStore.load(senderPrivateKeyStream, password.toCharArray())
|
||||
val alias = keyStore.aliases().nextElement()
|
||||
val certificate = keyStore.getCertificate(alias) as X509Certificate
|
||||
Log.d(TAG, "PKCS12 Certificate: $certificate")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
throw Exception(getString(R.string.invalid_sender_keystore))
|
||||
}
|
||||
}
|
||||
|
||||
"OpenPGP" -> {
|
||||
try {
|
||||
val senderPGPSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(senderPrivateKeyStream)
|
||||
val keyInfo = KeyRingInfo(senderPGPSecretKeyRing!!)
|
||||
Log.d(TAG, "senderPGPSecretKeyRing: $keyInfo")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
throw Exception(getString(R.string.invalid_sender_keystore))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return EmailSetting(mailType, fromEmail, pwd, nickname, host, port, ssl, startTls, title, recipients, "", keystore, password, encryptionProtocol)
|
||||
}
|
||||
|
||||
//recipient序号
|
||||
private var recipientItemId = 0
|
||||
|
||||
/**
|
||||
* 动态增删recipient
|
||||
*
|
||||
* @param email recipient的email
|
||||
* @param cert recipient的cert,为空则不设置
|
||||
*/
|
||||
private fun addRecipientItem(email: String = "", cert: Any? = null) {
|
||||
val itemAddRecipient = View.inflate(requireContext(), R.layout.item_add_recipient, null) as LinearLayout
|
||||
val etRecipientEmail = itemAddRecipient.findViewById<EditText>(R.id.et_recipient_email)
|
||||
val etRecipientKeystore = itemAddRecipient.findViewById<EditText>(R.id.et_recipient_keystore)
|
||||
val etRecipientPassword = itemAddRecipient.findViewById<EditText>(R.id.et_recipient_password)
|
||||
etRecipientEmail.setText(email)
|
||||
Log.d(TAG, "cert: $cert")
|
||||
when (cert) {
|
||||
is String -> etRecipientKeystore.setText(cert)
|
||||
is Pair<*, *> -> {
|
||||
Log.d(TAG, "cert.first: ${cert.first}")
|
||||
Log.d(TAG, "cert.second: ${cert.second}")
|
||||
etRecipientKeystore.setText(cert.first.toString())
|
||||
etRecipientPassword.setText(cert.second.toString())
|
||||
}
|
||||
}
|
||||
|
||||
val ivDel = itemAddRecipient.findViewById<ImageView>(R.id.iv_del)
|
||||
ivDel.tag = recipientItemId
|
||||
ivDel.setOnClickListener {
|
||||
val itemId = it.tag as Int
|
||||
binding!!.layoutRecipients.removeView(recipientItemMap[itemId])
|
||||
recipientItemMap.remove(itemId)
|
||||
}
|
||||
|
||||
val btnFilePicker = itemAddRecipient.findViewById<Button>(R.id.btn_file_picker)
|
||||
btnFilePicker.tag = recipientItemId
|
||||
btnFilePicker.setOnClickListener {
|
||||
val itemId = it.tag as Int
|
||||
val etKeyStore = recipientItemMap[itemId]!!.findViewById<EditText>(R.id.et_recipient_keystore)
|
||||
pickCert(etKeyStore)
|
||||
}
|
||||
|
||||
val layoutRecipientKeystore = itemAddRecipient.findViewById<LinearLayout>(R.id.layout_recipient_keystore)
|
||||
layoutRecipientKeystore.visibility = if (encryptionProtocol == "Plain") View.GONE else View.VISIBLE
|
||||
|
||||
binding!!.layoutRecipients.addView(itemAddRecipient)
|
||||
recipientItemMap[recipientItemId] = itemAddRecipient
|
||||
recipientItemId++
|
||||
}
|
||||
|
||||
/**
|
||||
* 从EditText控件中获取全部recipients
|
||||
*
|
||||
* @return 全部recipients
|
||||
*/
|
||||
private fun getRecipientsFromRecipientItemMap(): MutableMap<String, Pair<String, String>> {
|
||||
val recipients: MutableMap<String, Pair<String, String>> = mutableMapOf()
|
||||
for (recipientItem in recipientItemMap.values) {
|
||||
val etRecipientEmail = recipientItem.findViewById<EditText>(R.id.et_recipient_email)
|
||||
val etRecipientKeystore = recipientItem.findViewById<EditText>(R.id.et_recipient_keystore)
|
||||
val etRecipientPassword = recipientItem.findViewById<EditText>(R.id.et_recipient_password)
|
||||
val email = etRecipientEmail.text.toString().trim()
|
||||
val keystore = etRecipientKeystore.text.toString().trim()
|
||||
val password = etRecipientPassword.text.toString().trim()
|
||||
recipients[email] = Pair(keystore, password)
|
||||
}
|
||||
Log.d(TAG, "recipients: $recipients")
|
||||
return recipients
|
||||
}
|
||||
|
||||
//选择证书文件
|
||||
private fun pickCert(etKeyStore: EditText) {
|
||||
XXPermissions.with(this)
|
||||
.permission(Permission.MANAGE_EXTERNAL_STORAGE)
|
||||
.request(object : OnPermissionCallback {
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onGranted(permissions: List<String>, all: Boolean) {
|
||||
val fileList = findSupportedFiles(downloadPath)
|
||||
if (fileList.isEmpty()) {
|
||||
XToastUtils.error(String.format(getString(R.string.download_certificate_first), downloadPath))
|
||||
return
|
||||
}
|
||||
MaterialDialog.Builder(requireContext())
|
||||
.title(getString(R.string.keystore_path))
|
||||
.content(String.format(getString(R.string.root_directory), downloadPath))
|
||||
.items(fileList)
|
||||
.itemsCallbackSingleChoice(0) { _: MaterialDialog?, _: View?, _: Int, text: CharSequence ->
|
||||
val webPath = "$downloadPath/$text"
|
||||
etKeyStore.setText(webPath)
|
||||
true // allow selection
|
||||
}
|
||||
.positiveText(R.string.select)
|
||||
.negativeText(R.string.cancel)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onDenied(permissions: List<String>, never: Boolean) {
|
||||
if (never) {
|
||||
XToastUtils.error(R.string.toast_denied_never)
|
||||
// 如果是被永久拒绝就跳转到应用权限系统设置页面
|
||||
XXPermissions.startPermissionActivity(requireContext(), permissions)
|
||||
} else {
|
||||
XToastUtils.error(R.string.toast_denied)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun findSupportedFiles(directoryPath: String): List<String> {
|
||||
val audioFiles = mutableListOf<String>()
|
||||
val directory = File(directoryPath)
|
||||
|
||||
if (directory.exists() && directory.isDirectory) {
|
||||
directory.listFiles()?.let { files ->
|
||||
files.filter { it.isFile && isSupportedFile(it) }.forEach { audioFiles.add(it.name) }
|
||||
}
|
||||
}
|
||||
|
||||
return audioFiles
|
||||
}
|
||||
|
||||
private fun isSupportedFile(file: File): Boolean {
|
||||
val supportedExtensions = if (encryptionProtocol == "OpenPGP") {
|
||||
listOf("asc", "pgp")
|
||||
} else {
|
||||
listOf("pfx", "p12", "pem", "cer", "crt", "der")
|
||||
}
|
||||
return supportedExtensions.any { it.equals(file.extension, ignoreCase = true) }
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
package com.idormy.sms.forwarder.utils.mail
|
||||
|
||||
import android.text.Html
|
||||
import android.text.Spanned
|
||||
import com.idormy.sms.forwarder.utils.Log
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing
|
||||
import java.io.File
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.Properties
|
||||
import javax.mail.Authenticator
|
||||
import javax.mail.PasswordAuthentication
|
||||
|
||||
@Suppress("PrivatePropertyName", "unused", "DEPRECATION")
|
||||
class EmailSender(
|
||||
// SMTP参数
|
||||
private val host: String, // SMTP服务器地址
|
||||
private val port: String, // SMTP服务器端口
|
||||
private val from: String, // 发件人邮箱
|
||||
private val password: String, // 发件人邮箱密码/授权码
|
||||
// 邮件参数
|
||||
private val nickname: String, // 发件人昵称
|
||||
private val subject: String, // 邮件主题
|
||||
private val body: CharSequence, // 邮件正文
|
||||
private val attachFiles: MutableList<File> = mutableListOf(), // 附件
|
||||
// 收件人参数
|
||||
private val toAddress: MutableList<String> = mutableListOf(), // 收件人邮箱
|
||||
private val ccAddress: MutableList<String> = mutableListOf(), // 抄送者邮箱
|
||||
private val bccAddress: MutableList<String> = mutableListOf(), // 密送者邮箱
|
||||
// 监听器
|
||||
private val listener: EmailTaskListener? = null,
|
||||
// 安全选项
|
||||
private val openSSL: Boolean = false, //是否开启ssl验证 默认关闭
|
||||
private val sslFactory: String = "javax.net.ssl.SSLSocketFactory", //SSL构建类名
|
||||
private val startTls: Boolean = false, //是否开启starttls加密方式 默认关闭
|
||||
// 邮件加密方式: S/MIME、OpenPGP、Plain(不传证书)
|
||||
private val encryptionProtocol: String = "S/MIME",
|
||||
// 邮件 S/MIME 加密和签名
|
||||
private val recipientX509Cert: X509Certificate? = null, //收件人公钥(用于加密)
|
||||
private val senderPrivateKey: PrivateKey? = null, //发件人私玥(用于签名)
|
||||
private val senderX509Cert: X509Certificate? = null, //发件人公玥(用于签名)
|
||||
//邮件 PGP 加密和签名
|
||||
private var recipientPGPPublicKeyRing: PGPPublicKeyRing? = null, // 收件人公钥(用于加密)
|
||||
private var senderPGPSecretKeyRing: PGPSecretKeyRing? = null, // 发件人私钥(用于签名)
|
||||
private val senderPGPSecretKeyPassword: String = "", // 发件人私钥密码
|
||||
) {
|
||||
|
||||
private val TAG: String = EmailSender::class.java.simpleName
|
||||
|
||||
private val properties: Properties = Properties().apply {
|
||||
// 设置邮件服务器的主机名
|
||||
put("mail.smtp.host", host)
|
||||
// 设置邮件服务器的端口号
|
||||
put("mail.smtp.port", port)
|
||||
// 设置是否需要身份验证
|
||||
put("mail.smtp.auth", "true")
|
||||
// 设置是否启用 SSL 连接
|
||||
if (openSSL) {
|
||||
put("mail.smtp.ssl.enable", "true")
|
||||
put("mail.smtp.socketFactory.class", sslFactory)
|
||||
}
|
||||
// 设置是否启用 TLS 连接
|
||||
if (startTls) {
|
||||
put("mail.smtp.starttls.enable", "true")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendEmail() {
|
||||
try {
|
||||
val authenticator = MailAuthenticator(from, password)
|
||||
// 邮件正文
|
||||
val html = try {
|
||||
if (body is Spanned) Html.toHtml(body) else body.toString()
|
||||
} catch (e: Exception) {
|
||||
body.toString()
|
||||
}
|
||||
|
||||
// 发送 S/MIME 邮件
|
||||
when (encryptionProtocol) {
|
||||
"S/MIME" -> {
|
||||
val smimeUtils = SmimeUtils(
|
||||
properties,
|
||||
authenticator,
|
||||
from,
|
||||
nickname,
|
||||
subject,
|
||||
html,
|
||||
attachFiles,
|
||||
toAddress,
|
||||
ccAddress,
|
||||
bccAddress,
|
||||
recipientX509Cert,
|
||||
senderPrivateKey,
|
||||
senderX509Cert,
|
||||
)
|
||||
val isEncrypt: Boolean = recipientX509Cert != null
|
||||
val isSign: Boolean = senderX509Cert != null && senderPrivateKey != null
|
||||
Log.d(TAG, "isEncrypt=$isEncrypt, isSign=$isSign")
|
||||
val result = when {
|
||||
isEncrypt && isSign -> smimeUtils.sendSignedAndEncryptedEmail()
|
||||
isEncrypt -> smimeUtils.sendEncryptedEmail()
|
||||
isSign -> smimeUtils.sendSignedEmail()
|
||||
else -> smimeUtils.sendPlainEmail()
|
||||
}
|
||||
listener?.onEmailSent(result.first, result.second)
|
||||
}
|
||||
|
||||
"OpenPGP" -> {
|
||||
// 发送 PGP 邮件
|
||||
val pgpEmail = PgpUtils(
|
||||
properties,
|
||||
authenticator,
|
||||
from,
|
||||
nickname,
|
||||
subject,
|
||||
html,
|
||||
attachFiles,
|
||||
toAddress,
|
||||
ccAddress,
|
||||
bccAddress,
|
||||
recipientPGPPublicKeyRing,
|
||||
senderPGPSecretKeyRing,
|
||||
senderPGPSecretKeyPassword,
|
||||
)
|
||||
val isEncrypt: Boolean = recipientPGPPublicKeyRing != null
|
||||
val isSign: Boolean = senderPGPSecretKeyRing != null
|
||||
Log.d(TAG, "isEncrypt=$isEncrypt, isSign=$isSign")
|
||||
val result = when {
|
||||
isEncrypt && isSign -> pgpEmail.sendSignedAndEncryptedEmail()
|
||||
isEncrypt -> pgpEmail.sendEncryptedEmail()
|
||||
isSign -> pgpEmail.sendSignedEmail()
|
||||
else -> pgpEmail.sendPlainEmail()
|
||||
}
|
||||
listener?.onEmailSent(result.first, result.second)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// 发送普通邮件
|
||||
val simpleEmail = SmimeUtils(
|
||||
properties,
|
||||
authenticator,
|
||||
from,
|
||||
nickname,
|
||||
subject,
|
||||
html,
|
||||
attachFiles,
|
||||
toAddress,
|
||||
ccAddress,
|
||||
bccAddress,
|
||||
)
|
||||
val result = simpleEmail.sendPlainEmail()
|
||||
listener?.onEmailSent(result.first, result.second)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
listener?.onEmailSent(false, "Error sending email: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
interface EmailTaskListener {
|
||||
fun onEmailSent(success: Boolean, message: String)
|
||||
}
|
||||
|
||||
/**
|
||||
* 发件箱auth校验
|
||||
*/
|
||||
private class MailAuthenticator(username: String, private var password: String) : Authenticator() {
|
||||
private var userName: String? = username
|
||||
override fun getPasswordAuthentication(): PasswordAuthentication {
|
||||
return PasswordAuthentication(userName, password)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package com.idormy.sms.forwarder.utils.mail
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* desc: 邮件实体类
|
||||
* time: 2019/8/1
|
||||
* @author teprinciple
|
||||
*/
|
||||
data class Mail(
|
||||
var mailServerHost: String = "", // 发件箱邮箱服务器地址
|
||||
var mailServerPort: String = "", // 发件箱邮箱服务器端口
|
||||
var fromAddress: String = "", // 发件箱
|
||||
var fromNickname: String = "", // 发件人昵称
|
||||
var password: String = "", // 发件箱授权码(密码)
|
||||
|
||||
var toAddress: List<String> = ArrayList(), // 直接收件人邮箱
|
||||
var ccAddress: ArrayList<String> = ArrayList(), // 抄送者邮箱
|
||||
var bccAddress: ArrayList<String> = ArrayList(), // 密送者邮箱
|
||||
|
||||
var subject: String = "", // 邮件主题
|
||||
var content: CharSequence = "", // 邮件内容
|
||||
var attachFiles: ArrayList<File> = ArrayList(), // 附件
|
||||
|
||||
var openSSL: Boolean = false, //是否开启ssl验证 默认关闭
|
||||
var sslFactory: String = "javax.net.ssl.SSLSocketFactory", //SSL构建类名
|
||||
var startTls: Boolean = false, //是否开启starttls加密方式 默认关闭
|
||||
)
|
|
@ -1,49 +0,0 @@
|
|||
package com.idormy.sms.forwarder.utils.mail
|
||||
|
||||
import com.idormy.sms.forwarder.utils.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.mail.Transport
|
||||
|
||||
/**
|
||||
* 邮件发送器
|
||||
*/
|
||||
object MailSender {
|
||||
|
||||
/**
|
||||
* 获取单例
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getInstance() = this
|
||||
|
||||
/**
|
||||
* 发送邮件
|
||||
*/
|
||||
fun sendMail(mail: Mail, onMailSendListener: OnMailSendListener? = null) {
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
val send = GlobalScope.async(Dispatchers.IO) {
|
||||
Transport.send(MailUtil.createMailMessage(mail))
|
||||
}
|
||||
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
runCatching {
|
||||
send.await()
|
||||
onMailSendListener?.onSuccess()
|
||||
}.onFailure {
|
||||
Log.e("MailSender", it.message.toString())
|
||||
onMailSendListener?.onError(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送回调
|
||||
*/
|
||||
interface OnMailSendListener {
|
||||
fun onSuccess()
|
||||
fun onError(e: Throwable)
|
||||
}
|
||||
}
|
|
@ -1,136 +0,0 @@
|
|||
package com.idormy.sms.forwarder.utils.mail
|
||||
|
||||
import android.text.Html
|
||||
import android.text.Spanned
|
||||
import com.idormy.sms.forwarder.utils.Log
|
||||
import com.xuexiang.xrouter.utils.TextUtils
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.util.Properties
|
||||
import javax.activation.DataHandler
|
||||
import javax.activation.FileDataSource
|
||||
import javax.mail.Authenticator
|
||||
import javax.mail.Message
|
||||
import javax.mail.PasswordAuthentication
|
||||
import javax.mail.Session
|
||||
import javax.mail.internet.InternetAddress
|
||||
import javax.mail.internet.MimeBodyPart
|
||||
import javax.mail.internet.MimeMessage
|
||||
import javax.mail.internet.MimeMultipart
|
||||
import javax.mail.internet.MimeUtility
|
||||
|
||||
/**
|
||||
* desc: 邮件帮助类
|
||||
* time: 2019/8/1
|
||||
* @author teprinciple
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
object MailUtil {
|
||||
|
||||
/**
|
||||
* 创建邮件
|
||||
*/
|
||||
fun createMailMessage(mail: Mail): MimeMessage {
|
||||
Log.e("createMailMessage", mail.toString())
|
||||
val properties = Properties()
|
||||
properties["mail.debug"] = "true"
|
||||
properties["mail.smtp.host"] = mail.mailServerHost
|
||||
properties["mail.smtp.port"] = mail.mailServerPort
|
||||
properties["mail.smtp.auth"] = "true"
|
||||
properties["mail.smtp.ssl.enable"] = mail.openSSL
|
||||
if (mail.startTls) {
|
||||
properties["mail.smtp.starttls.enable"] = true
|
||||
}
|
||||
if (mail.openSSL) {
|
||||
properties["mail.smtp.socketFactory.class"] = mail.sslFactory
|
||||
}
|
||||
val authenticator = MailAuthenticator(mail.fromAddress, mail.password)
|
||||
val session = Session.getInstance(properties, authenticator)
|
||||
session.debug = true
|
||||
|
||||
Log.e("createMailMessage", session.toString())
|
||||
return MimeMessage(session).apply {
|
||||
|
||||
// 设置发件箱
|
||||
if (TextUtils.isEmpty(mail.fromNickname)) {
|
||||
setFrom(InternetAddress(mail.fromAddress))
|
||||
} else {
|
||||
var nickname = mail.fromNickname.replace(":", "-").replace("\n", "-")
|
||||
try {
|
||||
Log.d("createMailMessage", "nickname = $nickname")
|
||||
nickname = MimeUtility.encodeText(nickname)
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
e.printStackTrace()
|
||||
Log.e("createMailMessage", "UnsupportedEncodingException = ${e.message}")
|
||||
}
|
||||
|
||||
Log.d("createMailMessage", "nickname = $nickname")
|
||||
setFrom(InternetAddress("$nickname <${mail.fromAddress}>"))
|
||||
}
|
||||
|
||||
// 设置直接接收者收件箱
|
||||
val toAddress = mail.toAddress.map {
|
||||
InternetAddress(it)
|
||||
}.toTypedArray()
|
||||
setRecipients(Message.RecipientType.TO, toAddress)
|
||||
|
||||
// 设置抄送者收件箱
|
||||
val ccAddress = mail.ccAddress.map {
|
||||
InternetAddress(it)
|
||||
}.toTypedArray()
|
||||
setRecipients(Message.RecipientType.CC, ccAddress)
|
||||
|
||||
// 设置密送者收件箱
|
||||
val bccAddress = mail.bccAddress.map {
|
||||
InternetAddress(it)
|
||||
}.toTypedArray()
|
||||
setRecipients(Message.RecipientType.BCC, bccAddress)
|
||||
|
||||
// 邮件主题
|
||||
subject = mail.subject.replace(":", "-").replace("\n", "-")
|
||||
try {
|
||||
subject = MimeUtility.encodeText(subject)
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
e.printStackTrace()
|
||||
Log.e("createMailMessage", "UnsupportedEncodingException = ${e.message}")
|
||||
}
|
||||
|
||||
// 邮件内容
|
||||
val contentPart = MimeMultipart()
|
||||
|
||||
// 邮件正文
|
||||
val textBodyPart = MimeBodyPart()
|
||||
if (mail.content is Spanned) {
|
||||
textBodyPart.setContent(
|
||||
Html.toHtml(mail.content as Spanned),
|
||||
"text/html;charset=UTF-8"
|
||||
)
|
||||
} else {
|
||||
textBodyPart.setContent(mail.content, "text/html;charset=UTF-8")
|
||||
}
|
||||
contentPart.addBodyPart(textBodyPart)
|
||||
|
||||
// 邮件附件
|
||||
mail.attachFiles.forEach {
|
||||
val fileBodyPart = MimeBodyPart()
|
||||
val ds = FileDataSource(it)
|
||||
val dh = DataHandler(ds)
|
||||
fileBodyPart.dataHandler = dh
|
||||
fileBodyPart.fileName = MimeUtility.encodeText(dh.name)
|
||||
contentPart.addBodyPart(fileBodyPart)
|
||||
}
|
||||
contentPart.setSubType("mixed")
|
||||
setContent(contentPart)
|
||||
saveChanges()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发件箱auth校验
|
||||
*/
|
||||
class MailAuthenticator(username: String?, private var password: String?) : Authenticator() {
|
||||
private var userName: String? = username
|
||||
override fun getPasswordAuthentication(): PasswordAuthentication {
|
||||
return PasswordAuthentication(userName, password)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,295 @@
|
|||
package com.idormy.sms.forwarder.utils.mail
|
||||
|
||||
import com.idormy.sms.forwarder.utils.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing
|
||||
import org.bouncycastle.util.io.Streams
|
||||
import org.pgpainless.PGPainless
|
||||
import org.pgpainless.algorithm.DocumentSignatureType
|
||||
import org.pgpainless.algorithm.HashAlgorithm
|
||||
import org.pgpainless.encryption_signing.EncryptionOptions
|
||||
import org.pgpainless.encryption_signing.ProducerOptions
|
||||
import org.pgpainless.encryption_signing.SigningOptions
|
||||
import org.pgpainless.key.protection.SecretKeyRingProtector
|
||||
import org.pgpainless.util.Passphrase
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.security.Security
|
||||
import java.util.Date
|
||||
import java.util.Properties
|
||||
import javax.activation.DataHandler
|
||||
import javax.activation.FileDataSource
|
||||
import javax.mail.Authenticator
|
||||
import javax.mail.Message
|
||||
import javax.mail.Session
|
||||
import javax.mail.Transport
|
||||
import javax.mail.internet.InternetAddress
|
||||
import javax.mail.internet.MimeBodyPart
|
||||
import javax.mail.internet.MimeMessage
|
||||
import javax.mail.internet.MimeMultipart
|
||||
import javax.mail.internet.MimeUtility
|
||||
import javax.mail.util.ByteArrayDataSource
|
||||
|
||||
|
||||
@Suppress("PrivatePropertyName", "unused")
|
||||
class PgpUtils(
|
||||
private val properties: Properties,
|
||||
private val authenticator: Authenticator,
|
||||
// 邮件参数
|
||||
private val from: String, // 发件人邮箱
|
||||
private val nickname: String, // 发件人昵称
|
||||
private val subject: String, // 邮件主题
|
||||
private val body: String, // 邮件正文
|
||||
private val attachFiles: MutableList<File> = mutableListOf(), // 附件
|
||||
// 收件人参数
|
||||
private val toAddress: MutableList<String> = mutableListOf(), // 收件人邮箱
|
||||
private val ccAddress: MutableList<String> = mutableListOf(), // 抄送者邮箱
|
||||
private val bccAddress: MutableList<String> = mutableListOf(), // 密送者邮箱
|
||||
//邮件 PGP 加密和签名
|
||||
private var recipientPGPPublicKeyRing: PGPPublicKeyRing? = null, // 收件人公钥(用于加密)
|
||||
private var senderPGPSecretKeyRing: PGPSecretKeyRing? = null, // 发件人私钥(用于签名)
|
||||
private val senderPGPSecretKeyPassword: String = "", // 发件人私钥密码
|
||||
) {
|
||||
|
||||
private val TAG: String = PgpUtils::class.java.simpleName
|
||||
|
||||
init {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
}
|
||||
|
||||
// 发送明文邮件
|
||||
suspend fun sendPlainEmail(): Pair<Boolean, String> = withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "sendPlainEmail")
|
||||
try {
|
||||
val originalMessage = getOriginalMessage()
|
||||
Transport.send(originalMessage)
|
||||
Pair(true, "Email sent successfully")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Pair(false, "Failed to send email: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// 发送签名后的邮件
|
||||
suspend fun sendSignedEmail(): Pair<Boolean, String> = withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "sendSignedEmail")
|
||||
try {
|
||||
val originalMessage = getOriginalMessage()
|
||||
val signedMessage = getSignedMessage(originalMessage)
|
||||
Transport.send(signedMessage)
|
||||
Pair(true, "Email signed and sent successfully")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Pair(false, "Failed to sign and send email: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// 发送加密邮件
|
||||
suspend fun sendEncryptedEmail(): Pair<Boolean, String> = withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "sendEncryptedEmail")
|
||||
try {
|
||||
val originalMessage = getOriginalMessage()
|
||||
val producerOptions = ProducerOptions.encrypt(
|
||||
EncryptionOptions.encryptCommunications().addRecipient(recipientPGPPublicKeyRing!!)
|
||||
).setAsciiArmor(true)
|
||||
val encryptedMessage = getEncryptedMessage(originalMessage, producerOptions)
|
||||
Transport.send(encryptedMessage)
|
||||
Pair(true, "Encrypted email sent successfully")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Pair(false, "Failed to send encrypted email: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// 发送签名加密邮件
|
||||
suspend fun sendSignedAndEncryptedEmail(): Pair<Boolean, String> = withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "sendSignedAndEncryptedEmail")
|
||||
try {
|
||||
val originalMessage = getOriginalMessage()
|
||||
|
||||
val secretKeyDecryptor = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword(senderPGPSecretKeyPassword))
|
||||
val producerOptions = ProducerOptions.signAndEncrypt(
|
||||
EncryptionOptions.encryptCommunications().addRecipient(recipientPGPPublicKeyRing!!),
|
||||
SigningOptions()
|
||||
.addInlineSignature(secretKeyDecryptor, senderPGPSecretKeyRing!!, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)
|
||||
.overrideHashAlgorithm(HashAlgorithm.SHA256)
|
||||
).setAsciiArmor(true)
|
||||
val encryptedMessage = getEncryptedMessage(originalMessage, producerOptions)
|
||||
Transport.send(encryptedMessage)
|
||||
Pair(true, "Signed and encrypted email sent successfully")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Pair(false, "Failed to send signed and encrypted email: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// 获取原始邮件
|
||||
private fun getOriginalMessage(): MimeMessage {
|
||||
val session = Session.getInstance(properties, authenticator)
|
||||
session.debug = true
|
||||
val message = MimeMessage(session)
|
||||
// 设置直接接收者收件箱
|
||||
val toAddress = toAddress.map { InternetAddress(it) }.toTypedArray()
|
||||
message.setRecipients(Message.RecipientType.TO, toAddress)
|
||||
// 设置抄送者收件箱
|
||||
val ccAddress = ccAddress.map { InternetAddress(it) }.toTypedArray()
|
||||
message.setRecipients(Message.RecipientType.CC, ccAddress)
|
||||
// 设置密送者收件箱
|
||||
val bccAddress = bccAddress.map { InternetAddress(it) }.toTypedArray()
|
||||
message.setRecipients(Message.RecipientType.BCC, bccAddress)
|
||||
// 设置发件箱
|
||||
when {
|
||||
nickname.isEmpty() -> message.setFrom(InternetAddress(from))
|
||||
else -> try {
|
||||
var name = nickname.replace(":", "-").replace("\n", "-")
|
||||
name = MimeUtility.encodeText(name)
|
||||
message.setFrom(InternetAddress("$name <$from>"))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
message.setFrom(InternetAddress(from))
|
||||
}
|
||||
}
|
||||
// 邮件主题
|
||||
try {
|
||||
message.subject = MimeUtility.encodeText(subject.replace(":", "-").replace("\n", "-"))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
message.subject = subject
|
||||
}
|
||||
|
||||
// 邮件内容
|
||||
val contentPart = MimeMultipart("mixed")
|
||||
|
||||
// 邮件正文
|
||||
val textBodyPart = MimeBodyPart()
|
||||
textBodyPart.setContent(body, "text/html;charset=UTF-8")
|
||||
contentPart.addBodyPart(textBodyPart)
|
||||
|
||||
// 邮件附件
|
||||
attachFiles.forEach {
|
||||
val fileBodyPart = MimeBodyPart()
|
||||
val ds = FileDataSource(it)
|
||||
val dh = DataHandler(ds)
|
||||
fileBodyPart.dataHandler = dh
|
||||
fileBodyPart.fileName = MimeUtility.encodeText(dh.name)
|
||||
contentPart.addBodyPart(fileBodyPart)
|
||||
}
|
||||
|
||||
message.setContent(contentPart)
|
||||
message.sentDate = Date()
|
||||
message.saveChanges()
|
||||
return message
|
||||
}
|
||||
|
||||
// 获取签名邮件: https://datatracker.ietf.org/doc/html/rfc3156#autoid-5
|
||||
private fun getSignedMessage(originalMessage: MimeMessage): MimeMessage {
|
||||
// 将原始邮件作为第一个部分添加到 multipart 中
|
||||
val originalBodyPart = MimeBodyPart()
|
||||
originalBodyPart.setContent(originalMessage.content, originalMessage.contentType)
|
||||
|
||||
// 将原始消息写入InputStream
|
||||
val baos = ByteArrayOutputStream()
|
||||
originalBodyPart.writeTo(baos)
|
||||
val inputStream: InputStream = ByteArrayInputStream(baos.toByteArray())
|
||||
|
||||
// 签名数据
|
||||
val secretKeyDecryptor = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword(senderPGPSecretKeyPassword))
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
val encryptionStream = PGPainless.encryptAndOrSign()
|
||||
.onOutputStream(outputStream)
|
||||
.withOptions(
|
||||
ProducerOptions.sign(
|
||||
SigningOptions()
|
||||
.addDetachedSignature(secretKeyDecryptor, senderPGPSecretKeyRing!!, DocumentSignatureType.BINARY_DOCUMENT)
|
||||
.overrideHashAlgorithm(HashAlgorithm.SHA256)
|
||||
).setAsciiArmor(true)
|
||||
)
|
||||
Streams.pipeAll(inputStream, encryptionStream)
|
||||
encryptionStream.close()
|
||||
|
||||
// 签名部分
|
||||
val signaturePart = MimeBodyPart().apply {
|
||||
//dataHandler = DataHandler(ByteArrayDataSource(outputStream.toString(), "application/pgp-signature"))
|
||||
//fileName = "signature.asc"
|
||||
setContent(outputStream.toString(), "application/pgp-signature")
|
||||
//setHeader("Content-Type", "application/pgp-signature; name=\"signature.asc\"")
|
||||
addHeader("Content-Description", "OpenPGP digital signature")
|
||||
addHeader("Content-Disposition", "attachment; filename=\"signature.asc\"")
|
||||
}
|
||||
|
||||
val signedMultiPart = MimeMultipart("signed; micalg=pgp-sha256; protocol=\"application/pgp-signature\"")
|
||||
signedMultiPart.addBodyPart(originalBodyPart, 0)
|
||||
signedMultiPart.addBodyPart(signaturePart, 1)
|
||||
|
||||
val signedMessage = MimeMessage(originalMessage.session)
|
||||
signedMessage.setRecipients(Message.RecipientType.TO, originalMessage.getRecipients(Message.RecipientType.TO))
|
||||
signedMessage.setRecipients(Message.RecipientType.CC, originalMessage.getRecipients(Message.RecipientType.CC))
|
||||
signedMessage.setRecipients(Message.RecipientType.BCC, originalMessage.getRecipients(Message.RecipientType.BCC))
|
||||
signedMessage.addFrom(originalMessage.from)
|
||||
signedMessage.subject = originalMessage.subject
|
||||
signedMessage.sentDate = originalMessage.sentDate
|
||||
signedMessage.setContent(signedMultiPart)
|
||||
signedMessage.saveChanges()
|
||||
|
||||
return signedMessage
|
||||
}
|
||||
|
||||
// 获取加密邮件: https://datatracker.ietf.org/doc/html/rfc3156#section-4
|
||||
private fun getEncryptedMessage(originalMessage: MimeMessage, producerOptions: ProducerOptions): MimeMessage {
|
||||
// 将原始消息写入InputStream
|
||||
val baos = ByteArrayOutputStream()
|
||||
originalMessage.writeTo(baos)
|
||||
val inputStream: InputStream = ByteArrayInputStream(baos.toByteArray())
|
||||
|
||||
// 加密数据
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
val encryptionStream = PGPainless.encryptAndOrSign().onOutputStream(outputStream).withOptions(producerOptions)
|
||||
Streams.pipeAll(inputStream, encryptionStream)
|
||||
encryptionStream.close()
|
||||
val result = encryptionStream.result
|
||||
Log.d(TAG, result.toString())
|
||||
|
||||
// The first body part contains the control information necessary to
|
||||
// decrypt the data in the second body part and is labeled according to
|
||||
// the value of the protocol parameter.
|
||||
val versionPart = MimeBodyPart().apply {
|
||||
setText("Version: 1")
|
||||
addHeader("Content-Type", "application/pgp-encrypted")
|
||||
addHeader("Content-Description", "PGP/MIME version identification")
|
||||
//addHeader("Content-Transfer-Encoding", "base64")
|
||||
}
|
||||
|
||||
// The second body part contains the data which was encrypted
|
||||
// and is always labeled application/octet-stream.
|
||||
val encryptedPart = MimeBodyPart().apply {
|
||||
dataHandler = DataHandler(ByteArrayDataSource(outputStream.toByteArray(), "application/octet-stream"))
|
||||
fileName = "encrypted.asc"
|
||||
addHeader("Content-Type", "application/octet-stream; name=\"encrypted.asc\"")
|
||||
addHeader("Content-Description", "OpenPGP encrypted message")
|
||||
addHeader("Content-Disposition", "inline; filename=\"encrypted.asc\"")
|
||||
}
|
||||
|
||||
val encryptedMultiPart = MimeMultipart("encrypted; protocol=\"application/pgp-encrypted\"")
|
||||
encryptedMultiPart.addBodyPart(versionPart, 0)
|
||||
encryptedMultiPart.addBodyPart(encryptedPart, 1)
|
||||
|
||||
val encryptedMessage = MimeMessage(originalMessage.session)
|
||||
encryptedMessage.setRecipients(Message.RecipientType.TO, originalMessage.getRecipients(Message.RecipientType.TO))
|
||||
encryptedMessage.setRecipients(Message.RecipientType.CC, originalMessage.getRecipients(Message.RecipientType.CC))
|
||||
encryptedMessage.setRecipients(Message.RecipientType.BCC, originalMessage.getRecipients(Message.RecipientType.BCC))
|
||||
encryptedMessage.addFrom(originalMessage.from)
|
||||
encryptedMessage.subject = originalMessage.subject
|
||||
encryptedMessage.sentDate = originalMessage.sentDate
|
||||
encryptedMessage.setContent(encryptedMultiPart)
|
||||
encryptedMessage.saveChanges()
|
||||
|
||||
return encryptedMessage
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
package com.idormy.sms.forwarder.utils.mail
|
||||
|
||||
import com.idormy.sms.forwarder.utils.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.bouncycastle.cert.jcajce.JcaCertStore
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder
|
||||
import org.bouncycastle.cms.CMSAlgorithm
|
||||
import org.bouncycastle.cms.CMSEnvelopedDataGenerator
|
||||
import org.bouncycastle.cms.CMSProcessableByteArray
|
||||
import org.bouncycastle.cms.CMSSignedDataGenerator
|
||||
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder
|
||||
import org.bouncycastle.cms.jcajce.JceCMSContentEncryptorBuilder
|
||||
import org.bouncycastle.cms.jcajce.JceKeyTransRecipientInfoGenerator
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.bouncycastle.operator.OutputEncryptor
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.security.PrivateKey
|
||||
import java.security.Security
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.Date
|
||||
import java.util.Properties
|
||||
import javax.activation.DataHandler
|
||||
import javax.activation.FileDataSource
|
||||
import javax.mail.Authenticator
|
||||
import javax.mail.Message
|
||||
import javax.mail.Session
|
||||
import javax.mail.Transport
|
||||
import javax.mail.internet.InternetAddress
|
||||
import javax.mail.internet.MimeBodyPart
|
||||
import javax.mail.internet.MimeMessage
|
||||
import javax.mail.internet.MimeMultipart
|
||||
import javax.mail.internet.MimeUtility
|
||||
|
||||
@Suppress("PrivatePropertyName", "unused")
|
||||
class SmimeUtils(
|
||||
private val properties: Properties,
|
||||
private val authenticator: Authenticator,
|
||||
// 邮件参数
|
||||
private val from: String, // 发件人邮箱
|
||||
private val nickname: String, // 发件人昵称
|
||||
private val subject: String, // 邮件主题
|
||||
private val body: String, // 邮件正文
|
||||
private val attachFiles: MutableList<File> = mutableListOf(), // 附件
|
||||
// 收件人参数
|
||||
private val toAddress: MutableList<String> = mutableListOf(), // 收件人邮箱
|
||||
private val ccAddress: MutableList<String> = mutableListOf(), // 抄送者邮箱
|
||||
private val bccAddress: MutableList<String> = mutableListOf(), // 密送者邮箱
|
||||
// 邮件 S/MIME 加密和签名
|
||||
private val recipientX509Cert: X509Certificate? = null, //收件人公钥(用于加密)
|
||||
private val senderPrivateKey: PrivateKey? = null, //发件人私玥(用于签名)
|
||||
private val senderX509Cert: X509Certificate? = null, //发件人公玥(用于签名)
|
||||
) {
|
||||
|
||||
private val TAG: String = SmimeUtils::class.java.simpleName
|
||||
|
||||
init {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
}
|
||||
|
||||
// 发送明文邮件
|
||||
suspend fun sendPlainEmail(): Pair<Boolean, String> = withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "sendPlainEmail")
|
||||
try {
|
||||
val originalMessage = getOriginalMessage()
|
||||
Transport.send(originalMessage)
|
||||
Pair(true, "Email sent successfully")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Pair(false, "Failed to send email: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// 发送签名后的邮件
|
||||
suspend fun sendSignedEmail(): Pair<Boolean, String> = withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "sendSignedEmail")
|
||||
try {
|
||||
val originalMessage = getOriginalMessage()
|
||||
val signedMessage = getSignedMessage(originalMessage)
|
||||
Transport.send(signedMessage)
|
||||
Pair(true, "Email signed and sent successfully")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Pair(false, "Failed to sign and send email: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// 发送加密邮件
|
||||
suspend fun sendEncryptedEmail(): Pair<Boolean, String> = withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "sendEncryptedEmail")
|
||||
try {
|
||||
val originalMessage = getOriginalMessage()
|
||||
val encryptedMessage = getEncryptedMessage(originalMessage)
|
||||
Transport.send(encryptedMessage)
|
||||
Pair(true, "Encrypted email sent successfully")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Pair(false, "Failed to send encrypted email: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// 发送签名加密邮件
|
||||
suspend fun sendSignedAndEncryptedEmail(): Pair<Boolean, String> = withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "sendSignedAndEncryptedEmail")
|
||||
try {
|
||||
val originalMessage = getOriginalMessage()
|
||||
val signedMessage = getSignedMessage(originalMessage)
|
||||
val encryptedMessage = getEncryptedMessage(signedMessage)
|
||||
Transport.send(encryptedMessage)
|
||||
Pair(true, "Signed and encrypted email sent successfully")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Pair(false, "Failed to send signed and encrypted email: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// 获取原始邮件
|
||||
private fun getOriginalMessage(): MimeMessage {
|
||||
val session = Session.getInstance(properties, authenticator)
|
||||
session.debug = true
|
||||
val message = MimeMessage(session)
|
||||
// 设置直接接收者收件箱
|
||||
val toAddress = toAddress.map { InternetAddress(it) }.toTypedArray()
|
||||
message.setRecipients(Message.RecipientType.TO, toAddress)
|
||||
// 设置抄送者收件箱
|
||||
val ccAddress = ccAddress.map { InternetAddress(it) }.toTypedArray()
|
||||
message.setRecipients(Message.RecipientType.CC, ccAddress)
|
||||
// 设置密送者收件箱
|
||||
val bccAddress = bccAddress.map { InternetAddress(it) }.toTypedArray()
|
||||
message.setRecipients(Message.RecipientType.BCC, bccAddress)
|
||||
// 设置发件箱
|
||||
when {
|
||||
nickname.isEmpty() -> message.setFrom(InternetAddress(from))
|
||||
else -> try {
|
||||
var name = nickname.replace(":", "-").replace("\n", "-")
|
||||
name = MimeUtility.encodeText(name)
|
||||
message.setFrom(InternetAddress("$name <$from>"))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
message.setFrom(InternetAddress(from))
|
||||
}
|
||||
}
|
||||
// 邮件主题
|
||||
try {
|
||||
message.subject = MimeUtility.encodeText(subject.replace(":", "-").replace("\n", "-"))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
message.subject = subject
|
||||
}
|
||||
|
||||
// 邮件内容
|
||||
val contentPart = MimeMultipart("mixed")
|
||||
|
||||
// 邮件正文
|
||||
val textBodyPart = MimeBodyPart()
|
||||
textBodyPart.setContent(body, "text/html;charset=UTF-8")
|
||||
contentPart.addBodyPart(textBodyPart)
|
||||
|
||||
// 邮件附件
|
||||
attachFiles.forEach {
|
||||
val fileBodyPart = MimeBodyPart()
|
||||
val ds = FileDataSource(it)
|
||||
val dh = DataHandler(ds)
|
||||
fileBodyPart.dataHandler = dh
|
||||
fileBodyPart.fileName = MimeUtility.encodeText(dh.name)
|
||||
contentPart.addBodyPart(fileBodyPart)
|
||||
}
|
||||
|
||||
message.setContent(contentPart)
|
||||
message.sentDate = Date()
|
||||
message.saveChanges()
|
||||
return message
|
||||
}
|
||||
|
||||
// 获取签名邮件
|
||||
private fun getSignedMessage(originalMessage: MimeMessage): MimeMessage {
|
||||
// 创建签名者信息生成器
|
||||
val contentSigner = JcaContentSignerBuilder("SHA256withRSA").build(senderPrivateKey)
|
||||
val certificateHolder = JcaX509CertificateHolder(senderX509Cert)
|
||||
val signerInfoGenerator = JcaSignerInfoGeneratorBuilder(
|
||||
JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider()).build()
|
||||
).build(contentSigner, certificateHolder)
|
||||
|
||||
// 创建 CMSSignedDataGenerator 并添加签名者信息和证书
|
||||
val generator = CMSSignedDataGenerator()
|
||||
generator.addSignerInfoGenerator(signerInfoGenerator)
|
||||
val certStore = JcaCertStore(listOf(senderX509Cert))
|
||||
generator.addCertificates(certStore)
|
||||
|
||||
// 将邮件内容转换为 CMSSignedData
|
||||
//val originalContent = originalMessage.content as MimeMultipart //TODO: Outlook 不显示正文
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
//originalContent.writeTo(outputStream)
|
||||
originalMessage.writeTo(outputStream) //TODO: Thunderbird 会重复现实发件人
|
||||
val contentData = CMSProcessableByteArray(outputStream.toByteArray())
|
||||
val signedData = generator.generate(contentData, true)
|
||||
|
||||
// 创建 MimeMessage 并设置签名后的内容
|
||||
val signedMessage = MimeMessage(originalMessage.session, ByteArrayInputStream(signedData.encoded))
|
||||
signedMessage.setRecipients(Message.RecipientType.TO, originalMessage.getRecipients(Message.RecipientType.TO))
|
||||
signedMessage.setRecipients(Message.RecipientType.CC, originalMessage.getRecipients(Message.RecipientType.CC))
|
||||
signedMessage.setRecipients(Message.RecipientType.BCC, originalMessage.getRecipients(Message.RecipientType.BCC))
|
||||
signedMessage.addFrom(originalMessage.from)
|
||||
signedMessage.subject = originalMessage.subject
|
||||
signedMessage.sentDate = originalMessage.sentDate
|
||||
signedMessage.setContent(signedData.encoded, "application/pkcs7-mime; name=smime.p7m; smime-type=signed-data")
|
||||
signedMessage.saveChanges()
|
||||
|
||||
return signedMessage
|
||||
}
|
||||
|
||||
// 获取加密邮件
|
||||
private fun getEncryptedMessage(originalMessage: MimeMessage): MimeMessage {
|
||||
// 使用收件人的证书进行加密
|
||||
val cmsEnvelopedDataGenerator = CMSEnvelopedDataGenerator()
|
||||
val recipientInfoGenerator = JceKeyTransRecipientInfoGenerator(recipientX509Cert)
|
||||
cmsEnvelopedDataGenerator.addRecipientInfoGenerator(recipientInfoGenerator)
|
||||
|
||||
// 使用 3DES 加密
|
||||
val outputEncryptor: OutputEncryptor = JceCMSContentEncryptorBuilder(CMSAlgorithm.DES_EDE3_CBC).build()
|
||||
val originalContent = ByteArrayOutputStream()
|
||||
originalMessage.writeTo(originalContent)
|
||||
val inputStream = originalContent.toByteArray()
|
||||
val cmsEnvelopedData = cmsEnvelopedDataGenerator.generate(
|
||||
CMSProcessableByteArray(inputStream),
|
||||
outputEncryptor
|
||||
)
|
||||
|
||||
// 创建加密邮件
|
||||
val encryptedMessage = MimeMessage(originalMessage.session)
|
||||
encryptedMessage.setRecipients(Message.RecipientType.TO, originalMessage.getRecipients(Message.RecipientType.TO))
|
||||
encryptedMessage.setRecipients(Message.RecipientType.CC, originalMessage.getRecipients(Message.RecipientType.CC))
|
||||
encryptedMessage.setRecipients(Message.RecipientType.BCC, originalMessage.getRecipients(Message.RecipientType.BCC))
|
||||
encryptedMessage.addFrom(originalMessage.from)
|
||||
encryptedMessage.subject = originalMessage.subject
|
||||
encryptedMessage.sentDate = originalMessage.sentDate
|
||||
encryptedMessage.setContent(cmsEnvelopedData.encoded, "application/pkcs7-mime; name=smime.p7m; smime-type=enveloped-data")
|
||||
encryptedMessage.setHeader("Content-Type", "application/pkcs7-mime; name=smime.p7m; smime-type=enveloped-data")
|
||||
encryptedMessage.setHeader("Content-Disposition", "attachment; filename=smime.p7m")
|
||||
encryptedMessage.setHeader("Content-Description", "S/MIME Encrypted Message")
|
||||
encryptedMessage.addHeader("Content-Transfer-Encoding", "base64")
|
||||
encryptedMessage.saveChanges()
|
||||
|
||||
return encryptedMessage
|
||||
}
|
||||
|
||||
}
|
|
@ -7,14 +7,25 @@ import com.idormy.sms.forwarder.entity.setting.EmailSetting
|
|||
import com.idormy.sms.forwarder.utils.Log
|
||||
import com.idormy.sms.forwarder.utils.SendUtils
|
||||
import com.idormy.sms.forwarder.utils.SettingUtils
|
||||
import com.idormy.sms.forwarder.utils.mail.Mail
|
||||
import com.idormy.sms.forwarder.utils.mail.MailSender
|
||||
import com.idormy.sms.forwarder.utils.mail.EmailSender
|
||||
import com.xuexiang.xutil.resource.ResUtils.getString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing
|
||||
import org.pgpainless.PGPainless
|
||||
import org.pgpainless.key.info.KeyRingInfo
|
||||
import java.io.FileInputStream
|
||||
import java.security.KeyStore
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
class EmailUtils {
|
||||
companion object {
|
||||
|
||||
//private val TAG: String = EmailUtils::class.java.simpleName
|
||||
private val TAG: String = EmailUtils::class.java.simpleName
|
||||
|
||||
fun sendMsg(
|
||||
setting: EmailSetting,
|
||||
|
@ -127,36 +138,191 @@ class EmailUtils {
|
|||
else -> {}
|
||||
}
|
||||
|
||||
//收件地址
|
||||
val toAddressList = setting.toEmail.toString().replace("[,,;;]".toRegex(), ",").trim(',').split(',')
|
||||
runBlocking {
|
||||
val job = launch(Dispatchers.IO) {
|
||||
try {
|
||||
// 设置邮件参数
|
||||
val host = setting.host.toString()
|
||||
val port = setting.port.toString()
|
||||
val from = setting.fromEmail.toString()
|
||||
val password = setting.pwd.toString()
|
||||
val nickname = msgInfo.getTitleForSend(setting.nickname.toString())
|
||||
setting.recipients.ifEmpty {
|
||||
//兼容旧的设置
|
||||
val emails = setting.toEmail.toString().replace("[,,;;]".toRegex(), ",").trim(',').split(',')
|
||||
emails.forEach {
|
||||
setting.recipients[it] = Pair("", "")
|
||||
}
|
||||
}
|
||||
val content = message.replace("\n", "<br>")
|
||||
val openSSL = setting.ssl == true
|
||||
val startTls = setting.startTls == true
|
||||
|
||||
//创建邮箱
|
||||
val mail = Mail().apply {
|
||||
mailServerHost = setting.host.toString()
|
||||
mailServerPort = setting.port.toString()
|
||||
fromAddress = setting.fromEmail.toString()
|
||||
fromNickname = msgInfo.getTitleForSend(setting.nickname.toString())
|
||||
password = setting.pwd.toString()
|
||||
toAddress = toAddressList
|
||||
subject = title
|
||||
content = message.replace("\n", "<br>")
|
||||
openSSL = setting.ssl == true
|
||||
startTls = setting.startTls == true
|
||||
//发件人S/MIME私钥(用于签名)
|
||||
var signingPrivateKey: PrivateKey? = null
|
||||
var signingCertificate: X509Certificate? = null
|
||||
//发件人OpenPGP私钥(用于签名)
|
||||
var senderPGPSecretKeyRing: PGPSecretKeyRing? = null
|
||||
var senderPGPSecretKeyPassword = ""
|
||||
|
||||
if (!setting.keystore.isNullOrEmpty() && !setting.password.isNullOrEmpty()) {
|
||||
val keystoreStream = FileInputStream(setting.keystore)
|
||||
try {
|
||||
when (setting.encryptionProtocol) {
|
||||
"S/MIME" -> {
|
||||
val keystorePassword = setting.password.toString()
|
||||
val keyStore = KeyStore.getInstance("PKCS12")
|
||||
keyStore.load(keystoreStream, keystorePassword.toCharArray())
|
||||
val privateKeyAlias = keyStore.aliases().toList().first { keyStore.isKeyEntry(it) }
|
||||
signingPrivateKey = keyStore.getKey(privateKeyAlias, keystorePassword.toCharArray()) as PrivateKey
|
||||
signingCertificate = keyStore.getCertificate(privateKeyAlias) as X509Certificate
|
||||
}
|
||||
|
||||
MailSender.getInstance().sendMail(mail, object : MailSender.OnMailSendListener {
|
||||
override fun onError(e: Throwable) {
|
||||
Log.e("MailSender", e.message.toString())
|
||||
"OpenPGP" -> {
|
||||
senderPGPSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(keystoreStream)
|
||||
senderPGPSecretKeyPassword = setting.password.toString()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.w(TAG, "Failed to load keystore: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// 发送结果监听器
|
||||
val listener = object : EmailSender.EmailTaskListener {
|
||||
override fun onEmailSent(success: Boolean, message: String) {
|
||||
if (success) {
|
||||
SendUtils.updateLogs(logId, 2, getString(R.string.request_succeeded) + ": " + message)
|
||||
SendUtils.senderLogic(2, msgInfo, rule, senderIndex, msgId)
|
||||
} else {
|
||||
val status = 0
|
||||
SendUtils.updateLogs(logId, status, message)
|
||||
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//逐一发送加密邮件
|
||||
val recipientsWithoutCert = mutableListOf<String>()
|
||||
setting.recipients.forEach { (email, cert) ->
|
||||
val keystorePath = cert.first
|
||||
val keystorePassword = cert.second
|
||||
var recipientX509Cert: X509Certificate? = null
|
||||
var recipientPGPPublicKeyRing: PGPPublicKeyRing? = null
|
||||
try {
|
||||
when {
|
||||
//从私钥证书文件提取公钥
|
||||
keystorePath.isNotEmpty() && keystorePassword.isNotEmpty() -> {
|
||||
val keystoreStream = FileInputStream(keystorePath)
|
||||
when (setting.encryptionProtocol) {
|
||||
"S/MIME" -> {
|
||||
val keyStore = KeyStore.getInstance("PKCS12")
|
||||
keyStore.load(keystoreStream, keystorePassword.toCharArray())
|
||||
val alias = keyStore.aliases().nextElement()
|
||||
recipientX509Cert = keyStore.getCertificate(alias) as X509Certificate
|
||||
}
|
||||
|
||||
"OpenPGP" -> {
|
||||
val recipientPGPSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(keystoreStream)
|
||||
recipientPGPPublicKeyRing = recipientPGPSecretKeyRing?.let { PGPainless.extractCertificate(it) }
|
||||
if (recipientPGPPublicKeyRing != null) {
|
||||
val keyInfo = KeyRingInfo(recipientPGPPublicKeyRing)
|
||||
Log.d(TAG, "Recipient key info: $keyInfo")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//从证书文件提取公钥
|
||||
keystorePath.isNotEmpty() && keystorePassword.isEmpty() -> {
|
||||
val keystoreStream = FileInputStream(keystorePath)
|
||||
when (setting.encryptionProtocol) {
|
||||
"S/MIME" -> {
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
recipientX509Cert = certFactory.generateCertificate(FileInputStream(keystorePath)) as X509Certificate
|
||||
}
|
||||
|
||||
"OpenPGP" -> {
|
||||
recipientPGPPublicKeyRing = PGPainless.readKeyRing().publicKeyRing(keystoreStream)
|
||||
if (recipientPGPPublicKeyRing != null) {
|
||||
val keyInfo = KeyRingInfo(recipientPGPPublicKeyRing)
|
||||
Log.d(TAG, "Recipient key info: $keyInfo")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
recipientsWithoutCert.add(email)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.w(TAG, "Failed to load recipient($email) keystore($cert): ${e.message}")
|
||||
//无法加载证书时,发送明文邮件
|
||||
recipientsWithoutCert.add(email)
|
||||
}
|
||||
|
||||
if (recipientX509Cert != null || recipientPGPPublicKeyRing != null) {
|
||||
val senderWithRecipientCert = EmailSender(
|
||||
host,
|
||||
port,
|
||||
from,
|
||||
password,
|
||||
nickname,
|
||||
title,
|
||||
content,
|
||||
toAddress = mutableListOf(email),
|
||||
listener = listener,
|
||||
openSSL = openSSL,
|
||||
startTls = startTls,
|
||||
encryptionProtocol = setting.encryptionProtocol,
|
||||
recipientX509Cert = recipientX509Cert,
|
||||
senderPrivateKey = signingPrivateKey,
|
||||
senderX509Cert = signingCertificate,
|
||||
recipientPGPPublicKeyRing = recipientPGPPublicKeyRing,
|
||||
senderPGPSecretKeyRing = senderPGPSecretKeyRing,
|
||||
senderPGPSecretKeyPassword = senderPGPSecretKeyPassword,
|
||||
)
|
||||
senderWithRecipientCert.sendEmail()
|
||||
}
|
||||
}
|
||||
|
||||
//批量发送明文邮件
|
||||
if (recipientsWithoutCert.isNotEmpty()) {
|
||||
val senderWithoutRecipientCert = EmailSender(
|
||||
host,
|
||||
port,
|
||||
from,
|
||||
password,
|
||||
nickname,
|
||||
title,
|
||||
content,
|
||||
toAddress = recipientsWithoutCert,
|
||||
listener = listener,
|
||||
openSSL = openSSL,
|
||||
startTls = startTls,
|
||||
encryptionProtocol = setting.encryptionProtocol,
|
||||
senderPrivateKey = signingPrivateKey,
|
||||
senderX509Cert = signingCertificate,
|
||||
//TODO: OpenPGP 只签名不加密时,提示无效的数字签名,暂未解决
|
||||
senderPGPSecretKeyRing = senderPGPSecretKeyRing,
|
||||
senderPGPSecretKeyPassword = senderPGPSecretKeyPassword,
|
||||
)
|
||||
senderWithoutRecipientCert.sendEmail()
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(TAG, e.message.toString())
|
||||
val status = 0
|
||||
SendUtils.updateLogs(logId, status, e.message.toString())
|
||||
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
|
||||
}
|
||||
|
||||
override fun onSuccess() {
|
||||
SendUtils.updateLogs(logId, 2, getString(R.string.request_succeeded))
|
||||
SendUtils.senderLogic(2, msgInfo, rule, senderIndex, msgId)
|
||||
}
|
||||
})
|
||||
job.join() // 等待协程完成
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,8 @@ import java.net.URLEncoder
|
|||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
|
@ -103,9 +104,9 @@ class SocketUtils {
|
|||
} else if (setting.method == "MQTT") {
|
||||
// MQTT 连接参数
|
||||
val uriType = if (TextUtils.isEmpty(setting.uriType)) "tcp" else setting.uriType
|
||||
val brokerUrl = "${uriType}://${setting.address}:${setting.port}"
|
||||
var brokerUrl = "${uriType}://${setting.address}:${setting.port}"
|
||||
if (!TextUtils.isEmpty(setting.path)) {
|
||||
brokerUrl.plus(setting.path)
|
||||
brokerUrl += setting.path
|
||||
}
|
||||
Log.d(TAG, "MQTT brokerUrl: $brokerUrl")
|
||||
val clientId = if (TextUtils.isEmpty(setting.clientId)) UUID.randomUUID().toString() else setting.clientId
|
||||
|
|
|
@ -138,7 +138,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:text="@string/custom_template_tips"
|
||||
android:textSize="@dimen/text_size_small" />
|
||||
android:textSize="@dimen/text_size_mini" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
@ -302,30 +302,184 @@
|
|||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/email_to"
|
||||
android:text="@string/encryption_protocol"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:text="@string/email_to_tips"
|
||||
android:textSize="@dimen/text_size_small" />
|
||||
android:text="@string/encryption_protocol_tips"
|
||||
android:textSize="@dimen/text_size_mini" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
|
||||
android:id="@+id/et_to_email"
|
||||
<RadioGroup
|
||||
android:id="@+id/rg_encryption_protocol"
|
||||
style="@style/rg_style"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rb_encryption_protocol_plain"
|
||||
style="@style/rg_rb_style"
|
||||
android:checked="true"
|
||||
android:text="@string/plain" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rb_encryption_protocol_smime"
|
||||
style="@style/rg_rb_style"
|
||||
android:text="@string/smime" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rb_encryption_protocol_openpgp"
|
||||
style="@style/rg_rb_style"
|
||||
android:text="@string/openpgp" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_sender_keystore"
|
||||
style="@style/BarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_sender_keystore"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sender_smime_keystore"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/keystore_path"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
|
||||
android:id="@+id/et_sender_keystore"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/keystore_path_tips"
|
||||
android:importantForAutofill="no"
|
||||
android:singleLine="true"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:met_clearButton="true" />
|
||||
|
||||
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
|
||||
android:id="@+id/btn_sender_keystore_picker"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:gravity="center"
|
||||
android:padding="5dp"
|
||||
android:text="@string/select_file"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="@dimen/text_size_mini"
|
||||
app:sb_color_unpressed="@color/colorBlueGrey"
|
||||
app:sb_ripple_color="@color/white"
|
||||
app:sb_ripple_duration="500"
|
||||
app:sb_shape_type="rectangle"
|
||||
tools:ignore="SmallSp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/keystore_password"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
|
||||
android:id="@+id/et_sender_password"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/keystore_password_tips"
|
||||
android:importantForAutofill="no"
|
||||
android:singleLine="true"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:met_passWordButton="true" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_recipients"
|
||||
style="@style/BarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_email_to"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/email_to"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_email_to_tips"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/email_to_tips"
|
||||
android:textSize="@dimen/text_size_mini" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btn_add_recipient"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginStart="@dimen/config_margin_5dp"
|
||||
android:background="@color/colorBlueGrey"
|
||||
android:src="@drawable/icon_api_contact_add"
|
||||
app:tint="#FFFFFF"
|
||||
tools:ignore="ContentDescription,ImageContrastCheck" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
@ -352,7 +506,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:text="@string/custom_template_tips"
|
||||
android:textSize="@dimen/text_size_small" />
|
||||
android:textSize="@dimen/text_size_mini" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="@dimen/config_margin_5dp"
|
||||
android:background="?attr/xui_config_color_separator_light" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/recipient_email"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
|
||||
android:id="@+id/et_recipient_email"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:singleLine="true"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:met_clearButton="true" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_del"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="5dp"
|
||||
android:contentDescription="@string/del"
|
||||
android:src="@drawable/ic_delete"
|
||||
app:tint="#F15C58" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_recipient_keystore"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/keystore_path"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
|
||||
android:id="@+id/et_recipient_keystore"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/keystore_path_tips"
|
||||
android:importantForAutofill="no"
|
||||
android:singleLine="true"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:met_clearButton="true" />
|
||||
|
||||
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
|
||||
android:id="@+id/btn_file_picker"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:gravity="center"
|
||||
android:padding="5dp"
|
||||
android:text="@string/select_file"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="@dimen/text_size_mini"
|
||||
app:sb_color_unpressed="@color/colorBlueGrey"
|
||||
app:sb_ripple_color="@color/white"
|
||||
app:sb_ripple_duration="500"
|
||||
app:sb_shape_type="rectangle"
|
||||
tools:ignore="SmallSp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/keystore_password"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
|
||||
android:id="@+id/et_recipient_password"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/keystore_password_tips"
|
||||
android:importantForAutofill="no"
|
||||
android:singleLine="true"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:met_passWordButton="true" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
|
@ -293,6 +293,9 @@
|
|||
<string name="invalid_name">Channel name cannot be empty</string>
|
||||
<string name="invalid_token">invalid token</string>
|
||||
<string name="invalid_email">Email parameter is incomplete</string>
|
||||
<string name="invalid_recipient_email">Invalid recipient email address: %s</string>
|
||||
<string name="invalid_x509_certificate">The X.509 public key certificate for the recipient (%s) is invalid.</string>
|
||||
<string name="invalid_pkcs12_certificate">The PKCS12 private key certificate for the recipient (%s) is invalid.</string>
|
||||
<string name="invalid_email_server">Email Server parameter is incomplete</string>
|
||||
<string name="invalid_bark_icon">The bark-icon is not a valid URL</string>
|
||||
<string name="invalid_bark_url">The bark-url is not a valid URL</string>
|
||||
|
@ -323,11 +326,24 @@
|
|||
<string name="email_account">Account</string>
|
||||
<string name="email_password">Password/Auth Code</string>
|
||||
<string name="email_nickname">Nickname</string>
|
||||
<string name="email_to">Send To</string>
|
||||
<string name="email_to_tips">Tip: Separate multiple recipients with a ","</string>
|
||||
<string name="email_to">Recipients</string>
|
||||
<string name="email_to_tips">Tip: Click to add recipients.</string>
|
||||
<string name="email_to_smime">Recipients & S/MIME Encryption Cert.</string>
|
||||
<string name="email_to_smime_tips">Tip: Click to add recipients and S/MIME encryption public keys (opt.).</string>
|
||||
<string name="email_to_openpgp">Recipients & OpenPGP Public Cert.</string>
|
||||
<string name="email_to_openpgp_tips">Tip: Click to add recipients and OpenPGP public keys (opt.).</string>
|
||||
<string name="sender_smime_keystore">Sender S/MIME Cert. (Opt.)</string>
|
||||
<string name="sender_openpgp_keystore">Sender OpenPGP Cert. (Opt.)</string>
|
||||
<string name="invalid_sender_keystore">Invalid Sender Signing Private Key</string>
|
||||
<string name="smime_public_keys_tips">Add public keys for S/MIME encryption, leave blank otherwise.</string>
|
||||
<string name="recipient_email">Recipient</string>
|
||||
<string name="keystore_path">Cert. Path</string>
|
||||
<string name="keystore_path_tips">Opt., Copy keystore to the Download dir</string>
|
||||
<string name="keystore_password">Cert. Pwd.</string>
|
||||
<string name="keystore_password_tips">Import password for `Private key`</string>
|
||||
<string name="email_title">Email Title</string>
|
||||
<string name="feishu_webhook">Webhook</string>
|
||||
<string name="feishu_secret">Secret (optional)</string>
|
||||
<string name="feishu_secret">Secret (opt.)</string>
|
||||
<string name="feishu_receive_id_type">Receive Id Type"</string>
|
||||
<string name="feishu_msg_type">Msg Type</string>
|
||||
<string name="feishu_msg_type_text">Text</string>
|
||||
|
@ -353,10 +369,10 @@
|
|||
<string name="server_chan_send_key">SendKey</string>
|
||||
<string name="server_chan_channel">Message Channel</string>
|
||||
<string name="server_chan_channel_tips">Tip: Dynamically specified, supports up to two channels, separated by a vertical bar |</string>
|
||||
<string name="server_chan_channel_hint">Optional, e.g. to send service number and enterprise WeChat application, then fill in 9|66</string>
|
||||
<string name="server_chan_channel_hint">Opt., e.g. to send service number and enterprise WeChat application, then fill in 9|66</string>
|
||||
<string name="server_chan_openid">CC OpenID</string>
|
||||
<string name="server_chan_openid_tips">Tip: Only test accounts and Wework application message channels are supported</string>
|
||||
<string name="server_chan_openid_hint">Optional, multiple openids are separated by commas</string>
|
||||
<string name="server_chan_openid_hint">Opt., multiple openids are separated by commas</string>
|
||||
<string name="TelegramApiToken">ApiToken or Custom Proxy Address (startwith http)</string>
|
||||
<string name="TelegramChatId">ChatId</string>
|
||||
<string name="Method" formatted="false">Method</string>
|
||||
|
@ -493,6 +509,11 @@
|
|||
<string name="account">✱Account</string>
|
||||
<string name="servers">✱Servers</string>
|
||||
<string name="email_settings">✱Email</string>
|
||||
<string name="encryption_protocol">E2EE</string>
|
||||
<string name="encryption_protocol_tips">Tip: To encrypt or sign emails, specify OpenPGP or S/MIME cert.</string>
|
||||
<string name="plain">Plain</string>
|
||||
<string name="smime">S/MIME</string>
|
||||
<string name="openpgp">OpenPGP</string>
|
||||
<string name="below"><![CDATA[≤]]></string>
|
||||
<string name="percent">%</string>
|
||||
<string name="above"><![CDATA[≥]]></string>
|
||||
|
@ -515,7 +536,7 @@
|
|||
<string name="out_message_topic">Out Message Topic</string>
|
||||
<string name="out_message_topic_hint">Send a message on the corresponding topic</string>
|
||||
<string name="uri_type">Uri Type</string>
|
||||
<string name="uri_type_hint">Optional, default is tcp</string>
|
||||
<string name="uri_type_hint">Opt., default is tcp</string>
|
||||
<string name="path">Path</string>
|
||||
<string name="path_hint">Used to set the uri when communicating using ws</string>
|
||||
<string name="client_id">Client Id</string>
|
||||
|
@ -571,8 +592,8 @@
|
|||
<string name="outgoing_call_ended">Out Ended</string>
|
||||
<string name="missed_call">Missed</string>
|
||||
<string name="unknown_call">Unknown</string>
|
||||
<string name="optional_action">Optional: </string>
|
||||
<string name="optional_type">Optional: </string>
|
||||
<string name="optional_action">Opt.: </string>
|
||||
<string name="optional_type">Opt.: </string>
|
||||
<string name="active_request">Active request</string>
|
||||
<string name="active_request_tips">Obtain instructions through passive reception or active polling to operate the machine</string>
|
||||
<string name="httpserver">Local HttpServer</string>
|
||||
|
@ -708,7 +729,7 @@
|
|||
<string name="one_pixel">One Pixel</string>
|
||||
<string name="one_pixel_activity">One Pixel Activity</string>
|
||||
<string name="one_pixel_activity_tips">This can change the process priority from 4 to 1</string>
|
||||
<string name="optional">Optional</string>
|
||||
<string name="optional">Opt.</string>
|
||||
<string name="TelegramChatIdTips">Follow the steps in the wiki to obtain it</string>
|
||||
<string name="network_model">Network model</string>
|
||||
<string name="offline_mode">Offline mode</string>
|
||||
|
@ -762,15 +783,15 @@
|
|||
<string name="bark_server_regex">^https?://[^/]+/[^/]+/$</string>
|
||||
<string name="bark_server_error">The Url format is wrong, e.g. https://api.day.app/XXXXXXXX/</string>
|
||||
<string name="bark_group">Group Name</string>
|
||||
<string name="bark_group_tips">Optional, e.g. SmsForwarder</string>
|
||||
<string name="bark_group_tips">Opt., e.g. SmsForwarder</string>
|
||||
<string name="bark_icon">Message Icon</string>
|
||||
<string name="bark_icon_tips">Optional, fill in Url, the picture should not be too big</string>
|
||||
<string name="bark_icon_tips">Opt., fill in Url, the picture should not be too big</string>
|
||||
<string name="bark_sound">Message Sound</string>
|
||||
<string name="bark_sound_tips">Optional, e.g. minuet.caf</string>
|
||||
<string name="bark_sound_tips">Opt., e.g. minuet.caf</string>
|
||||
<string name="bark_badge">Message Badge</string>
|
||||
<string name="bark_badge_tips">Optional, e.g. 888</string>
|
||||
<string name="bark_badge_tips">Opt., e.g. 888</string>
|
||||
<string name="bark_url">Message Link</string>
|
||||
<string name="bark_url_tips">Optional, e.g. https://github.com/pppscn/SmsForwarder</string>
|
||||
<string name="bark_url_tips">Opt., e.g. https://github.com/pppscn/SmsForwarder</string>
|
||||
<string name="bark_level">Notification Level</string>
|
||||
<string name="bark_level_active">Immediately display notifications</string>
|
||||
<string name="bark_level_timeSensitive">Time-sensitive notifications that can be displayed in a focused state</string>
|
||||
|
@ -941,7 +962,7 @@
|
|||
|
||||
<string name="sim_slot">Sim Slot</string>
|
||||
<string name="display_name">Display Name</string>
|
||||
<string name="display_name_hint">Optional, address book display name</string>
|
||||
<string name="display_name_hint">Opt., address book display name</string>
|
||||
<string name="phone_numbers">Phone Numbers</string>
|
||||
<string name="phone_numbers_hint">Required, separate multiple phone numbers with semicolons</string>
|
||||
<string name="phone_numbers_error">Invalid Phone Numbers, eg. 15888888888;19999999999</string>
|
||||
|
@ -1042,7 +1063,7 @@
|
|||
<string name="enabling_pure_task_mode">Do you want to quit the app immediately and start it manually to take effect in pure task mode?</string>
|
||||
<string name="debug_mode">Enable debug mode</string>
|
||||
<string name="debug_mode_tips">Save Log.* to file for troubleshooting; export to download directory.</string>
|
||||
<string name="optional_components">Optional:</string>
|
||||
<string name="optional_components">Opt.:</string>
|
||||
<string name="enable_cactus">Enable Cactus Keep Alive</string>
|
||||
<string name="enabe_cactus_tips">Dual foreground service/JobScheduler/WorkManager/1px/silent music</string>
|
||||
<string name="load_app_list">Get installed app info async at startup</string>
|
||||
|
@ -1078,6 +1099,7 @@
|
|||
<string name="restarting_httpserver">Restarting HttpServer</string>
|
||||
<string name="download_first">Download and unzip to:\n%s</string>
|
||||
<string name="download_music_first">Download music file to:\n%s</string>
|
||||
<string name="download_certificate_first">Download certificate file to:\n%s</string>
|
||||
<string name="root_directory">Root Directory:\n%s</string>
|
||||
<string name="select_web_client_directory">Select WebClient Directory</string>
|
||||
<string name="invalid_feishu_app_parameter">AppId/AppSecret/UserId cannot be empty</string>
|
||||
|
@ -1401,7 +1423,7 @@
|
|||
<string name="stop_alarm">Stop Alarm</string>
|
||||
<string name="alarm_play_settings">Playback Settings</string>
|
||||
<string name="alarm_music">Specify Music</string>
|
||||
<string name="alarm_music_tips">Optional, download mp3/ogg/wav to the Download directory.</string>
|
||||
<string name="alarm_music_tips">Opt., download mp3/ogg/wav to the Download directory.</string>
|
||||
<string name="alarm_volume">Alarm Volume</string>
|
||||
<string name="alarm_play_times">Play Times(0=Infinite)</string>
|
||||
|
||||
|
|
|
@ -294,6 +294,9 @@
|
|||
<string name="invalid_name">通道名称不能为空</string>
|
||||
<string name="invalid_token">token不合法</string>
|
||||
<string name="invalid_email">发件邮箱/登录密码/收件地址不可为空</string>
|
||||
<string name="invalid_recipient_email">收件地址(%s)不合法</string>
|
||||
<string name="invalid_x509_certificate">收件地址(%s)的X.509公钥证书无效</string>
|
||||
<string name="invalid_pkcs12_certificate">收件地址(%s)的PKCS12私钥证书无效</string>
|
||||
<string name="invalid_email_server">服务器信息的主机/端口不可为空</string>
|
||||
<string name="invalid_bark_icon">消息图标不是有效URL</string>
|
||||
<string name="invalid_bark_url">消息链接不是有效URL</string>
|
||||
|
@ -324,8 +327,21 @@
|
|||
<string name="email_account">发件邮箱</string>
|
||||
<string name="email_password">登录密码</string>
|
||||
<string name="email_nickname">发件人昵称</string>
|
||||
<string name="email_to">收件地址</string>
|
||||
<string name="email_to_tips">Tip:多个收件人以半角逗号,分隔</string>
|
||||
<string name="email_to">收件人邮箱</string>
|
||||
<string name="email_to_tips">Tip: 点击按钮添加收件人邮箱</string>
|
||||
<string name="email_to_smime">收件人邮箱 & S/MIME加密公钥</string>
|
||||
<string name="email_to_smime_tips">Tip: 点击按钮添加收件人邮箱、S/MIME加密公钥(可选)</string>
|
||||
<string name="email_to_openpgp">收件人邮箱 & OpenPGP加密公钥</string>
|
||||
<string name="email_to_openpgp_tips">Tip: 点击按钮添加收件人邮箱、OpenPGP加密公钥(可选)</string>
|
||||
<string name="sender_smime_keystore">发件人S/MIME签名私钥(可选)</string>
|
||||
<string name="sender_openpgp_keystore">发件人OpenPGP签名私钥(可选)</string>
|
||||
<string name="invalid_sender_keystore">发件人签名私钥无效</string>
|
||||
<string name="smime_public_keys_tips">对邮件进行S/MIME签名加密,则添加公钥,否则留空</string>
|
||||
<string name="recipient_email">收件人邮箱</string>
|
||||
<string name="keystore_path">证书路径</string>
|
||||
<string name="keystore_path_tips">可选,下载证书文件到 Download 目录</string>
|
||||
<string name="keystore_password">证书密码</string>
|
||||
<string name="keystore_password_tips">`私钥证书`对应的导入密钥</string>
|
||||
<string name="email_title">邮件主题</string>
|
||||
<string name="feishu_webhook">Webhook 地址</string>
|
||||
<string name="feishu_secret">加签 Secret (没有可不填)</string>
|
||||
|
@ -494,6 +510,11 @@
|
|||
<string name="account">✱帐户管理</string>
|
||||
<string name="servers">✱服务器信息</string>
|
||||
<string name="email_settings">✱邮件设置</string>
|
||||
<string name="encryption_protocol">端对端加密</string>
|
||||
<string name="encryption_protocol_tips">Tip:若要加密或签名邮件,需指定OpenPGP或S/MIME证书</string>
|
||||
<string name="plain">明文</string>
|
||||
<string name="smime">S/MIME</string>
|
||||
<string name="openpgp">OpenPGP</string>
|
||||
<string name="below">低于</string>
|
||||
<string name="percent">%</string>
|
||||
<string name="above">高于</string>
|
||||
|
@ -1079,6 +1100,7 @@
|
|||
<string name="restarting_httpserver">正在重启HttpServer</string>
|
||||
<string name="download_first">请先下载Web客户端并解压到:\n%s</string>
|
||||
<string name="download_music_first">请先下载音乐文件到:\n%s</string>
|
||||
<string name="download_certificate_first">请先下载证书文件到:\n%s</string>
|
||||
<string name="root_directory">根目录:\n%s</string>
|
||||
<string name="select_web_client_directory">选择Web客户端目录</string>
|
||||
<string name="invalid_feishu_app_parameter">AppId/AppSecret/UserId都不能为空</string>
|
||||
|
|
|
@ -294,6 +294,9 @@
|
|||
<string name="invalid_name">通道名稱不能為空</string>
|
||||
<string name="invalid_token">token不合法</string>
|
||||
<string name="invalid_email">發件郵箱/登錄密碼/收件地址不可為空</string>
|
||||
<string name="invalid_recipient_email">收件地址(%s)無效</string>
|
||||
<string name="invalid_x509_certificate">收件地址(%s)的X.509公鑰憑證無效</string>
|
||||
<string name="invalid_pkcs12_certificate">收件地址(%s)的PKCS12私鑰憑證無效</string>
|
||||
<string name="invalid_email_server">服務器信息的主機/端口不可為空</string>
|
||||
<string name="invalid_bark_icon">消息圖標不是有效URL</string>
|
||||
<string name="invalid_bark_url">消息鏈接不是有效URL</string>
|
||||
|
@ -324,8 +327,21 @@
|
|||
<string name="email_account">發件郵箱</string>
|
||||
<string name="email_password">登錄密碼</string>
|
||||
<string name="email_nickname">發件人昵稱</string>
|
||||
<string name="email_to">收件地址</string>
|
||||
<string name="email_to_tips">Tip:多個收件人以半角逗號,分隔</string>
|
||||
<string name="email_to">收件人郵箱</string>
|
||||
<string name="email_to_tips">提示:點擊按鈕添加收件人郵箱</string>
|
||||
<string name="email_to_smime">收件人郵箱 & S/MIME加密公鑰</string>
|
||||
<string name="email_to_smime_tips">提示:點擊按鈕添加收件人郵箱、S/MIME加密公鑰(可選)</string>
|
||||
<string name="email_to_openpgp">收件人郵箱 & OpenPGP加密公鑰</string>
|
||||
<string name="email_to_openpgp_tips">提示:點擊按鈕添加收件人郵箱、OpenPGP加密公鑰(可選)</string>
|
||||
<string name="sender_smime_keystore">發件人S/MIME簽名私鑰(可選)</string>
|
||||
<string name="sender_openpgp_keystore">發件人OpenPGP簽名私鑰(可選)</string>
|
||||
<string name="invalid_sender_keystore">發件人簽名私鑰無效</string>
|
||||
<string name="smime_public_keys_tips">對郵件進行 S/MIME 簽名加密,則添加公鑰,否則留空</string>
|
||||
<string name="recipient_email">收件人郵箱</string>
|
||||
<string name="keystore_path">證書路徑</string>
|
||||
<string name="keystore_path_tips">可選,下載證書文件到 Download 目錄</string>
|
||||
<string name="keystore_password">證書密碼</string>
|
||||
<string name="keystore_password_tips">「私鑰證書」相對應的導入密碼</string>
|
||||
<string name="email_title">郵件主題</string>
|
||||
<string name="feishu_webhook">Webhook 地址</string>
|
||||
<string name="feishu_secret">加簽 Secret (沒有可不填)</string>
|
||||
|
@ -494,6 +510,11 @@
|
|||
<string name="account">✱帳戶管理</string>
|
||||
<string name="servers">✱伺服器信息</string>
|
||||
<string name="email_settings">✱郵件設置</string>
|
||||
<string name="encryption_protocol">端對端加密</string>
|
||||
<string name="encryption_protocol_tips">Tip:若要加密或簽名郵件,需指定OpenPGP或S/MIME證書</string>
|
||||
<string name="plain">明文</string>
|
||||
<string name="smime">S/MIME</string>
|
||||
<string name="openpgp">OpenPGP</string>
|
||||
<string name="below">低於</string>
|
||||
<string name="percent">%</string>
|
||||
<string name="above">高於</string>
|
||||
|
@ -1079,6 +1100,7 @@
|
|||
<string name="restarting_httpserver">正在重啟HttpServer</string>
|
||||
<string name="download_first">請先下載Web客戶端並解壓到:\n%s</string>
|
||||
<string name="download_music_first">請先下載音樂文件到:\n%s</string>
|
||||
<string name="download_certificate_first">請先下載證書文件到:\n%s</string>
|
||||
<string name="root_directory">根目錄:\n%s</string>
|
||||
<string name="select_web_client_directory">選擇Web客戶端目錄</string>
|
||||
<string name="invalid_feishu_app_parameter">AppId/AppSecret/UserId都不能為空</string>
|
||||
|
|
|
@ -294,6 +294,9 @@
|
|||
<string name="invalid_name">通道名称不能为空</string>
|
||||
<string name="invalid_token">token不合法</string>
|
||||
<string name="invalid_email">发件邮箱/登录密码/收件地址不可为空</string>
|
||||
<string name="invalid_recipient_email">收件地址(%s)不合法</string>
|
||||
<string name="invalid_x509_certificate">收件地址(%s)的X.509公钥证书无效</string>
|
||||
<string name="invalid_pkcs12_certificate">收件地址(%s)的PKCS12私钥证书无效</string>
|
||||
<string name="invalid_email_server">服务器信息的主机/端口不可为空</string>
|
||||
<string name="invalid_bark_icon">消息图标不是有效URL</string>
|
||||
<string name="invalid_bark_url">消息链接不是有效URL</string>
|
||||
|
@ -324,8 +327,21 @@
|
|||
<string name="email_account">发件邮箱</string>
|
||||
<string name="email_password">登录密码</string>
|
||||
<string name="email_nickname">发件人昵称</string>
|
||||
<string name="email_to">收件地址</string>
|
||||
<string name="email_to_tips">Tip:多个收件人以半角逗号,分隔</string>
|
||||
<string name="email_to">收件人邮箱</string>
|
||||
<string name="email_to_tips">Tip: 点击按钮添加收件人邮箱</string>
|
||||
<string name="email_to_smime">收件人邮箱 & S/MIME加密公钥</string>
|
||||
<string name="email_to_smime_tips">Tip: 点击按钮添加收件人邮箱、S/MIME加密公钥(可选)</string>
|
||||
<string name="email_to_openpgp">收件人邮箱 & OpenPGP加密公钥</string>
|
||||
<string name="email_to_openpgp_tips">Tip: 点击按钮添加收件人邮箱、OpenPGP加密公钥(可选)</string>
|
||||
<string name="sender_smime_keystore">发件人S/MIME签名私钥(可选)</string>
|
||||
<string name="sender_openpgp_keystore">发件人OpenPGP签名私钥(可选)</string>
|
||||
<string name="invalid_sender_keystore">发件人签名私钥无效</string>
|
||||
<string name="smime_public_keys_tips">对邮件进行S/MIME签名加密,则添加公钥,否则留空</string>
|
||||
<string name="recipient_email">收件人邮箱</string>
|
||||
<string name="keystore_path">证书路径</string>
|
||||
<string name="keystore_path_tips">可选,下载证书文件到 Download 目录</string>
|
||||
<string name="keystore_password">证书密码</string>
|
||||
<string name="keystore_password_tips">`私钥证书`对应的导入密钥</string>
|
||||
<string name="email_title">邮件主题</string>
|
||||
<string name="feishu_webhook">Webhook 地址</string>
|
||||
<string name="feishu_secret">加签 Secret (没有可不填)</string>
|
||||
|
@ -494,6 +510,11 @@
|
|||
<string name="account">✱帐户管理</string>
|
||||
<string name="servers">✱服务器信息</string>
|
||||
<string name="email_settings">✱邮件设置</string>
|
||||
<string name="encryption_protocol">端对端加密</string>
|
||||
<string name="encryption_protocol_tips">Tip:若要加密或签名邮件,需指定OpenPGP或S/MIME证书</string>
|
||||
<string name="plain">明文</string>
|
||||
<string name="smime">S/MIME</string>
|
||||
<string name="openpgp">OpenPGP</string>
|
||||
<string name="below">低于</string>
|
||||
<string name="percent">%</string>
|
||||
<string name="above">高于</string>
|
||||
|
@ -1079,6 +1100,7 @@
|
|||
<string name="restarting_httpserver">正在重启HttpServer</string>
|
||||
<string name="download_first">请先下载Web客户端并解压到:\n%s</string>
|
||||
<string name="download_music_first">请先下载音乐文件到:\n%s</string>
|
||||
<string name="download_certificate_first">请先下载证书文件到:\n%s</string>
|
||||
<string name="root_directory">根目录:\n%s</string>
|
||||
<string name="select_web_client_directory">选择Web客户端目录</string>
|
||||
<string name="invalid_feishu_app_parameter">AppId/AppSecret/UserId都不能为空</string>
|
||||
|
|
Loading…
Reference in New Issue