mirror of
https://github.com/pppscn/SmsForwarder
synced 2024-11-08 01:10:23 +00:00
新增:发送通道电子邮箱
支持S/MIME
或OpenPGP
加密 #417
优化:以`Base64`形式保存证书(同时兼容`文件路径`形式) #437
This commit is contained in:
parent
40ee077ea7
commit
a53fa6db12
@ -24,6 +24,7 @@ import com.idormy.sms.forwarder.database.viewmodel.SenderViewModel
|
|||||||
import com.idormy.sms.forwarder.databinding.FragmentSendersEmailBinding
|
import com.idormy.sms.forwarder.databinding.FragmentSendersEmailBinding
|
||||||
import com.idormy.sms.forwarder.entity.MsgInfo
|
import com.idormy.sms.forwarder.entity.MsgInfo
|
||||||
import com.idormy.sms.forwarder.entity.setting.EmailSetting
|
import com.idormy.sms.forwarder.entity.setting.EmailSetting
|
||||||
|
import com.idormy.sms.forwarder.utils.Base64
|
||||||
import com.idormy.sms.forwarder.utils.CommonUtils
|
import com.idormy.sms.forwarder.utils.CommonUtils
|
||||||
import com.idormy.sms.forwarder.utils.EVENT_TOAST_ERROR
|
import com.idormy.sms.forwarder.utils.EVENT_TOAST_ERROR
|
||||||
import com.idormy.sms.forwarder.utils.KEY_SENDER_CLONE
|
import com.idormy.sms.forwarder.utils.KEY_SENDER_CLONE
|
||||||
@ -51,6 +52,7 @@ import io.reactivex.disposables.Disposable
|
|||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
import org.pgpainless.PGPainless
|
import org.pgpainless.PGPainless
|
||||||
import org.pgpainless.key.info.KeyRingInfo
|
import org.pgpainless.key.info.KeyRingInfo
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
@ -377,8 +379,14 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
|
|||||||
cert.first.isNotEmpty() && cert.second.isNotEmpty() -> {
|
cert.first.isNotEmpty() && cert.second.isNotEmpty() -> {
|
||||||
try {
|
try {
|
||||||
// 判断是否有效的PKCS12私钥证书
|
// 判断是否有效的PKCS12私钥证书
|
||||||
|
val fileInputStream = if (cert.first.startsWith("/")) {
|
||||||
|
FileInputStream(cert.first)
|
||||||
|
} else {
|
||||||
|
val decodedBytes = Base64.decode(cert.first)
|
||||||
|
ByteArrayInputStream(decodedBytes)
|
||||||
|
}
|
||||||
val keyStore = KeyStore.getInstance("PKCS12")
|
val keyStore = KeyStore.getInstance("PKCS12")
|
||||||
keyStore.load(FileInputStream(cert.first), cert.second.toCharArray())
|
keyStore.load(fileInputStream, cert.second.toCharArray())
|
||||||
val alias = keyStore.aliases().nextElement()
|
val alias = keyStore.aliases().nextElement()
|
||||||
val recipientPublicKey = keyStore.getCertificate(alias) as X509Certificate
|
val recipientPublicKey = keyStore.getCertificate(alias) as X509Certificate
|
||||||
Log.d(TAG, "PKCS12 Certificate: $recipientPublicKey")
|
Log.d(TAG, "PKCS12 Certificate: $recipientPublicKey")
|
||||||
@ -391,8 +399,13 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
|
|||||||
cert.first.isNotEmpty() && cert.second.isEmpty() -> {
|
cert.first.isNotEmpty() && cert.second.isEmpty() -> {
|
||||||
try {
|
try {
|
||||||
// 判断是否有效的X.509公钥证书
|
// 判断是否有效的X.509公钥证书
|
||||||
|
val fileInputStream = if (cert.first.startsWith("/")) {
|
||||||
|
FileInputStream(cert.first)
|
||||||
|
} else {
|
||||||
|
val decodedBytes = Base64.decode(cert.first)
|
||||||
|
ByteArrayInputStream(decodedBytes)
|
||||||
|
}
|
||||||
val certFactory = CertificateFactory.getInstance("X.509")
|
val certFactory = CertificateFactory.getInstance("X.509")
|
||||||
val fileInputStream = FileInputStream(cert.first)
|
|
||||||
val recipientPublicKey = certFactory.generateCertificate(fileInputStream) as X509Certificate
|
val recipientPublicKey = certFactory.generateCertificate(fileInputStream) as X509Certificate
|
||||||
Log.d(TAG, "X.509 Certificate: $recipientPublicKey")
|
Log.d(TAG, "X.509 Certificate: $recipientPublicKey")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -408,7 +421,12 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
|
|||||||
cert.first.isNotEmpty() && cert.second.isNotEmpty() -> {
|
cert.first.isNotEmpty() && cert.second.isNotEmpty() -> {
|
||||||
try {
|
try {
|
||||||
//从私钥证书文件提取公钥
|
//从私钥证书文件提取公钥
|
||||||
val recipientPrivateKeyStream = FileInputStream(cert.first)
|
val recipientPrivateKeyStream = if (cert.first.startsWith("/")) {
|
||||||
|
FileInputStream(cert.first)
|
||||||
|
} else {
|
||||||
|
val decodedBytes = Base64.decode(cert.first)
|
||||||
|
ByteArrayInputStream(decodedBytes)
|
||||||
|
}
|
||||||
val recipientPGPSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(recipientPrivateKeyStream)
|
val recipientPGPSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(recipientPrivateKeyStream)
|
||||||
val recipientPGPPublicKeyRing = PGPainless.extractCertificate(recipientPGPSecretKeyRing!!)
|
val recipientPGPPublicKeyRing = PGPainless.extractCertificate(recipientPGPSecretKeyRing!!)
|
||||||
val keyInfo = KeyRingInfo(recipientPGPPublicKeyRing)
|
val keyInfo = KeyRingInfo(recipientPGPPublicKeyRing)
|
||||||
@ -422,7 +440,12 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
|
|||||||
cert.first.isNotEmpty() && cert.second.isEmpty() -> {
|
cert.first.isNotEmpty() && cert.second.isEmpty() -> {
|
||||||
try {
|
try {
|
||||||
//从证书文件提取公钥
|
//从证书文件提取公钥
|
||||||
val recipientPublicKeyStream = FileInputStream(cert.first)
|
val recipientPublicKeyStream = if (cert.first.startsWith("/")) {
|
||||||
|
FileInputStream(cert.first)
|
||||||
|
} else {
|
||||||
|
val decodedBytes = Base64.decode(cert.first)
|
||||||
|
ByteArrayInputStream(decodedBytes)
|
||||||
|
}
|
||||||
val recipientPGPPublicKeyRing = PGPainless.readKeyRing().publicKeyRing(recipientPublicKeyStream)
|
val recipientPGPPublicKeyRing = PGPainless.readKeyRing().publicKeyRing(recipientPublicKeyStream)
|
||||||
val keyInfo = KeyRingInfo(recipientPGPPublicKeyRing!!)
|
val keyInfo = KeyRingInfo(recipientPGPPublicKeyRing!!)
|
||||||
Log.d(TAG, "recipientPGPPublicKeyRing: $keyInfo")
|
Log.d(TAG, "recipientPGPPublicKeyRing: $keyInfo")
|
||||||
@ -448,7 +471,12 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
|
|||||||
val keystore = binding!!.etSenderKeystore.text.toString().trim()
|
val keystore = binding!!.etSenderKeystore.text.toString().trim()
|
||||||
val password = binding!!.etSenderPassword.text.toString().trim()
|
val password = binding!!.etSenderPassword.text.toString().trim()
|
||||||
if (keystore.isNotEmpty()) {
|
if (keystore.isNotEmpty()) {
|
||||||
val senderPrivateKeyStream = FileInputStream(keystore)
|
val senderPrivateKeyStream = if (keystore.startsWith("/")) {
|
||||||
|
FileInputStream(keystore)
|
||||||
|
} else {
|
||||||
|
val decodedBytes = Base64.decode(keystore)
|
||||||
|
ByteArrayInputStream(decodedBytes)
|
||||||
|
}
|
||||||
if (senderPrivateKeyStream.available() <= 0) {
|
if (senderPrivateKeyStream.available() <= 0) {
|
||||||
throw Exception(getString(R.string.invalid_sender_keystore))
|
throw Exception(getString(R.string.invalid_sender_keystore))
|
||||||
}
|
}
|
||||||
@ -567,12 +595,12 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
MaterialDialog.Builder(requireContext())
|
MaterialDialog.Builder(requireContext())
|
||||||
.title(getString(R.string.keystore_path))
|
.title(getString(R.string.keystore_base64))
|
||||||
.content(String.format(getString(R.string.root_directory), downloadPath))
|
.content(String.format(getString(R.string.root_directory), downloadPath))
|
||||||
.items(fileList)
|
.items(fileList)
|
||||||
.itemsCallbackSingleChoice(0) { _: MaterialDialog?, _: View?, _: Int, text: CharSequence ->
|
.itemsCallbackSingleChoice(0) { _: MaterialDialog?, _: View?, _: Int, text: CharSequence ->
|
||||||
val webPath = "$downloadPath/$text"
|
val webPath = "$downloadPath/$text"
|
||||||
etKeyStore.setText(webPath)
|
etKeyStore.setText(convertCertToBase64String(webPath))
|
||||||
true // allow selection
|
true // allow selection
|
||||||
}
|
}
|
||||||
.positiveText(R.string.select)
|
.positiveText(R.string.select)
|
||||||
@ -614,6 +642,12 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
|
|||||||
return supportedExtensions.any { it.equals(file.extension, ignoreCase = true) }
|
return supportedExtensions.any { it.equals(file.extension, ignoreCase = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun convertCertToBase64String(pfxFilePath: String): String {
|
||||||
|
val pfxInputStream = FileInputStream(pfxFilePath)
|
||||||
|
val pfxBytes = pfxInputStream.readBytes()
|
||||||
|
return Base64.encode(pfxBytes)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
|
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
|
@ -4,6 +4,7 @@ import com.idormy.sms.forwarder.R
|
|||||||
import com.idormy.sms.forwarder.database.entity.Rule
|
import com.idormy.sms.forwarder.database.entity.Rule
|
||||||
import com.idormy.sms.forwarder.entity.MsgInfo
|
import com.idormy.sms.forwarder.entity.MsgInfo
|
||||||
import com.idormy.sms.forwarder.entity.setting.EmailSetting
|
import com.idormy.sms.forwarder.entity.setting.EmailSetting
|
||||||
|
import com.idormy.sms.forwarder.utils.Base64
|
||||||
import com.idormy.sms.forwarder.utils.Log
|
import com.idormy.sms.forwarder.utils.Log
|
||||||
import com.idormy.sms.forwarder.utils.SendUtils
|
import com.idormy.sms.forwarder.utils.SendUtils
|
||||||
import com.idormy.sms.forwarder.utils.SettingUtils
|
import com.idormy.sms.forwarder.utils.SettingUtils
|
||||||
@ -16,6 +17,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing
|
|||||||
import org.bouncycastle.openpgp.PGPSecretKeyRing
|
import org.bouncycastle.openpgp.PGPSecretKeyRing
|
||||||
import org.pgpainless.PGPainless
|
import org.pgpainless.PGPainless
|
||||||
import org.pgpainless.key.info.KeyRingInfo
|
import org.pgpainless.key.info.KeyRingInfo
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
@ -166,8 +168,13 @@ class EmailUtils {
|
|||||||
var senderPGPSecretKeyPassword = ""
|
var senderPGPSecretKeyPassword = ""
|
||||||
|
|
||||||
if (!setting.keystore.isNullOrEmpty() && !setting.password.isNullOrEmpty()) {
|
if (!setting.keystore.isNullOrEmpty() && !setting.password.isNullOrEmpty()) {
|
||||||
val keystoreStream = FileInputStream(setting.keystore)
|
|
||||||
try {
|
try {
|
||||||
|
val keystoreStream = if (setting.keystore!!.startsWith("/")) {
|
||||||
|
FileInputStream(setting.keystore)
|
||||||
|
} else {
|
||||||
|
val decodedBytes = Base64.decode(setting.keystore!!)
|
||||||
|
ByteArrayInputStream(decodedBytes)
|
||||||
|
}
|
||||||
when (setting.encryptionProtocol) {
|
when (setting.encryptionProtocol) {
|
||||||
"S/MIME" -> {
|
"S/MIME" -> {
|
||||||
val keystorePassword = setting.password.toString()
|
val keystorePassword = setting.password.toString()
|
||||||
@ -206,15 +213,21 @@ class EmailUtils {
|
|||||||
//逐一发送加密邮件
|
//逐一发送加密邮件
|
||||||
val recipientsWithoutCert = mutableListOf<String>()
|
val recipientsWithoutCert = mutableListOf<String>()
|
||||||
setting.recipients.forEach { (email, cert) ->
|
setting.recipients.forEach { (email, cert) ->
|
||||||
val keystorePath = cert.first
|
val keystoreBase64 = cert.first
|
||||||
val keystorePassword = cert.second
|
val keystorePassword = cert.second
|
||||||
var recipientX509Cert: X509Certificate? = null
|
var recipientX509Cert: X509Certificate? = null
|
||||||
var recipientPGPPublicKeyRing: PGPPublicKeyRing? = null
|
var recipientPGPPublicKeyRing: PGPPublicKeyRing? = null
|
||||||
try {
|
try {
|
||||||
when {
|
when {
|
||||||
//从私钥证书文件提取公钥
|
//从私钥证书文件提取公钥
|
||||||
keystorePath.isNotEmpty() && keystorePassword.isNotEmpty() -> {
|
keystoreBase64.isNotEmpty() && keystorePassword.isNotEmpty() -> {
|
||||||
val keystoreStream = FileInputStream(keystorePath)
|
val keystoreStream = if (keystoreBase64.startsWith("/")) {
|
||||||
|
FileInputStream(keystoreBase64)
|
||||||
|
} else {
|
||||||
|
val decodedBytes = Base64.decode(keystoreBase64)
|
||||||
|
ByteArrayInputStream(decodedBytes)
|
||||||
|
}
|
||||||
|
|
||||||
when (setting.encryptionProtocol) {
|
when (setting.encryptionProtocol) {
|
||||||
"S/MIME" -> {
|
"S/MIME" -> {
|
||||||
val keyStore = KeyStore.getInstance("PKCS12")
|
val keyStore = KeyStore.getInstance("PKCS12")
|
||||||
@ -235,12 +248,18 @@ class EmailUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//从证书文件提取公钥
|
//从证书文件提取公钥
|
||||||
keystorePath.isNotEmpty() && keystorePassword.isEmpty() -> {
|
keystoreBase64.isNotEmpty() && keystorePassword.isEmpty() -> {
|
||||||
val keystoreStream = FileInputStream(keystorePath)
|
val keystoreStream = if (keystoreBase64.startsWith("/")) {
|
||||||
|
FileInputStream(keystoreBase64)
|
||||||
|
} else {
|
||||||
|
val decodedBytes = Base64.decode(keystoreBase64)
|
||||||
|
ByteArrayInputStream(decodedBytes)
|
||||||
|
}
|
||||||
|
|
||||||
when (setting.encryptionProtocol) {
|
when (setting.encryptionProtocol) {
|
||||||
"S/MIME" -> {
|
"S/MIME" -> {
|
||||||
val certFactory = CertificateFactory.getInstance("X.509")
|
val certFactory = CertificateFactory.getInstance("X.509")
|
||||||
recipientX509Cert = certFactory.generateCertificate(FileInputStream(keystorePath)) as X509Certificate
|
recipientX509Cert = certFactory.generateCertificate(FileInputStream(keystoreBase64)) as X509Certificate
|
||||||
}
|
}
|
||||||
|
|
||||||
"OpenPGP" -> {
|
"OpenPGP" -> {
|
||||||
|
@ -368,7 +368,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/keystore_path"
|
android:text="@string/keystore_base64"
|
||||||
android:textSize="@dimen/text_size_small"
|
android:textSize="@dimen/text_size_small"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
@ -378,10 +378,13 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="5dp"
|
android:layout_marginStart="5dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:hint="@string/keystore_path_tips"
|
android:hint="@string/keystore_base64_tips"
|
||||||
android:importantForAutofill="no"
|
android:importantForAutofill="no"
|
||||||
android:singleLine="true"
|
android:inputType="textMultiLine"
|
||||||
android:textSize="@dimen/text_size_small"
|
android:maxLines="5"
|
||||||
|
android:minLines="2"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:textSize="@dimen/text_size_mini"
|
||||||
app:met_clearButton="true" />
|
app:met_clearButton="true" />
|
||||||
|
|
||||||
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
|
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/keystore_path"
|
android:text="@string/keystore_base64"
|
||||||
android:textSize="@dimen/text_size_small"
|
android:textSize="@dimen/text_size_small"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
@ -73,10 +73,13 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="5dp"
|
android:layout_marginStart="5dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:hint="@string/keystore_path_tips"
|
android:hint="@string/keystore_base64_tips"
|
||||||
android:importantForAutofill="no"
|
android:importantForAutofill="no"
|
||||||
android:singleLine="true"
|
android:inputType="textMultiLine"
|
||||||
android:textSize="@dimen/text_size_small"
|
android:maxLines="5"
|
||||||
|
android:minLines="2"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:textSize="@dimen/text_size_mini"
|
||||||
app:met_clearButton="true" />
|
app:met_clearButton="true" />
|
||||||
|
|
||||||
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
|
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
|
||||||
|
@ -235,8 +235,8 @@
|
|||||||
<string name="sender_openpgp_keystore">Sender OpenPGP 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="invalid_sender_keystore">Invalid Sender Signing Private Key</string>
|
||||||
<string name="recipient_email">Recipient</string>
|
<string name="recipient_email">Recipient</string>
|
||||||
<string name="keystore_path">Cert. Path</string>
|
<string name="keystore_base64">Specified Cert.</string>
|
||||||
<string name="keystore_path_tips">Opt., Copy keystore to the Download dir</string>
|
<string name="keystore_base64_tips">Opt., Copy keystore to the Download dir</string>
|
||||||
<string name="keystore_password">Cert. Pwd.</string>
|
<string name="keystore_password">Cert. Pwd.</string>
|
||||||
<string name="keystore_password_tips">Import password for `Private key`</string>
|
<string name="keystore_password_tips">Import password for `Private key`</string>
|
||||||
<string name="email_title">Email Title</string>
|
<string name="email_title">Email Title</string>
|
||||||
|
@ -236,8 +236,8 @@
|
|||||||
<string name="sender_openpgp_keystore">发件人OpenPGP签名私钥(可选)</string>
|
<string name="sender_openpgp_keystore">发件人OpenPGP签名私钥(可选)</string>
|
||||||
<string name="invalid_sender_keystore">发件人签名私钥无效</string>
|
<string name="invalid_sender_keystore">发件人签名私钥无效</string>
|
||||||
<string name="recipient_email">收件人邮箱</string>
|
<string name="recipient_email">收件人邮箱</string>
|
||||||
<string name="keystore_path">证书路径</string>
|
<string name="keystore_base64">指定证书</string>
|
||||||
<string name="keystore_path_tips">可选,下载证书文件到 Download 目录</string>
|
<string name="keystore_base64_tips">可选,下载证书文件到 Download 目录</string>
|
||||||
<string name="keystore_password">证书密码</string>
|
<string name="keystore_password">证书密码</string>
|
||||||
<string name="keystore_password_tips">`私钥证书`对应的导入密钥</string>
|
<string name="keystore_password_tips">`私钥证书`对应的导入密钥</string>
|
||||||
<string name="email_title">邮件主题</string>
|
<string name="email_title">邮件主题</string>
|
||||||
|
@ -236,8 +236,8 @@
|
|||||||
<string name="sender_openpgp_keystore">發件人OpenPGP簽名私鑰(可選)</string>
|
<string name="sender_openpgp_keystore">發件人OpenPGP簽名私鑰(可選)</string>
|
||||||
<string name="invalid_sender_keystore">發件人簽名私鑰無效</string>
|
<string name="invalid_sender_keystore">發件人簽名私鑰無效</string>
|
||||||
<string name="recipient_email">收件人郵箱</string>
|
<string name="recipient_email">收件人郵箱</string>
|
||||||
<string name="keystore_path">證書路徑</string>
|
<string name="keystore_base64">指定證書</string>
|
||||||
<string name="keystore_path_tips">可選,下載證書文件到 Download 目錄</string>
|
<string name="keystore_base64_tips">可選,下載證書文件到 Download 目錄</string>
|
||||||
<string name="keystore_password">證書密碼</string>
|
<string name="keystore_password">證書密碼</string>
|
||||||
<string name="keystore_password_tips">「私鑰證書」相對應的導入密碼</string>
|
<string name="keystore_password_tips">「私鑰證書」相對應的導入密碼</string>
|
||||||
<string name="email_title">郵件主題</string>
|
<string name="email_title">郵件主題</string>
|
||||||
|
@ -262,8 +262,8 @@
|
|||||||
<string name="sender_openpgp_keystore">发件人OpenPGP签名私钥(可选)</string>
|
<string name="sender_openpgp_keystore">发件人OpenPGP签名私钥(可选)</string>
|
||||||
<string name="invalid_sender_keystore">发件人签名私钥无效</string>
|
<string name="invalid_sender_keystore">发件人签名私钥无效</string>
|
||||||
<string name="recipient_email">收件人邮箱</string>
|
<string name="recipient_email">收件人邮箱</string>
|
||||||
<string name="keystore_path">证书路径</string>
|
<string name="keystore_base64">指定证书</string>
|
||||||
<string name="keystore_path_tips">可选,下载证书文件到 Download 目录</string>
|
<string name="keystore_base64_tips">可选,下载证书文件到 Download 目录</string>
|
||||||
<string name="keystore_password">证书密码</string>
|
<string name="keystore_password">证书密码</string>
|
||||||
<string name="keystore_password_tips">`私钥证书`对应的导入密钥</string>
|
<string name="keystore_password_tips">`私钥证书`对应的导入密钥</string>
|
||||||
<string name="email_title">邮件主题</string>
|
<string name="email_title">邮件主题</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user