La integracion de Office 365 E-Mail en SAP BTP ABAP Environment permite el envio de correos electronicos directamente desde aplicaciones ABAP a traves de la Microsoft Graph API. En este articulo mostramos la configuracion completa desde la configuracion en Azure hasta el codigo ABAP funcional.
Vision general de la arquitectura
+--------------------------------------------------------------------------+| Arquitectura de integracion Office 365 Mail |+--------------------------------------------------------------------------+| || +-----------------+ +-----------------+ +-----------------+ || | BTP ABAP | | Microsoft | | Office 365 | || | Environment |---->| Entra ID |---->| Mail Server | || | | | (Azure AD) | | | || +-----------------+ +-----------------+ +-----------------+ || | | || | | || v v || +-----------------+ +-----------------+ || | Communication | | OAuth 2.0 | || | Arrangement | | Access Token | || +-----------------+ +-----------------+ || || Protocolo: HTTPS + OAuth 2.0 Client Credentials Flow || API: Microsoft Graph API (https://graph.microsoft.com) || |+--------------------------------------------------------------------------+Requisitos previos
| Componente | Requisito |
|---|---|
| Azure AD | Tenant con permisos Global Admin o Application Admin |
| Office 365 | Licencia con Exchange Online (Business Basic o superior) |
| BTP | ABAP Environment con acceso ADT |
| Mailbox | Shared Mailbox o Service Account para el envio |
1. Registro de App en Azure AD
Primero se debe crear un registro de app en Microsoft Entra ID (anteriormente Azure AD).
Paso 1: Registrar App
1. Abrir Azure Portal (portal.azure.com)2. Microsoft Entra ID -> App registrations -> New registration
Name: SAP-BTP-Mail-IntegrationSupported account types: Accounts in this organizational directory onlyRedirect URI: (dejar vacio)
3. Click en "Register"Paso 2: Crear Client Secret
1. Abrir App -> Certificates & secrets2. Client secrets -> New client secret
Description: SAP BTP ABAP EnvironmentExpires: 24 months (recomendado)
3. Click en "Add"4. Copiar Secret Value INMEDIATAMENTE y guardar de forma segura! (Solo se muestra una vez)Paso 3: Configurar permisos API
Para el envio de correos via Microsoft Graph necesitamos Application Permissions:
1. API permissions -> Add a permission2. Microsoft Graph -> Application permissions3. Agregar los siguientes permisos:
[x] Mail.Send (Enviar correos) [x] Mail.ReadWrite (opcional: Crear borradores) [x] User.Read.All (opcional: Obtener info de usuario)
4. Click en "Add permissions"5. Click en "Grant admin consent for [Tenant]"Importante: Application Permissions requieren Admin Consent y permiten enviar como cualquier usuario en el Tenant.
Paso 4: Anotar IDs importantes
Despues del registro necesitas los siguientes valores:
| Valor | Donde encontrarlo |
|---|---|
| Application (Client) ID | Pagina Overview de la App |
| Directory (Tenant) ID | Pagina Overview de la App |
| Client Secret | Certificates & secrets (copiado al crear) |
Ejemplo:
Application (Client) ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890Directory (Tenant) ID: 12345678-90ab-cdef-1234-567890abcdefClient Secret: Xyz123~AbCdEfGhIjKlMnOpQrStUvWx.YZ2. Crear Communication Scenario en ABAP
Crea un Communication Scenario personalizado para la integracion con Microsoft Graph.
Definicion del Scenario
En ADT: Click derecho en Package -> New -> Other ABAP Repository Object -> Communication Scenario
<?xml version="1.0" encoding="utf-8"?><communicationScenario id="Z_MS_GRAPH_MAIL" scenarioType="customer"> <description>Microsoft Graph API - Mail Integration</description>
<outboundServices> <service id="Z_MS_GRAPH_API"> <description>Microsoft Graph REST API</description> <serviceType>http</serviceType> </service> </outboundServices>
<supportedAuthenticationMethods> <method>oauth2_client_credentials</method> </supportedAuthenticationMethods></communicationScenario>Despues de activar, el Scenario esta disponible para la configuracion del administrador.
3. Configurar Communication Arrangement
La configuracion se realiza en las apps Fiori del ABAP Environment.
Crear Communication System
Fiori App: "Maintain Communication Systems"
System ID: MS_GRAPH_PRODSystem Name: Microsoft Graph API
General:+-- Host Name: graph.microsoft.com+-- Port: 443+-- HTTPS: [x]
OAuth 2.0 Settings:+-- Token Service URL Type: Dedicated+-- Token Service URL: https://login.microsoftonline.com/{Tenant-ID}/oauth2/v2.0/token+-- (Reemplaza {Tenant-ID} con tu Directory/Tenant ID)Crear Communication Arrangement
Fiori App: "Maintain Communication Arrangements"
Arrangement ID: Z_MS_GRAPH_MAIL_PRODScenario: Z_MS_GRAPH_MAILCommunication System: MS_GRAPH_PROD
Outbound Services:+-- Z_MS_GRAPH_API: Active+-- Path: /v1.0
Authentication Method: OAuth 2.0 Client Credentials+-- Client ID: [Application (Client) ID de Azure]+-- Client Secret: [Secret de Azure]+-- Token Service Scope: https://graph.microsoft.com/.defaultImportante: El scope https://graph.microsoft.com/.default es requerido para Application Permissions.
4. Implementacion ABAP
Ahora implementamos la clase para el envio de correos via Microsoft Graph.
Clase de envio de correos
CLASS zcl_ms_graph_mail DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. INTERFACES if_oo_adt_classrun.
TYPES: BEGIN OF ty_email_address, address TYPE string, name TYPE string, END OF ty_email_address, tt_email_addresses TYPE STANDARD TABLE OF ty_email_address WITH EMPTY KEY.
TYPES: BEGIN OF ty_attachment, name TYPE string, content_type TYPE string, content TYPE xstring, END OF ty_attachment, tt_attachments TYPE STANDARD TABLE OF ty_attachment WITH EMPTY KEY.
METHODS send_mail IMPORTING iv_sender_mail TYPE string iv_subject TYPE string iv_body TYPE string iv_is_html TYPE abap_bool DEFAULT abap_false it_to TYPE tt_email_addresses it_cc TYPE tt_email_addresses OPTIONAL it_bcc TYPE tt_email_addresses OPTIONAL it_attachments TYPE tt_attachments OPTIONAL RAISING cx_http_dest_provider_error cx_web_http_client_error.
PRIVATE SECTION. CONSTANTS: c_scenario TYPE if_com_scenario_factory=>ty_cscn_id VALUE 'Z_MS_GRAPH_MAIL', c_service_id TYPE if_com_scenario_factory=>ty_cscn_outb_srv_id VALUE 'Z_MS_GRAPH_API'.
METHODS get_destination RETURNING VALUE(ro_destination) TYPE REF TO if_http_destination RAISING cx_http_dest_provider_error.
METHODS build_mail_json IMPORTING iv_subject TYPE string iv_body TYPE string iv_is_html TYPE abap_bool it_to TYPE tt_email_addresses it_cc TYPE tt_email_addresses it_bcc TYPE tt_email_addresses it_attachments TYPE tt_attachments RETURNING VALUE(rv_json) TYPE string.
METHODS build_recipients_json IMPORTING it_addresses TYPE tt_email_addresses RETURNING VALUE(rv_json) TYPE string.
METHODS build_attachments_json IMPORTING it_attachments TYPE tt_attachments RETURNING VALUE(rv_json) TYPE string.ENDCLASS.
CLASS zcl_ms_graph_mail IMPLEMENTATION. METHOD if_oo_adt_classrun~main. " Enviar correo de prueba TRY. send_mail( iv_subject = 'Test desde SAP BTP' iv_body = |<html><body>| && |<h1>Bienvenido</h1>| && |<p>Este es un correo de prueba desde SAP BTP ABAP Environment.</p>| && |<p>Enviado: { cl_abap_context_info=>get_system_date( ) }</p>| && |</body></html>| iv_is_html = abap_true name = 'Juan Perez' ) ) ).
out->write( 'Correo enviado exitosamente!' ).
CATCH cx_http_dest_provider_error INTO DATA(lx_dest). out->write( |Error de Destination: { lx_dest->get_text( ) }| ). CATCH cx_web_http_client_error INTO DATA(lx_http). out->write( |Error HTTP: { lx_http->get_text( ) }| ). ENDTRY. ENDMETHOD.
METHOD get_destination. ro_destination = cl_http_destination_provider=>create_by_comm_arrangement( comm_scenario = c_scenario service_id = c_service_id ). ENDMETHOD.
METHOD send_mail. " 1. Obtener destination DATA(lo_destination) = get_destination( ).
" 2. Crear cliente HTTP DATA(lo_client) = cl_web_http_client_manager=>create_by_http_destination( i_destination = lo_destination ).
TRY. " 3. Configurar request DATA(lo_request) = lo_client->get_http_request( ).
" Graph API Endpoint: /users/{user-id}/sendMail lo_request->set_uri_path( |/users/{ iv_sender_mail }/sendMail| ).
" 4. Crear JSON body DATA(lv_json) = build_mail_json( iv_subject = iv_subject iv_body = iv_body iv_is_html = iv_is_html it_to = it_to it_cc = it_cc it_bcc = it_bcc it_attachments = it_attachments ).
lo_request->set_text( lv_json ). lo_request->set_header_field( i_name = 'Content-Type' i_value = 'application/json' ).
" 5. Ejecutar POST DATA(lo_response) = lo_client->execute( if_web_http_client=>post ). DATA(lv_status) = lo_response->get_status( )-code.
" 202 Accepted = Exitoso IF lv_status <> 202. DATA(lv_error) = lo_response->get_text( ). RAISE EXCEPTION TYPE cx_web_http_client_error EXPORTING text = |Envio de correo fallido: { lv_status } - { lv_error }|. ENDIF.
CLEANUP. lo_client->close( ). ENDTRY. ENDMETHOD.
METHOD build_mail_json. " Crear estructura JSON de Microsoft Graph Mail DATA(lv_content_type) = COND string( WHEN iv_is_html = abap_true THEN 'HTML' ELSE 'Text' ).
rv_json = |\{| && | "message": \{| && | "subject": "{ escape( val = iv_subject format = cl_abap_format=>e_json_string ) }",| && | "body": \{| && | "contentType": "{ lv_content_type }",| && | "content": "{ escape( val = iv_body format = cl_abap_format=>e_json_string ) }"| && | \},| && | "toRecipients": { build_recipients_json( it_to ) }|.
" Agregar CC (si existe) IF it_cc IS NOT INITIAL. rv_json = rv_json && |,| && | "ccRecipients": { build_recipients_json( it_cc ) }|. ENDIF.
" Agregar BCC (si existe) IF it_bcc IS NOT INITIAL. rv_json = rv_json && |,| && | "bccRecipients": { build_recipients_json( it_bcc ) }|. ENDIF.
" Agregar adjuntos (si existen) IF it_attachments IS NOT INITIAL. rv_json = rv_json && |,| && | "attachments": { build_attachments_json( it_attachments ) }|. ENDIF.
rv_json = rv_json && | \},| && | "saveToSentItems": "true"| && |\}|. ENDMETHOD.
METHOD build_recipients_json. DATA: lt_json TYPE TABLE OF string.
LOOP AT it_addresses INTO DATA(ls_address). DATA(lv_entry) = |\{| && | "emailAddress": \{| && | "address": "{ ls_address-address }"|.
IF ls_address-name IS NOT INITIAL. lv_entry = lv_entry && |,| && | "name": "{ escape( val = ls_address-name format = cl_abap_format=>e_json_string ) }"|. ENDIF.
lv_entry = lv_entry && | \}| && |\}|.
APPEND lv_entry TO lt_json. ENDLOOP.
rv_json = |[{ concat_lines_of( table = lt_json sep = `, ` ) }]|. ENDMETHOD.
METHOD build_attachments_json. DATA: lt_json TYPE TABLE OF string.
LOOP AT it_attachments INTO DATA(ls_attachment). " Codificacion Base64 del contenido DATA(lv_base64) = cl_web_http_utility=>encode_x_base64( ls_attachment-content ).
DATA(lv_entry) = |\{| && | "@odata.type": "#microsoft.graph.fileAttachment",| && | "name": "{ escape( val = ls_attachment-name format = cl_abap_format=>e_json_string ) }",| && | "contentType": "{ ls_attachment-content_type }",| && | "contentBytes": "{ lv_base64 }"| && |\}|.
APPEND lv_entry TO lt_json. ENDLOOP.
rv_json = |[{ concat_lines_of( table = lt_json sep = `, ` ) }]|. ENDMETHOD.ENDCLASS.5. Ejemplos practicos
Correo de texto simple
DATA(lo_mail) = NEW zcl_ms_graph_mail( ).
lo_mail->send_mail( iv_subject = 'Pedido confirmado' iv_body = |Estimado cliente,\n\n| && |Su pedido ha sido registrado exitosamente.\n\n| && |Saludos cordiales\n| && |Su sistema SAP| name = 'Juan Perez' ) ) ).Correo HTML con formato
DATA(lo_mail) = NEW zcl_ms_graph_mail( ).
DATA(lv_html) = |<html>| && |<head>| && | <style>| && | body \{ font-family: Arial, sans-serif; \}| && | h1 \{ color: #0070c0; \}| && | .highlight \{ background-color: #fff3cd; padding: 10px; \}| && | </style>| && |</head>| && |<body>| && | <h1>Confirmacion de reserva</h1>| && | <p>Estimado Sr. Perez,</p>| && | <p>Su reserva de vuelo ha sido creada exitosamente:</p>| && | <div class="highlight">| && | <strong>Numero de reserva:</strong> FB-2026-001234<br>| && | <strong>Vuelo:</strong> LH 100<br>| && | <strong>Ruta:</strong> Frankfurt - New York<br>| && | <strong>Fecha:</strong> 15.03.2026| && | </div>| && | <p>Atentamente,<br>Su agencia de viajes</p>| && |</body>| && |</html>|.
lo_mail->send_mail( iv_subject = 'Su confirmacion de reserva FB-2026-001234' iv_body = lv_html iv_is_html = abap_true name = 'Juan Perez' ) )Correo con adjuntos
DATA(lo_mail) = NEW zcl_ms_graph_mail( ).
" Datos CSV como adjuntoDATA(lv_csv) = |NumeroReserva;Cliente;Importe\n| && |FB-001;Mueller;1250.00\n| && |FB-002;Schmidt;890.50\n| && |FB-003;Weber;2100.00|.
" Convertir String a XStringDATA(lv_csv_xstring) = cl_abap_codepage=>convert_to( source = lv_csv codepage = 'UTF-8' ).
" Contenido PDF (ej. desde RAP Action o Adobe Forms)DATA: lv_pdf_xstring TYPE xstring." ... generar PDF ...
lo_mail->send_mail( iv_subject = 'Informe mensual de reservas' iv_body = |<html><body>| && |<p>Adjunto encontrara el informe de reservas de febrero 2026.</p>| && |</body></html>| iv_is_html = abap_true name = 'Direccion' ) ) it_attachments = VALUE #( ( name = 'reservas_2026-02.csv' content_type = 'text/csv' content = lv_csv_xstring ) ( name = 'informe_2026-02.pdf' content_type = 'application/pdf' content = lv_pdf_xstring ) ) ).Enviar correo desde RAP Action
METHOD send_booking_confirmation. " RAP Action: Enviar confirmacion de reserva por correo
READ ENTITIES OF zi_flightbook IN LOCAL MODE ENTITY FlightBook ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_bookings).
LOOP AT lt_bookings INTO DATA(ls_booking). " Obtener direccion de correo del cliente SELECT SINGLE email FROM zcustomer WHERE customer_id = @ls_booking-CustomerId INTO @DATA(lv_customer_email).
IF sy-subrc = 0 AND lv_customer_email IS NOT INITIAL. TRY. DATA(lo_mail) = NEW zcl_ms_graph_mail( ).
DATA(lv_body) = |<html><body>| && |<h2>Confirmacion de reserva</h2>| && |<p>Su reserva ha sido confirmada:</p>| && |<ul>| && | <li><strong>No. reserva:</strong> { ls_booking-BookingId }</li>| && | <li><strong>Vuelo:</strong> { ls_booking-FlightNumber }</li>| && | <li><strong>Fecha:</strong> { ls_booking-FlightDate }</li>| && | <li><strong>Estado:</strong> { ls_booking-Status }</li>| && |</ul>| && |</body></html>|.
lo_mail->send_mail( iv_subject = |Confirmacion de reserva { ls_booking-BookingId }| iv_body = lv_body iv_is_html = abap_true it_to = VALUE #( ( address = lv_customer_email ) ) ).
" Mensaje de exito APPEND VALUE #( %tky = ls_booking-%tky %msg = new_message_with_text( severity = if_abap_behv_message=>severity-success text = |Correo enviado a { lv_customer_email }| ) ) TO reported-flightbook.
CATCH cx_root INTO DATA(lx_error). " Mensaje de error APPEND VALUE #( %tky = ls_booking-%tky %msg = new_message_with_text( severity = if_abap_behv_message=>severity-error text = |Error de correo: { lx_error->get_text( ) }| ) ) TO reported-flightbook. ENDTRY. ENDIF. ENDLOOP.ENDMETHOD.6. Manejo de errores
Clase extendida con manejo de errores
CLASS zcl_ms_graph_mail_ext DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. TYPES: BEGIN OF ty_mail_result, success TYPE abap_bool, message TYPE string, error_code TYPE string, END OF ty_mail_result.
METHODS send_mail_safe IMPORTING iv_sender_mail TYPE string iv_subject TYPE string iv_body TYPE string iv_is_html TYPE abap_bool DEFAULT abap_false it_to TYPE zcl_ms_graph_mail=>tt_email_addresses RETURNING VALUE(rs_result) TYPE ty_mail_result.
PRIVATE SECTION. METHODS parse_graph_error IMPORTING iv_json TYPE string RETURNING VALUE(rv_error) TYPE string.ENDCLASS.
CLASS zcl_ms_graph_mail_ext IMPLEMENTATION. METHOD send_mail_safe. TRY. DATA(lo_mail) = NEW zcl_ms_graph_mail( ).
lo_mail->send_mail( iv_sender_mail = iv_sender_mail iv_subject = iv_subject iv_body = iv_body iv_is_html = iv_is_html it_to = it_to ).
rs_result = VALUE #( success = abap_true message = 'Correo enviado exitosamente' ).
CATCH cx_http_dest_provider_error INTO DATA(lx_dest). rs_result = VALUE #( success = abap_false message = lx_dest->get_text( ) error_code = 'DESTINATION_ERROR' ).
CATCH cx_web_http_client_error INTO DATA(lx_http). rs_result = VALUE #( success = abap_false message = lx_http->get_text( ) error_code = 'HTTP_ERROR' ). ENDTRY. ENDMETHOD.
METHOD parse_graph_error. " Parsear JSON de error de Microsoft Graph " Ejemplo: {"error":{"code":"InvalidAuthenticationToken","message":"..."}} TRY. DATA: BEGIN OF ls_error, BEGIN OF error, code TYPE string, message TYPE string, END OF error, END OF ls_error.
/ui2/cl_json=>deserialize( EXPORTING json = iv_json CHANGING data = ls_error ).
rv_error = |{ ls_error-error-code }: { ls_error-error-message }|.
CATCH cx_root. rv_error = iv_json. ENDTRY. ENDMETHOD.ENDCLASS.Errores frecuentes y soluciones
| Codigo de error | Descripcion | Solucion |
|---|---|---|
| 401 Unauthorized | Token invalido o expirado | Renovar Client Secret, verificar Tenant-ID |
| 403 Forbidden | Sin permiso | Otorgar Admin Consent, verificar permiso Mail.Send |
| 404 Not Found | Mailbox del remitente no encontrado | Verificar direccion de correo del remitente |
| 400 Bad Request | JSON invalido o campos faltantes | Validar estructura JSON |
| 429 Too Many Requests | Rate Limiting | Implementar Retry con Exponential Backoff |
Considerar Rate Limiting
Microsoft Graph tiene Rate Limits. Para envio de correos:
Limite: 10,000 solicitudes por 10 minutos por App 10,000 correos por dia por Mailbox
Mejores practicas:- Evitar envio en batch- Con 429: Respetar header Retry-After- Application Logging para monitoreo7. Shared Mailbox vs. Service Account
Para uso productivo se recomienda un Shared Mailbox:
| Aspecto | Shared Mailbox | Service Account |
|---|---|---|
| Licencia | No necesita licencia propia | Requiere licencia Exchange |
| Costos | Gratuito | Costos mensuales |
| Administracion | Via Exchange Admin Center | Como usuario normal |
| Recomendacion | Para correos automaticos | Solo si es necesario |
Configurar Shared Mailbox
1. Microsoft 365 Admin Center -> Teams & Groups -> Shared Mailboxes2. Click en "Add a shared mailbox"3. Name: SAP-Notifications Email: [email protected]4. Crear Mailbox5. En Azure AD: App-Permission Mail.Send permite enviar via esta mailbox8. Mejores practicas de seguridad
| Tema | Recomendacion |
|---|---|
| Rotacion de Secret | Renovar Client Secret cada 6-12 meses |
| Minimo privilegio | Solo permiso Mail.Send, sin permisos Read |
| Whitelist de remitentes | Solo permitir mailboxes dedicados como remitentes |
| Logging | Protocolar todas las acciones de correo con Application Logging |
| Monitoreo | Monitorear Azure AD Sign-in Logs |
| Manejo de errores | No exponer datos sensibles en mensajes de error |
Integrar Application Logging
METHOD send_mail_with_logging. " Inicializar logging DATA(lo_log) = cl_bali_log=>create_with_header( header = cl_bali_header_setter=>create( )->set_object( 'ZMS_GRAPH' ) ->set_subobject( 'MAIL' ) ).
TRY. DATA(lo_mail) = NEW zcl_ms_graph_mail( ). lo_mail->send_mail( iv_sender_mail = iv_sender iv_subject = iv_subject iv_body = iv_body it_to = it_recipients ).
" Registrar exito lo_log->add_item( cl_bali_message_setter=>create( severity = if_bali_constants=>c_severity_status id = 'ZMS_GRAPH' number = '001' )->set_text( |Correo a { it_recipients[ 1 ]-address } enviado| ) ).
CATCH cx_root INTO DATA(lx_error). " Registrar error lo_log->add_item( cl_bali_message_setter=>create( severity = if_bali_constants=>c_severity_error id = 'ZMS_GRAPH' number = '002' )->set_text( |Error: { lx_error->get_text( ) }| ) ). ENDTRY.
" Guardar log cl_bali_log_db=>get_instance( )->save_log( lo_log ).ENDMETHOD.Conclusion
La integracion de Office 365 Mail en SAP BTP ABAP Environment ofrece una alternativa moderna a las soluciones de correo clasicas. La combinacion de Microsoft Graph API, OAuth 2.0 y el Communication Management en ABAP Cloud permite una funcionalidad de correo segura y bien mantenible.
Puntos importantes:
- Registro de App en Azure AD con Application Permissions (Mail.Send)
- Communication Scenario para separacion limpia entre desarrollo y configuracion
- OAuth 2.0 Client Credentials para autenticacion Machine-to-Machine
- Shared Mailbox como solucion de remitente economica
- Manejo de errores y Logging para operacion productiva
Articulos relacionados
- Guia de Communication Scenarios - Guia completa de Communication Management
- HTTP Client en ABAP - Llamar APIs REST
- Application Logging - Logging en ABAP Cloud
- OAuth JWT en ABAP Cloud - Autenticacion basada en tokens