Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ linters:
wrapcheck:
extra-ignore-sigs:
- .JSON(
- .Send(
- .SendStatus(

exclusions:
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ The following scopes are available for token generation:

- `devices:delete` - Delete devices
- `devices:list` - List connected devices
- `inbox:list` - List incoming messages with filters
- `inbox:read` - Read incoming messages
- `inbox:list` - List inbox messages with filters
- `inbox:read` - Read inbox messages
- `logs:read` - Read server logs
- `messages:export` - Export messages
- `messages:list` - List messages
Expand Down
94 changes: 94 additions & 0 deletions api/mobile.http
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,97 @@ Authorization: Bearer {{mobileToken}}
###
GET {{baseUrl}}/events HTTP/1.1
Authorization: Bearer {{mobileToken}}

###
POST {{baseUrl}}/inbox HTTP/1.1
Authorization: Bearer {{mobileToken}}
Content-Type: application/json

[
{
"id": "sms-001",
"type": "SMS",
"sender": "+79990001234",
"recipient": "+79990005678",
"simNumber": 1,
"content": "a2V5X2VuY3J5cHRlZF9jb250ZW50",
"isEncrypted": true,
"createdAt": "2026-01-15T10:30:00Z"
}
]

###
POST {{baseUrl}}/inbox HTTP/1.1
Authorization: Bearer {{mobileToken}}
Content-Type: application/json

[
{
"id": "mms-001",
"type": "MMS",
"sender": "+79990001234",
"recipient": "+79990005678",
"simNumber": 1,
"content": "a2V5X2VuY3J5cHRlZF9jb250ZW50",
"isEncrypted": true,
"createdAt": "2026-01-15T11:00:00Z",
"attachments": [
{
"partId": 1,
"contentType": "image/jpeg",
"name": "enc_photo.jpg",
"size": 102400,
"data": "base64encodeddata"
},
{
"partId": 2,
"contentType": "text/plain",
"name": "enc_text.txt",
"data": "base64encodeddata"
Comment thread
capcom6 marked this conversation as resolved.
}
]
}
]

###
POST {{baseUrl}}/inbox HTTP/1.1
Authorization: Bearer {{mobileToken}}
Content-Type: application/json

[
{
"id": "sms-batch-001",
"type": "SMS",
"sender": "+79990001111",
"content": "a2V5X2VuY3J5cHRlZF9jb250ZW50XzE=",
"isEncrypted": true,
"createdAt": "2026-01-15T12:00:00Z"
},
{
"id": "sms-batch-002",
"type": "DATA_SMS",
"sender": "+79990002222",
"recipient": "+79990005678",
"simNumber": 2,
"content": "a2V5X2VuY3J5cHRlZF9jb250ZW50XzI=",
"isEncrypted": true,
"createdAt": "2026-01-15T12:01:00Z"
},
{
"id": "mms-batch-001",
"type": "MMS_DOWNLOADED",
"sender": "+79990003333",
"content": "a2V5X2VuY3J5cHRlZF9jb250ZW50XzM=",
"isEncrypted": true,
"createdAt": "2026-01-15T12:02:00Z",
"attachments": [
{
"partId": 1,
"contentType": "image/png",
"name": "enc_image.png",
"size": 204800,
"data": "base64encodeddata"
}
]
}
]
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.25.8

require (
firebase.google.com/go/v4 v4.21.0
github.com/android-sms-gateway/client-go v1.14.5
github.com/android-sms-gateway/client-go v1.14.6-0.20260818003959-ca8711dc4282
github.com/ansrivas/fiberprometheus/v2 v2.17.0
github.com/capcom6/go-helpers v0.4.0
github.com/capcom6/go-infra-fx v0.5.9
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
github.com/android-sms-gateway/client-go v1.14.5 h1:CtyXAHPdyDtCFTzeVPt5EUfnWhQ2RsIWbCUk52tbGjw=
github.com/android-sms-gateway/client-go v1.14.5/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
github.com/android-sms-gateway/client-go v1.14.6-0.20260818003959-ca8711dc4282 h1:L02pWbWnYwZJAoxdtyKi9tru7HDCLAGyJcFInwws8TQ=
github.com/android-sms-gateway/client-go v1.14.6-0.20260818003959-ca8711dc4282/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/ansrivas/fiberprometheus/v2 v2.17.0 h1:p0gqs5LsSCWGoSFF44fCJkyU+XcE6TLRqEMu80b2iCo=
Expand Down
4 changes: 4 additions & 0 deletions internal/config/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"time"

"github.com/android-sms-gateway/server/internal/sms-gateway/handlers"
"github.com/android-sms-gateway/server/internal/sms-gateway/inbox"
"github.com/android-sms-gateway/server/internal/sms-gateway/jwt"
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/auth"
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/devices"
Expand Down Expand Up @@ -128,6 +129,9 @@ func Module() fx.Option {
fx.Provide(func(_ Config) devices.Config {
return devices.Config{}
}),
fx.Provide(func(_ Config) inbox.Config {
return inbox.Config{}
}),
fx.Provide(func(cfg Config) sse.Config {
return sse.NewConfig(
sse.WithKeepAlivePeriod(time.Duration(cfg.SSE.KeepAlivePeriodSeconds) * time.Second),
Expand Down
97 changes: 90 additions & 7 deletions internal/sms-gateway/handlers/inbox/3rdparty.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package inbox

import (
"errors"
"fmt"
"strconv"

"github.com/android-sms-gateway/client-go/smsgateway"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/base"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/permissions"
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/userauth"
"github.com/android-sms-gateway/server/internal/sms-gateway/inbox"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
"go.uber.org/zap"
)

Expand All @@ -33,23 +38,25 @@ func NewThirdPartyController(
}

func (h *ThirdPartyController) Register(router fiber.Router) {
router.Use(errorHandler)
router.Get("", permissions.RequireScope(ScopeList), userauth.WithUserID(h.list))
router.Get("/:id/attachments/:partId", permissions.RequireScope(ScopeRead), userauth.WithUserID(h.getAttachment))
router.Post("/refresh", permissions.RequireScope(ScopeRefresh), userauth.WithUserID(h.refresh))
}

// @Summary Get incoming messages
// @Description Retrieves incoming messages with filtering and pagination.
// @Summary Get inbox messages
// @Description Retrieves inbox messages with filtering and pagination.
// @Security ApiAuth
// @Security JWTAuth
// @Tags User, Inbox
// @Produce json
// @Param type query string false "Filter incoming messages by type" Enums(SMS,DATA_SMS,MMS,MMS_DOWNLOADED)
// @Param type query string false "Filter inbox messages by type" Enums(SMS,DATA_SMS,MMS,MMS_DOWNLOADED)
// @Param limit query int false "Maximum number of messages to return" minimum(1) maximum(500) default(50)
// @Param offset query int false "Number of messages to skip" minimum(0) default(0)
// @Param from query string false "Start of date range (ISO 8601)" Format(date-time)
// @Param to query string false "End of date range (ISO 8601)" Format(date-time)
// @Param deviceId query string false "Device ID"
// @Success 200 {array} smsgateway.IncomingMessage "A list of incoming messages"
// @Success 200 {array} smsgateway.IncomingMessage "A list of inbox messages"
// @Header 200 {integer} X-Total-Count "Total number of items available"
// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request"
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
Expand All @@ -58,9 +65,46 @@ func (h *ThirdPartyController) Register(router fiber.Router) {
// @Failure 501 {object} smsgateway.ErrorResponse "Not implemented"
// @Router /3rdparty/v1/inbox [get]
//
// Get incoming messages.
func (h *ThirdPartyController) list(_ string, _ *fiber.Ctx) error {
return fiber.NewError(fiber.StatusNotImplemented, "Inbox API is not implemented yet")
// Get inbox messages.
func (h *ThirdPartyController) list(userID string, c *fiber.Ctx) error {
var params thirdPartyListParams
if err := c.QueryParser(&params); err != nil {
h.Logger.Error("failed to parse query parameters", zap.Error(err))
return fiber.NewError(fiber.StatusBadRequest, "failed to parse query parameters")
}

messages, total, err := h.inboxSvc.List(userID, params.toFilter(), params.toOptions())
if err != nil {
h.Logger.Error("failed to list inbox messages", zap.Error(err), zap.String("user_id", userID))
return fiber.NewError(fiber.StatusInternalServerError, "failed to list inbox messages")
}

result := make([]smsgateway.IncomingMessage, len(messages))
for i, m := range messages {
atts := make([]smsgateway.InboxAttachment, len(m.Attachments))
for j, a := range m.Attachments {
atts[j] = smsgateway.InboxAttachment{
PartID: a.PartID,
Name: a.Name,
Size: lo.FromPtrOr(a.Size, 0),
ContentType: a.ContentType,
}
}
result[i] = smsgateway.IncomingMessage{
ID: m.ExtID,
Type: smsgateway.IncomingMessageType(m.Type),
Sender: m.Sender,
Recipient: m.Recipient,
SimNumber: m.SimNumber,
ContentPreview: m.Content,
IsEncrypted: m.IsEncrypted,
CreatedAt: m.CreatedAt,
Attachments: atts,
}
}

c.Set("X-Total-Count", strconv.Itoa(int(total)))
return c.JSON(result)
}

// @Summary Request inbox messages refresh
Expand Down Expand Up @@ -98,3 +142,42 @@ func (h *ThirdPartyController) refresh(userID string, c *fiber.Ctx) error {

return c.SendStatus(fiber.StatusAccepted)
}

// @Summary Get attachment
// @Description Downloads an attachment from an inbox message by message ID and part ID.
// @Security ApiAuth
// @Security JWTAuth
// @Tags User, Inbox
// @Produce application/octet-stream
// @Param id path string true "Inbox message ID"
// @Param partId path int true "Attachment part ID"
// @Success 200 {file} binary "Attachment file"
// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request"
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
// @Failure 403 {object} smsgateway.ErrorResponse "Forbidden"
// @Failure 404 {object} smsgateway.ErrorResponse "Not found"
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
// @Router /3rdparty/v1/inbox/{id}/attachments/{partId} [get]
//
// Get attachment.
func (h *ThirdPartyController) getAttachment(userID string, c *fiber.Ctx) error {
id := c.Params("id")
partID, err := strconv.ParseInt(c.Params("partId"), 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid partId")
}

att, err := h.inboxSvc.GetAttachment(c.Context(), userID, id, partID)
if err != nil {
if errors.Is(err, inbox.ErrNotFound) {
return fiber.NewError(fiber.StatusNotFound, "attachment not found")
}
h.Logger.Error("failed to get attachment", zap.Error(err), zap.String("user_id", userID))
return fiber.NewError(fiber.StatusInternalServerError, "failed to get attachment")
}

c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, att.Name))
c.Set("Content-Type", att.ContentType)

return c.Send(att.Data)
}
26 changes: 26 additions & 0 deletions internal/sms-gateway/handlers/inbox/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package inbox

import (
"errors"

"github.com/android-sms-gateway/server/internal/sms-gateway/inbox"
"github.com/gofiber/fiber/v2"
)

func errorHandler(c *fiber.Ctx) error {
err := c.Next()
if err == nil {
return nil
}

switch {
case errors.Is(err, inbox.ErrNotEncrypted):
return fiber.NewError(fiber.StatusBadRequest, err.Error())
case errors.Is(err, inbox.ErrNotFound):
return fiber.NewError(fiber.StatusNotFound, err.Error())
case errors.Is(err, inbox.ErrEmptyBatch):
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}

return err //nolint:wrapcheck // passed through to fiber's error handler
}
Loading
Loading