Skip to content

Commit 75459a1

Browse files
committed
Add fix for MMS attachments
1 parent 50fd7ae commit 75459a1

File tree

16 files changed

+169
-282
lines changed

16 files changed

+169
-282
lines changed

android/app/src/main/java/com/httpsms/Constants.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,7 @@ class Constants {
1818
const val SIM2 = "SIM2"
1919

2020
const val TIMESTAMP_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'000000'ZZZZZ"
21+
22+
const val MAX_MMS_ATTACHMENT_SIZE = 1.5 * 1024 * 1024;
2123
}
2224
}

android/app/src/main/java/com/httpsms/FirebaseMessagingService.kt

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import com.google.android.mms.pdu_alt.PduBody
1515
import com.google.android.mms.pdu_alt.PduComposer
1616
import com.google.android.mms.pdu_alt.PduPart
1717
import com.google.android.mms.pdu_alt.SendReq
18+
import okhttp3.MediaType
19+
import java.io.File
1820

1921
class MyFirebaseMessagingService : FirebaseMessagingService() {
2022
// [START receive_message]
@@ -177,20 +179,35 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
177179
return handleMultipartMessage(message, parts)
178180
}
179181

182+
fun extractFileName(url: String, prefix: String, mimeType: String? = null): String {
183+
val fileName = url.substringAfterLast("/")
184+
.substringBefore("?")
185+
.takeIf { it.isNotBlank() && it.contains(".") }
186+
?: run {
187+
val extension = mimeType?.let { mime ->
188+
val ext = mime.substringAfterLast("/")
189+
if (ext.isNotBlank()) ".$ext" else ""
190+
} ?: ""
191+
"attachment$extension"
192+
}
193+
194+
return "${prefix}_$fileName"
195+
}
196+
180197
private fun handleMmsMessage(message: Message): Result {
181198
Timber.d("Processing MMS for message ID [${message.id}]")
182199
val apiService = HttpSmsApiService.create(applicationContext)
183200

184-
val downloadedFiles = mutableListOf<java.io.File>()
201+
val downloadedFiles = mutableListOf<Pair<File, MediaType>>()
185202

186203
try {
187204
for ((index, attachment) in message.attachments!!.withIndex()) {
188-
val file = apiService.downloadAttachment(applicationContext, attachment.url, message.id, index)
189-
if (file == null) {
205+
val file = apiService.downloadAttachment(applicationContext, attachment, message.id, index)
206+
if (file.first == null || file.second == null) {
190207
handleFailed(applicationContext, message.id, "Failed to download attachment or file size exceeded 1.5MB.")
191208
return Result.failure()
192209
}
193-
downloadedFiles.add(file)
210+
downloadedFiles.add(Pair(file.first!!, file.second!!))
194211
}
195212

196213
val sendReq = SendReq()
@@ -207,30 +224,32 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
207224
textPart.name = "text".toByteArray()
208225
textPart.contentId = "text".toByteArray()
209226
textPart.contentLocation = "text".toByteArray()
210-
227+
211228
var messageBody = message.content
212229
val encryptionKey = Settings.getEncryptionKey(applicationContext)
213230
if (message.encrypted && !encryptionKey.isNullOrEmpty()) {
214231
messageBody = Encrypter.decrypt(encryptionKey, messageBody)
215232
}
216233
textPart.data = messageBody.toByteArray(Charsets.UTF_8)
217-
234+
218235
pduBody.addPart(textPart)
219236
}
220237

221238
for ((index, file) in downloadedFiles.withIndex()) {
222-
val attachment = message.attachments[index]
223-
val fileBytes = file.readBytes()
239+
val fileBytes = file.first.readBytes()
224240

225241
val mediaPart = PduPart()
226-
mediaPart.contentType = attachment.contentType.toByteArray()
227-
228-
val fileName = "attachment_$index".toByteArray()
229-
mediaPart.name = fileName
230-
mediaPart.contentId = fileName
231-
mediaPart.contentLocation = fileName
242+
mediaPart.contentType = file.second.toString().toByteArray()
243+
244+
245+
val fileName = extractFileName(message.attachments[index], index.toString(), file.second.toString())
246+
mediaPart.name = fileName.toByteArray()
247+
mediaPart.contentId = fileName.toByteArray()
248+
mediaPart.contentLocation = fileName.toByteArray()
232249
mediaPart.data = fileBytes
233-
250+
251+
Timber.d("Adding MMS attachment with name [$fileName] and size [${fileBytes.size}] and type [${file.second}]")
252+
234253
pduBody.addPart(mediaPart)
235254
}
236255

@@ -249,7 +268,7 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
249268
if (!mmsDir.exists()) {
250269
mmsDir.mkdirs()
251270
}
252-
271+
253272
val pduFile = java.io.File(mmsDir, "pdu_${message.id}.dat")
254273
java.io.FileOutputStream(pduFile).use { it.write(pduBytes) }
255274

@@ -272,15 +291,15 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
272291
} finally {
273292
// Clean up any downloaded temporary files
274293
downloadedFiles.forEach { file ->
275-
if (file.exists()) {
276-
file.delete()
294+
if (file.first.exists()) {
295+
file.first.delete()
277296
}
278297
}
279298

280299
// Also clean up the MMS PDU file to avoid cache buildup in cases where
281300
// sendMultimediaMessage fails before the sent broadcast is delivered.
282301
try {
283-
val pduFile = java.io.File(applicationContext.cacheDir, "pdu_${message.id}.dat")
302+
val pduFile = File(applicationContext.cacheDir, "pdu_${message.id}.dat")
284303
if (pduFile.exists()) {
285304
val deleted = pduFile.delete()
286305
if (!deleted) {

android/app/src/main/java/com/httpsms/HttpSmsApiService.kt

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
package com.httpsms
22

33
import android.content.Context
4+
import com.httpsms.Constants.Companion.MAX_MMS_ATTACHMENT_SIZE
5+
import okhttp3.MediaType
46
import okhttp3.MediaType.Companion.toMediaType
57
import okhttp3.OkHttpClient
68
import okhttp3.Request
79
import okhttp3.RequestBody.Companion.toRequestBody
810
import org.apache.commons.text.StringEscapeUtils
911
import timber.log.Timber
10-
import java.net.URI
11-
import java.net.URL
12-
import java.util.logging.Level
13-
import java.util.logging.Logger.getLogger
1412
import java.io.File
1513
import java.io.FileOutputStream
1614
import java.io.IOException
1715
import java.io.InputStream
1816
import java.io.OutputStream
17+
import java.net.URI
18+
import java.net.URL
19+
import java.util.logging.Level
20+
import java.util.logging.Logger.getLogger
1921

2022

2123
class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) {
@@ -162,49 +164,43 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) {
162164
}
163165

164166
fun InputStream.copyToWithLimit(
165-
out: OutputStream,
166-
limit: Long,
167+
out: OutputStream,
168+
limit: Long,
167169
bufferSize: Int = DEFAULT_BUFFER_SIZE
168170
): Long {
169171
var bytesCopied: Long = 0
170172
val buffer = ByteArray(bufferSize)
171173
var bytes = read(buffer)
172-
174+
173175
while (bytes >= 0) {
174176
bytesCopied += bytes
175-
177+
176178
if (bytesCopied > limit) {
177179
throw IOException("Download aborted: File exceeded maximum allowed size of $limit bytes.")
178180
}
179-
181+
180182
out.write(buffer, 0, bytes)
181183
bytes = read(buffer)
182184
}
183185
return bytesCopied
184186
}
185187

186188
// Downloads the attachment URL content locally
187-
fun downloadAttachment(context: Context, urlString: String, messageId: String, attachmentIndex: Int): File? {
189+
fun downloadAttachment(context: Context, urlString: String, messageId: String, attachmentIndex: Int): Pair<File?, MediaType?> {
188190
val request = Request.Builder().url(urlString).build()
189191

190192
try {
191193
client.newCall(request).execute().use { response ->
192194
if (!response.isSuccessful) {
193195
Timber.e("Failed to download attachment: ${response.code}")
194-
return null
196+
return Pair(null, null)
195197
}
196198

197199
val body = response.body
198-
if (body == null) {
199-
Timber.e("Failed to download attachment: response body is null")
200-
return null
201-
}
202-
203-
val maxSizeBytes = 1.5 * 1024 * 1024 // most (modern?) carriers have a 2MB limit, so targetting 1.5MB should be safe
204200
val contentLength = body.contentLength()
205-
if (contentLength > maxSizeBytes) {
201+
if (contentLength > MAX_MMS_ATTACHMENT_SIZE) {
206202
Timber.e("Attachment is too large ($contentLength bytes).")
207-
return null
203+
return Pair(null, null)
208204
}
209205

210206
val mmsDir = File(context.cacheDir, "mms_attachments")
@@ -216,15 +212,15 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) {
216212
val inputStream = body.byteStream()
217213
FileOutputStream(tempFile).use { outputStream ->
218214
inputStream.use { input ->
219-
input.copyToWithLimit(outputStream, maxSizeBytes.toLong())
215+
input.copyToWithLimit(outputStream, MAX_MMS_ATTACHMENT_SIZE.toLong())
220216
}
221217
}
222218

223-
return tempFile
219+
return Pair(tempFile, response.body.contentType())
224220
}
225221
} catch (e: Exception) {
226222
Timber.e(e, "Exception while downloading attachment")
227-
return null
223+
return Pair(null, null)
228224
}
229225
}
230226

android/app/src/main/java/com/httpsms/Models.kt

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,6 @@ data class Phone (
2929
val userID: String,
3030
)
3131

32-
// mms attachment
33-
data class Attachment (
34-
@Json(name = "content_type")
35-
val contentType: String,
36-
37-
val url: String
38-
)
39-
4032
data class Message (
4133
val contact: String,
4234
val content: String,
@@ -78,5 +70,5 @@ data class Message (
7870
@Json(name = "updated_at")
7971
val updatedAt: String,
8072

81-
val attachments: List<Attachment>? = null
73+
val attachments: List<String>? = null
8274
)

api/pkg/entities/message.go

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package entities
22

33
import (
4-
"strings"
54
"time"
65

76
"github.com/google/uuid"
7+
"github.com/lib/pq"
88
)
99

1010
// MessageType is the type of message if it is incoming or outgoing
@@ -82,23 +82,18 @@ func (s SIM) String() string {
8282
return string(s)
8383
}
8484

85-
type MessageAttachment struct {
86-
ContentType string `json:"content_type" example:"image/jpeg"`
87-
URL string `json:"url" example:"https://example.com/image.jpg"`
88-
}
89-
9085
// Message represents a message sent between 2 phone numbers
9186
type Message struct {
92-
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
93-
RequestID *string `json:"request_id" example:"153554b5-ae44-44a0-8f4f-7bbac5657ad4" validate:"optional"`
94-
Owner string `json:"owner" example:"+18005550199"`
95-
UserID UserID `json:"user_id" gorm:"index:idx_messages__user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
96-
Contact string `json:"contact" example:"+18005550100"`
97-
Content string `json:"content" example:"This is a sample text message"`
98-
Attachments []MessageAttachment `json:"attachments,omitempty" gorm:"type:json;serializer:json"`
99-
Encrypted bool `json:"encrypted" example:"false" gorm:"default:false"`
100-
Type MessageType `json:"type" example:"mobile-terminated"`
101-
Status MessageStatus `json:"status" example:"pending"`
87+
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
88+
RequestID *string `json:"request_id" example:"153554b5-ae44-44a0-8f4f-7bbac5657ad4" validate:"optional"`
89+
Owner string `json:"owner" example:"+18005550199"`
90+
UserID UserID `json:"user_id" gorm:"index:idx_messages__user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
91+
Contact string `json:"contact" example:"+18005550100"`
92+
Content string `json:"content" example:"This is a sample text message"`
93+
Attachments pq.StringArray `json:"attachments" gorm:"type:text[]" swaggertype:"array,string"`
94+
Encrypted bool `json:"encrypted" example:"false" gorm:"default:false"`
95+
Type MessageType `json:"type" example:"mobile-terminated"`
96+
Status MessageStatus `json:"status" example:"pending"`
10297
// SIM is the SIM card to use to send the message
10398
// * SMS1: use the SIM card in slot 1
10499
// * SMS2: use the SIM card in slot 2
@@ -234,19 +229,3 @@ func (message *Message) updateOrderTimestamp(timestamp time.Time) {
234229
message.OrderTimestamp = timestamp
235230
}
236231
}
237-
238-
func GetAttachmentContentType(url string) string {
239-
// Since there's no easy way to set a type in the CSV, defaulting to octet-stream and then just checking the file extension in the URL
240-
contentType := "application/octet-stream"
241-
lowerURL := strings.ToLower(url)
242-
if strings.HasSuffix(lowerURL, ".jpg") || strings.HasSuffix(lowerURL, ".jpeg") {
243-
contentType = "image/jpeg"
244-
} else if strings.HasSuffix(lowerURL, ".png") {
245-
contentType = "image/png"
246-
} else if strings.HasSuffix(lowerURL, ".gif") {
247-
contentType = "image/gif"
248-
} else if strings.HasSuffix(lowerURL, ".mp4") {
249-
contentType = "video/mp4"
250-
}
251-
return contentType
252-
}

api/pkg/events/message_api_sent_event.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@ const EventTypeMessageAPISent = "message.api.sent"
1313

1414
// MessageAPISentPayload is the payload of the EventTypeMessageSent event
1515
type MessageAPISentPayload struct {
16-
MessageID uuid.UUID `json:"message_id"`
17-
UserID entities.UserID `json:"user_id"`
18-
Owner string `json:"owner"`
19-
RequestID *string `json:"request_id"`
20-
MaxSendAttempts uint `json:"max_send_attempts"`
21-
Contact string `json:"contact"`
22-
ScheduledSendTime *time.Time `json:"scheduled_send_time"`
23-
RequestReceivedAt time.Time `json:"request_received_at"`
24-
Content string `json:"content"`
25-
Attachments []entities.MessageAttachment `json:"attachments"`
26-
Encrypted bool `json:"encrypted"`
27-
SIM entities.SIM `json:"sim"`
16+
MessageID uuid.UUID `json:"message_id"`
17+
UserID entities.UserID `json:"user_id"`
18+
Owner string `json:"owner"`
19+
RequestID *string `json:"request_id"`
20+
MaxSendAttempts uint `json:"max_send_attempts"`
21+
Contact string `json:"contact"`
22+
ScheduledSendTime *time.Time `json:"scheduled_send_time"`
23+
RequestReceivedAt time.Time `json:"request_received_at"`
24+
Content string `json:"content"`
25+
Attachments []string `json:"attachments"`
26+
Encrypted bool `json:"encrypted"`
27+
SIM entities.SIM `json:"sim"`
2828
}

0 commit comments

Comments
 (0)